Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 169028f

Browse files
authoredOct 25, 2024··
feat(browser): allow custom HTML path, respect plugins transformIndexHtml (#6725)
1 parent 5df7414 commit 169028f

22 files changed

+450
-102
lines changed
 

‎docs/config/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,14 @@ Run the browser in a `headless` mode. If you are running Vitest in CI, it will b
16401640

16411641
Run every test in a separate iframe.
16421642

1643+
#### browser.testerHtmlPath
1644+
1645+
- **Type:** `string`
1646+
- **Default:** `@vitest/browser/tester.html`
1647+
- **Version:** Since Vitest 2.1.4
1648+
1649+
A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook.
1650+
16431651
#### browser.api
16441652

16451653
- **Type:** `number | { port?, strictPort?, host? }`

‎packages/browser/rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default () =>
107107
input: './src/client/tester/state.ts',
108108
output: {
109109
file: 'dist/state.js',
110-
format: 'esm',
110+
format: 'iife',
111111
},
112112
plugins: [
113113
esbuild({
Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,52 @@
1-
const moduleCache = new Map();
2-
3-
function wrapModule(module) {
4-
if (typeof module === "function") {
5-
const promise = new Promise((resolve, reject) => {
6-
if (typeof __vitest_mocker__ === "undefined")
7-
return module().then(resolve, reject);
8-
__vitest_mocker__.prepare().finally(() => {
9-
module().then(resolve, reject);
1+
(() => {
2+
const moduleCache = new Map();
3+
4+
function wrapModule(module) {
5+
if (typeof module === "function") {
6+
const promise = new Promise((resolve, reject) => {
7+
if (typeof __vitest_mocker__ === "undefined")
8+
return module().then(resolve, reject);
9+
__vitest_mocker__.prepare().finally(() => {
10+
module().then(resolve, reject);
11+
});
1012
});
11-
});
12-
moduleCache.set(promise, { promise, evaluated: false });
13-
return promise.finally(() => moduleCache.delete(promise));
13+
moduleCache.set(promise, { promise, evaluated: false });
14+
return promise.finally(() => moduleCache.delete(promise));
15+
}
16+
return module;
1417
}
15-
return module;
16-
}
17-
18-
window.__vitest_browser_runner__ = {
19-
wrapModule,
20-
wrapDynamicImport: wrapModule,
21-
moduleCache,
22-
config: { __VITEST_CONFIG__ },
23-
viteConfig: { __VITEST_VITE_CONFIG__ },
24-
files: { __VITEST_FILES__ },
25-
type: { __VITEST_TYPE__ },
26-
contextId: { __VITEST_CONTEXT_ID__ },
27-
testerId: { __VITEST_TESTER_ID__ },
28-
provider: { __VITEST_PROVIDER__ },
29-
providedContext: { __VITEST_PROVIDED_CONTEXT__ },
30-
};
31-
32-
const config = __vitest_browser_runner__.config;
33-
34-
if (config.testNamePattern)
35-
config.testNamePattern = parseRegexp(config.testNamePattern);
36-
37-
function parseRegexp(input) {
38-
// Parse input
39-
const m = input.match(/(\/?)(.+)\1([a-z]*)/i);
40-
41-
// match nothing
42-
if (!m) return /$^/;
43-
44-
// Invalid flags
45-
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3]))
46-
return RegExp(input);
47-
48-
// Create the regular expression
49-
return new RegExp(m[2], m[3]);
50-
}
18+
19+
window.__vitest_browser_runner__ = {
20+
wrapModule,
21+
wrapDynamicImport: wrapModule,
22+
moduleCache,
23+
config: { __VITEST_CONFIG__ },
24+
viteConfig: { __VITEST_VITE_CONFIG__ },
25+
files: { __VITEST_FILES__ },
26+
type: { __VITEST_TYPE__ },
27+
contextId: { __VITEST_CONTEXT_ID__ },
28+
testerId: { __VITEST_TESTER_ID__ },
29+
provider: { __VITEST_PROVIDER__ },
30+
providedContext: { __VITEST_PROVIDED_CONTEXT__ },
31+
};
32+
33+
const config = __vitest_browser_runner__.config;
34+
35+
if (config.testNamePattern)
36+
config.testNamePattern = parseRegexp(config.testNamePattern);
37+
38+
function parseRegexp(input) {
39+
// Parse input
40+
const m = input.match(/(\/?)(.+)\1([a-z]*)/i);
41+
42+
// match nothing
43+
if (!m) return /$^/;
44+
45+
// Invalid flags
46+
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3]))
47+
return RegExp(input);
48+
49+
// Create the regular expression
50+
return new RegExp(m[2], m[3]);
51+
}
52+
})();

‎packages/browser/src/client/tester/tester.html

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" href="{__VITEST_FAVICON__}" type="image/svg+xml">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>{__VITEST_TITLE__}</title>
7+
<title>Vitest Browser Tester</title>
88
<style>
99
html {
1010
padding: 0;
@@ -16,13 +16,8 @@
1616
min-height: 100vh;
1717
}
1818
</style>
19-
{__VITEST_INJECTOR__}
20-
<script>{__VITEST_STATE__}</script>
21-
{__VITEST_INTERNAL_SCRIPTS__}
22-
{__VITEST_SCRIPTS__}
2319
</head>
2420
<body>
2521
<script type="module" src="./tester.ts"></script>
26-
{__VITEST_APPEND__}
2722
</body>
2823
</html>

‎packages/browser/src/node/plugin.ts

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Stats } from 'node:fs'
2+
import type { HtmlTagDescriptor } from 'vite'
23
import type { WorkspaceProject } from 'vitest/node'
34
import type { BrowserServer } from './server'
45
import { lstatSync, readFileSync } from 'node:fs'
@@ -72,9 +73,11 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
7273
return
7374
}
7475

