Skip to content

Commit 2b8cdc6

Browse files
Copilotfranky47
andauthored
feat: Add per-test configurable TypeScript mocks for flexible offline testing (#12)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: franky47 <1174092+franky47@users.noreply.github.com>
1 parent ee6898f commit 2b8cdc6

8 files changed

Lines changed: 697 additions & 23 deletions

File tree

packages/mtsv/src/lib/services.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
// test/example.test.ts
22
// @vitest-environment node
33
import { describe } from 'node:test'
4-
import { expect, it } from 'vitest'
4+
import { expect, it, beforeEach, afterEach } from 'vitest'
55
import { fetchTypeScriptVersions } from './services'
6+
import { configureTestServer, resetTestServer } from '../mocks/test-setup'
67

78
describe('fetchTypeScriptVersions', () => {
9+
beforeEach(() => {
10+
// Configure a standard set of versions for services testing
11+
configureTestServer({
12+
'4.0.0': true,
13+
'5.0.0': true
14+
})
15+
})
16+
17+
afterEach(() => {
18+
resetTestServer()
19+
})
20+
821
it('fetches a list of versions for TypeScript', async () => {
922
const versionsPromise = fetchTypeScriptVersions()
1023
await expect(versionsPromise).resolves.toEqual(['4.0.0', '5.0.0'])
1124
})
25+
1226
it('includes dev versions when asked to', async () => {
1327
const versionsPromise = fetchTypeScriptVersions({ includeDev: true })
1428
await expect(versionsPromise).resolves.toEqual([
@@ -17,6 +31,7 @@ describe('fetchTypeScriptVersions', () => {
1731
'5.1.0-dev.20240101'
1832
])
1933
})
34+
2035
it('includes rc versions when asked to', async () => {
2136
const versionsPromise = fetchTypeScriptVersions({ includeRc: true })
2237
await expect(versionsPromise).resolves.toEqual([
@@ -25,6 +40,7 @@ describe('fetchTypeScriptVersions', () => {
2540
'5.1.0-rc.20240101'
2641
])
2742
})
43+
2844
it('includes insiders versions when asked to', async () => {
2945
const versionsPromise = fetchTypeScriptVersions({ includeInsiders: true })
3046
await expect(versionsPromise).resolves.toEqual([
@@ -33,6 +49,7 @@ describe('fetchTypeScriptVersions', () => {
3349
'5.1.0-insiders.20240101'
3450
])
3551
})
52+
3653
it('includes beta versions when asked to', async () => {
3754
const versionsPromise = fetchTypeScriptVersions({ includeBeta: true })
3855
await expect(versionsPromise).resolves.toEqual([
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// @vitest-environment node
2+
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
3+
import { mkdtempSync, rmSync } from 'node:fs'
4+
import { tmpdir } from 'node:os'
5+
import { resolve } from 'node:path'
6+
import {
7+
TypeScriptCDNCache,
8+
TypeScriptMemoryCache,
9+
type TypeScriptCacheInterface
10+
} from './typescript-cache'
11+
import {
12+
configureTestServer,
13+
resetTestServer,
14+
testConfigs
15+
} from '../mocks/test-setup'
16+
17+
describe('TypeScript Cache Classes', () => {
18+
describe('TypeScriptMemoryCache', () => {
19+
let cache: TypeScriptMemoryCache
20+
21+
beforeEach(() => {
22+
cache = new TypeScriptMemoryCache()
23+
})
24+
25+
it('should create an empty cache', () => {
26+
expect(cache.memoryCache.size).toBe(0)
27+
expect(cache.path).toBe('(in-memory)')
28+
})
29+
30+
it('should throw error for unknown versions', async () => {
31+
await expect(cache.load('4.0.0')).rejects.toThrow(
32+
'TypeScript version 4.0.0 not in cache'
33+
)
34+
})
35+
36+
it('should clear the cache', () => {
37+
// Add a mock module to the cache
38+
const mockModule = { version: '4.0.0' } as any
39+
cache.memoryCache.set('4.0.0', mockModule)
40+
41+
expect(cache.memoryCache.size).toBe(1)
42+
cache.clear()
43+
expect(cache.memoryCache.size).toBe(0)
44+
})
45+
46+
it('should free specific versions', () => {
47+
const mockModule = { version: '4.0.0' } as any
48+
cache.memoryCache.set('4.0.0', mockModule)
49+
50+
expect(cache.memoryCache.has('4.0.0')).toBe(true)
51+
cache.free('4.0.0')
52+
expect(cache.memoryCache.has('4.0.0')).toBe(false)
53+
})
54+
55+
it('should purge the cache', async () => {
56+
const mockModule = { version: '4.0.0' } as any
57+
cache.memoryCache.set('4.0.0', mockModule)
58+
59+
expect(cache.memoryCache.size).toBe(1)
60+
await cache.purge()
61+
expect(cache.memoryCache.size).toBe(0)
62+
})
63+
})
64+
65+
describe('TypeScriptCDNCache', () => {
66+
let cache: TypeScriptCDNCache
67+
let tempDir: string
68+
69+
beforeEach(() => {
70+
tempDir = mkdtempSync(resolve(tmpdir(), 'mtsv-test-'))
71+
cache = new TypeScriptCDNCache(tempDir)
72+
})
73+
74+
afterEach(() => {
75+
// Clean up temp directory
76+
rmSync(tempDir, { recursive: true, force: true })
77+
resetTestServer() // Reset to default handlers after each test
78+
})
79+
80+
it('should create cache with correct path', () => {
81+
expect(cache.path).toBe(resolve(tempDir, 'mtsv-cache'))
82+
})
83+
84+
it('should load TypeScript version from CDN (mocked)', async () => {
85+
// Configure test to provide a passing TypeScript 4.0.0 version
86+
configureTestServer(testConfigs.singlePass('4.0.0'))
87+
88+
const tsModule = await cache.load('4.0.0')
89+
90+
expect(tsModule).toBeDefined()
91+
expect(tsModule.version).toBe('4.0.0')
92+
expect(tsModule.ScriptTarget).toBeDefined()
93+
})
94+
95+
it('should throw error for non-existent versions', async () => {
96+
// Configure test with only 4.0.0 available
97+
configureTestServer(testConfigs.singlePass('4.0.0'))
98+
99+
await expect(cache.load('99.0.0')).rejects.toThrow(
100+
'Failed to fetch TypeScript 99.0.0'
101+
)
102+
})
103+
104+
it('should cache loaded modules in memory', async () => {
105+
// Configure test to provide TypeScript 4.0.0
106+
configureTestServer(testConfigs.singlePass('4.0.0'))
107+
108+
// Load the same version twice
109+
const tsModule1 = await cache.load('4.0.0')
110+
const tsModule2 = await cache.load('4.0.0')
111+
112+
// Both should be valid TypeScript modules
113+
expect(tsModule1).toBeDefined()
114+
expect(tsModule2).toBeDefined()
115+
expect(tsModule1.version).toBe('4.0.0')
116+
expect(tsModule2.version).toBe('4.0.0')
117+
expect(tsModule1.ScriptTarget).toEqual(tsModule2.ScriptTarget)
118+
})
119+
120+
it('should free cached modules', async () => {
121+
// Configure test to provide TypeScript 4.0.0
122+
configureTestServer(testConfigs.singlePass('4.0.0'))
123+
124+
const tsModule1 = await cache.load('4.0.0')
125+
cache.free('4.0.0')
126+
127+
// Loading again should still work (from disk cache since mocked)
128+
const tsModule2 = await cache.load('4.0.0')
129+
expect(tsModule1.version).toBe('4.0.0')
130+
expect(tsModule2.version).toBe('4.0.0')
131+
})
132+
133+
it('should clear all cached modules', async () => {
134+
// Configure test to provide multiple TypeScript versions
135+
configureTestServer({
136+
'4.0.0': true,
137+
'5.0.0': true
138+
})
139+
140+
await cache.load('4.0.0')
141+
await cache.load('5.0.0')
142+
143+
// Clear should remove from memory but files should still exist
144+
cache.clear()
145+
146+
// Loading again should work (from disk)
147+
const tsModule = await cache.load('4.0.0')
148+
expect(tsModule).toBeDefined()
149+
})
150+
151+
it('should purge cache directory', async () => {
152+
// Configure test to provide TypeScript 4.0.0
153+
configureTestServer(testConfigs.singlePass('4.0.0'))
154+
155+
await cache.load('4.0.0')
156+
157+
// Purge should remove everything
158+
await cache.purge()
159+
160+
// Loading again should fetch from CDN
161+
const tsModule = await cache.load('4.0.0')
162+
expect(tsModule).toBeDefined()
163+
})
164+
})
165+
166+
describe('Cache Interface Compliance', () => {
167+
const testCacheInterface = (
168+
createCache: () => TypeScriptCacheInterface
169+
) => {
170+
let cache: TypeScriptCacheInterface
171+
172+
beforeEach(() => {
173+
cache = createCache()
174+
})
175+
176+
it('should have all required methods', () => {
177+
expect(typeof cache.load).toBe('function')
178+
expect(typeof cache.free).toBe('function')
179+
expect(typeof cache.clear).toBe('function')
180+
expect(typeof cache.purge).toBe('function')
181+
expect(typeof cache.path).toBe('string')
182+
})
183+
}
184+
185+
describe('TypeScriptMemoryCache interface compliance', () => {
186+
testCacheInterface(() => new TypeScriptMemoryCache())
187+
})
188+
189+
describe('TypeScriptCDNCache interface compliance', () => {
190+
let tempDir: string
191+
192+
beforeEach(() => {
193+
tempDir = mkdtempSync(resolve(tmpdir(), 'mtsv-test-'))
194+
})
195+
196+
afterEach(() => {
197+
rmSync(tempDir, { recursive: true, force: true })
198+
resetTestServer() // Reset handlers after each test
199+
})
200+
201+
testCacheInterface(() => new TypeScriptCDNCache(tempDir))
202+
})
203+
})
204+
})

packages/mtsv/src/lib/typescript-cache.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, rmdir, stat, writeFile } from 'node:fs/promises'
1+
import { mkdir, rmdir, stat, writeFile, rm } from 'node:fs/promises'
22
import { createRequire } from 'node:module'
33
import { tmpdir } from 'node:os'
44
import { dirname, resolve } from 'node:path'
@@ -91,7 +91,9 @@ export class TypeScriptCDNCache implements TypeScriptCacheInterface {
9191

9292
async load(version: string): Promise<TypeScriptModule> {
9393
const filePath = this.#resolveVersionFile(version)
94-
let tsModule = this.require.cache[filePath] as TypeScriptModule | undefined
94+
let tsModule = this.require.cache[filePath]?.exports as
95+
| TypeScriptModule
96+
| undefined
9597
if (tsModule) {
9698
return tsModule
9799
}
@@ -112,9 +114,8 @@ export class TypeScriptCDNCache implements TypeScriptCacheInterface {
112114
}
113115

114116
free(version: string): void {
115-
delete this.require.cache[
116-
this.require.resolve(this.#resolveVersionFile(version))
117-
]
117+
const filePath = this.#resolveVersionFile(version)
118+
delete this.require.cache[filePath]
118119
}
119120

120121
clear(): void {
@@ -128,7 +129,7 @@ export class TypeScriptCDNCache implements TypeScriptCacheInterface {
128129

129130
async purge(): Promise<void> {
130131
// Also remove from disk
131-
return rmdir(this.basePath, { recursive: true }).catch(() => {})
132+
return rm(this.basePath, { recursive: true }).catch(() => {})
132133
}
133134
get path(): string {
134135
return this.basePath
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// src/mocks/configurable-handlers.test.ts
2+
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
3+
import { configureTestServer, resetTestServer, testConfigs } from './test-setup'
4+
import { TypeScriptCDNCache } from '../lib/typescript-cache'
5+
import { mkdtempSync, rmSync } from 'node:fs'
6+
import { tmpdir } from 'node:os'
7+
import { resolve } from 'node:path'
8+
9+
describe('Configurable TypeScript Handlers', () => {
10+
let tempDir: string
11+
let cache: TypeScriptCDNCache
12+
13+
beforeEach(() => {
14+
tempDir = mkdtempSync(resolve(tmpdir(), 'mtsv-configurable-test-'))
15+
cache = new TypeScriptCDNCache(tempDir)
16+
})
17+
18+
afterEach(() => {
19+
rmSync(tempDir, { recursive: true, force: true })
20+
resetTestServer() // Reset to default handlers after each test
21+
})
22+
23+
describe('single passing version', () => {
24+
it('should load a passing TypeScript version successfully', async () => {
25+
configureTestServer(testConfigs.singlePass('4.0.0'))
26+
27+
const tsModule = await cache.load('4.0.0')
28+
29+
expect(tsModule).toBeDefined()
30+
expect(tsModule.version).toBe('4.0.0')
31+
expect(tsModule.ScriptTarget).toBeDefined()
32+
33+
// Verify it's configured to pass (no diagnostics errors)
34+
const program = tsModule.createProgram()
35+
expect(program.getSyntacticDiagnostics()).toEqual([])
36+
expect(program.getSemanticDiagnostics()).toEqual([])
37+
})
38+
39+
it('should return 404 for non-configured versions', async () => {
40+
configureTestServer(testConfigs.singlePass('4.0.0'))
41+
42+
await expect(cache.load('99.0.0')).rejects.toThrow(
43+
'Failed to fetch TypeScript 99.0.0'
44+
)
45+
})
46+
})
47+
48+
describe('single failing version', () => {
49+
it('should load a failing TypeScript version with diagnostics', async () => {
50+
configureTestServer(testConfigs.singleFail('3.9.0'))
51+
52+
const tsModule = await cache.load('3.9.0')
53+
54+
expect(tsModule).toBeDefined()
55+
expect(tsModule.version).toBe('3.9.0')
56+
57+
// Verify it's configured to fail (has diagnostics errors)
58+
const program = tsModule.createProgram()
59+
const diagnostics = program.getSyntacticDiagnostics()
60+
expect(diagnostics).toHaveLength(1)
61+
expect(diagnostics[0]).toMatchObject({
62+
category: 1, // Error
63+
code: 2345
64+
})
65+
})
66+
})
67+
68+
describe('mixed configuration', () => {
69+
it('should handle passing and failing versions correctly', async () => {
70+
configureTestServer(testConfigs.mixed())
71+
72+
// Test a passing version
73+
const passingTs = await cache.load('4.0.0')
74+
expect(passingTs.version).toBe('4.0.0')
75+
expect(passingTs.createProgram().getSyntacticDiagnostics()).toEqual([])
76+
77+
// Test a failing version
78+
const failingTs = await cache.load('3.9.0')
79+
expect(failingTs.version).toBe('3.9.0')
80+
expect(failingTs.createProgram().getSyntacticDiagnostics()).toHaveLength(
81+
1
82+
)
83+
})
84+
})
85+
})

0 commit comments

Comments
 (0)