Skip to content

Commit 154cb22

Browse files
authored
feat(browser): add an option to take screenshots if the browser test fails (#5975)
1 parent 14a217d commit 154cb22

File tree

18 files changed

+172
-31
lines changed

18 files changed

+172
-31
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ docs/public/sponsors
2222
.eslintcache
2323
docs/.vitepress/cache/
2424
!test/cli/fixtures/dotted-files/**/.cache
25-
test/browser/test/__screenshots__/**/*
25+
test/**/__screenshots__/**/*
2626
test/browser/fixtures/update-snapshot/basic.test.ts
2727
.vitest-reports

docs/config/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,20 @@ Should Vitest UI be injected into the page. By default, injects UI iframe during
16201620

16211621
Default iframe's viewport.
16221622

1623+
#### browser.screenshotDirectory {#browser-screenshotdirectory}
1624+
1625+
- **Type:** `string`
1626+
- **Default:** `__snapshots__` in the test file directory
1627+
1628+
Path to the snapshots directory relative to the `root`.
1629+
1630+
#### browser.screenshotFailures {#browser-screenshotfailures}
1631+
1632+
- **Type:** `boolean`
1633+
- **Default:** `!browser.ui`
1634+
1635+
Should Vitest take screenshots if the test fails.
1636+
16231637
#### browser.orchestratorScripts {#browser-orchestratorscripts}
16241638

16251639
- **Type:** `BrowserScript[]`

packages/browser/src/client/tester/mocker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class VitestBrowserClientMocker {
6464
const actualUrl = `${url.pathname}${
6565
url.search ? `${url.search}&${query}` : `?${query}`
6666
}${url.hash}`
67-
return getBrowserState().wrapModule(() => import(actualUrl))
67+
return getBrowserState().wrapModule(() => import(/* @vite-ignore */ actualUrl))
6868
}
6969

7070
public async importMock(rawId: string, importer: string) {
@@ -86,11 +86,11 @@ export class VitestBrowserClientMocker {
8686

8787
if (type === 'redirect') {
8888
const url = new URL(`/@id/${mockPath}`, location.href)
89-
return import(url.toString())
89+
return import(/* @vite-ignore */ url.toString())
9090
}
9191
const url = new URL(`/@id/${resolvedId}`, location.href)
9292
const query = url.search ? `${url.search}&t=${now()}` : `?t=${now()}`
93-
const moduleObject = await import(`${url.pathname}${query}${url.hash}`)
93+
const moduleObject = await import(/* @vite-ignore */ `${url.pathname}${query}${url.hash}`)
9494
return this.mockObject(moduleObject)
9595
}
9696

packages/browser/src/client/tester/runner.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { VitestExecutor } from 'vitest/execute'
44
import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners'
55
import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser'
66
import { TraceMap, originalPositionFor } from 'vitest/utils'
7-
import { importId } from '../utils'
7+
import { page } from '@vitest/browser/context'
8+
import { importFs, importId } from '../utils'
89
import { globalChannel } from '../channel'
910
import { VitestBrowserSnapshotEnvironment } from './snapshot'
1011
import { rpc } from './rpc'
@@ -53,6 +54,12 @@ export function createBrowserRunner(
5354
}
5455
}
5556

57+
onTaskFinished = async (task: Task) => {
58+
if (this.config.browser.screenshotFailures && task.result?.state === 'fail') {
59+
await page.screenshot()
60+
}
61+
}
62+
5663
onCancel = (reason: CancelReason) => {
5764
super.onCancel?.(reason)
5865
globalChannel.postMessage({ type: 'cancel', reason })
@@ -123,7 +130,7 @@ export function createBrowserRunner(
123130
const prefix = `/${/^\w:/.test(filepath) ? '@fs/' : ''}`
124131
const query = `${test ? 'browserv' : 'v'}=${hash}`
125132
const importpath = `${prefix}${filepath}?${query}`.replace(/\/+/g, '/')
126-
await import(importpath)
133+
await import(/* @vite-ignore */ importpath)
127134
}
128135
}
129136
}
@@ -140,17 +147,25 @@ export async function initiateRunner(
140147
}
141148
const runnerClass
142149
= config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner
150+
151+
const executeId = (id: string) => {
152+
if (id[0] === '/' || id[1] === ':') {
153+
return importFs(id)
154+
}
155+
return importId(id)
156+
}
157+
143158
const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, {
144159
takeCoverage: () =>
145-
takeCoverageInsideWorker(config.coverage, { executeId: importId }),
160+
takeCoverageInsideWorker(config.coverage, { executeId }),
146161
})
147162
if (!config.snapshotOptions.snapshotEnvironment) {
148163
config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment()
149164
}
150165
const runner = new BrowserRunner({
151166
config,
152167
})
153-
const executor = { executeId: importId } as VitestExecutor
168+
const executor = { executeId } as VitestExecutor
154169
const [diffOptions] = await Promise.all([
155170
loadDiffConfig(config, executor),
156171
loadSnapshotSerializers(config, executor),

packages/browser/src/client/tester/tester.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ async function runTests(files: string[]) {
9292
try {
9393
preparedData = await prepareTestEnvironment(files)
9494
}
95-
catch (error) {
96-
debug('data cannot be loaded because it threw an error')
95+
catch (error: any) {
96+
debug('runner cannot be loaded because it threw an error', error.stack || error.message)
9797
await client.rpc.onUnhandledError(serializeError(error), 'Preload Error')
9898
done(files)
9999
return

packages/browser/src/client/utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import type { ResolvedConfig, WorkerGlobalState } from 'vitest'
22

33
export async function importId(id: string) {
4-
const name = `/@id/${id}`
5-
return getBrowserState().wrapModule(() => import(name))
4+
const name = `/@id/${id}`.replace(/\\/g, '/')
5+
return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
6+
}
7+
8+
export async function importFs(id: string) {
9+
const name = `/@fs/${id}`.replace(/\\/g, '/')
10+
return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
611
}
712

813
export function getConfig(): ResolvedConfig {

packages/browser/src/client/vite.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export default defineConfig({
88
server: {
99
watch: { ignored: ['**/**'] },
1010
},
11+
esbuild: {
12+
legalComments: 'inline',
13+
},
1114
build: {
1215
minify: false,
1316
outDir: '../../dist/client',
@@ -19,7 +22,7 @@ export default defineConfig({
1922
orchestrator: resolve(__dirname, './orchestrator.html'),
2023
tester: resolve(__dirname, './tester/tester.html'),
2124
},
22-
external: [/__virtual_vitest__/],
25+
external: [/__virtual_vitest__/, '@vitest/browser/context'],
2326
},
2427
},
2528
plugins: [

packages/browser/src/node/esmInjector.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ export function injectDynamicImport(
2626
// s.update(node.start, node.end, viImportMetaKey)
2727
},
2828
onDynamicImport(node) {
29-
const replace = '__vitest_browser_runner__.wrapModule(() => import('
29+
const replaceString = '__vitest_browser_runner__.wrapModule(() => import('
30+
const importSubstring = code.substring(node.start, node.end)
31+
const hasIgnore = importSubstring.includes('/* @vite-ignore */')
3032
s.overwrite(
3133
node.start,
3234
(node.source as Positioned<Expression>).start,
33-
replace,
35+
replaceString + (hasIgnore ? '/* @vite-ignore */ ' : ''),
3436
)
3537
s.overwrite(node.end - 1, node.end, '))')
3638
},

packages/browser/src/node/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function createBrowserServer(
2525
const vite = await createServer({
2626
...project.options, // spread project config inlined in root workspace config
2727
base: '/',
28-
logLevel: 'error',
28+
logLevel: (process.env.VITEST_BROWSER_DEBUG as 'info') ?? 'info',
2929
mode: project.config.mode,
3030
configFile: configPath,
3131
// watch is handled by Vitest

packages/browser/src/node/plugin.ts

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,44 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
122122
define[`import.meta.env.${env}`] = stringValue
123123
}
124124

125+
const entries: string[] = [
126+
...browserTestFiles,
127+
...setupFiles,
128+
resolve(vitestDist, 'index.js'),
129+
resolve(vitestDist, 'browser.js'),
130+
resolve(vitestDist, 'runners.js'),
131+
resolve(vitestDist, 'utils.js'),
132+
...(project.config.snapshotSerializers || []),
133+
]
134+
135+
if (project.config.diff) {
136+
entries.push(project.config.diff)
137+
}
138+
139+
if (project.ctx.coverageProvider) {
140+
const coverage = project.ctx.config.coverage
141+
const provider = coverage.provider
142+
if (provider === 'v8') {
143+
const path = tryResolve('@vitest/coverage-v8', [project.ctx.config.root])
144+
if (path) {
145+
entries.push(path)
146+
}
147+
}
148+
else if (provider === 'istanbul') {
149+
const path = tryResolve('@vitest/coverage-istanbul', [project.ctx.config.root])
150+
if (path) {
151+
entries.push(path)
152+
}
153+
}
154+
else if (provider === 'custom' && coverage.customProviderModule) {
155+
entries.push(coverage.customProviderModule)
156+
}
157+
}
158+
125159
return {
126160
define,
127161
optimizeDeps: {
128-
entries: [
129-
...browserTestFiles,
130-
...setupFiles,
131-
resolve(vitestDist, 'index.js'),
132-
resolve(vitestDist, 'browser.js'),
133-
resolve(vitestDist, 'runners.js'),
134-
resolve(vitestDist, 'utils.js'),
135-
],
162+
entries,
136163
exclude: [
137164
'vitest',
138165
'vitest/utils',
@@ -163,6 +190,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
163190
'vitest > chai > loupe',
164191
'vitest > @vitest/runner > p-limit',
165192
'vitest > @vitest/utils > diff-sequences',
193+
'vitest > @vitest/utils > loupe',
166194
'@vitest/browser > @testing-library/user-event',
167195
'@vitest/browser > @testing-library/dom',
168196
],
@@ -235,10 +263,9 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
235263
enforce: 'post',
236264
async config(viteConfig) {
237265
// Enables using ignore hint for coverage providers with @preserve keyword
238-
if (viteConfig.esbuild !== false) {
239-
viteConfig.esbuild ||= {}
240-
viteConfig.esbuild.legalComments = 'inline'
241-
}
266+
viteConfig.esbuild ||= {}
267+
viteConfig.esbuild.legalComments = 'inline'
268+
242269
const server = resolveApiServerConfig(
243270
viteConfig.test?.browser || {},
244271
defaultBrowserPort,
@@ -294,8 +321,8 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
294321
{
295322
name: 'test-utils-rewrite',
296323
setup(build) {
297-
const _require = createRequire(import.meta.url)
298324
build.onResolve({ filter: /@vue\/test-utils/ }, (args) => {
325+
const _require = getRequire()
299326
// resolve to CJS instead of the browser because the browser version expects a global Vue object
300327
const resolved = _require.resolve(args.path, {
301328
paths: [args.importer],
@@ -313,6 +340,24 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
313340
]
314341
}
315342

343+
function tryResolve(path: string, paths: string[]) {
344+
try {
345+
const _require = getRequire()
346+
return _require.resolve(path, { paths })
347+
}
348+
catch {
349+
return undefined
350+
}
351+
}
352+
353+
let _require: NodeRequire
354+
function getRequire() {
355+
if (!_require) {
356+
_require = createRequire(import.meta.url)
357+
}
358+
return _require
359+
}
360+
316361
function resolveCoverageFolder(project: WorkspaceProject) {
317362
const options = project.ctx.config
318363
const htmlReporter = options.coverage?.enabled

0 commit comments

Comments
 (0)