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/**/*'],