[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(browser): correctly update inline snapshot if changed (#5925)

authored by

Vladimir and committed by
GitHub
(Jun 19, 2024, 9:37 PM +0200) 2380cb95 489785d5

+201 -50
+1
.gitignore
··· 23 23 docs/.vitepress/cache/ 24 24 !test/cli/fixtures/dotted-files/**/.cache 25 25 test/browser/test/__screenshots__/**/* 26 + test/browser/fixtures/update-snapshot/basic.test.ts 26 27 .vitest-reports
+1
test/browser/package.json
··· 10 10 "test:safaridriver": "PROVIDER=webdriverio BROWSER=safari pnpm run test:unit", 11 11 "test-fixtures": "vitest", 12 12 "test-mocking": "vitest --root ./fixtures/mocking", 13 + "test-snapshots": "vitest --root ./fixtures/update-snapshot", 13 14 "coverage": "vitest --coverage.enabled --coverage.provider=istanbul --browser.headless=yes", 14 15 "test:browser:playwright": "PROVIDER=playwright vitest", 15 16 "test:browser:webdriverio": "PROVIDER=webdriverio vitest"
-1
test/browser/vitest.config.mts
··· 27 27 include: ['@vitest/cjs-lib'], 28 28 }, 29 29 test: { 30 - testTimeout: process.env.CI ? 120_000 : 10_000, 31 30 include: ['test/**.test.{ts,js}'], 32 31 // having a snapshot environment doesn't affect browser tests 33 32 snapshotEnvironment: './custom-snapshot-env.ts',
+23 -6
test/browser/specs/update-snapshot.test.ts
··· 1 - import fs from 'node:fs' 2 - import { expect, onTestFinished, test } from 'vitest' 3 - import { editFile } from '../../test-utils' 1 + import { readFileSync } from 'node:fs' 2 + import { expect, onTestFailed, onTestFinished, test } from 'vitest' 3 + import { createFile, editFile } from '../../test-utils' 4 4 import { runBrowserTests } from './utils' 5 5 6 6 test('update snapshot', async () => { 7 7 // setup wrong snapshot value 8 8 const snapshotPath = './fixtures/update-snapshot/__snapshots__/basic.test.ts.snap' 9 9 editFile(snapshotPath, data => data.replace('`1`', '`2`')) 10 + const basicFixturePath = './fixtures/update-snapshot/basic-fixture.ts' 11 + const testPath = './fixtures/update-snapshot/basic.test.ts' 12 + createFile(testPath, readFileSync(basicFixturePath, 'utf-8')) 10 13 11 14 // run vitest watch mode 12 - const { exitCode, ctx: vitest } = await runBrowserTests({ 15 + const ctx = await runBrowserTests({ 13 16 watch: true, 14 17 root: './fixtures/update-snapshot', 15 - reporters: ['tap-flat'], // use simple reporter to not pollute stdout 18 + reporters: ['default'], // use simple reporter to not pollute stdout 16 19 browser: { headless: true }, 20 + }, [], { 21 + server: { 22 + // ignore the watcher update 23 + watch: null, 24 + }, 17 25 }) 26 + const { exitCode, ctx: vitest } = ctx 18 27 onTestFinished(() => vitest.close()) 28 + onTestFailed(() => { 29 + console.error(ctx.stdout) 30 + console.error(ctx.stderr) 31 + }) 19 32 20 33 // test fails 21 34 expect(exitCode).toBe(1) ··· 28 41 await vitest.updateSnapshot() 29 42 30 43 // verify snapshot value is updated 31 - const snapshotData = await fs.promises.readFile(snapshotPath, 'utf-8') 44 + const snapshotData = readFileSync(snapshotPath, 'utf-8') 32 45 expect(snapshotData).toContain('`1`') 46 + 47 + const testFile = readFileSync(testPath, 'utf-8') 48 + expect(testFile).toContain('expect(fn).toMatchInlineSnapshot(`[MockFunction spy]`)') 49 + expect(testFile).toMatchSnapshot() 33 50 34 51 // test passes 35 52 expect(vitest.state.getFiles()[0].result.state).toBe('pass')
+7 -2
test/browser/specs/utils.ts
··· 1 1 import { readFile } from 'node:fs/promises' 2 + import type { UserConfig as ViteUserConfig } from 'vite' 2 3 import type { UserConfig } from 'vitest' 3 4 import { runVitest } from '../../test-utils' 4 5 5 6 export const browser = process.env.BROWSER || (process.env.PROVIDER !== 'playwright' ? 'chromium' : 'chrome') 6 7 7 - export async function runBrowserTests(config?: Omit<UserConfig, 'browser'> & { browser?: Partial<UserConfig['browser']> }, include?: string[]) { 8 + export async function runBrowserTests( 9 + config?: Omit<UserConfig, 'browser'> & { browser?: Partial<UserConfig['browser']> }, 10 + include?: string[], 11 + viteOverrides?: Partial<ViteUserConfig>, 12 + ) { 8 13 const result = await runVitest({ 9 14 watch: false, 10 15 reporters: 'none', ··· 13 18 headless: browser !== 'safari', 14 19 ...config?.browser, 15 20 } as UserConfig['browser'], 16 - }, include) 21 + }, include, 'test', viteOverrides) 17 22 18 23 const browserResult = await readFile('./browser.json', 'utf-8') 19 24 const browserResultJson = JSON.parse(browserResult)
+1 -1
packages/browser/src/client/vite.config.ts
··· 27 27 name: 'virtual:msw', 28 28 enforce: 'pre', 29 29 resolveId(id) { 30 - if (id.startsWith('msw') || id.startsWith('vitest')) { 30 + if (id.startsWith('msw') || id.startsWith('vitest') || id.startsWith('@vitest/browser')) { 31 31 return `/__virtual_vitest__?id=${encodeURIComponent(id)}` 32 32 } 33 33 },
-3
packages/browser/src/node/index.ts
··· 45 45 await vite.listen() 46 46 47 47 setupBrowserRpc(server) 48 - // if (project.config.browser.ui) { 49 - // setupUiRpc(project.ctx, server) 50 - // } 51 48 52 49 return server 53 50 }
+1
packages/browser/src/node/plugin.ts
··· 127 127 'vitest/browser', 128 128 'vitest/runners', 129 129 '@vitest/utils', 130 + '@vitest/utils/source-map', 130 131 '@vitest/runner', 131 132 '@vitest/spy', 132 133 '@vitest/utils/error',
+1 -1
packages/browser/src/node/pool.ts
··· 42 42 43 43 if (!origin) { 44 44 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('", "')}"`, 46 46 ) 47 47 } 48 48
+31 -7
packages/snapshot/src/port/inlineSnapshot.ts
··· 23 23 await Promise.all( 24 24 Array.from(files).map(async (file) => { 25 25 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 27 27 const s = new MagicString(code) 28 28 29 29 for (const snap of snaps) { ··· 116 116 .replace(/\$\{/g, '\\${')}\n${indent}${quote}` 117 117 } 118 118 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 + 119 144 const startRegex 120 145 = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\s\S]*\*\/\s*|\/\/.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF]))*[\w$]*(['"`)])/ 121 146 export function replaceInlineSnap( 122 147 code: string, 123 148 s: MagicString, 124 - index: number, 149 + currentIndex: number, 125 150 newSnap: string, 126 151 ) { 127 - const codeStartingAtIndex = code.slice(index) 152 + const { code: codeStartingAtIndex, index } = getCodeStartingAtIndex(code, currentIndex) 128 153 129 154 const startMatch = startRegex.exec(codeStartingAtIndex) 130 155 131 - const firstKeywordMatch 132 - = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec( 133 - codeStartingAtIndex, 134 - ) 156 + const firstKeywordMatch = /toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot/.exec( 157 + codeStartingAtIndex, 158 + ) 135 159 136 160 if (!startMatch || startMatch.index !== firstKeywordMatch?.index) { 137 161 return replaceObjectSnap(code, s, index, newSnap)
+5 -3
packages/snapshot/src/port/state.ts
··· 140 140 ): void { 141 141 this._dirty = true 142 142 if (options.isInline) { 143 + const error = options.error || new Error('snapshot') 143 144 const stacks = parseErrorStacktrace( 144 - options.error || new Error('snapshot'), 145 + error, 145 146 { ignoreStackEntries: [] }, 146 147 ) 147 - const stack = this._inferInlineSnapshotStack(stacks) 148 - if (!stack) { 148 + const _stack = this._inferInlineSnapshotStack(stacks) 149 + if (!_stack) { 149 150 throw new Error( 150 151 `@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify( 151 152 stacks, 152 153 )}`, 153 154 ) 154 155 } 156 + const stack = this.environment.processStackTrace?.(_stack) || _stack 155 157 // removing 1 column, because source map points to the wrong 156 158 // location for js files, but `column-1` points to the same in both js/ts 157 159 // https://github.com/vitejs/vite/issues/8657
+3
packages/snapshot/src/types/environment.ts
··· 1 + import type { ParsedStack } from '@vitest/utils' 2 + 1 3 export interface SnapshotEnvironment { 2 4 getVersion: () => string 3 5 getHeader: () => string ··· 6 8 saveSnapshotFile: (filepath: string, snapshot: string) => Promise<void> 7 9 readSnapshotFile: (filepath: string) => Promise<string | null> 8 10 removeSnapshotFile: (filepath: string) => Promise<void> 11 + processStackTrace?: (stack: ParsedStack) => ParsedStack 9 12 } 10 13 11 14 export interface SnapshotEnvironmentOptions {
+1 -4
packages/vitest/src/public/utils.ts
··· 1 1 export * from '@vitest/utils' 2 - 3 - export function loadSourceMapUtils() { 4 - return import('@vitest/utils/source-map') 5 - } 2 + export * from '@vitest/utils/source-map'
+27
test/browser/fixtures/update-snapshot/basic-fixture.ts
··· 1 + import { expect, test, vi } from 'vitest' 2 + 3 + interface _BasicInterface { 4 + willBeRemoved: boolean 5 + leavingSourceMapIncorrect: boolean 6 + } 7 + 8 + test('inline snapshot', () => { 9 + expect(1).toMatchInlineSnapshot('1') 10 + }) 11 + 12 + test('basic', () => { 13 + expect(1).toMatchSnapshot() 14 + }) 15 + 16 + test('renders inline mock snapshot', () => { 17 + const fn = vi.fn() 18 + expect(fn).toMatchInlineSnapshot() 19 + fn('hello', 'world', 2) 20 + expect(fn).toMatchInlineSnapshot() 21 + }) 22 + 23 + test('file snapshot', async () => { 24 + await expect('my snapshot content') 25 + .toMatchFileSnapshot('./__snapshots__/custom/my_snapshot') 26 + }) 27 +
-5
test/browser/fixtures/update-snapshot/basic.test.ts
··· 1 - import { expect, test } from 'vitest' 2 - 3 - test('basic', () => { 4 - expect(1).toMatchSnapshot() 5 - })
+1 -1
test/browser/fixtures/update-snapshot/vitest.config.ts
··· 7 7 pnpm -C test/browser test-fixtures --root fixtures/update-snapshot 8 8 */ 9 9 10 - const provider = process.env.PROVIDER || 'webdriverio' 10 + const provider = process.env.PROVIDER || 'playwright' 11 11 const browser = 12 12 process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') 13 13
+48
test/browser/specs/__snapshots__/update-snapshot.test.ts.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`update snapshot 1`] = ` 4 + "import { expect, test, vi } from 'vitest' 5 + 6 + interface _BasicInterface { 7 + willBeRemoved: boolean 8 + leavingSourceMapIncorrect: boolean 9 + } 10 + 11 + test('inline snapshot', () => { 12 + expect(1).toMatchInlineSnapshot('1') 13 + }) 14 + 15 + test('basic', () => { 16 + expect(1).toMatchSnapshot() 17 + }) 18 + 19 + test('renders inline mock snapshot', () => { 20 + const fn = vi.fn() 21 + expect(fn).toMatchInlineSnapshot(\`[MockFunction spy]\`) 22 + fn('hello', 'world', 2) 23 + expect(fn).toMatchInlineSnapshot(\` 24 + [MockFunction spy] { 25 + "calls": [ 26 + [ 27 + "hello", 28 + "world", 29 + 2, 30 + ], 31 + ], 32 + "results": [ 33 + { 34 + "type": "return", 35 + "value": undefined, 36 + }, 37 + ], 38 + } 39 + \`) 40 + }) 41 + 42 + test('file snapshot', async () => { 43 + await expect('my snapshot content') 44 + .toMatchFileSnapshot('./__snapshots__/custom/my_snapshot') 45 + }) 46 + 47 + " 48 + `;
+24 -16
packages/browser/src/client/tester/runner.ts
··· 1 - import type { File, Task, TaskResultPack, VitestRunner } from '@vitest/runner' 1 + import type { File, Suite, Task, TaskResultPack, VitestRunner } from '@vitest/runner' 2 2 import type { ResolvedConfig, WorkerGlobalState } from 'vitest' 3 3 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' 4 7 import { importId } from '../utils' 5 8 import { VitestBrowserSnapshotEnvironment } from './snapshot' 6 9 import { rpc } from './rpc' ··· 28 31 return class BrowserTestRunner extends runnerClass implements VitestRunner { 29 32 public config: ResolvedConfig 30 33 hashMap = browserHashMap 34 + public sourceMapCache = new Map<string, any>() 31 35 32 36 constructor(options: BrowserRunnerOptions) { 33 37 super(options.config) ··· 46 50 this.onCancel?.('test-failure') 47 51 } 48 52 } 53 + } 54 + 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 + ]) 49 69 } 50 70 51 71 onAfterRunFiles = async (files: File[]) => { ··· 75 95 76 96 if (this.config.includeTaskLocation) { 77 97 try { 78 - await updateFilesLocations(files) 98 + await updateFilesLocations(files, this.sourceMapCache) 79 99 } 80 100 catch (_) {} 81 101 } ··· 112 132 if (cachedRunner) { 113 133 return cachedRunner 114 134 } 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 - ]) 122 135 const runnerClass 123 136 = config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner 124 137 const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, { ··· 141 154 return runner 142 155 } 143 156 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>) { 150 158 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) 152 160 if (!result) { 153 161 return null 154 162 }
+25
packages/browser/src/client/tester/snapshot.ts
··· 1 1 import type { SnapshotEnvironment } from 'vitest/snapshot' 2 + import { type ParsedStack, TraceMap, originalPositionFor } from 'vitest/utils' 2 3 import type { VitestBrowserClient } from '../client' 3 4 4 5 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 + 5 13 getVersion(): string { 6 14 return '1' 7 15 } ··· 28 36 29 37 removeSnapshotFile(filepath: string): Promise<void> { 30 38 return rpc().removeSnapshotFile(filepath) 39 + } 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 31 56 } 32 57 } 33 58
+1
test/browser/fixtures/update-snapshot/__snapshots__/custom/my_snapshot
··· 1 + my snapshot content