Skip to content

Commit 2380cb9

Browse files
authored
fix(browser): correctly update inline snapshot if changed (#5925)
1 parent 489785d commit 2380cb9

File tree

20 files changed

+201
-50
lines changed

20 files changed

+201
-50
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ docs/public/sponsors
2323
docs/.vitepress/cache/
2424
!test/cli/fixtures/dotted-files/**/.cache
2525
test/browser/test/__screenshots__/**/*
26+
test/browser/fixtures/update-snapshot/basic.test.ts
2627
.vitest-reports

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

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import type { File, Task, TaskResultPack, VitestRunner } from '@vitest/runner'
1+
import type { File, Suite, Task, TaskResultPack, VitestRunner } from '@vitest/runner'
22
import type { ResolvedConfig, WorkerGlobalState } from 'vitest'
33
import type { VitestExecutor } from 'vitest/execute'
4+
import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners'
5+
import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser'
6+
import { TraceMap, originalPositionFor } from 'vitest/utils'
47
import { importId } from '../utils'
58
import { VitestBrowserSnapshotEnvironment } from './snapshot'
69
import { rpc } from './rpc'
@@ -28,6 +31,7 @@ export function createBrowserRunner(
2831
return class BrowserTestRunner extends runnerClass implements VitestRunner {
2932
public config: ResolvedConfig
3033
hashMap = browserHashMap
34+
public sourceMapCache = new Map<string, any>()
3135

3236
constructor(options: BrowserRunnerOptions) {
3337
super(options.config)
@@ -48,6 +52,22 @@ export function createBrowserRunner(
4852
}
4953
}
5054

55+
onBeforeRunSuite = async (suite: Suite | File) => {
56+
await Promise.all([
57+
super.onBeforeRunSuite?.(suite),
58+
(async () => {
59+
if ('filepath' in suite) {
60+
const map = await rpc().getBrowserFileSourceMap(suite.filepath)
61+
this.sourceMapCache.set(suite.filepath, map)
62+
const snapshotEnvironment = this.config.snapshotOptions.snapshotEnvironment
63+
if (snapshotEnvironment instanceof VitestBrowserSnapshotEnvironment) {
64+
snapshotEnvironment.addSourceMap(suite.filepath, map)
65+
}
66+
}
67+
})(),
68+
])
69+
}
70+
5171
onAfterRunFiles = async (files: File[]) => {
5272
const [coverage] = await Promise.all([
5373
coverageModule?.takeCoverage?.(),
@@ -75,7 +95,7 @@ export function createBrowserRunner(
7595

7696
if (this.config.includeTaskLocation) {
7797
try {
78-
await updateFilesLocations(files)
98+
await updateFilesLocations(files, this.sourceMapCache)
7999
}
80100
catch (_) {}
81101
}
@@ -112,13 +132,6 @@ export async function initiateRunner(
112132
if (cachedRunner) {
113133
return cachedRunner
114134
}
115-
const [
116-
{ VitestTestRunner, NodeBenchmarkRunner },
117-
{ takeCoverageInsideWorker, loadDiffConfig, loadSnapshotSerializers },
118-
] = await Promise.all([
119-
importId('vitest/runners') as Promise<typeof import('vitest/runners')>,
120-
importId('vitest/browser') as Promise<typeof import('vitest/browser')>,
121-
])
122135
const runnerClass
123136
= config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner
124137
const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, {
@@ -141,14 +154,9 @@ export async function initiateRunner(
141154
return runner
142155
}
143156

144-
async function updateFilesLocations(files: File[]) {
145-
const { loadSourceMapUtils } = (await importId(
146-
'vitest/utils',
147-
)) as typeof import('vitest/utils')
148-
const { TraceMap, originalPositionFor } = await loadSourceMapUtils()
149-
157+
async function updateFilesLocations(files: File[], sourceMaps: Map<string, any>) {
150158
const promises = files.map(async (file) => {
151-
const result = await rpc().getBrowserFileSourceMap(file.filepath)
159+
const result = sourceMaps.get(file.filepath) || await rpc().getBrowserFileSourceMap(file.filepath)
152160
if (!result) {
153161
return null
154162
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import type { SnapshotEnvironment } from 'vitest/snapshot'
2+
import { type ParsedStack, TraceMap, originalPositionFor } from 'vitest/utils'
23
import type { VitestBrowserClient } from '../client'
34

45
export class VitestBrowserSnapshotEnvironment implements SnapshotEnvironment {
6+
private sourceMaps = new Map<string, any>()
7+
private traceMaps = new Map<string, TraceMap>()
8+
9+
public addSourceMap(filepath: string, map: any) {
10+
this.sourceMaps.set(filepath, map)
11+
}
12+
513
getVersion(): string {
614
return '1'
715
}
@@ -29,6 +37,23 @@ export class VitestBrowserSnapshotEnvironment implements SnapshotEnvironment {
2937
removeSnapshotFile(filepath: string): Promise<void> {
3038
return rpc().removeSnapshotFile(filepath)
3139
}
40+
41+
processStackTrace(stack: ParsedStack): ParsedStack {
42+
const map = this.sourceMaps.get(stack.file)
43+
if (!map) {
44+
return stack
45+
}
46+
let traceMap = this.traceMaps.get(stack.file)
47+
if (!traceMap) {
48+
traceMap = new TraceMap(map)
49+
this.traceMaps.set(stack.file, traceMap)
50+
}
51+
const { line, column } = originalPositionFor(traceMap, stack)
52+
if (line != null && column != null) {
53+
return { ...stack, line, column }
54+
}
55+
return stack
56+
}
3257
}
3358

3459
function rpc(): VitestBrowserClient['rpc'] {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default defineConfig({
2727
name: 'virtual:msw',
2828
enforce: 'pre',
2929
resolveId(id) {
30-
if (id.startsWith('msw') || id.startsWith('vitest')) {
30+
if (id.startsWith('msw') || id.startsWith('vitest') || id.startsWith('@vitest/browser')) {
3131
return `/__virtual_vitest__?id=${encodeURIComponent(id)}`
3232
}
3333
},

packages/browser/src/node/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ export async function createBrowserServer(
4545
await vite.listen()
4646

4747
setupBrowserRpc(server)
48-
// if (project.config.browser.ui) {
49-
// setupUiRpc(project.ctx, server)
50-
// }
5148

5249
return server
5350
}

packages/browser/src/node/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
127127
'vitest/browser',
128128
'vitest/runners',
129129
'@vitest/utils',
130+
'@vitest/utils/source-map',
130131
'@vitest/runner',
131132
'@vitest/spy',
132133
'@vitest/utils/error',

packages/browser/src/node/pool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
4242

4343
if (!origin) {
4444
throw new Error(
45-
`Can't find browser origin URL for project "${project.config.name}"`,
45+
`Can't find browser origin URL for project "${project.getName()}" when running tests for files "${files.join('", "')}"`,
4646
)
4747
}
4848

packages/snapshot/src/port/inlineSnapshot.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export async function saveInlineSnapshots(
2323
await Promise.all(
2424
Array.from(files).map(async (file) => {
2525
const snaps = snapshots.filter(i => i.file === file)
26-
const code = (await environment.readSnapshotFile(file)) as string
26+
const code = await environment.readSnapshotFile(file) as string
2727
const s = new MagicString(code)
2828

2929
for (const snap of snaps) {
@@ -116,22 +116,46 @@ function prepareSnapString(snap: string, source: string, index: number) {
116116
.replace(/\$\{/g, '\\${')}\n${indent}${quote}`
117117
}
118118

119+
const toMatchInlineName = 'toMatchInlineSnapshot'
120+
const toThrowErrorMatchingInlineName = 'toThrowErrorMatchingInlineSnapshot'
121+
122+
// on webkit, the line number is at the end of the method, not at the start
123+
function getCodeStartingAtIndex(code: string, index: number) {
124+
const indexInline = index - toMatchInlineName.length
125+
if (code.slice(indexInline, index) === toMatchInlineName) {
126+
return {
127+
code: code.slice(indexInline),
128+
index: indexInline,
129+
}
130+
}
131+
const indexThrowInline = index - toThrowErrorMatchingInlineName.length
132+
if (code.slice(index - indexThrowInline, index) === toThrowErrorMatchingInlineName) {
133+
return {
134+
code: code.slice(index - indexThrowInline),
135+
index: index - indexThrowInline,
136+
}
137+
}
138+
return {
139+
code: code.slice(index),
140+
index,
141+
}
142+
}
143+
119144
const startRegex
120145
= /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/
121146
export function replaceInlineSnap(
122147
code: string,
123148
s: MagicString,
124-
index: number,
149+
currentIndex: number,
125150
newSnap: string,
126151
) {
127-
const codeStartingAtIndex = code.slice(index)
152+
const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex)
128153

129154
const startMatch = startRegex.exec(codeStartingAtIndex)
130155

131-
const firstKeywordMatch
132-
= /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec(
133-
codeStartingAtIndex,
134-
)
156+
const firstKeywordMatch = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec(
157+
codeStartingAtIndex,
158+
)
135159

136160
if (!startMatch || startMatch.index !== firstKeywordMatch?.index) {
137161
return replaceObjectSnap(code, s, index, newSnap)

packages/snapshot/src/port/state.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,20 @@ export default class SnapshotState {
140140
): void {
141141
this._dirty = true
142142
if (options.isInline) {
143+
const error = options.error || new Error('snapshot')
143144
const stacks = parseErrorStacktrace(
144-
options.error || new Error('snapshot'),
145+
error,
145146
{ ignoreStackEntries: [] },
146147
)
147-
const stack = this._inferInlineSnapshotStack(stacks)
148-
if (!stack) {
148+
const _stack = this._inferInlineSnapshotStack(stacks)
149+
if (!_stack) {
149150
throw new Error(
150151
`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify(
151152
stacks,
152153
)}`,
153154
)
154155
}
156+
const stack = this.environment.processStackTrace?.(_stack) || _stack
155157
// removing 1 column, because source map points to the wrong
156158
// location for js files, but `column-1` points to the same in both js/ts
157159
// https://github.com/vitejs/vite/issues/8657

packages/snapshot/src/types/environment.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ParsedStack } from '@vitest/utils'
2+
13
export interface SnapshotEnvironment {
24
getVersion: () => string
35
getHeader: () => string
@@ -6,6 +8,7 @@ export interface SnapshotEnvironment {
68
saveSnapshotFile: (filepath: string, snapshot: string) => Promise<void>
79
readSnapshotFile: (filepath: string) => Promise<string | null>
810
removeSnapshotFile: (filepath: string) => Promise<void>
11+
processStackTrace?: (stack: ParsedStack) => ParsedStack
912
}
1013

1114
export interface SnapshotEnvironmentOptions {

0 commit comments

Comments
 (0)