75-
const html = await resolveTester(browserServer, url, res)
76-
res.write(html, 'utf-8')
77-
res.end()
76+
const html = await resolveTester(browserServer, url, res, next)
77+
if (html) {
78+
res.write(html, 'utf-8')
79+
res.end()
80+
}
7881
})
7982

8083
server.middlewares.use(
@@ -394,6 +397,102 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
394397
}
395398
},
396399
},
400+
{
401+
name: 'vitest:browser:transform-tester-html',
402+
enforce: 'pre',
403+
async transformIndexHtml(html, ctx) {
404+
if (!ctx.path.startsWith(browserServer.prefixTesterUrl)) {
405+
return
406+
}
407+
408+
if (!browserServer.testerScripts) {
409+
const testerScripts = await browserServer.formatScripts(
410+
project.config.browser.testerScripts,
411+
)
412+
browserServer.testerScripts = testerScripts
413+
}
414+
const stateJs = typeof browserServer.stateJs === 'string'
415+
? browserServer.stateJs
416+
: await browserServer.stateJs
417+
418+
const testerScripts: HtmlTagDescriptor[] = []
419+
if (resolve(distRoot, 'client/tester/tester.html') !== browserServer.testerFilepath) {
420+
const manifestContent = browserServer.manifest instanceof Promise
421+
? await browserServer.manifest
422+
: browserServer.manifest
423+
const testerEntry = manifestContent['tester/tester.html']
424+
425+
testerScripts.push({
426+
tag: 'script',
427+
attrs: {
428+
type: 'module',
429+
crossorigin: '',
430+
src: `${browserServer.base}${testerEntry.file}`,
431+
},
432+
injectTo: 'head',
433+
})
434+
435+
for (const importName of testerEntry.imports || []) {
436+
const entryManifest = manifestContent[importName]
437+
if (entryManifest) {
438+
testerScripts.push(
439+
{
440+
tag: 'link',
441+
attrs: {
442+
href: `${browserServer.base}${entryManifest.file}`,
443+
rel: 'modulepreload',
444+
crossorigin: '',
445+
},
446+
injectTo: 'head',
447+
},
448+
)
449+
}
450+
}
451+
}
452+
453+
return [
454+
{
455+
tag: 'script',
456+
children: '{__VITEST_INJECTOR__}',
457+
injectTo: 'head-prepend' as const,
458+
},
459+
{
460+
tag: 'script',
461+
children: stateJs,
462+
injectTo: 'head-prepend',
463+
} as const,
464+
{
465+
tag: 'script',
466+
attrs: {
467+
type: 'module',
468+
src: browserServer.errorCatcherUrl,
469+
},
470+
injectTo: 'head' as const,
471+
},
472+
browserServer.locatorsUrl
473+
? {
474+
tag: 'script',
475+
attrs: {
476+
type: 'module',
477+
src: browserServer.locatorsUrl,
478+
},
479+
injectTo: 'head',
480+
} as const
481+
: null,
482+
...browserServer.testerScripts,
483+
...testerScripts,
484+
{
485+
tag: 'script',
486+
attrs: {
487+
'type': 'module',
488+
'data-vitest-append': '',
489+
},
490+
children: '{__VITEST_APPEND__}',
491+
injectTo: 'body',
492+
} as const,
493+
].filter(s => s != null)
494+
},
495+
},
397496
{
398497
name: 'vitest:browser:support-testing-library',
399498
config() {

‎packages/browser/src/node/pool.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
101101
url.searchParams.set('contextId', contextId)
102102
const page = provider
103103
.openPage(contextId, url.toString(), () => setBreakpoint(contextId, files[0]))
104-
.then(() => waitPromise)
105-
promises.push(page)
104+
promises.push(page, waitPromise)
106105
}
107106
})
108107

‎packages/browser/src/node/server.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ErrorWithDiff } from '@vitest/utils'
2-
import type { SerializedConfig } from 'vitest'
1+
import type { HtmlTagDescriptor } from 'vite'
2+
import type { ErrorWithDiff, SerializedConfig } from 'vitest'
33
import type {
44
BrowserProvider,
55
BrowserScript,
@@ -8,6 +8,7 @@ import type {
88
Vite,
99
WorkspaceProject,
1010
} from 'vitest/node'
11+
import { existsSync } from 'node:fs'
1112
import { readFile } from 'node:fs/promises'
1213
import { fileURLToPath } from 'node:url'
1314
import { slash } from '@vitest/utils'
@@ -22,10 +23,11 @@ export class BrowserServer implements IBrowserServer {
2223
public prefixTesterUrl: string
2324

2425
public orchestratorScripts: string | undefined
25-
public testerScripts: string | undefined
26+
public testerScripts: HtmlTagDescriptor[] | undefined
2627

2728
public manifest: Promise<Vite.Manifest> | Vite.Manifest
2829
public testerHtml: Promise<string> | string
30+
public testerFilepath: string
2931
public orchestratorHtml: Promise<string> | string
3032
public injectorJs: Promise<string> | string
3133
public errorCatcherUrl: string
@@ -76,8 +78,16 @@ export class BrowserServer implements IBrowserServer {
7678
)
7779
})().then(manifest => (this.manifest = manifest))
7880

