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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy