From ad51616cf80d83f50a1a0d36ac3ea523df7052a1 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 13 Oct 2026 18:15:24 +0900 Subject: [PATCH] test: restore global state and scoped spies Refs #683 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../src/__tests__/coordinator.test.ts | 164 +++--- .../next-plugin/src/__tests__/loader.test.ts | 511 +++++++++--------- .../next-plugin/src/__tests__/plugin.test.ts | 88 +-- .../next-plugin/src/__tests__/profile.test.ts | 32 +- .../plugin-utils/src/import-graph.test.ts | 52 +- .../hooks/__tests__/use-safe-effect.test.ts | 19 +- .../utils/__tests__/get-theme.browser.test.ts | 13 +- .../__tests__/init-theme.browser.test.ts | 54 +- .../src/__tests__/plugin.test.ts | 110 ++-- .../vite-plugin/src/__tests__/plugin.test.ts | 48 +- .../src/__tests__/loader.test.ts | 63 ++- .../src/__tests__/plugin.test.ts | 112 ++-- 12 files changed, 692 insertions(+), 574 deletions(-) diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index 72d812adb..e7930b94c 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -323,26 +323,27 @@ describe('coordinator', () => { it('profiles both base CSS serialization paths separately from writes', async () => { const originalProfile = process.env.DEVUP_UI_PROFILE - process.env.DEVUP_UI_PROFILE = '1' const infoSpy = spyOn(console, 'info').mockImplementation(() => {}) - codeExtractSpy.mockReturnValue({ - code: 'transformed code', - map: undefined, - css: 'collected css', - cssFile: 'devup-ui-1.css', - updatedBaseStyle: false, - free: mock(), - [Symbol.dispose]: mock(), - }) - getCssSpy.mockImplementation((fileNum: number | null) => - fileNum === null ? 'base-css' : `file-css-${fileNum}`, - ) - exportSheetSpy.mockReturnValue('sheet-json') - exportClassMapSpy.mockReturnValue('classmap-json') - exportFileMapSpy.mockReturnValue('filemap-json') - - const coordinator = startCoordinator(makeOptions()) + let coordinator: ReturnType | undefined try { + process.env.DEVUP_UI_PROFILE = '1' + codeExtractSpy.mockReturnValue({ + code: 'transformed code', + map: undefined, + css: 'collected css', + cssFile: 'devup-ui-1.css', + updatedBaseStyle: false, + free: mock(), + [Symbol.dispose]: mock(), + }) + getCssSpy.mockImplementation((fileNum: number | null) => + fileNum === null ? 'base-css' : `file-css-${fileNum}`, + ) + exportSheetSpy.mockReturnValue('sheet-json') + exportClassMapSpy.mockReturnValue('classmap-json') + exportFileMapSpy.mockReturnValue('filemap-json') + + coordinator = startCoordinator(makeOptions()) await new Promise((resolve) => setTimeout(resolve, 100)) const port = parseInt( (writeFileSyncSpy.mock.calls[0] as [string, string])[1], @@ -433,13 +434,13 @@ describe('coordinator', () => { }) expect(baseProfile.cssSnapshotMs).toBeTypeOf('number') } finally { - coordinator.close() infoSpy.mockRestore() if (originalProfile === undefined) { delete process.env.DEVUP_UI_PROFILE } else { process.env.DEVUP_UI_PROFILE = originalProfile } + coordinator?.close() } }) @@ -759,18 +760,20 @@ describe('coordinator', () => { return 61_000 }) - // Request CSS with waitForIdle=true but no extractions ever happen - // (totalExtractions === 0), so it should timeout and return CSS anyway - const res = await httpRequest( - port, - 'GET', - '/css?importMainCss=false&waitForIdle=true', - ) - - expect(res.status).toBe(200) - expect(res.body).toBe('timeout-css') + try { + // Request CSS with waitForIdle=true but no extractions ever happen + // (totalExtractions === 0), so it should timeout and return CSS anyway + const res = await httpRequest( + port, + 'GET', + '/css?importMainCss=false&waitForIdle=true', + ) - dateNowSpy.mockRestore() + expect(res.status).toBe(200) + expect(res.body).toBe('timeout-css') + } finally { + dateNowSpy.mockRestore() + } coordinator.close() }) @@ -1383,31 +1386,34 @@ describe('coordinator per-bucket completion', () => { codeExtractSpy.mockReturnValue(extractResult('devup-ui-1.css')) getCssSpy.mockReturnValue('partial-css') const warnSpy = spyOn(console, 'warn').mockReturnValue(undefined) - // m1 is a member of the bucket but is never POSTed to /extract. - const canonicalMap = { 'src/m1.tsx': 'src/bucket.tsx' } - const { coordinator, port } = await startAndGetPort( - makeOptions({ canonicalMap, idleThresholdMs: 100, maxWaitMs: 150 }), - ) - // Extract only the bucket root; src/m1.tsx stays missing forever. - await extract(port, 'src/bucket.tsx') + try { + // m1 is a member of the bucket but is never POSTed to /extract. + const canonicalMap = { 'src/m1.tsx': 'src/bucket.tsx' } + const { coordinator, port } = await startAndGetPort( + makeOptions({ canonicalMap, idleThresholdMs: 100, maxWaitMs: 150 }), + ) + // Extract only the bucket root; src/m1.tsx stays missing forever. + await extract(port, 'src/bucket.tsx') - const t0 = Date.now() - const res = await httpRequest( - port, - 'GET', - '/css?fileNum=1&importMainCss=true&waitForIdle=true', - ) - const elapsed = Date.now() - t0 + const t0 = Date.now() + const res = await httpRequest( + port, + 'GET', + '/css?fileNum=1&importMainCss=true&waitForIdle=true', + ) + const elapsed = Date.now() - t0 - // Resolves via the hard timeout (fail open) rather than hanging. - expect(res.status).toBe(200) - expect(res.body).toBe('partial-css') - expect(elapsed).toBeGreaterThanOrEqual(150) - expect(warnSpy).toHaveBeenCalled() - expect(getCssSpy).toHaveBeenCalledWith(1, true) + // Resolves via the hard timeout (fail open) rather than hanging. + expect(res.status).toBe(200) + expect(res.body).toBe('partial-css') + expect(elapsed).toBeGreaterThanOrEqual(150) + expect(warnSpy).toHaveBeenCalled() + expect(getCssSpy).toHaveBeenCalledWith(1, true) - warnSpy.mockRestore() - coordinator.close() + coordinator.close() + } finally { + warnSpy.mockRestore() + } }) // T4: base css resolves DETERMINISTICALLY once every route-reachable runtime @@ -1548,31 +1554,37 @@ describe('coordinator per-bucket completion', () => { codeExtractSpy.mockReturnValue(extractResult('devup-ui-1.css')) getCssSpy.mockReturnValue('bucket-css') const infoSpy = spyOn(console, 'info').mockReturnValue(undefined) - const warnSpy = spyOn(console, 'warn').mockReturnValue(undefined) - const canonicalMap = { 'src/phantom.tsx': 'src/bucket.tsx' } - const { coordinator, port } = await startAndGetPort( - makeOptions({ canonicalMap, quietMs: 100, maxWaitMs: 10_000 }), - ) - await extract(port, 'src/bucket.tsx') - - const t0 = Date.now() - const res = await httpRequest( - port, - 'GET', - '/css?fileNum=1&importMainCss=true&waitForIdle=true', - ) - const elapsed = Date.now() - t0 - - expect(res.status).toBe(200) - expect(res.body).toBe('bucket-css') - // Resolved by the quiet exit (~100ms), NOT the 10s wall-clock backstop. - expect(elapsed).toBeLessThan(5000) - expect(infoSpy).toHaveBeenCalled() - expect(warnSpy).not.toHaveBeenCalled() + try { + const warnSpy = spyOn(console, 'warn').mockReturnValue(undefined) + try { + const canonicalMap = { 'src/phantom.tsx': 'src/bucket.tsx' } + const { coordinator, port } = await startAndGetPort( + makeOptions({ canonicalMap, quietMs: 100, maxWaitMs: 10_000 }), + ) + await extract(port, 'src/bucket.tsx') - infoSpy.mockRestore() - warnSpy.mockRestore() - coordinator.close() + const t0 = Date.now() + const res = await httpRequest( + port, + 'GET', + '/css?fileNum=1&importMainCss=true&waitForIdle=true', + ) + const elapsed = Date.now() - t0 + + expect(res.status).toBe(200) + expect(res.body).toBe('bucket-css') + // Resolved by the quiet exit (~100ms), NOT the 10s wall-clock backstop. + expect(elapsed).toBeLessThan(5000) + expect(infoSpy).toHaveBeenCalled() + expect(warnSpy).not.toHaveBeenCalled() + + coordinator.close() + } finally { + warnSpy.mockRestore() + } + } finally { + infoSpy.mockRestore() + } }) // T8: the same legacy quiet fallback applies to an expected base file when diff --git a/packages/next-plugin/src/__tests__/loader.test.ts b/packages/next-plugin/src/__tests__/loader.test.ts index 3f4df0644..30954734e 100644 --- a/packages/next-plugin/src/__tests__/loader.test.ts +++ b/packages/next-plugin/src/__tests__/loader.test.ts @@ -504,52 +504,57 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))( - Buffer.from('source code'), - 'src/App.tsx', - ) + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith( - null, - 'coordinator code', - '{"version":3}', + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('source code'), + 'src/App.tsx', ) - }) - - expect(t.addDependency).toHaveBeenCalledWith(resolve('src/tokens.ts')) - expect(t.addDependency).not.toHaveBeenCalledWith(resolve('1')) - // Verify HTTP request was made - expect(requestSpy).toHaveBeenCalledTimes(1) - const reqOptions = requestSpy.mock.calls[0]![0] as Record - expect(reqOptions.hostname).toBe('127.0.0.1') - expect(reqOptions.port).toBe(12345) - expect(reqOptions.path).toBe('/extract') - expect(reqOptions.method).toBe('POST') + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith( + null, + 'coordinator code', + '{"version":3}', + ) + }) - // Verify NO WASM functions were called - expect(codeExtractSpy).not.toHaveBeenCalled() + expect(t.addDependency).toHaveBeenCalledWith(resolve('src/tokens.ts')) + expect(t.addDependency).not.toHaveBeenCalledWith(resolve('1')) - requestSpy.mockRestore() + // Verify HTTP request was made + expect(requestSpy).toHaveBeenCalledTimes(1) + const reqOptions = requestSpy.mock.calls[0]![0] as Record< + string, + unknown + > + expect(reqOptions.hostname).toBe('127.0.0.1') + expect(reqOptions.port).toBe(12345) + expect(reqOptions.path).toBe('/extract') + expect(reqOptions.method).toBe('POST') + + // Verify NO WASM functions were called + expect(codeExtractSpy).not.toHaveBeenCalled() + } finally { + requestSpy.mockRestore() + } }) it('should handle coordinator HTTP error', async () => { @@ -573,34 +578,36 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))( - Buffer.from('source code'), - 'src/App.tsx', - ) + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith(expect.any(Error)) - }) + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('source code'), + 'src/App.tsx', + ) - requestSpy.mockRestore() + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith(expect.any(Error)) + }) + } finally { + requestSpy.mockRestore() + } }) it('should handle coordinator non-200 response', async () => { @@ -630,36 +637,38 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))( - Buffer.from('source code'), - 'src/App.tsx', - ) + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith( - new Error('extraction failed on server'), + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('source code'), + 'src/App.tsx', ) - }) - requestSpy.mockRestore() + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith( + new Error('extraction failed on server'), + ) + }) + } finally { + requestSpy.mockRestore() + } }) it('should handle non-object coordinator response', async () => { @@ -685,36 +694,38 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))( - Buffer.from('source code'), - 'src/App.tsx', - ) + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith( - new Error('Coordinator response missing code'), + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('source code'), + 'src/App.tsx', ) - }) - requestSpy.mockRestore() + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith( + new Error('Coordinator response missing code'), + ) + }) + } finally { + requestSpy.mockRestore() + } }) it('should handle coordinator response without code', async () => { @@ -741,36 +752,38 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))( - Buffer.from('source code'), - 'src/App.tsx', - ) + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith( - new Error('Coordinator response missing code'), + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('source code'), + 'src/App.tsx', ) - }) - requestSpy.mockRestore() + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith( + new Error('Coordinator response missing code'), + ) + }) + } finally { + requestSpy.mockRestore() + } }) it('should handle malformed coordinator response', async () => { @@ -796,34 +809,36 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))( - Buffer.from('source code'), - 'src/App.tsx', - ) + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith(expect.any(Error)) - }) + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('source code'), + 'src/App.tsx', + ) - requestSpy.mockRestore() + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith(expect.any(Error)) + }) + } finally { + requestSpy.mockRestore() + } }) it('should retry and error when coordinatorPortFile never appears', async () => { @@ -902,36 +917,42 @@ describe('devupUILoader', () => { }, ) - const asyncCallback = mock() - const t = { - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - }), - async: mock().mockReturnValue(asyncCallback), - resourcePath: join(process.cwd(), 'src', 'App.tsx'), - addDependency: mock(), - } - - devupUILoader.bind(asLoaderContext(t))(Buffer.from('code'), 'src/App.tsx') + try { + const asyncCallback = mock() + const t = { + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + }), + async: mock().mockReturnValue(asyncCallback), + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + addDependency: mock(), + } - await waitFor(() => { - expect(asyncCallback).toHaveBeenCalledWith( - null, - 'coordinator code', - null, + devupUILoader.bind(asLoaderContext(t))( + Buffer.from('code'), + 'src/App.tsx', ) - }) - expect(requestSpy).toHaveBeenCalledTimes(1) - requestSpy.mockRestore() + await waitFor(() => { + expect(asyncCallback).toHaveBeenCalledWith( + null, + 'coordinator code', + null, + ) + }) + + expect(requestSpy).toHaveBeenCalledTimes(1) + } finally { + requestSpy.mockRestore() + } }) it('should handle error when reading coordinator port file fails', async () => { @@ -1003,49 +1024,51 @@ describe('devupUILoader', () => { }, ) - const makeContext = () => ({ - getOptions: () => ({ - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - themeFile: 'themeFile', - watch: true, - singleCss: true, - coordinatorPortFile: 'coordinator.port', - }), - async: mock().mockReturnValue(mock()), - resourcePath: join(process.cwd(), 'src', 'test.tsx'), - addDependency: mock(), - }) - - // First call reads port from file - devupUILoader.bind(asLoaderContext(makeContext()))( - Buffer.from('code'), - 'test.tsx', - ) - await waitFor(() => { - expect(requestSpy).toHaveBeenCalledTimes(1) - }) - - // Second call should use cached port (readFileSync called only once for port) - devupUILoader.bind(asLoaderContext(makeContext()))( - Buffer.from('code'), - 'test.tsx', - ) - await waitFor(() => { - expect(requestSpy).toHaveBeenCalledTimes(2) - }) - - // readFileSync should only be called once for the port file - // (existsSync is called each time, but readFileSync for port file only once due to caching) - const portReads = readFileSyncSpy.mock.calls.filter( - (call: unknown[]) => call[0] === 'coordinator.port', - ) - expect(portReads.length).toBe(1) - - requestSpy.mockRestore() + try { + const makeContext = () => ({ + getOptions: () => ({ + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + themeFile: 'themeFile', + watch: true, + singleCss: true, + coordinatorPortFile: 'coordinator.port', + }), + async: mock().mockReturnValue(mock()), + resourcePath: join(process.cwd(), 'src', 'test.tsx'), + addDependency: mock(), + }) + + // First call reads port from file + devupUILoader.bind(asLoaderContext(makeContext()))( + Buffer.from('code'), + 'test.tsx', + ) + await waitFor(() => { + expect(requestSpy).toHaveBeenCalledTimes(1) + }) + + // Second call should use cached port (readFileSync called only once for port) + devupUILoader.bind(asLoaderContext(makeContext()))( + Buffer.from('code'), + 'test.tsx', + ) + await waitFor(() => { + expect(requestSpy).toHaveBeenCalledTimes(2) + }) + + // readFileSync should only be called once for the port file + // (existsSync is called each time, but readFileSync for port file only once due to caching) + const portReads = readFileSyncSpy.mock.calls.filter( + (call: unknown[]) => call[0] === 'coordinator.port', + ) + expect(portReads.length).toBe(1) + } finally { + requestSpy.mockRestore() + } }) }) }) diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index 8bc599b7b..a58ca74c8 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -91,6 +91,7 @@ let devupUIWebpackPluginSpy: ReturnType let startCoordinatorSpy: ReturnType let originalEnv: NodeJS.ProcessEnv +let originalEnvValues: NodeJS.ProcessEnv let originalFetch: typeof global.fetch let originalDebugPort: number @@ -145,7 +146,8 @@ beforeEach(() => { setWasmForTesting(wasm) setWebpackPluginForTesting(webpackPluginModule) - originalEnv = { ...process.env } + originalEnv = process.env + originalEnvValues = { ...originalEnv } originalFetch = global.fetch originalDebugPort = process.debugPort global.fetch = mock(() => Promise.resolve({} as Response)) @@ -155,6 +157,10 @@ afterEach(() => { resetTurboSetupCacheForTesting() setWasmForTesting(undefined) setWebpackPluginForTesting(undefined) + for (const key of Object.keys(originalEnv)) { + if (!Object.hasOwn(originalEnvValues, key)) delete originalEnv[key] + } + Object.assign(originalEnv, originalEnvValues) process.env = originalEnv global.fetch = originalFetch process.debugPort = originalDebugPort @@ -865,49 +871,51 @@ export const box = style({ color: 'red' })` }, ) - DevupUI({}) - - // Verify coordinator was started with correct options - expect(startCoordinatorSpy).toHaveBeenCalledWith({ - wasm, - package: '@devup-ui/react', - cssDir: resolve('df', 'devup-ui'), - singleCss: false, - sheetFile: join('df', 'sheet.json'), - classMapFile: join('df', 'classMap.json'), - fileMapFile: join('df', 'fileMap.json'), - importAliases: { - '@emotion/react': null, - '@emotion/styled': 'styled', - '@vanilla-extract/css': null, - 'styled-components': 'styled', - }, - coordinatorPortFile: join('df', 'coordinator.port'), - canonicalMap: expect.any(Object), - expectedBaseFiles: expect.any(Array), - prewarmedFiles: [], - prewarmedOutputs: expect.any(Map), - sourceMap: true, - }) - expect(codeExtractSpy).not.toHaveBeenCalled() - expect(codeExtractWithoutSourceMapSpy).not.toHaveBeenCalled() + try { + DevupUI({}) - // Verify initial CSS file is written - expect(writeFileSyncSpy).toHaveBeenCalledWith( - join(resolve('df', 'devup-ui'), 'devup-ui.css'), - '', - ) + // Verify coordinator was started with correct options + expect(startCoordinatorSpy).toHaveBeenCalledWith({ + wasm, + package: '@devup-ui/react', + cssDir: resolve('df', 'devup-ui'), + singleCss: false, + sheetFile: join('df', 'sheet.json'), + classMapFile: join('df', 'classMap.json'), + fileMapFile: join('df', 'fileMap.json'), + importAliases: { + '@emotion/react': null, + '@emotion/styled': 'styled', + '@vanilla-extract/css': null, + 'styled-components': 'styled', + }, + coordinatorPortFile: join('df', 'coordinator.port'), + canonicalMap: expect.any(Object), + expectedBaseFiles: expect.any(Array), + prewarmedFiles: [], + prewarmedOutputs: expect.any(Map), + sourceMap: true, + }) + expect(codeExtractSpy).not.toHaveBeenCalled() + expect(codeExtractWithoutSourceMapSpy).not.toHaveBeenCalled() - // Verify exit handler was registered and calls coordinator.close() - expect(exitHandlers.length).toBeGreaterThan(0) - exitHandlers[0]!() - expect(closeMock).toHaveBeenCalledTimes(1) + // Verify initial CSS file is written + expect(writeFileSyncSpy).toHaveBeenCalledWith( + join(resolve('df', 'devup-ui'), 'devup-ui.css'), + '', + ) - // Verify --inspect-brk env vars are NOT set - expect(process.env.TURBOPACK_DEBUG_JS).toBeUndefined() - expect(process.env.NODE_OPTIONS ?? '').not.toContain('--inspect-brk') + // Verify exit handler was registered and calls coordinator.close() + expect(exitHandlers.length).toBeGreaterThan(0) + exitHandlers[0]!() + expect(closeMock).toHaveBeenCalledTimes(1) - processOnSpy.mockRestore() + // Verify --inspect-brk env vars are NOT set + expect(process.env.TURBOPACK_DEBUG_JS).toBeUndefined() + expect(process.env.NODE_OPTIONS ?? '').not.toContain('--inspect-brk') + } finally { + processOnSpy.mockRestore() + } }) it('hands the coordinator the full compiled-file set, not the static-only route map', () => { diff --git a/packages/next-plugin/src/__tests__/profile.test.ts b/packages/next-plugin/src/__tests__/profile.test.ts index b2e902e54..a63cd8cf8 100644 --- a/packages/next-plugin/src/__tests__/profile.test.ts +++ b/packages/next-plugin/src/__tests__/profile.test.ts @@ -8,12 +8,18 @@ import { } from '../profile' let originalEnv: NodeJS.ProcessEnv +let originalEnvValues: NodeJS.ProcessEnv beforeEach(() => { - originalEnv = { ...process.env } + originalEnv = process.env + originalEnvValues = { ...originalEnv } }) afterEach(() => { + for (const key of Object.keys(originalEnv)) { + if (!Object.hasOwn(originalEnvValues, key)) delete originalEnv[key] + } + Object.assign(originalEnv, originalEnvValues) process.env = originalEnv }) @@ -22,22 +28,28 @@ describe('profile', () => { delete process.env.DEVUP_UI_PROFILE const consoleSpy = spyOn(console, 'info').mockImplementation(() => {}) - expect(isProfileEnabled()).toBe(false) - reportProfile('next.prewarm') - expect(consoleSpy).not.toHaveBeenCalled() - consoleSpy.mockRestore() + try { + expect(isProfileEnabled()).toBe(false) + reportProfile('next.prewarm') + expect(consoleSpy).not.toHaveBeenCalled() + } finally { + consoleSpy.mockRestore() + } }) it('reports structured measurements when enabled', () => { process.env.DEVUP_UI_PROFILE = '1' const consoleSpy = spyOn(console, 'info').mockImplementation(() => {}) - reportProfile('next.prewarm', { durationMs: 12.34, files: 2 }) + try { + reportProfile('next.prewarm', { durationMs: 12.34, files: 2 }) - expect(consoleSpy).toHaveBeenCalledWith( - '[devup-ui:profile] {"phase":"next.prewarm","durationMs":12.34,"files":2}', - ) - consoleSpy.mockRestore() + expect(consoleSpy).toHaveBeenCalledWith( + '[devup-ui:profile] {"phase":"next.prewarm","durationMs":12.34,"files":2}', + ) + } finally { + consoleSpy.mockRestore() + } }) it('returns a non-negative elapsed duration', () => { diff --git a/packages/plugin-utils/src/import-graph.test.ts b/packages/plugin-utils/src/import-graph.test.ts index d99a4fca4..9736af4d1 100644 --- a/packages/plugin-utils/src/import-graph.test.ts +++ b/packages/plugin-utils/src/import-graph.test.ts @@ -1291,29 +1291,34 @@ describe('runImportGraphCli', () => { it('prints usage and exits when the srcDir arg is missing', () => { const errorSpy = spyOn(console, 'error').mockReturnValue(undefined) - const exitSpy = spyOn(process, 'exit').mockImplementation( - (() => undefined) as never, - ) - - runImportGraphCli([]) - - expect(errorSpy).toHaveBeenCalled() - expect(exitSpy).toHaveBeenCalledWith(1) - - errorSpy.mockRestore() - exitSpy.mockRestore() + try { + const exitSpy = spyOn(process, 'exit').mockImplementation( + (() => undefined) as never, + ) + try { + runImportGraphCli([]) + + expect(errorSpy).toHaveBeenCalled() + expect(exitSpy).toHaveBeenCalledWith(1) + } finally { + exitSpy.mockRestore() + } + } finally { + errorSpy.mockRestore() + } }) it('prints the canonical map JSON to stdout when no outFile is given', () => { const { root, cwd } = makeProject() const infoSpy = spyOn(console, 'info').mockReturnValue(undefined) + try { + runImportGraphCli(['src', cwd]) - runImportGraphCli(['src', cwd]) - - const printed = (infoSpy.mock.calls[0] as [string])[0] - expect(printed).toContain('src/b.tsx') - - infoSpy.mockRestore() + const printed = (infoSpy.mock.calls[0] as [string])[0] + expect(printed).toContain('src/b.tsx') + } finally { + infoSpy.mockRestore() + } rmSync(root, { recursive: true, force: true }) }) @@ -1334,13 +1339,14 @@ describe('runImportGraphCli', () => { it('defaults cwd to process.cwd() when only srcDir is given', () => { const infoSpy = spyOn(console, 'info').mockReturnValue(undefined) + try { + // A non-existent srcDir -> empty map -> prints "{}" without touching files. + runImportGraphCli(['__devup_nonexistent_src__']) - // A non-existent srcDir -> empty map -> prints "{}" without touching files. - runImportGraphCli(['__devup_nonexistent_src__']) - - expect(infoSpy).toHaveBeenCalledWith('{}') - - infoSpy.mockRestore() + expect(infoSpy).toHaveBeenCalledWith('{}') + } finally { + infoSpy.mockRestore() + } }) }) describe('createModuleResolver', () => { diff --git a/packages/react/src/hooks/__tests__/use-safe-effect.test.ts b/packages/react/src/hooks/__tests__/use-safe-effect.test.ts index 71943f517..3830a364f 100644 --- a/packages/react/src/hooks/__tests__/use-safe-effect.test.ts +++ b/packages/react/src/hooks/__tests__/use-safe-effect.test.ts @@ -1,11 +1,22 @@ -import { afterAll, describe, expect, it } from 'bun:test' +import { afterEach, beforeEach, describe, expect, it } from 'bun:test' import { useEffect, useLayoutEffect } from 'react' describe('useSafeEffect', () => { - const originalWindow = globalThis.window + let originalWindowDescriptor: PropertyDescriptor | undefined - afterAll(() => { - globalThis.window = originalWindow + beforeEach(() => { + originalWindowDescriptor = Object.getOwnPropertyDescriptor( + globalThis, + 'window', + ) + }) + + afterEach(() => { + if (originalWindowDescriptor) { + Object.defineProperty(globalThis, 'window', originalWindowDescriptor) + } else { + Reflect.deleteProperty(globalThis, 'window') + } }) // The module picks its effect at evaluation time, so each case needs a fresh diff --git a/packages/react/src/utils/__tests__/get-theme.browser.test.ts b/packages/react/src/utils/__tests__/get-theme.browser.test.ts index b8f07122b..5c741230f 100644 --- a/packages/react/src/utils/__tests__/get-theme.browser.test.ts +++ b/packages/react/src/utils/__tests__/get-theme.browser.test.ts @@ -4,7 +4,16 @@ import { getTheme } from '../get-theme' describe('getTheme', () => { it('should return theme', async () => { - document.documentElement.setAttribute('data-theme', 'dark') - expect(getTheme()).toBe('dark') + const originalTheme = document.documentElement.getAttribute('data-theme') + try { + document.documentElement.setAttribute('data-theme', 'dark') + expect(getTheme()).toBe('dark') + } finally { + if (originalTheme === null) { + document.documentElement.removeAttribute('data-theme') + } else { + document.documentElement.setAttribute('data-theme', originalTheme) + } + } }) }) diff --git a/packages/react/src/utils/__tests__/init-theme.browser.test.ts b/packages/react/src/utils/__tests__/init-theme.browser.test.ts index 9b7face1b..ac56b670f 100644 --- a/packages/react/src/utils/__tests__/init-theme.browser.test.ts +++ b/packages/react/src/utils/__tests__/init-theme.browser.test.ts @@ -1,29 +1,42 @@ -import { - afterAll, - beforeEach, - describe, - expect, - it, - mock, - spyOn, -} from 'bun:test' +import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test' import { initTheme } from '../init-theme' -afterAll(() => { - mock.restore() -}) +let originalTheme: string | null +let originalSelection: string | null + beforeEach(() => { + originalTheme = document.documentElement.getAttribute('data-theme') + originalSelection = localStorage.getItem('__DF_THEME_SELECTED__') localStorage.removeItem('__DF_THEME_SELECTED__') }) +afterEach(() => { + if (originalTheme === null) { + document.documentElement.removeAttribute('data-theme') + } else { + document.documentElement.setAttribute('data-theme', originalTheme) + } + if (originalSelection === null) { + localStorage.removeItem('__DF_THEME_SELECTED__') + } else { + localStorage.setItem('__DF_THEME_SELECTED__', originalSelection) + } +}) + describe('initTheme', () => { it('should initialize the theme', () => { - spyOn(window, 'matchMedia').mockReturnValue({ + const matchMediaSpy = spyOn(window, 'matchMedia').mockReturnValue({ matches: false, } as MediaQueryList) - initTheme() - expect(document.documentElement.getAttribute('data-theme')).toBe('default') + try { + initTheme() + expect(document.documentElement.getAttribute('data-theme')).toBe( + 'default', + ) + } finally { + matchMediaSpy.mockRestore() + } }) it('should initialize the theme with the given theme', () => { initTheme(false, 'light') @@ -31,11 +44,14 @@ describe('initTheme', () => { }) it('should initialize the theme with the default theme', () => { - spyOn(window, 'matchMedia').mockReturnValue({ + const matchMediaSpy = spyOn(window, 'matchMedia').mockReturnValue({ matches: true, } as MediaQueryList) - - initTheme(true) - expect(document.documentElement.getAttribute('data-theme')).toBe('dark') + try { + initTheme(true) + expect(document.documentElement.getAttribute('data-theme')).toBe('dark') + } finally { + matchMediaSpy.mockRestore() + } }) }) diff --git a/packages/rsbuild-plugin/src/__tests__/plugin.test.ts b/packages/rsbuild-plugin/src/__tests__/plugin.test.ts index 1a4393a97..74a051255 100644 --- a/packages/rsbuild-plugin/src/__tests__/plugin.test.ts +++ b/packages/rsbuild-plugin/src/__tests__/plugin.test.ts @@ -110,27 +110,30 @@ describe('DevupUIRsbuildPlugin', () => { wasm, 'setModuleResolver', ).mockReturnValue(undefined) - const plugin = DevupUI() - const transform = mock() - await plugin.setup(createSetupContext({ transform })) - const resolveModule = setModuleResolverSpy.mock.calls.at(-1)?.[0] as ( - specifier: string, - importer: string, - ) => { path: string } | undefined - expect(resolveModule('./plugin.test', import.meta.path)?.path).toBe( - import.meta.path, - ) - codeExtractSpy.mockReturnValue( - createCodeExtractResult({ dependencies: ['/src/tokens.ts'] }), - ) - const addDependency = mock() - await transform.mock.calls[1][1]({ - code: "import { Box } from '@devup-ui/react'", - resourcePath: 'src/App.tsx', - addDependency, - }) - expect(addDependency).toHaveBeenCalledWith('/src/tokens.ts') - setModuleResolverSpy.mockRestore() + try { + const plugin = DevupUI() + const transform = mock() + await plugin.setup(createSetupContext({ transform })) + const resolveModule = setModuleResolverSpy.mock.calls.at(-1)?.[0] as ( + specifier: string, + importer: string, + ) => { path: string } | undefined + expect(resolveModule('./plugin.test', import.meta.path)?.path).toBe( + import.meta.path, + ) + codeExtractSpy.mockReturnValue( + createCodeExtractResult({ dependencies: ['/src/tokens.ts'] }), + ) + const addDependency = mock() + await transform.mock.calls[1][1]({ + code: "import { Box } from '@devup-ui/react'", + resourcePath: 'src/App.tsx', + addDependency, + }) + expect(addDependency).toHaveBeenCalledWith('/src/tokens.ts') + } finally { + setModuleResolverSpy.mockRestore() + } }) it('should return a plugin object with correct name', async () => { @@ -192,26 +195,28 @@ describe('DevupUIRsbuildPlugin', () => { }) it('should error when write data files', async () => { - const originalConsoleError = console.error - console.error = mock() - readFileSpy.mockRejectedValueOnce('error') - existsSyncSpy.mockImplementation((path: string) => { - if (path === 'devup.json') return true - return false - }) - const plugin = DevupUI() - expect(plugin).toBeDefined() - expect(plugin.setup).toBeDefined() - const transform = mock() - const modifyRsbuildConfig = mock() - await plugin.setup( - createSetupContext({ - transform, - modifyRsbuildConfig, - }), - ) - expect(console.error).toHaveBeenCalledWith('error') - console.error = originalConsoleError + const consoleErrorSpy = spyOn(console, 'error').mockReturnValue(undefined) + try { + readFileSpy.mockRejectedValueOnce('error') + existsSyncSpy.mockImplementation((path: string) => { + if (path === 'devup.json') return true + return false + }) + const plugin = DevupUI() + expect(plugin).toBeDefined() + expect(plugin.setup).toBeDefined() + const transform = mock() + const modifyRsbuildConfig = mock() + await plugin.setup( + createSetupContext({ + transform, + modifyRsbuildConfig, + }), + ) + expect(console.error).toHaveBeenCalledWith('error') + } finally { + consoleErrorSpy.mockRestore() + } }) it('should not register css transform', async () => { @@ -265,16 +270,19 @@ describe('DevupUIRsbuildPlugin', () => { ) const getCssSpy = spyOn(wasm, 'getCss').mockReturnValue('file css') - expect( - transform.mock.calls[0][1]({ - code: '/* placeholder */', - resourcePath: resolve('df', 'devup-ui', 'devup-ui-1.css'), - environment: { name: 'web' }, - }), - ).toBe('file css') - // A file's stylesheet imports the shared base - expect(getCssSpy).toHaveBeenCalledWith(1, true) - getCssSpy.mockRestore() + try { + expect( + transform.mock.calls[0][1]({ + code: '/* placeholder */', + resourcePath: resolve('df', 'devup-ui', 'devup-ui-1.css'), + environment: { name: 'web' }, + }), + ).toBe('file css') + // A file's stylesheet imports the shared base + expect(getCssSpy).toHaveBeenCalledWith(1, true) + } finally { + getCssSpy.mockRestore() + } }) it('should transform code', async () => { const plugin = DevupUI() diff --git a/packages/vite-plugin/src/__tests__/plugin.test.ts b/packages/vite-plugin/src/__tests__/plugin.test.ts index 4e14b5fd8..b829c1938 100644 --- a/packages/vite-plugin/src/__tests__/plugin.test.ts +++ b/packages/vite-plugin/src/__tests__/plugin.test.ts @@ -186,6 +186,7 @@ let getThemeInterfaceSpy: ReturnType let registerThemeSpy: ReturnType let setDebugSpy: ReturnType let setPrefixSpy: ReturnType +let consoleErrorSpy: ReturnType beforeEach(() => { existsSyncSpy = spyOn(fs, 'existsSync').mockReturnValue(false) @@ -206,6 +207,7 @@ beforeEach(() => { registerThemeSpy = spyOn(wasm, 'registerTheme').mockReturnValue(undefined) setDebugSpy = spyOn(wasm, 'setDebug').mockReturnValue(undefined) setPrefixSpy = spyOn(wasm, 'setPrefix').mockReturnValue(undefined) + consoleErrorSpy = spyOn(console, 'error').mockReturnValue(undefined) }) afterEach(() => { @@ -221,11 +223,10 @@ afterEach(() => { registerThemeSpy.mockRestore() setDebugSpy.mockRestore() setPrefixSpy.mockRestore() + consoleErrorSpy.mockRestore() }) describe('devupUIVitePlugin', () => { - console.error = mock() - it('should apply default options', () => { const plugin = createPlugin({}) expect(plugin).toEqual({ @@ -1663,25 +1664,28 @@ describe('module resolver', () => { wasm, 'setModuleResolver', ).mockReturnValue(undefined) - codeExtractSpy.mockReturnValue( - createCodeExtractResult({ dependencies: ['/p/src/tokens.ts'] }), - ) - const plugin = createPlugin({}) - await plugin.configResolved({ root: '/p' }) - const resolveModule = setModuleResolverSpy.mock.calls[0][0] as ( - specifier: string, - importer: string, - ) => { path: string } | undefined - expect(resolveModule('./plugin.test', import.meta.path)?.path).toBe( - import.meta.path.replaceAll('\\', '/'), - ) - const addWatchFile = mock() - await plugin.transform.call( - { addWatchFile } as never, - 'code', - '/p/src/App.tsx', - ) - expect(addWatchFile).toHaveBeenCalledWith('/p/src/tokens.ts') - setModuleResolverSpy.mockRestore() + try { + codeExtractSpy.mockReturnValue( + createCodeExtractResult({ dependencies: ['/p/src/tokens.ts'] }), + ) + const plugin = createPlugin({}) + await plugin.configResolved({ root: '/p' }) + const resolveModule = setModuleResolverSpy.mock.calls[0][0] as ( + specifier: string, + importer: string, + ) => { path: string } | undefined + expect(resolveModule('./plugin.test', import.meta.path)?.path).toBe( + import.meta.path.replaceAll('\\', '/'), + ) + const addWatchFile = mock() + await plugin.transform.call( + { addWatchFile } as never, + 'code', + '/p/src/App.tsx', + ) + expect(addWatchFile).toHaveBeenCalledWith('/p/src/tokens.ts') + } finally { + setModuleResolverSpy.mockRestore() + } }) }) diff --git a/packages/webpack-plugin/src/__tests__/loader.test.ts b/packages/webpack-plugin/src/__tests__/loader.test.ts index 8697cb090..57de275aa 100644 --- a/packages/webpack-plugin/src/__tests__/loader.test.ts +++ b/packages/webpack-plugin/src/__tests__/loader.test.ts @@ -130,35 +130,40 @@ describe('devupUILoader', () => { wasm, 'setModuleResolver', ).mockReturnValue(undefined) - codeExtractSpy.mockReturnValue( - createCodeExtractResult({ dependencies: ['src/tokens.ts'] }), - ) - const callback = mock() - const t = createLoaderContext( - { - package: 'package', - cssDir: 'cssDir', - sheetFile: 'sheetFile', - classMapFile: 'classMapFile', - fileMapFile: 'fileMapFile', - watch: false, - singleCss: true, - }, - callback, - ) - devupUILoader.bind(t)(Buffer.from('code'), 'index.tsx') - await waitFor(() => expect(callback).toHaveBeenCalled()) - expect(t.addDependency).toHaveBeenCalledWith( - nodePath.resolve('src/tokens.ts'), - ) - const resolveModule = setModuleResolverSpy.mock.calls[0][0] as ( - specifier: string, - importer: string, - ) => { path: string } | undefined - expect(resolveModule('./loader.test', import.meta.path)?.path).toBe( - nodePath.relative(process.cwd(), import.meta.path).replaceAll('\\', '/'), - ) - setModuleResolverSpy.mockRestore() + try { + codeExtractSpy.mockReturnValue( + createCodeExtractResult({ dependencies: ['src/tokens.ts'] }), + ) + const callback = mock() + const t = createLoaderContext( + { + package: 'package', + cssDir: 'cssDir', + sheetFile: 'sheetFile', + classMapFile: 'classMapFile', + fileMapFile: 'fileMapFile', + watch: false, + singleCss: true, + }, + callback, + ) + devupUILoader.bind(t)(Buffer.from('code'), 'index.tsx') + await waitFor(() => expect(callback).toHaveBeenCalled()) + expect(t.addDependency).toHaveBeenCalledWith( + nodePath.resolve('src/tokens.ts'), + ) + const resolveModule = setModuleResolverSpy.mock.calls[0][0] as ( + specifier: string, + importer: string, + ) => { path: string } | undefined + expect(resolveModule('./loader.test', import.meta.path)?.path).toBe( + nodePath + .relative(process.cwd(), import.meta.path) + .replaceAll('\\', '/'), + ) + } finally { + setModuleResolverSpy.mockRestore() + } }) it.each( createTestMatrix({ diff --git a/packages/webpack-plugin/src/__tests__/plugin.test.ts b/packages/webpack-plugin/src/__tests__/plugin.test.ts index 524e755bd..ea86c14b8 100644 --- a/packages/webpack-plugin/src/__tests__/plugin.test.ts +++ b/packages/webpack-plugin/src/__tests__/plugin.test.ts @@ -85,6 +85,7 @@ let mkdirSpy: ReturnType let readFileSpy: ReturnType let statSpy: ReturnType let writeFileSpy: ReturnType +let consoleErrorSpy: ReturnType beforeEach(() => { codeExtractSpy = spyOn(wasm, 'codeExtract').mockImplementation( @@ -111,6 +112,7 @@ beforeEach(() => { readFileSpy = spyOn(fsPromises, 'readFile').mockResolvedValue('{}') statSpy = spyOn(fsPromises, 'stat').mockResolvedValue(createStats(0)) writeFileSpy = spyOn(fsPromises, 'writeFile').mockResolvedValue(undefined) + consoleErrorSpy = spyOn(console, 'error').mockReturnValue(undefined) }) afterEach(() => { @@ -133,6 +135,7 @@ afterEach(() => { readFileSpy.mockRestore() statSpy.mockRestore() writeFileSpy.mockRestore() + consoleErrorSpy.mockRestore() }) function createCompiler(): MockCompiler { @@ -171,8 +174,6 @@ function createCompiler(): MockCompiler { } describe('devupUIWebpackPlugin', () => { - console.error = mock() - it('should apply default options', () => { expect(new DevupUIWebpackPlugin({}).options).toEqual({ include: [], @@ -544,59 +545,62 @@ describe('devupUIWebpackPlugin', () => { wasm, 'setModuleResolver', ).mockReturnValue(undefined) - const plugin = new DevupUIWebpackPlugin({ - package: '@devup-ui/react', - singleCss: true, - }) - const compiler = createCompiler() - compiler.options.entry = { - main: { import: ['./src/parent', './src/other'] }, - worker: {}, + try { + const plugin = new DevupUIWebpackPlugin({ + package: '@devup-ui/react', + singleCss: true, + }) + const compiler = createCompiler() + compiler.options.entry = { + main: { import: ['./src/parent', './src/other'] }, + worker: {}, + } + compiler.options.context = '/project' + plugin.apply(asCompiler(compiler)) + expect(computeReachableFilesSpy).toHaveBeenCalledWith({ + srcDir: resolve(process.cwd(), 'src'), + tsconfigPath: resolve(process.cwd(), 'tsconfig.json'), + entries: [ + resolve('/project', './src/parent'), + resolve('/project', './src/other'), + ], + }) + // The loader's resolver is registered before the first extraction, so + // imported constants inline as they do in the loader + expect(setModuleResolverSpy.mock.invocationCallOrder[0]).toBeLessThan( + codeExtractSpy.mock.invocationCallOrder[0]!, + ) + const resolveModule = setModuleResolverSpy.mock.calls[0]![0] as ( + specifier: string, + importer: string, + ) => { path: string } | undefined + expect(resolveModule('./plugin.test', import.meta.path)?.path).toBe( + relative(process.cwd(), import.meta.path).replaceAll('\\', '/'), + ) + expect(codeExtractSpy).toHaveBeenCalledTimes(2) + expect(codeExtractSpy).toHaveBeenCalledWith( + 'src/parent.tsx', + 'source', + '@devup-ui/react', + expect.any(String), + true, + false, + true, + expect.anything(), + ) + expect(codeExtractSpy).toHaveBeenCalledWith( + 'src/child.tsx', + 'source', + '@devup-ui/react', + expect.any(String), + true, + false, + true, + expect.anything(), + ) + } finally { + setModuleResolverSpy.mockRestore() } - compiler.options.context = '/project' - plugin.apply(asCompiler(compiler)) - expect(computeReachableFilesSpy).toHaveBeenCalledWith({ - srcDir: resolve(process.cwd(), 'src'), - tsconfigPath: resolve(process.cwd(), 'tsconfig.json'), - entries: [ - resolve('/project', './src/parent'), - resolve('/project', './src/other'), - ], - }) - // The loader's resolver is registered before the first extraction, so - // imported constants inline as they do in the loader - expect(setModuleResolverSpy.mock.invocationCallOrder[0]).toBeLessThan( - codeExtractSpy.mock.invocationCallOrder[0]!, - ) - const resolveModule = setModuleResolverSpy.mock.calls[0]![0] as ( - specifier: string, - importer: string, - ) => { path: string } | undefined - expect(resolveModule('./plugin.test', import.meta.path)?.path).toBe( - relative(process.cwd(), import.meta.path).replaceAll('\\', '/'), - ) - setModuleResolverSpy.mockRestore() - expect(codeExtractSpy).toHaveBeenCalledTimes(2) - expect(codeExtractSpy).toHaveBeenCalledWith( - 'src/parent.tsx', - 'source', - '@devup-ui/react', - expect.any(String), - true, - false, - true, - expect.anything(), - ) - expect(codeExtractSpy).toHaveBeenCalledWith( - 'src/child.tsx', - 'source', - '@devup-ui/react', - expect.any(String), - true, - false, - true, - expect.anything(), - ) }) it('pre-warms without collapse, from no entries when they are a function', () => {