[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(browser): allow injecting scripts (#5656)

authored by

Vladimir and committed by
GitHub
(May 3, 2024, 4:00 PM +0200) 21e58bd8 30f728bc

+193 -26
+7 -15
pnpm-lock.yaml
··· 879 879 version: 4.3.10 880 880 debug: 881 881 specifier: ^4.3.4 882 - version: 4.3.4 882 + version: 4.3.4(supports-color@8.1.1) 883 883 execa: 884 884 specifier: ^8.0.1 885 885 version: 8.0.1 ··· 1060 1060 '@vitest/cjs-lib': 1061 1061 specifier: link:./cjs-lib 1062 1062 version: link:cjs-lib 1063 + '@vitest/injected-lib': 1064 + specifier: link:./injected-lib 1065 + version: link:injected-lib 1063 1066 execa: 1064 1067 specifier: ^7.1.1 1065 1068 version: 7.1.1 ··· 7064 7067 resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} 7065 7068 engines: {node: '>= 14'} 7066 7069 dependencies: 7067 - debug: 4.3.4 7070 + debug: 4.3.4(supports-color@8.1.1) 7068 7071 transitivePeerDependencies: 7069 7072 - supports-color 7070 7073 dev: true ··· 8569 8572 ms: 2.1.3 8570 8573 supports-color: 8.1.1 8571 8574 dev: true 8572 - 8573 - /debug@4.3.4: 8574 - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 8575 - engines: {node: '>=6.0'} 8576 - peerDependencies: 8577 - supports-color: '*' 8578 - peerDependenciesMeta: 8579 - supports-color: 8580 - optional: true 8581 - dependencies: 8582 - ms: 2.1.2 8583 8575 8584 8576 /debug@4.3.4(supports-color@8.1.1): 8585 8577 resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} ··· 10866 10858 engines: {node: '>= 14'} 10867 10859 dependencies: 10868 10860 agent-base: 7.1.0 10869 - debug: 4.3.4 10861 + debug: 4.3.4(supports-color@8.1.1) 10870 10862 transitivePeerDependencies: 10871 10863 - supports-color 10872 10864 dev: true ··· 10917 10909 engines: {node: '>= 14'} 10918 10910 dependencies: 10919 10911 agent-base: 7.1.0 10920 - debug: 4.3.4 10912 + debug: 4.3.4(supports-color@8.1.1) 10921 10913 transitivePeerDependencies: 10922 10914 - supports-color 10923 10915 dev: true
+49
docs/config/index.md
··· 1612 1612 1613 1613 If you rely on spying on ES modules with `vi.spyOn`, you can enable this experimental feature to allow spying on module exports. 1614 1614 1615 + #### browser.indexScripts <Version>1.6.0</Version> {#browser-indexscripts} 1616 + 1617 + - **Type:** `BrowserScript[]` 1618 + - **Default:** `[]` 1619 + 1620 + Custom scripts that should be injected into the index HTML before test iframes are initiated. This HTML document only sets up iframes and doesn't actually import your code. 1621 + 1622 + The script `src` and `content` will be processed by Vite plugins. Script should be provided in the following shape: 1623 + 1624 + ```ts 1625 + export interface BrowserScript { 1626 + /** 1627 + * If "content" is provided and type is "module", this will be its identifier. 1628 + * 1629 + * If you are using TypeScript, you can add `.ts` extension here for example. 1630 + * @default `injected-${index}.js` 1631 + */ 1632 + id?: string 1633 + /** 1634 + * JavaScript content to be injected. This string is processed by Vite plugins if type is "module". 1635 + * 1636 + * You can use `id` to give Vite a hint about the file extension. 1637 + */ 1638 + content?: string 1639 + /** 1640 + * Path to the script. This value is resolved by Vite so it can be a node module or a file path. 1641 + */ 1642 + src?: string 1643 + /** 1644 + * If the script should be loaded asynchronously. 1645 + */ 1646 + async?: boolean 1647 + /** 1648 + * Script type. 1649 + * @default 'module' 1650 + */ 1651 + type?: string 1652 + } 1653 + ``` 1654 + 1655 + #### browser.testerScripts <Version>1.6.0</Version> {#browser-testerscripts} 1656 + 1657 + - **Type:** `BrowserScript[]` 1658 + - **Default:** `[]` 1659 + 1660 + Custom scripts that should be injected into the tester HTML before the tests environment is initiated. This is useful to inject polyfills required for Vitest browser implementation. It is recommended to use [`setupFiles`](#setupfiles) in almost all cases instead of this. 1661 + 1662 + The script `src` and `content` will be processed by Vite plugins. 1663 + 1615 1664 ### clearMocks 1616 1665 1617 1666 - **Type:** `boolean`
+2
test/browser/injected.ts
··· 1 + // @ts-expect-error not typed global 2 + ;(__injected as string[]).push(3)
+1
test/browser/package.json
··· 15 15 "@vitejs/plugin-basic-ssl": "^1.0.2", 16 16 "@vitest/browser": "workspace:*", 17 17 "@vitest/cjs-lib": "link:./cjs-lib", 18 + "@vitest/injected-lib": "link:./injected-lib", 18 19 "execa": "^7.1.1", 19 20 "playwright": "^1.41.0", 20 21 "url": "^0.11.3",
+31
test/browser/vitest.config.mts
··· 29 29 provider, 30 30 isolate: false, 31 31 slowHijackESM: true, 32 + testerScripts: [ 33 + { 34 + content: 'globalThis.__injected = []', 35 + type: 'text/javascript', 36 + }, 37 + { 38 + content: '__injected.push(1)', 39 + }, 40 + { 41 + id: 'ts.ts', 42 + content: '(__injected as string[]).push(2)', 43 + }, 44 + { 45 + src: './injected.ts', 46 + }, 47 + { 48 + src: '@vitest/injected-lib', 49 + }, 50 + ], 51 + indexScripts: [ 52 + { 53 + content: 'console.log("Hello, World");globalThis.__injected = []', 54 + type: 'text/javascript', 55 + }, 56 + { 57 + content: 'import "./injected.ts"', 58 + }, 59 + { 60 + content: 'if(__injected[0] !== 3) throw new Error("injected not working")', 61 + }, 62 + ], 32 63 }, 33 64 alias: { 34 65 '#src': resolve(dir, './src'),
+1
test/browser/injected-lib/index.js
··· 1 + __injected.push(4)
+7
test/browser/injected-lib/package.json
··· 1 + { 2 + "name": "@vitest/injected-lib", 3 + "type": "module", 4 + "exports": { 5 + "default": "./index.js" 6 + } 7 + }
+7 -3
test/browser/specs/runner.test.ts
··· 1 - import { beforeAll, describe, expect, test } from 'vitest' 1 + import { beforeAll, describe, expect, onTestFailed, test } from 'vitest' 2 2 import { runBrowserTests } from './utils' 3 3 4 4 describe.each([ ··· 26 26 }) 27 27 28 28 test(`[${description}] tests are actually running`, () => { 29 - expect(browserResultJson.testResults).toHaveLength(14) 30 - expect(passedTests).toHaveLength(12) 29 + onTestFailed(() => { 30 + console.error(stderr) 31 + }) 32 + 33 + expect(browserResultJson.testResults).toHaveLength(15) 34 + expect(passedTests).toHaveLength(13) 31 35 expect(failedTests).toHaveLength(2) 32 36 33 37 expect(stderr).not.toContain('has been externalized for browser compatibility')
+10
test/browser/test/injected.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('injected values are correct', () => { 4 + expect((globalThis as any).__injected).toEqual([ 5 + 1, 6 + 2, 7 + 3, 8 + 4, 9 + ]) 10 + })
+1
packages/browser/src/client/index.html
··· 22 22 } 23 23 </style> 24 24 <script>{__VITEST_INJECTOR__}</script> 25 + {__VITEST_SCRIPTS__} 25 26 </head> 26 27 <body> 27 28 <iframe id="vitest-ui" src=""></iframe>
+1
packages/browser/src/client/tester.html
··· 16 16 } 17 17 </style> 18 18 <script>{__VITEST_INJECTOR__}</script> 19 + {__VITEST_SCRIPTS__} 19 20 </head> 20 21 <body> 21 22 <script type="module" src="/tester.ts"></script>
+33 -7
packages/browser/src/node/index.ts
··· 1 1 import { fileURLToPath } from 'node:url' 2 2 import { readFile } from 'node:fs/promises' 3 - import { basename, resolve } from 'pathe' 3 + import { basename, join, resolve } from 'pathe' 4 4 import sirv from 'sirv' 5 - import type { Plugin } from 'vite' 5 + import type { Plugin, ViteDevServer } from 'vite' 6 6 import type { ResolvedConfig } from 'vitest' 7 - import type { WorkspaceProject } from 'vitest/node' 7 + import type { BrowserScript, WorkspaceProject } from 'vitest/node' 8 8 import { coverageConfigDefaults } from 'vitest/config' 9 + import { slash } from '@vitest/utils' 9 10 import { injectVitestModule } from './esmInjector' 10 - 11 - function replacer(code: string, values: Record<string, string>) { 12 - return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? '') 13 - } 14 11 15 12 export default (project: WorkspaceProject, base = '/'): Plugin[] => { 16 13 const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') ··· 41 38 } 42 39 next() 43 40 }) 41 + let indexScripts: string | undefined 42 + let testerScripts: string | undefined 44 43 server.middlewares.use(async (req, res, next) => { 45 44 if (!req.url) 46 45 return next() ··· 63 62 }) 64 63 65 64 if (url.pathname === base) { 65 + if (!indexScripts) 66 + indexScripts = await formatScripts(project.config.browser.indexScripts, server) 67 + 66 68 const html = replacer(await runnerHtml, { 67 69 __VITEST_FAVICON__: favicon, 68 70 __VITEST_TITLE__: 'Vitest Browser Runner', 71 + __VITEST_SCRIPTS__: indexScripts, 69 72 __VITEST_INJECTOR__: injector, 70 73 }) 71 74 res.write(html, 'utf-8') ··· 77 80 // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests 78 81 const tests = decodedTestFile === '__vitest_all__' || !files.includes(decodedTestFile) ? '__vitest_browser_runner__.files' : JSON.stringify([decodedTestFile]) 79 82 83 + if (!testerScripts) 84 + testerScripts = await formatScripts(project.config.browser.testerScripts, server) 85 + 80 86 const html = replacer(await testerHtml, { 81 87 __VITEST_FAVICON__: favicon, 82 88 __VITEST_TITLE__: 'Vitest Browser Tester', 89 + __VITEST_SCRIPTS__: testerScripts, 83 90 __VITEST_INJECTOR__: injector, 84 91 __VITEST_APPEND__: 85 92 // TODO: have only a single global variable to not pollute the global scope ··· 232 239 ? config.testNamePattern.toString() as any as RegExp 233 240 : undefined, 234 241 } 242 + } 243 + 244 + function replacer(code: string, values: Record<string, string>) { 245 + return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? '') 246 + } 247 + 248 + async function formatScripts(scripts: BrowserScript[] | undefined, server: ViteDevServer) { 249 + if (!scripts?.length) 250 + return '' 251 + const promises = scripts.map(async ({ content, src, async, id, type = 'module' }, index) => { 252 + const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src 253 + const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`) 254 + await server.moduleGraph.ensureEntryFromUrl(transformId) 255 + const contentProcessed = content && type === 'module' 256 + ? (await server.pluginContainer.transform(content, transformId)).code 257 + : content 258 + return `<script type="${type}"${async ? ' async' : ''}${srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ''}>${contentProcessed || ''}</script>` 259 + }) 260 + return (await Promise.all(promises)).join('\n') 235 261 }
+1 -1
packages/vitest/src/node/index.ts
··· 13 13 export type { TestSequencer, TestSequencerConstructor } from './sequencers/types' 14 14 export { BaseSequencer } from './sequencers/BaseSequencer' 15 15 16 - export type { BrowserProviderInitializationOptions, BrowserProvider, BrowserProviderOptions } from '../types/browser' 16 + export type { BrowserProviderInitializationOptions, BrowserProvider, BrowserProviderOptions, BrowserScript } from '../types/browser'
+39
packages/vitest/src/types/browser.ts
··· 93 93 * @default test.fileParallelism 94 94 */ 95 95 fileParallelism?: boolean 96 + 97 + /** 98 + * Scripts injected into the tester iframe. 99 + */ 100 + testerScripts?: BrowserScript[] 101 + 102 + /** 103 + * Scripts injected into the main window. 104 + */ 105 + indexScripts?: BrowserScript[] 106 + } 107 + 108 + export interface BrowserScript { 109 + /** 110 + * If "content" is provided and type is "module", this will be its identifier. 111 + * 112 + * If you are using TypeScript, you can add `.ts` extension here for example. 113 + * @default `injected-${index}.js` 114 + */ 115 + id?: string 116 + /** 117 + * JavaScript content to be injected. This string is processed by Vite plugins if type is "module". 118 + * 119 + * You can use `id` to give Vite a hint about the file extension. 120 + */ 121 + content?: string 122 + /** 123 + * Path to the script. This value is resolved by Vite so it can be a node module or a file path. 124 + */ 125 + src?: string 126 + /** 127 + * If the script should be loaded asynchronously. 128 + */ 129 + async?: boolean 130 + /** 131 + * Script type. 132 + * @default 'module' 133 + */ 134 + type?: string 96 135 } 97 136 98 137 export interface ResolvedBrowserOptions extends BrowserConfigOptions {
+1
packages/vitest/src/types/config.ts
··· 16 16 import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser' 17 17 import type { Pool, PoolOptions } from './pool-options' 18 18 19 + export type { BrowserScript, BrowserConfigOptions } from './browser' 19 20 export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' 20 21 21 22 export type BuiltinEnvironment = 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime'
+2
packages/vitest/src/node/cli/cli-config.ts
··· 350 350 fileParallelism: { 351 351 description: 'Should all test files run in parallel. Use `--browser.file-parallelism=false` to disable (default: same as `--file-parallelism`)', 352 352 }, 353 + indexScripts: null, 354 + testerScripts: null, 353 355 }, 354 356 }, 355 357 pool: {