···1612161216131613If you rely on spying on ES modules with `vi.spyOn`, you can enable this experimental feature to allow spying on module exports.
1614161416151615+#### browser.indexScripts <Version>1.6.0</Version> {#browser-indexscripts}
16161616+16171617+- **Type:** `BrowserScript[]`
16181618+- **Default:** `[]`
16191619+16201620+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.
16211621+16221622+The script `src` and `content` will be processed by Vite plugins. Script should be provided in the following shape:
16231623+16241624+```ts
16251625+export interface BrowserScript {
16261626+ /**
16271627+ * If "content" is provided and type is "module", this will be its identifier.
16281628+ *
16291629+ * If you are using TypeScript, you can add `.ts` extension here for example.
16301630+ * @default `injected-${index}.js`
16311631+ */
16321632+ id?: string
16331633+ /**
16341634+ * JavaScript content to be injected. This string is processed by Vite plugins if type is "module".
16351635+ *
16361636+ * You can use `id` to give Vite a hint about the file extension.
16371637+ */
16381638+ content?: string
16391639+ /**
16401640+ * Path to the script. This value is resolved by Vite so it can be a node module or a file path.
16411641+ */
16421642+ src?: string
16431643+ /**
16441644+ * If the script should be loaded asynchronously.
16451645+ */
16461646+ async?: boolean
16471647+ /**
16481648+ * Script type.
16491649+ * @default 'module'
16501650+ */
16511651+ type?: string
16521652+}
16531653+```
16541654+16551655+#### browser.testerScripts <Version>1.6.0</Version> {#browser-testerscripts}
16561656+16571657+- **Type:** `BrowserScript[]`
16581658+- **Default:** `[]`
16591659+16601660+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.
16611661+16621662+The script `src` and `content` will be processed by Vite plugins.
16631663+16151664### clearMocks
1616166516171666- **Type:** `boolean`
+2
test/browser/injected.ts
···11+// @ts-expect-error not typed global
22+;(__injected as string[]).push(3)
···11import { fileURLToPath } from 'node:url'
22import { readFile } from 'node:fs/promises'
33-import { basename, resolve } from 'pathe'
33+import { basename, join, resolve } from 'pathe'
44import sirv from 'sirv'
55-import type { Plugin } from 'vite'
55+import type { Plugin, ViteDevServer } from 'vite'
66import type { ResolvedConfig } from 'vitest'
77-import type { WorkspaceProject } from 'vitest/node'
77+import type { BrowserScript, WorkspaceProject } from 'vitest/node'
88import { coverageConfigDefaults } from 'vitest/config'
99+import { slash } from '@vitest/utils'
910import { injectVitestModule } from './esmInjector'
1010-1111-function replacer(code: string, values: Record<string, string>) {
1212- return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? '')
1313-}
14111512export default (project: WorkspaceProject, base = '/'): Plugin[] => {
1613 const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
···4138 }
4239 next()
4340 })
4141+ let indexScripts: string | undefined
4242+ let testerScripts: string | undefined
4443 server.middlewares.use(async (req, res, next) => {
4544 if (!req.url)
4645 return next()
···6362 })
64636564 if (url.pathname === base) {
6565+ if (!indexScripts)
6666+ indexScripts = await formatScripts(project.config.browser.indexScripts, server)
6767+6668 const html = replacer(await runnerHtml, {
6769 __VITEST_FAVICON__: favicon,
6870 __VITEST_TITLE__: 'Vitest Browser Runner',
7171+ __VITEST_SCRIPTS__: indexScripts,
6972 __VITEST_INJECTOR__: injector,
7073 })
7174 res.write(html, 'utf-8')
···7780 // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests
7881 const tests = decodedTestFile === '__vitest_all__' || !files.includes(decodedTestFile) ? '__vitest_browser_runner__.files' : JSON.stringify([decodedTestFile])
79828383+ if (!testerScripts)
8484+ testerScripts = await formatScripts(project.config.browser.testerScripts, server)
8585+8086 const html = replacer(await testerHtml, {
8187 __VITEST_FAVICON__: favicon,
8288 __VITEST_TITLE__: 'Vitest Browser Tester',
8989+ __VITEST_SCRIPTS__: testerScripts,
8390 __VITEST_INJECTOR__: injector,
8491 __VITEST_APPEND__:
8592 // TODO: have only a single global variable to not pollute the global scope
···232239 ? config.testNamePattern.toString() as any as RegExp
233240 : undefined,
234241 }
242242+}
243243+244244+function replacer(code: string, values: Record<string, string>) {
245245+ return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? '')
246246+}
247247+248248+async function formatScripts(scripts: BrowserScript[] | undefined, server: ViteDevServer) {
249249+ if (!scripts?.length)
250250+ return ''
251251+ const promises = scripts.map(async ({ content, src, async, id, type = 'module' }, index) => {
252252+ const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src
253253+ const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`)
254254+ await server.moduleGraph.ensureEntryFromUrl(transformId)
255255+ const contentProcessed = content && type === 'module'
256256+ ? (await server.pluginContainer.transform(content, transformId)).code
257257+ : content
258258+ return `<script type="${type}"${async ? ' async' : ''}${srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ''}>${contentProcessed || ''}</script>`
259259+ })
260260+ return (await Promise.all(promises)).join('\n')
235261}
+1-1
packages/vitest/src/node/index.ts
···1313export type { TestSequencer, TestSequencerConstructor } from './sequencers/types'
1414export { BaseSequencer } from './sequencers/BaseSequencer'
15151616-export type { BrowserProviderInitializationOptions, BrowserProvider, BrowserProviderOptions } from '../types/browser'
1616+export type { BrowserProviderInitializationOptions, BrowserProvider, BrowserProviderOptions, BrowserScript } from '../types/browser'
+39
packages/vitest/src/types/browser.ts
···9393 * @default test.fileParallelism
9494 */
9595 fileParallelism?: boolean
9696+9797+ /**
9898+ * Scripts injected into the tester iframe.
9999+ */
100100+ testerScripts?: BrowserScript[]
101101+102102+ /**
103103+ * Scripts injected into the main window.
104104+ */
105105+ indexScripts?: BrowserScript[]
106106+}
107107+108108+export interface BrowserScript {
109109+ /**
110110+ * If "content" is provided and type is "module", this will be its identifier.
111111+ *
112112+ * If you are using TypeScript, you can add `.ts` extension here for example.
113113+ * @default `injected-${index}.js`
114114+ */
115115+ id?: string
116116+ /**
117117+ * JavaScript content to be injected. This string is processed by Vite plugins if type is "module".
118118+ *
119119+ * You can use `id` to give Vite a hint about the file extension.
120120+ */
121121+ content?: string
122122+ /**
123123+ * Path to the script. This value is resolved by Vite so it can be a node module or a file path.
124124+ */
125125+ src?: string
126126+ /**
127127+ * If the script should be loaded asynchronously.
128128+ */
129129+ async?: boolean
130130+ /**
131131+ * Script type.
132132+ * @default 'module'
133133+ */
134134+ type?: string
96135}
9713698137export interface ResolvedBrowserOptions extends BrowserConfigOptions {
+1
packages/vitest/src/types/config.ts
···1616import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
1717import type { Pool, PoolOptions } from './pool-options'
18181919+export type { BrowserScript, BrowserConfigOptions } from './browser'
1920export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner'
20212122export type BuiltinEnvironment = 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime'
+2
packages/vitest/src/node/cli/cli-config.ts
···350350 fileParallelism: {
351351 description: 'Should all test files run in parallel. Use `--browser.file-parallelism=false` to disable (default: same as `--file-parallelism`)',
352352 },
353353+ indexScripts: null,
354354+ testerScripts: null,
353355 },
354356 },
355357 pool: {