From fd05580e3e36a7cfc6a5283fca4d1e076956803e Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Fri, 11 Oct 2024 13:14:55 -0400 Subject: [PATCH 1/4] fix(vite): set `ssr.noExternal` even if not present in project package.json --- src/__tests__/utils.js | 2 + src/__tests__/vite-plugin.test.js | 186 ++++++++++++++++++++++++++++++ src/vite.js | 47 +++++++- vite.config.js | 1 + 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/vite-plugin.test.js diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index 68be33c..f637d6f 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -4,6 +4,8 @@ export const IS_JSDOM = window.navigator.userAgent.includes('jsdom') export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js +export const IS_JEST = Boolean(process.env.JEST_WORKER_ID) + export const IS_SVELTE_5 = SVELTE_VERSION >= '5' export const MODE_LEGACY = 'legacy' diff --git a/src/__tests__/vite-plugin.test.js b/src/__tests__/vite-plugin.test.js new file mode 100644 index 0000000..f87f713 --- /dev/null +++ b/src/__tests__/vite-plugin.test.js @@ -0,0 +1,186 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest' + +import { svelteTesting } from '../vite.js' +import { IS_JEST } from './utils.js' + +describe.skipIf(IS_JEST)('vite plugin', () => { + beforeEach(() => { + vi.stubEnv('VITEST', '1') + }) + + test('does not modify config if disabled', () => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: false, + }) + + const config = {} + subject.config(config) + + expect(config).toEqual({}) + }) + + test('does not modify config if not Vitest', () => { + vi.stubEnv('VITEST', '') + + const subject = svelteTesting() + const config = {} + + subject.config(config) + + expect(config).toEqual({}) + }) + + test.each([ + { + config: { resolve: { conditions: ['node'] } }, + expectedConditions: ['browser', 'node'], + }, + { + config: { resolve: { conditions: ['svelte', 'node'] } }, + expectedConditions: ['svelte', 'browser', 'node'], + }, + ])( + 'adds browser condition if necessary', + ({ config, expectedConditions }) => { + const subject = svelteTesting() + const viteConfig = structuredClone(config) + + subject.config(viteConfig) + + expect(viteConfig).toMatchObject({ + resolve: { + conditions: expectedConditions, + }, + }) + } + ) + + test.each([ + { + config: {}, + expectedConditions: [], + }, + { + config: { resolve: { conditions: [] } }, + expectedConditions: [], + }, + { + config: { resolve: { conditions: ['svelte'] } }, + expectedConditions: ['svelte'], + }, + ])( + 'skips browser condition if possible', + ({ config, expectedConditions }) => { + const subject = svelteTesting() + const viteConfig = structuredClone(config) + + subject.config(viteConfig) + + expect(viteConfig).toMatchObject({ + resolve: { + conditions: expectedConditions, + }, + }) + } + ) + + test.each([ + { + config: {}, + expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], + }, + { + config: { test: { setupFiles: [] } }, + expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], + }, + { + config: { test: { setupFiles: 'other-file.js' } }, + expectedSetupFiles: [ + 'other-file.js', + expect.stringMatching(/src\/vitest.js$/u), + ], + }, + ])('adds cleanup', ({ config, expectedSetupFiles }) => { + const subject = svelteTesting() + const viteConfig = structuredClone(config) + + subject.config(viteConfig) + + expect(viteConfig).toMatchObject({ + test: { + setupFiles: expectedSetupFiles, + }, + }) + }) + + test.each([ + { + config: { ssr: { noExternal: [] } }, + expectedNoExternal: ['@testing-library/svelte'], + }, + { + config: {}, + expectedNoExternal: ['@testing-library/svelte'], + }, + { + config: { ssr: { noExternal: 'other-file.js' } }, + expectedNoExternal: ['other-file.js', '@testing-library/svelte'], + }, + { + config: { ssr: { noExternal: /other/u } }, + expectedNoExternal: [/other/u, '@testing-library/svelte'], + }, + ])('adds noExternal rule', ({ config, expectedNoExternal }) => { + const subject = svelteTesting() + const viteConfig = structuredClone(config) + + subject.config(viteConfig) + + expect(viteConfig).toMatchObject({ + ssr: { + noExternal: expectedNoExternal, + }, + }) + }) + + test.each([ + { + config: { ssr: { noExternal: true } }, + expectedNoExternal: true, + }, + { + config: { ssr: { noExternal: '@testing-library/svelte' } }, + expectedNoExternal: '@testing-library/svelte', + }, + { + config: { ssr: { noExternal: /svelte/u } }, + expectedNoExternal: /svelte/u, + }, + ])('skips noExternal if able', ({ config, expectedNoExternal }) => { + const subject = svelteTesting() + const viteConfig = structuredClone(config) + + subject.config(viteConfig) + + expect(viteConfig).toMatchObject({ + ssr: { + noExternal: expectedNoExternal, + }, + }) + }) + + test('bails on noExternal if input is unexpected', () => { + const subject = svelteTesting() + const viteConfig = structuredClone({ ssr: { noExternal: false } }) + + subject.config(viteConfig) + + expect(viteConfig).toMatchObject({ + ssr: { + noExternal: false, + }, + }) + }) +}) diff --git a/src/vite.js b/src/vite.js index 0062b89..d8b8e20 100644 --- a/src/vite.js +++ b/src/vite.js @@ -7,12 +7,13 @@ import { fileURLToPath } from 'node:url' * Ensures Svelte is imported correctly in tests * and that the DOM is cleaned up after each test. * - * @param {{resolveBrowser?: boolean, autoCleanup?: boolean}} options + * @param {{resolveBrowser?: boolean, autoCleanup?: boolean, noExternal?: boolean}} options * @returns {import('vite').Plugin} */ export const svelteTesting = ({ resolveBrowser = true, autoCleanup = true, + noExternal = true, } = {}) => ({ name: 'vite-plugin-svelte-testing-library', config: (config) => { @@ -27,6 +28,10 @@ export const svelteTesting = ({ if (autoCleanup) { addAutoCleanup(config) } + + if (noExternal) { + addNoExternal(config) + } }, }) @@ -73,3 +78,43 @@ const addAutoCleanup = (config) => { test.setupFiles = setupFiles config.test = test } + +/** + * Add `@testing-library/svelte` to Vite's noExternal rules, if not present. + * + * This ensures `@testing-library/svelte` is processed by `@sveltejs/vite-plugin-svelte` + * in certain monorepo setups. + */ +const addNoExternal = (config) => { + const ssr = config.ssr ?? {} + let noExternal = ssr.noExternal ?? [] + + if (noExternal === true) { + return + } + + if (typeof noExternal === 'string' || noExternal instanceof RegExp) { + noExternal = [noExternal] + } + + if (!Array.isArray(noExternal)) { + return + } + + for (const rule of noExternal) { + if (typeof rule === 'string' && rule === '@testing-library/svelte') { + return + } + + if ( + noExternal instanceof RegExp && + noExternal.test('@testing-library/svelte') + ) { + return + } + } + + noExternal.push('@testing-library/svelte') + ssr.noExternal = noExternal + config.ssr = ssr +} diff --git a/vite.config.js b/vite.config.js index 76baf61..1ddeea5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -11,6 +11,7 @@ export default defineConfig({ setupFiles: ['./src/__tests__/_vitest-setup.js'], mockReset: true, unstubGlobals: true, + unstubEnvs: true, coverage: { provider: 'v8', include: ['src/**/*'], From 1be6f67ed999a156a65f467f1db1eae278b64fed Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Fri, 11 Oct 2024 13:32:27 -0400 Subject: [PATCH 2/4] fixup: deepClone from vitest --- src/__tests__/vite-plugin.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/__tests__/vite-plugin.test.js b/src/__tests__/vite-plugin.test.js index f87f713..7ba3e3f 100644 --- a/src/__tests__/vite-plugin.test.js +++ b/src/__tests__/vite-plugin.test.js @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest' +import { beforeEach, deepClone, describe, expect, test, vi } from 'vitest' import { svelteTesting } from '../vite.js' import { IS_JEST } from './utils.js' @@ -45,7 +45,7 @@ describe.skipIf(IS_JEST)('vite plugin', () => { 'adds browser condition if necessary', ({ config, expectedConditions }) => { const subject = svelteTesting() - const viteConfig = structuredClone(config) + const viteConfig = deepClone(config) subject.config(viteConfig) @@ -74,7 +74,7 @@ describe.skipIf(IS_JEST)('vite plugin', () => { 'skips browser condition if possible', ({ config, expectedConditions }) => { const subject = svelteTesting() - const viteConfig = structuredClone(config) + const viteConfig = deepClone(config) subject.config(viteConfig) @@ -104,7 +104,7 @@ describe.skipIf(IS_JEST)('vite plugin', () => { }, ])('adds cleanup', ({ config, expectedSetupFiles }) => { const subject = svelteTesting() - const viteConfig = structuredClone(config) + const viteConfig = deepClone(config) subject.config(viteConfig) @@ -134,7 +134,7 @@ describe.skipIf(IS_JEST)('vite plugin', () => { }, ])('adds noExternal rule', ({ config, expectedNoExternal }) => { const subject = svelteTesting() - const viteConfig = structuredClone(config) + const viteConfig = deepClone(config) subject.config(viteConfig) @@ -160,7 +160,7 @@ describe.skipIf(IS_JEST)('vite plugin', () => { }, ])('skips noExternal if able', ({ config, expectedNoExternal }) => { const subject = svelteTesting() - const viteConfig = structuredClone(config) + const viteConfig = deepClone(config) subject.config(viteConfig) @@ -173,7 +173,7 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test('bails on noExternal if input is unexpected', () => { const subject = svelteTesting() - const viteConfig = structuredClone({ ssr: { noExternal: false } }) + const viteConfig = deepClone({ ssr: { noExternal: false } }) subject.config(viteConfig) From f435ff5c8f7a4df3a2e98b259acc3000bb3e5b51 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Fri, 11 Oct 2024 14:03:47 -0400 Subject: [PATCH 3/4] fixup: silliness --- src/__tests__/vite-plugin.test.js | 80 +++++++++++++++---------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/__tests__/vite-plugin.test.js b/src/__tests__/vite-plugin.test.js index 7ba3e3f..1d89ae8 100644 --- a/src/__tests__/vite-plugin.test.js +++ b/src/__tests__/vite-plugin.test.js @@ -1,4 +1,4 @@ -import { beforeEach, deepClone, describe, expect, test, vi } from 'vitest' +import { beforeEach, describe, expect, test, vi } from 'vitest' import { svelteTesting } from '../vite.js' import { IS_JEST } from './utils.js' @@ -15,41 +15,41 @@ describe.skipIf(IS_JEST)('vite plugin', () => { noExternal: false, }) - const config = {} - subject.config(config) + const result = {} + subject.config(result) - expect(config).toEqual({}) + expect(result).toEqual({}) }) test('does not modify config if not Vitest', () => { vi.stubEnv('VITEST', '') const subject = svelteTesting() - const config = {} - subject.config(config) + const result = {} + subject.config(result) - expect(config).toEqual({}) + expect(result).toEqual({}) }) test.each([ { - config: { resolve: { conditions: ['node'] } }, + config: () => ({ resolve: { conditions: ['node'] } }), expectedConditions: ['browser', 'node'], }, { - config: { resolve: { conditions: ['svelte', 'node'] } }, + config: () => ({ resolve: { conditions: ['svelte', 'node'] } }), expectedConditions: ['svelte', 'browser', 'node'], }, ])( 'adds browser condition if necessary', ({ config, expectedConditions }) => { const subject = svelteTesting() - const viteConfig = deepClone(config) - subject.config(viteConfig) + const result = config() + subject.config(result) - expect(viteConfig).toMatchObject({ + expect(result).toMatchObject({ resolve: { conditions: expectedConditions, }, @@ -59,26 +59,26 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test.each([ { - config: {}, + config: () => ({}), expectedConditions: [], }, { - config: { resolve: { conditions: [] } }, + config: () => ({ resolve: { conditions: [] } }), expectedConditions: [], }, { - config: { resolve: { conditions: ['svelte'] } }, + config: () => ({ resolve: { conditions: ['svelte'] } }), expectedConditions: ['svelte'], }, ])( 'skips browser condition if possible', ({ config, expectedConditions }) => { const subject = svelteTesting() - const viteConfig = deepClone(config) - subject.config(viteConfig) + const result = config() + subject.config(result) - expect(viteConfig).toMatchObject({ + expect(result).toMatchObject({ resolve: { conditions: expectedConditions, }, @@ -88,15 +88,15 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test.each([ { - config: {}, + config: () => ({}), expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], }, { - config: { test: { setupFiles: [] } }, + config: () => ({ test: { setupFiles: [] } }), expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], }, { - config: { test: { setupFiles: 'other-file.js' } }, + config: () => ({ test: { setupFiles: 'other-file.js' } }), expectedSetupFiles: [ 'other-file.js', expect.stringMatching(/src\/vitest.js$/u), @@ -104,11 +104,11 @@ describe.skipIf(IS_JEST)('vite plugin', () => { }, ])('adds cleanup', ({ config, expectedSetupFiles }) => { const subject = svelteTesting() - const viteConfig = deepClone(config) - subject.config(viteConfig) + const result = config() + subject.config(result) - expect(viteConfig).toMatchObject({ + expect(result).toMatchObject({ test: { setupFiles: expectedSetupFiles, }, @@ -117,28 +117,28 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test.each([ { - config: { ssr: { noExternal: [] } }, + config: () => ({ ssr: { noExternal: [] } }), expectedNoExternal: ['@testing-library/svelte'], }, { - config: {}, + config: () => ({}), expectedNoExternal: ['@testing-library/svelte'], }, { - config: { ssr: { noExternal: 'other-file.js' } }, + config: () => ({ ssr: { noExternal: 'other-file.js' } }), expectedNoExternal: ['other-file.js', '@testing-library/svelte'], }, { - config: { ssr: { noExternal: /other/u } }, + config: () => ({ ssr: { noExternal: /other/u } }), expectedNoExternal: [/other/u, '@testing-library/svelte'], }, ])('adds noExternal rule', ({ config, expectedNoExternal }) => { const subject = svelteTesting() - const viteConfig = deepClone(config) - subject.config(viteConfig) + const result = config() + subject.config(result) - expect(viteConfig).toMatchObject({ + expect(result).toMatchObject({ ssr: { noExternal: expectedNoExternal, }, @@ -147,24 +147,24 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test.each([ { - config: { ssr: { noExternal: true } }, + config: () => ({ ssr: { noExternal: true } }), expectedNoExternal: true, }, { - config: { ssr: { noExternal: '@testing-library/svelte' } }, + config: () => ({ ssr: { noExternal: '@testing-library/svelte' } }), expectedNoExternal: '@testing-library/svelte', }, { - config: { ssr: { noExternal: /svelte/u } }, + config: () => ({ ssr: { noExternal: /svelte/u } }), expectedNoExternal: /svelte/u, }, ])('skips noExternal if able', ({ config, expectedNoExternal }) => { const subject = svelteTesting() - const viteConfig = deepClone(config) - subject.config(viteConfig) + const result = config() + subject.config(result) - expect(viteConfig).toMatchObject({ + expect(result).toMatchObject({ ssr: { noExternal: expectedNoExternal, }, @@ -173,11 +173,11 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test('bails on noExternal if input is unexpected', () => { const subject = svelteTesting() - const viteConfig = deepClone({ ssr: { noExternal: false } }) - subject.config(viteConfig) + const result = { ssr: { noExternal: false } } + subject.config(result) - expect(viteConfig).toMatchObject({ + expect(result).toMatchObject({ ssr: { noExternal: false, }, From 9126fcdbeaf5284c873ca19b8b5671fef279b7ba Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Fri, 18 Oct 2024 12:06:14 -0400 Subject: [PATCH 4/4] fixup: skip cleanup in globals mode --- src/__tests__/vite-plugin.test.js | 65 +++++++++++++++++++++++++------ src/vite.js | 9 +++-- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/__tests__/vite-plugin.test.js b/src/__tests__/vite-plugin.test.js index 1d89ae8..9232f65 100644 --- a/src/__tests__/vite-plugin.test.js +++ b/src/__tests__/vite-plugin.test.js @@ -44,12 +44,16 @@ describe.skipIf(IS_JEST)('vite plugin', () => { ])( 'adds browser condition if necessary', ({ config, expectedConditions }) => { - const subject = svelteTesting() + const subject = svelteTesting({ + resolveBrowser: true, + autoCleanup: false, + noExternal: false, + }) const result = config() subject.config(result) - expect(result).toMatchObject({ + expect(result).toEqual({ resolve: { conditions: expectedConditions, }, @@ -73,12 +77,16 @@ describe.skipIf(IS_JEST)('vite plugin', () => { ])( 'skips browser condition if possible', ({ config, expectedConditions }) => { - const subject = svelteTesting() + const subject = svelteTesting({ + resolveBrowser: true, + autoCleanup: false, + noExternal: false, + }) const result = config() subject.config(result) - expect(result).toMatchObject({ + expect(result).toEqual({ resolve: { conditions: expectedConditions, }, @@ -103,18 +111,39 @@ describe.skipIf(IS_JEST)('vite plugin', () => { ], }, ])('adds cleanup', ({ config, expectedSetupFiles }) => { - const subject = svelteTesting() + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: true, + noExternal: false, + }) const result = config() subject.config(result) - expect(result).toMatchObject({ + expect(result).toEqual({ test: { setupFiles: expectedSetupFiles, }, }) }) + test('skips cleanup in global mode', () => { + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: true, + noExternal: false, + }) + + const result = { test: { globals: true } } + subject.config(result) + + expect(result).toEqual({ + test: { + globals: true, + }, + }) + }) + test.each([ { config: () => ({ ssr: { noExternal: [] } }), @@ -133,12 +162,16 @@ describe.skipIf(IS_JEST)('vite plugin', () => { expectedNoExternal: [/other/u, '@testing-library/svelte'], }, ])('adds noExternal rule', ({ config, expectedNoExternal }) => { - const subject = svelteTesting() + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: true, + }) const result = config() subject.config(result) - expect(result).toMatchObject({ + expect(result).toEqual({ ssr: { noExternal: expectedNoExternal, }, @@ -159,12 +192,16 @@ describe.skipIf(IS_JEST)('vite plugin', () => { expectedNoExternal: /svelte/u, }, ])('skips noExternal if able', ({ config, expectedNoExternal }) => { - const subject = svelteTesting() + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: true, + }) const result = config() subject.config(result) - expect(result).toMatchObject({ + expect(result).toEqual({ ssr: { noExternal: expectedNoExternal, }, @@ -172,12 +209,16 @@ describe.skipIf(IS_JEST)('vite plugin', () => { }) test('bails on noExternal if input is unexpected', () => { - const subject = svelteTesting() + const subject = svelteTesting({ + resolveBrowser: false, + autoCleanup: false, + noExternal: true, + }) const result = { ssr: { noExternal: false } } subject.config(result) - expect(result).toMatchObject({ + expect(result).toEqual({ ssr: { noExternal: false, }, diff --git a/src/vite.js b/src/vite.js index d8b8e20..1ad712f 100644 --- a/src/vite.js +++ b/src/vite.js @@ -69,6 +69,10 @@ const addAutoCleanup = (config) => { const test = config.test ?? {} let setupFiles = test.setupFiles ?? [] + if (test.globals) { + return + } + if (typeof setupFiles === 'string') { setupFiles = [setupFiles] } @@ -106,10 +110,7 @@ const addNoExternal = (config) => { return } - if ( - noExternal instanceof RegExp && - noExternal.test('@testing-library/svelte') - ) { + if (rule instanceof RegExp && rule.test('@testing-library/svelte')) { return } }