81+
const testerHtmlPath = project.config.browser.testerHtmlPath
82+
? resolve(project.config.root, project.config.browser.testerHtmlPath)
83+
: resolve(distRoot, 'client/tester/tester.html')
84+
if (!existsSync(testerHtmlPath)) {
85+
throw new Error(`Tester HTML file "${testerHtmlPath}" doesn't exist.`)
86+
}
87+
this.testerFilepath = testerHtmlPath
88+
7989
this.testerHtml = readFile(
80-
resolve(distRoot, 'client/tester/tester.html'),
90+
testerHtmlPath,
8191
'utf8',
8292
).then(html => (this.testerHtml = html))
8393
this.orchestratorHtml = (project.config.browser.ui
@@ -124,24 +134,35 @@ export class BrowserServer implements IBrowserServer {
124134
scripts: BrowserScript[] | undefined,
125135
) {
126136
if (!scripts?.length) {
127-
return ''
137+
return []
128138
}
129139
const server = this.vite
130140
const promises = scripts.map(
131-
async ({ content, src, async, id, type = 'module' }, index) => {
141+
async ({ content, src, async, id, type = 'module' }, index): Promise<HtmlTagDescriptor> => {
132142
const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src
133143
const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`)
134144
await server.moduleGraph.ensureEntryFromUrl(transformId)
135145
const contentProcessed
136146
= content && type === 'module'
137147
? (await server.pluginContainer.transform(content, transformId)).code
138148
: content
139-
return `<script type="${type}"${async ? ' async' : ''}${
140-
srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ''
141-
}>${contentProcessed || ''}</script>`
149+
return {
150+
tag: 'script',
151+
attrs: {
152+
type,
153+
...(async ? { async: '' } : {}),
154+
...(srcLink
155+
? {
156+
src: srcLink.startsWith('http') ? srcLink : slash(`/@fs/${srcLink}`),
157+
}
158+
: {}),
159+
},
160+
injectTo: 'head',
161+
children: contentProcessed || '',
162+
}
142163
},
143164
)
144-
return (await Promise.all(promises)).join('\n')
165+
return (await Promise.all(promises))
145166
}
146167

147168
async initBrowserProvider() {

‎packages/browser/src/node/serverOrchestrator.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,16 @@ export async function resolveOrchestrator(
3838
res.removeHeader('Content-Security-Policy')
3939

4040
if (!server.orchestratorScripts) {
41-
server.orchestratorScripts = await server.formatScripts(
41+
server.orchestratorScripts = (await server.formatScripts(
4242
project.config.browser.orchestratorScripts,
43-
)
43+
)).map((script) => {
44+
let html = '<script '
45+
for (const attr in script.attrs || {}) {
46+
html += `${attr}="${script.attrs![attr]}" `
47+
}
48+
html += `>${script.children}</script>`
49+
return html
50+
}).join('\n')
4451
}
4552

4653
let baseHtml = typeof server.orchestratorHtml === 'string'

‎packages/browser/src/node/serverTester.ts

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { IncomingMessage, ServerResponse } from 'node:http'
2+
import type { Connect } from 'vite'
23
import type { BrowserServer } from './server'
34
import crypto from 'node:crypto'
45
import { stringify } from 'flatted'
@@ -8,7 +9,8 @@ export async function resolveTester(
89
server: BrowserServer,
910
url: URL,
1011
res: ServerResponse<IncomingMessage>,
11-
): Promise<string> {
12+
next: Connect.NextFunction,
13+
): Promise<string | undefined> {
1214
const csp = res.getHeader('Content-Security-Policy')
1315
if (typeof csp === 'string') {
1416
// add frame-ancestors to allow the iframe to be loaded by Vitest,
@@ -51,36 +53,24 @@ export async function resolveTester(
5153
__VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())),
5254
})
5355

54-
if (!server.testerScripts) {
55-
const testerScripts = await server.formatScripts(
56-
project.config.browser.testerScripts,
57-
)
58-
const clientScript = `<script type="module" src="${server.base}@vite/client"></script>`
59-
const stateJs = typeof server.stateJs === 'string'
60-
? server.stateJs
61-
: await server.stateJs
62-
const stateScript = `<script type="module">${stateJs}</script>`
63-
server.testerScripts = `${stateScript}${clientScript}${testerScripts}`
64-
}
65-
6656
const testerHtml = typeof server.testerHtml === 'string'
6757
? server.testerHtml
6858
: await server.testerHtml
6959

70-
return replacer(testerHtml, {
71-
__VITEST_FAVICON__: server.faviconUrl,
72-
__VITEST_TITLE__: 'Vitest Browser Tester',
73-
__VITEST_SCRIPTS__: server.testerScripts,
74-
__VITEST_INJECTOR__: `<script type="module">${injector}</script>`,
75-
__VITEST_INTERNAL_SCRIPTS__: [
76-
`<script type="module" src="${server.errorCatcherUrl}"></script>`,
77-
server.locatorsUrl ? `<script type="module" src="${server.locatorsUrl}"></script>` : '',
78-
].join('\n'),
79-
__VITEST_APPEND__: `<script data-vitest-append type="module">
80-
__vitest_browser_runner__.runningFiles = ${tests}
81-
__vitest_browser_runner__.iframeId = ${iframeId}
82-
__vitest_browser_runner__.${method === 'run' ? 'runTests' : 'collectTests'}(__vitest_browser_runner__.runningFiles)
83-
document.querySelector('script[data-vitest-append]').remove()
84-
</script>`,
85-
})
60+
try {
61+
const indexhtml = await server.vite.transformIndexHtml(url.pathname, testerHtml)
62+
return replacer(indexhtml, {
63+
__VITEST_INJECTOR__: injector,
64+
__VITEST_APPEND__: `
65+
__vitest_browser_runner__.runningFiles = ${tests}
66+
__vitest_browser_runner__.iframeId = ${iframeId}
67+
__vitest_browser_runner__.${method === 'run' ? 'runTests' : 'collectTests'}(__vitest_browser_runner__.runningFiles)
68+
document.querySelector('script[data-vitest-append]').remove()
69+
`,
70+
})
71+
}
72+
catch (err) {
73+
context?.reject(err)
74+
next(err)
75+
}
8676
}

‎packages/browser/src/node/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BrowserProviderModule, ResolvedBrowserOptions, WorkspaceProject } from 'vitest/node'
22

33
export function replacer(code: string, values: Record<string, string>) {
4-
return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? '')
4+
return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _)
55
}
66

77
const builtinProviders = ['webdriverio', 'playwright', 'preview']

‎packages/vitest/src/node/cli/cli-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
415415
screenshotDirectory: null,
416416
screenshotFailures: null,
417417
locators: null,
418+
testerHtmlPath: null,
418419
},
419420
},
420421
pool: {

‎packages/vitest/src/node/types/browser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,13 @@ export interface BrowserConfigOptions {
156156

157157
/**
158158
* Scripts injected into the tester iframe.
159+
* @deprecated Will be removed in the future, use `testerHtmlPath` instead.
159160
*/
160161
testerScripts?: BrowserScript[]
162+
/**
163+
* Path to the index.html file that will be used to run tests.
164+
*/
165+
testerHtmlPath?: string
161166

162167
/**
163168
* Scripts injected into the main window.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test, expect } from 'vitest';
2+
3+
test('basic', async () => {
4+
const div = document.createElement('div')
5+
div.textContent = ' Vitest'
6+
document.body.appendChild(div)
7+
expect(document.body.textContent).toContain('HELLO WORLD')
8+
expect(document.body.textContent).toContain('Vitest')
9+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test, expect } from 'vitest';
2+
3+
test('custom', () => {
4+
expect(window).toHaveProperty('CUSTOM_INJECTED', true)
5+
})
6+
7+
test('importmap is injected', () => {
8+
expect(import.meta.resolve('some-lib')).toBe('https://vitest.dev/some-lib')
9+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Document</title>
7+
</head>
8+
<body>
9+
HELLO WORLD
10+
</body>
11+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
include: ['browser-basic.test.ts'],
6+
browser: {
7+
name: 'chromium',
8+
enabled: true,
9+
headless: true,
10+
provider: 'playwright',
11+
testerHtmlPath: './custom-html.html'
12+
},
13+
},
14+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
plugins: [
5+
{
6+
name: 'test:html',
7+
transformIndexHtml() {
8+
return [
9+
{
10+
tag: 'script',
11+
injectTo: 'head-prepend',
12+
attrs: {
13+
type: 'importmap'
14+
},
15+
children: JSON.stringify({
16+
"imports": {
17+
"some-lib": "https://vitest.dev/some-lib",
18+
},
19+
})
20+
},
21+
{
22+
tag: 'script',
23+
children: 'window.CUSTOM_INJECTED = true',
24+
injectTo: 'head',
25+
},
26+
]
27+
},
28+
},
29+
],
30+
test: {
31+
include: ['./browser-custom.test.ts'],
32+
browser: {
33+
name: 'chromium',
34+
enabled: true,
35+
headless: true,
36+
provider: 'playwright',
37+
testerHtmlPath: './custom-html.html',
38+
},
39+
},
40+
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
plugins: [
5+
{
6+
name: 'test:html',
7+
transformIndexHtml() {
8+
return [
9+
{
10+
tag: 'script',
11+
injectTo: 'head-prepend',
12+
attrs: {
13+
type: 'importmap'
14+
},
15+
children: JSON.stringify({
16+
"imports": {
17+
"some-lib": "https://vitest.dev/some-lib",
18+
},
19+
})
20+
},
21+
{
22+
tag: 'script',
23+
children: 'window.CUSTOM_INJECTED = true',
24+
injectTo: 'head',
25+
}
26+
]
27+
},
28+
},
29+
],
30+
test: {
31+
include: ['./browser-custom.test.ts'],
32+
browser: {
33+
name: 'chromium',
34+
enabled: true,
35+
headless: true,
36+
provider: 'playwright',
37+
},
38+
},
39+
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
plugins: [
5+
{
6+
name: 'test:html',
7+
transformIndexHtml() {
8+
throw new Error('expected error in transformIndexHtml')
9+
},
10+
},
11+
],
12+
test: {
13+
include: ['./browser-basic.test.ts'],
14+
browser: {
15+
name: 'chromium',
16+
enabled: true,
17+
headless: true,
18+
provider: 'playwright',
19+
testerHtmlPath: './custom-html.html'
20+
},
21+
},
22+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
browser: {
6+
name: 'chromium',
7+
enabled: true,
8+
headless: true,
9+
provider: 'playwright',
10+
testerHtmlPath: './some-non-existing-path'
11+
},
12+
},
13+
})

‎test/config/test/browser-html.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { resolve } from 'pathe'
2+
import { expect, test } from 'vitest'
3+
import { runVitest } from '../../test-utils'
4+
5+
const root = resolve(import.meta.dirname, '../fixtures/browser-custom-html')
6+
7+
test('throws an error with non-existing path', async () => {
8+
const { stderr, thrown } = await runVitest({
9+
root,
10+
config: './vitest.config.non-existing.ts',
11+
}, [], 'test', {}, { fails: true })
12+
expect(thrown).toBe(true)
13+
expect(stderr).toContain(`Tester HTML file "${resolve(root, './some-non-existing-path')}" doesn't exist.`)
14+
})
15+
16+
test('throws an error and exits if there is an error in the html file hook', async () => {
17+
const { stderr, stdout, exitCode } = await runVitest({
18+
root,
19+
config: './vitest.config.error-hook.ts',
20+
})
21+
expect(stderr).toContain('expected error in transformIndexHtml')
22+
// error happens when browser is opened
23+
expect(stdout).toContain('Browser runner started by playwright')
24+
expect(exitCode).toBe(1)
25+
})
26+
27+
test('allows correct custom html', async () => {
28+
const { stderr, stdout, exitCode } = await runVitest({
29+
root,
30+
config: './vitest.config.correct.ts',
31+
reporters: ['basic'],
32+
})
33+
expect(stderr).toBe('')
34+
expect(stdout).toContain('Browser runner started by playwright')
35+
expect(stdout).toContain('✓ browser-basic.test.ts')
36+
expect(exitCode).toBe(0)
37+
})
38+
39+
test('allows custom transformIndexHtml with custom html file', async () => {
40+
const { stderr, stdout, exitCode } = await runVitest({
41+
root,
42+
config: './vitest.config.custom-transformIndexHtml.ts',
43+
reporters: ['basic'],
44+
})
45+
expect(stderr).toBe('')
46+
expect(stdout).toContain('Browser runner started by playwright')
47+
expect(stdout).toContain('✓ browser-custom.test.ts')
48+
expect(exitCode).toBe(0)
49+
})
50+
51+
test('allows custom transformIndexHtml without custom html file', async () => {
52+
const { stderr, stdout, exitCode } = await runVitest({
53+
root,
54+
config: './vitest.config.default-transformIndexHtml.ts',
55+
reporters: ['basic'],
56+
})
57+
expect(stderr).toBe('')
58+
expect(stdout).toContain('Browser runner started by playwright')
59+
expect(stdout).toContain('✓ browser-custom.test.ts')
60+
expect(exitCode).toBe(0)
61+
})

‎test/test-utils/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export async function runVitest(
5555
const cli = new Cli({ stdin, stdout, stderr })
5656

5757
let ctx: Vitest | undefined
58+
let thrown = false
5859
try {
5960
const { reporters, ...rest } = config
6061

@@ -88,6 +89,7 @@ export async function runVitest(
8889
if (runnerOptions.fails !== true) {
8990
console.error(e)
9091
}
92+
thrown = true
9193
cli.stderr += e.stack
9294
}
9395
finally {
@@ -111,6 +113,7 @@ export async function runVitest(
111113
}
112114

113115
return {
116+
thrown,
114117
ctx,
115118
exitCode,
116119
vitest: cli,

0 commit comments

Comments
 (0)
Please sign in to comment.