diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index cfa9508eb2d3..39858802feca 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -34,8 +34,13 @@ export interface Context { interface AssertOptions { received: unknown - filepath?: string - name?: string + filepath: string + name: string + /** + * Not required but needed for `SnapshotClient.clearTest` to implement test-retry behavior. + * @default name + */ + testId?: string message?: string isInline?: boolean properties?: object @@ -50,51 +55,55 @@ export interface SnapshotClientOptions { } export class SnapshotClient { - filepath?: string - name?: string - snapshotState: SnapshotState | undefined snapshotStateMap: Map = new Map() constructor(private options: SnapshotClientOptions = {}) {} - async startCurrentRun( + async setup( filepath: string, - name: string, options: SnapshotStateOptions, ): Promise { - this.filepath = filepath - this.name = name - - if (this.snapshotState?.testFilePath !== filepath) { - await this.finishCurrentRun() - - if (!this.getSnapshotState(filepath)) { - this.snapshotStateMap.set( - filepath, - await SnapshotState.create(filepath, options), - ) - } - this.snapshotState = this.getSnapshotState(filepath) + if (this.snapshotStateMap.has(filepath)) { + return } + this.snapshotStateMap.set( + filepath, + await SnapshotState.create(filepath, options), + ) } - getSnapshotState(filepath: string): SnapshotState { - return this.snapshotStateMap.get(filepath)! + async finish(filepath: string): Promise { + const state = this.getSnapshotState(filepath) + const result = await state.pack() + this.snapshotStateMap.delete(filepath) + return result + } + + skipTest(filepath: string, testName: string): void { + const state = this.getSnapshotState(filepath) + state.markSnapshotsAsCheckedForTest(testName) } - clearTest(): void { - this.filepath = undefined - this.name = undefined + clearTest(filepath: string, testId: string): void { + const state = this.getSnapshotState(filepath) + state.clearTest(testId) } - skipTestSnapshots(name: string): void { - this.snapshotState?.markSnapshotsAsCheckedForTest(name) + getSnapshotState(filepath: string): SnapshotState { + const state = this.snapshotStateMap.get(filepath) + if (!state) { + throw new Error( + `The snapshot state for '${filepath}' is not found. Did you call 'SnapshotClient.setup()'?`, + ) + } + return state } assert(options: AssertOptions): void { const { - filepath = this.filepath, - name = this.name, + filepath, + name, + testId = name, message, isInline = false, properties, @@ -109,6 +118,8 @@ export class SnapshotClient { throw new Error('Snapshot cannot be used outside of test') } + const snapshotState = this.getSnapshotState(filepath) + if (typeof properties === 'object') { if (typeof received !== 'object' || !received) { throw new Error( @@ -122,7 +133,7 @@ export class SnapshotClient { if (!pass) { throw createMismatchError( 'Snapshot properties mismatched', - this.snapshotState?.expand, + snapshotState.expand, received, properties, ) @@ -139,9 +150,8 @@ export class SnapshotClient { const testName = [name, ...(message ? [message] : [])].join(' > ') - const snapshotState = this.getSnapshotState(filepath) - const { actual, expected, key, pass } = snapshotState.match({ + testId, testName, received, isInline, @@ -153,7 +163,7 @@ export class SnapshotClient { if (!pass) { throw createMismatchError( `Snapshot \`${key || 'unknown'}\` mismatched`, - this.snapshotState?.expand, + snapshotState.expand, actual?.trim(), expected?.trim(), ) @@ -165,7 +175,7 @@ export class SnapshotClient { throw new Error('Raw snapshot is required') } - const { filepath = this.filepath, rawSnapshot } = options + const { filepath, rawSnapshot } = options if (rawSnapshot.content == null) { if (!filepath) { @@ -189,16 +199,6 @@ export class SnapshotClient { return this.assert(options) } - async finishCurrentRun(): Promise { - if (!this.snapshotState) { - return null - } - const result = await this.snapshotState.pack() - - this.snapshotState = undefined - return result - } - clear(): void { this.snapshotStateMap.clear() } diff --git a/packages/snapshot/src/port/inlineSnapshot.ts b/packages/snapshot/src/port/inlineSnapshot.ts index 2f252017fa88..09f059661655 100644 --- a/packages/snapshot/src/port/inlineSnapshot.ts +++ b/packages/snapshot/src/port/inlineSnapshot.ts @@ -9,6 +9,7 @@ import { export interface InlineSnapshot { snapshot: string + testId: string file: string line: number column: number diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index d12f66746068..89e777e4155f 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -23,6 +23,8 @@ import { saveRawSnapshots } from './rawSnapshot' import { addExtraLineBreaks, + CounterMap, + DefaultMap, getSnapshotData, keyToTestName, normalizeNewlines, @@ -47,24 +49,24 @@ interface SaveStatus { } export default class SnapshotState { - private _counters: Map + private _counters = new CounterMap() private _dirty: boolean private _updateSnapshot: SnapshotUpdateState private _snapshotData: SnapshotData private _initialData: SnapshotData private _inlineSnapshots: Array - private _inlineSnapshotStacks: Array + private _inlineSnapshotStacks: Array + private _testIdToKeys = new DefaultMap(() => []) private _rawSnapshots: Array private _uncheckedKeys: Set private _snapshotFormat: PrettyFormatOptions private _environment: SnapshotEnvironment private _fileExists: boolean - - added: number + private added = new CounterMap() + private matched = new CounterMap() + private unmatched = new CounterMap() + private updated = new CounterMap() expand: boolean - matched: number - unmatched: number - updated: number private constructor( public testFilePath: string, @@ -74,20 +76,15 @@ export default class SnapshotState { ) { const { data, dirty } = getSnapshotData(snapshotContent, options) this._fileExists = snapshotContent != null // TODO: update on watch? - this._initialData = data - this._snapshotData = data + this._initialData = { ...data } + this._snapshotData = { ...data } this._dirty = dirty this._inlineSnapshots = [] this._inlineSnapshotStacks = [] this._rawSnapshots = [] this._uncheckedKeys = new Set(Object.keys(this._snapshotData)) - this._counters = new Map() this.expand = options.expand || false - this.added = 0 - this.matched = 0 - this.unmatched = 0 this._updateSnapshot = options.updateSnapshot - this.updated = 0 this._snapshotFormat = { printBasicPrototype: false, escapeString: false, @@ -118,6 +115,31 @@ export default class SnapshotState { }) } + clearTest(testId: string): void { + // clear inline + this._inlineSnapshots = this._inlineSnapshots.filter(s => s.testId !== testId) + this._inlineSnapshotStacks = this._inlineSnapshotStacks.filter(s => s.testId !== testId) + + // clear file + for (const key of this._testIdToKeys.get(testId)) { + const name = keyToTestName(key) + const count = this._counters.get(name) + if (count > 0) { + if (key in this._snapshotData || key in this._initialData) { + this._snapshotData[key] = this._initialData[key] + } + this._counters.set(name, count - 1) + } + } + this._testIdToKeys.delete(testId) + + // clear stats + this.added.delete(testId) + this.updated.delete(testId) + this.matched.delete(testId) + this.unmatched.delete(testId) + } + protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null { // if called inside resolves/rejects, stacktrace is different const promiseIndex = stacks.findIndex(i => @@ -138,12 +160,13 @@ export default class SnapshotState { private _addSnapshot( key: string, receivedSerialized: string, - options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack }, + options: { rawSnapshot?: RawSnapshotInfo; stack?: ParsedStack; testId: string }, ): void { this._dirty = true if (options.stack) { this._inlineSnapshots.push({ snapshot: receivedSerialized, + testId: options.testId, ...options.stack, }) } @@ -158,17 +181,6 @@ export default class SnapshotState { } } - clear(): void { - this._snapshotData = this._initialData - // this._inlineSnapshots = [] - this._counters = new Map() - this.added = 0 - this.matched = 0 - this.unmatched = 0 - this.updated = 0 - this._dirty = false - } - async save(): Promise { const hasExternalSnapshots = Object.keys(this._snapshotData).length const hasInlineSnapshots = this._inlineSnapshots.length @@ -228,6 +240,7 @@ export default class SnapshotState { } match({ + testId, testName, received, key, @@ -236,12 +249,14 @@ export default class SnapshotState { error, rawSnapshot, }: SnapshotMatchOptions): SnapshotReturnOptions { - this._counters.set(testName, (this._counters.get(testName) || 0) + 1) - const count = Number(this._counters.get(testName)) + // this also increments counter for inline snapshots. maybe we shouldn't? + this._counters.increment(testName) + const count = this._counters.get(testName) if (!key) { key = testNameToKey(testName, count) } + this._testIdToKeys.get(testId).push(key) // Do not mark the snapshot as "checked" if the snapshot is inline and // there's an external snapshot. This way the external snapshot can be @@ -320,7 +335,7 @@ export default class SnapshotState { this._inlineSnapshots = this._inlineSnapshots.filter(s => !(s.file === stack!.file && s.line === stack!.line && s.column === stack!.column)) throw new Error('toMatchInlineSnapshot cannot be called multiple times at the same location.') } - this._inlineSnapshotStacks.push(stack) + this._inlineSnapshotStacks.push({ ...stack, testId }) } // These are the conditions on when to write snapshots: @@ -338,27 +353,29 @@ export default class SnapshotState { if (this._updateSnapshot === 'all') { if (!pass) { if (hasSnapshot) { - this.updated++ + this.updated.increment(testId) } else { - this.added++ + this.added.increment(testId) } this._addSnapshot(key, receivedSerialized, { stack, + testId, rawSnapshot, }) } else { - this.matched++ + this.matched.increment(testId) } } else { this._addSnapshot(key, receivedSerialized, { stack, + testId, rawSnapshot, }) - this.added++ + this.added.increment(testId) } return { @@ -371,7 +388,7 @@ export default class SnapshotState { } else { if (!pass) { - this.unmatched++ + this.unmatched.increment(testId) return { actual: removeExtraLineBreaks(receivedSerialized), count, @@ -384,7 +401,7 @@ export default class SnapshotState { } } else { - this.matched++ + this.matched.increment(testId) return { actual: '', count, @@ -415,10 +432,10 @@ export default class SnapshotState { const status = await this.save() snapshot.fileDeleted = status.deleted - snapshot.added = this.added - snapshot.matched = this.matched - snapshot.unmatched = this.unmatched - snapshot.updated = this.updated + snapshot.added = this.added.total() + snapshot.matched = this.matched.total() + snapshot.unmatched = this.unmatched.total() + snapshot.updated = this.updated.total() snapshot.unchecked = !status.deleted ? uncheckedCount : 0 snapshot.uncheckedKeys = Array.from(uncheckedKeys) diff --git a/packages/snapshot/src/port/utils.ts b/packages/snapshot/src/port/utils.ts index d3435a493354..64902bdef1da 100644 --- a/packages/snapshot/src/port/utils.ts +++ b/packages/snapshot/src/port/utils.ts @@ -265,3 +265,37 @@ export function deepMergeSnapshot(target: any, source: any): any { } return target } + +export class DefaultMap extends Map { + constructor( + private defaultFn: (key: K) => V, + entries?: Iterable, + ) { + super(entries) + } + + override get(key: K): V { + if (!this.has(key)) { + this.set(key, this.defaultFn(key)) + } + return super.get(key)! + } +} + +export class CounterMap extends DefaultMap { + constructor() { + super(() => 0) + } + + increment(key: K): void { + this.set(key, this.get(key) + 1) + } + + total(): number { + let total = 0 + for (const x of this.values()) { + total += x + } + return total + } +} diff --git a/packages/snapshot/src/types/index.ts b/packages/snapshot/src/types/index.ts index b5378e5a8cb6..f314d3befa15 100644 --- a/packages/snapshot/src/types/index.ts +++ b/packages/snapshot/src/types/index.ts @@ -24,6 +24,7 @@ export interface SnapshotStateOptions { } export interface SnapshotMatchOptions { + testId: string testName: string received: unknown key?: string diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index f0eff3d94adc..27bd424e4fef 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -44,17 +44,23 @@ function getError(expected: () => void | Error, promise: string | undefined) { throw new Error('snapshot function didn\'t throw') } -function getTestNames(test?: Test) { - if (!test) { - return {} - } +function getTestNames(test: Test) { return { filepath: test.file.filepath, name: getNames(test).slice(1).join(' > '), + testId: test.id, } } export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { + function getTest(assertionName: string, obj: object) { + const test = utils.flag(obj, 'vitest-test') + if (!test) { + throw new Error(`'${assertionName}' cannot be used without test context`) + } + return test as Test + } + for (const key of ['matchSnapshot', 'toMatchSnapshot']) { utils.addMethod( chai.Assertion.prototype, @@ -70,7 +76,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { throw new Error(`${key} cannot be used with "not"`) } const expected = utils.flag(this, 'object') - const test = utils.flag(this, 'vitest-test') + const test = getTest(key, this) if (typeof properties === 'string' && typeof message === 'undefined') { message = properties properties = undefined @@ -99,7 +105,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { } const error = new Error('resolves') const expected = utils.flag(this, 'object') - const test = utils.flag(this, 'vitest-test') as Test + const test = getTest('toMatchFileSnapshot', this) const errorMessage = utils.flag(this, 'message') const promise = getSnapshotClient().assertRaw({ @@ -136,8 +142,8 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { if (isNot) { throw new Error('toMatchInlineSnapshot cannot be used with "not"') } - const test = utils.flag(this, 'vitest-test') - const isInsideEach = test && (test.each || test.suite?.each) + const test = getTest('toMatchInlineSnapshot', this) + const isInsideEach = test.each || test.suite?.each if (isInsideEach) { throw new Error( 'InlineSnapshot cannot be used inside of test.each or describe.each', @@ -179,7 +185,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { ) } const expected = utils.flag(this, 'object') - const test = utils.flag(this, 'vitest-test') + const test = getTest('toThrowErrorMatchingSnapshot', this) const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') getSnapshotClient().assert({ @@ -204,8 +210,8 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { 'toThrowErrorMatchingInlineSnapshot cannot be used with "not"', ) } - const test = utils.flag(this, 'vitest-test') - const isInsideEach = test && (test.each || test.suite?.each) + const test = getTest('toThrowErrorMatchingInlineSnapshot', this) + const isInsideEach = test.each || test.suite?.each if (isInsideEach) { throw new Error( 'InlineSnapshot cannot be used inside of test.each or describe.each', diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 216e0fbd52c9..54c46adf3d08 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -44,11 +44,8 @@ export class VitestTestRunner implements VitestRunner { this.workerState.current = file } - onBeforeRunFiles() { - this.snapshotClient.clear() - } - onAfterRunFiles() { + this.snapshotClient.clear() this.workerState.current = undefined } @@ -62,22 +59,18 @@ export class VitestTestRunner implements VitestRunner { for (const test of getTests(suite)) { if (test.mode === 'skip') { const name = getNames(test).slice(1).join(' > ') - this.snapshotClient.skipTestSnapshots(name) + this.snapshotClient.skipTest(suite.file.filepath, name) } } - const result = await this.snapshotClient.finishCurrentRun() - if (result) { - await rpc().snapshotSaved(result) - } + const result = await this.snapshotClient.finish(suite.file.filepath) + await rpc().snapshotSaved(result) } this.workerState.current = suite.suite || suite.file } onAfterRunTask(test: Task) { - this.snapshotClient.clearTest() - if (this.config.logHeapUsage && typeof process !== 'undefined') { test.result!.heap = process.memoryUsage().heapUsed } @@ -116,11 +109,8 @@ export class VitestTestRunner implements VitestRunner { // initialize snapshot state before running file suite if (suite.mode !== 'skip' && 'filepath' in suite) { - // default "name" is irrelevant for Vitest since each snapshot assertion - // (e.g. `toMatchSnapshot`) specifies "filepath" / "name" pair explicitly - await this.snapshotClient.startCurrentRun( - (suite as File).filepath, - '__default_name_', + await this.snapshotClient.setup( + suite.file.filepath, this.workerState.config.snapshotOptions, ) } @@ -129,6 +119,7 @@ export class VitestTestRunner implements VitestRunner { } onBeforeTryTask(test: Task) { + this.snapshotClient.clearTest(test.file.filepath, test.id) setState( { assertionCalls: 0, @@ -138,7 +129,7 @@ export class VitestTestRunner implements VitestRunner { expectedAssertionsNumberErrorGen: null, testPath: test.file.filepath, currentTestName: getTestName(test), - snapshotState: this.snapshotClient.snapshotState, + snapshotState: this.snapshotClient.getSnapshotState(test.file.filepath), }, (globalThis as any)[GLOBAL_EXPECT], ) diff --git a/test/snapshots/package.json b/test/snapshots/package.json index 2e062a10cd7f..a5b783fedc9a 100644 --- a/test/snapshots/package.json +++ b/test/snapshots/package.json @@ -3,10 +3,13 @@ "type": "module", "private": true, "scripts": { - "test": "pnpm run test:generate && pnpm run test:update && pnpm run test:snaps", - "test:generate": "node tools/generate-inline-test.mjs", + "test": "pnpm run test:generate && pnpm run test:update && pnpm test:update-new && pnpm test:update-none && pnpm run test:snaps", + "test:generate": "rm -rf ./test-update && cp -r ./test/fixtures/test-update ./test-update", "test:snaps": "vitest run --dir test", "test:update": "vitest run -u --dir test-update", + "test:update-none": "CI=true vitest run --dir test-update", + "test:update-new": "CI=false vitest run --dir test-update", + "test:fixtures": "vitest", "coverage": "vitest run --coverage" }, "dependencies": { diff --git a/test/snapshots/test-update/__snapshots__/retry-file.test.ts.snap b/test/snapshots/test-update/__snapshots__/retry-file.test.ts.snap new file mode 100644 index 000000000000..55fb93d2deec --- /dev/null +++ b/test/snapshots/test-update/__snapshots__/retry-file.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`file repeats 1`] = `"foo"`; + +exports[`file repeats many 1`] = `"foo"`; + +exports[`file repeats many 2`] = `"bar"`; + +exports[`file retry 1`] = `"foo"`; + +exports[`file retry many 1`] = `"foo"`; + +exports[`file retry many 2`] = `"bar"`; + +exports[`file retry partial 1`] = `"foo"`; + +exports[`file retry partial 2`] = `"bar"`; diff --git a/test/snapshots/test-update/__snapshots__/same-title-file.test.js.snap b/test/snapshots/test-update/__snapshots__/same-title-file.test.js.snap new file mode 100644 index 000000000000..acf7b18f1f93 --- /dev/null +++ b/test/snapshots/test-update/__snapshots__/same-title-file.test.js.snap @@ -0,0 +1,11 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`same title exist 1`] = `"a"`; + +exports[`same title exist 2`] = `"b"`; + +exports[`same title new 1`] = `"a"`; + +exports[`same title new 2`] = `"b"`; + +exports[`same title new 3`] = `"c"`; diff --git a/test/snapshots/test-update/inline-test-template-concurrent.test.js b/test/snapshots/test-update/inline-concurrent.test.js similarity index 100% rename from test/snapshots/test-update/inline-test-template-concurrent.test.js rename to test/snapshots/test-update/inline-concurrent.test.js diff --git a/test/snapshots/test-update/snapshots-inline-js.test.js b/test/snapshots/test-update/inline.test.js similarity index 100% rename from test/snapshots/test-update/snapshots-inline-js.test.js rename to test/snapshots/test-update/inline.test.js diff --git a/test/snapshots/test-update/retry-file.test.ts b/test/snapshots/test-update/retry-file.test.ts new file mode 100644 index 000000000000..a8a31a8f58d8 --- /dev/null +++ b/test/snapshots/test-update/retry-file.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest' + +test('file repeats', { repeats: 1 }, () => { + expect('foo').toMatchSnapshot() +}) + +test('file retry', { retry: 1 }, (ctx) => { + expect('foo').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) + +test('file repeats many', { repeats: 1 }, () => { + expect('foo').toMatchSnapshot() + expect('bar').toMatchSnapshot() +}) + +test('file retry many', { retry: 1 }, (ctx) => { + expect('foo').toMatchSnapshot() + expect('bar').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) + +test('file retry partial', { retry: 1 }, (ctx) => { + expect('foo').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } + expect('bar').toMatchSnapshot() +}) diff --git a/test/snapshots/test-update/retry-inline.test.ts b/test/snapshots/test-update/retry-inline.test.ts new file mode 100644 index 000000000000..0d93ccf02176 --- /dev/null +++ b/test/snapshots/test-update/retry-inline.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' + +test('inline repeats', { repeats: 1 }, () => { + expect('foo').toMatchInlineSnapshot(`"foo"`) +}) + +test('inline retry', { retry: 1 }, (ctx) => { + expect('foo').toMatchInlineSnapshot(`"foo"`) + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) diff --git a/test/snapshots/test-update/same-title-file.test.js b/test/snapshots/test-update/same-title-file.test.js new file mode 100644 index 000000000000..cf4955532f88 --- /dev/null +++ b/test/snapshots/test-update/same-title-file.test.js @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest' + +test('same title exist', () => { + // correct entry exists in .snap + expect('a').toMatchSnapshot() +}) + +test('same title exist', () => { + // wrong entry exists in .snap + expect('b').toMatchSnapshot() +}) + +test('same title new', () => { + expect('a').toMatchSnapshot() +}) + +test('same title new', () => { + expect('b').toMatchSnapshot() + expect('c').toMatchSnapshot() +}) diff --git a/test/snapshots/test-update/same-title-inline.test.js b/test/snapshots/test-update/same-title-inline.test.js new file mode 100644 index 000000000000..84fab3fe3448 --- /dev/null +++ b/test/snapshots/test-update/same-title-inline.test.js @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest' + +test('same title', () => { + expect('new').toMatchInlineSnapshot(`"new"`) + expect('new').toMatchInlineSnapshot(`"new"`) +}) + +test('same title', () => { + expect('a').toMatchInlineSnapshot(`"a"`) + expect('a').toMatchInlineSnapshot(`"a"`) +}) + +test('same title', () => { + expect('b').toMatchInlineSnapshot(`"b"`) + expect('b').toMatchInlineSnapshot(`"b"`) +}) diff --git a/test/snapshots/test/__snapshots__/snapshots.test.ts.snap b/test/snapshots/test/__snapshots__/test-update.test.ts.snap similarity index 69% rename from test/snapshots/test/__snapshots__/snapshots.test.ts.snap rename to test/snapshots/test/__snapshots__/test-update.test.ts.snap index d42ea091ab42..3321ee7ef68e 100644 --- a/test/snapshots/test/__snapshots__/snapshots.test.ts.snap +++ b/test/snapshots/test/__snapshots__/test-update.test.ts.snap @@ -1,23 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`concurrent snapshot update 1`] = ` -"import { it } from 'vitest' - -it.concurrent('1st', ({ expect }) => { - expect('hi1').toMatchInlineSnapshot(\`"hi1"\`) -}) - -it.concurrent('2nd', ({ expect }) => { - expect('hi2').toMatchInlineSnapshot(\`"hi2"\`) -}) - -it.concurrent('3rd', ({ expect }) => { - expect('hi3').toMatchInlineSnapshot(\`"hi3"\`) -}) -" -`; - -exports[`js snapshots generated correctly 1`] = ` +exports[`inline.test.js 1`] = ` "import { describe, expect, test } from 'vitest' // when snapshots are generated Vitest reruns \`toMatchInlineSnapshot\` checks @@ -89,3 +72,56 @@ describe('snapshots with properties', () => { }) " `; + +exports[`inline-concurrent.test.js 1`] = ` +"import { it } from 'vitest' + +it.concurrent('1st', ({ expect }) => { + expect('hi1').toMatchInlineSnapshot(\`"hi1"\`) +}) + +it.concurrent('2nd', ({ expect }) => { + expect('hi2').toMatchInlineSnapshot(\`"hi2"\`) +}) + +it.concurrent('3rd', ({ expect }) => { + expect('hi3').toMatchInlineSnapshot(\`"hi3"\`) +}) +" +`; + +exports[`retry-inline.test.ts 1`] = ` +"import { expect, test } from 'vitest' + +test('inline repeats', { repeats: 1 }, () => { + expect('foo').toMatchInlineSnapshot(\`"foo"\`) +}) + +test('inline retry', { retry: 1 }, (ctx) => { + expect('foo').toMatchInlineSnapshot(\`"foo"\`) + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) +" +`; + +exports[`same-title-inline.test.js 1`] = ` +"import { expect, test } from 'vitest' + +test('same title', () => { + expect('new').toMatchInlineSnapshot(\`"new"\`) + expect('new').toMatchInlineSnapshot(\`"new"\`) +}) + +test('same title', () => { + expect('a').toMatchInlineSnapshot(\`"a"\`) + expect('a').toMatchInlineSnapshot(\`"a"\`) +}) + +test('same title', () => { + expect('b').toMatchInlineSnapshot(\`"b"\`) + expect('b').toMatchInlineSnapshot(\`"b"\`) +}) +" +`; diff --git a/test/snapshots/test/fixtures/summary/__snapshots__/basic.test.ts.snap b/test/snapshots/test/fixtures/summary/__snapshots__/basic.test.ts.snap new file mode 100644 index 000000000000..708517958d0f --- /dev/null +++ b/test/snapshots/test/fixtures/summary/__snapshots__/basic.test.ts.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`file normal 1`] = `"@SNAP4"`; + +exports[`file repeats 1`] = `"@SNAP5"`; + +exports[`file repeats many 1`] = `"@SNAP7"`; + +exports[`file repeats many 2`] = `"@SNAP8"`; + +exports[`file retry 1`] = `"@SNAP6"`; + +exports[`file retry many 1`] = `"@SNAP9"`; + +exports[`file retry many 2`] = `"@SNAP10"`; + +exports[`file retry partial 1`] = `"@SNAP11"`; + +exports[`file retry partial 2`] = `"@SNAP12"`; diff --git a/test/snapshots/test/fixtures/summary/basic.test.ts b/test/snapshots/test/fixtures/summary/basic.test.ts new file mode 100644 index 000000000000..98ae9ea0a8ab --- /dev/null +++ b/test/snapshots/test/fixtures/summary/basic.test.ts @@ -0,0 +1,52 @@ +import { expect, test } from 'vitest' + +test('inline normal', () => { + expect('@SNAP1').toMatchInlineSnapshot(`"@SNAP1"`) +}) + +test('inline repeats', { repeats: 1 }, () => { + expect('@SNAP2').toMatchInlineSnapshot(`"@SNAP2"`) +}) + +test('inline retry', { retry: 1 }, (ctx) => { + expect('@SNAP3').toMatchInlineSnapshot(`"@SNAP3"`) + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) + +test('file normal', () => { + expect('@SNAP4').toMatchSnapshot() +}) + +test('file repeats', { repeats: 1 }, () => { + expect('@SNAP5').toMatchSnapshot() +}) + +test('file retry', { retry: 1 }, (ctx) => { + expect('@SNAP6').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('@retry') + } +}) + +test('file repeats many', { repeats: 1 }, () => { + expect('@SNAP7').toMatchSnapshot() + expect('@SNAP8').toMatchSnapshot() +}) + +test('file retry many', { retry: 1 }, (ctx) => { + expect('@SNAP9').toMatchSnapshot() + expect('@SNAP10').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('@retry') + } +}) + +test('file retry partial', { retry: 1 }, (ctx) => { + expect('@SNAP11').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('@retry') + } + expect('@SNAP12').toMatchSnapshot() +}) diff --git a/test/snapshots/test/fixtures/summary/vitest.config.ts b/test/snapshots/test/fixtures/summary/vitest.config.ts new file mode 100644 index 000000000000..b1c6ea436a54 --- /dev/null +++ b/test/snapshots/test/fixtures/summary/vitest.config.ts @@ -0,0 +1 @@ +export default {} diff --git a/test/snapshots/test/fixtures/test-update/__snapshots__/retry-file.test.ts.snap b/test/snapshots/test/fixtures/test-update/__snapshots__/retry-file.test.ts.snap new file mode 100644 index 000000000000..55fb93d2deec --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/__snapshots__/retry-file.test.ts.snap @@ -0,0 +1,17 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`file repeats 1`] = `"foo"`; + +exports[`file repeats many 1`] = `"foo"`; + +exports[`file repeats many 2`] = `"bar"`; + +exports[`file retry 1`] = `"foo"`; + +exports[`file retry many 1`] = `"foo"`; + +exports[`file retry many 2`] = `"bar"`; + +exports[`file retry partial 1`] = `"foo"`; + +exports[`file retry partial 2`] = `"bar"`; diff --git a/test/snapshots/test/fixtures/test-update/__snapshots__/same-title-file.test.js.snap b/test/snapshots/test/fixtures/test-update/__snapshots__/same-title-file.test.js.snap new file mode 100644 index 000000000000..90d340867340 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/__snapshots__/same-title-file.test.js.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`same title exist 1`] = `"a"`; + +exports[`same title exist 2`] = `"wrong"`; diff --git a/test/snapshots/tools/inline-test-template-concurrent.js b/test/snapshots/test/fixtures/test-update/inline-concurrent.test.js similarity index 100% rename from test/snapshots/tools/inline-test-template-concurrent.js rename to test/snapshots/test/fixtures/test-update/inline-concurrent.test.js diff --git a/test/snapshots/tools/inline-test-template.js b/test/snapshots/test/fixtures/test-update/inline.test.js similarity index 100% rename from test/snapshots/tools/inline-test-template.js rename to test/snapshots/test/fixtures/test-update/inline.test.js diff --git a/test/snapshots/test/fixtures/test-update/retry-file.test.ts b/test/snapshots/test/fixtures/test-update/retry-file.test.ts new file mode 100644 index 000000000000..a8a31a8f58d8 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/retry-file.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest' + +test('file repeats', { repeats: 1 }, () => { + expect('foo').toMatchSnapshot() +}) + +test('file retry', { retry: 1 }, (ctx) => { + expect('foo').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) + +test('file repeats many', { repeats: 1 }, () => { + expect('foo').toMatchSnapshot() + expect('bar').toMatchSnapshot() +}) + +test('file retry many', { retry: 1 }, (ctx) => { + expect('foo').toMatchSnapshot() + expect('bar').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) + +test('file retry partial', { retry: 1 }, (ctx) => { + expect('foo').toMatchSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } + expect('bar').toMatchSnapshot() +}) diff --git a/test/snapshots/test/fixtures/test-update/retry-inline.test.ts b/test/snapshots/test/fixtures/test-update/retry-inline.test.ts new file mode 100644 index 000000000000..9af58c26d837 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/retry-inline.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' + +test('inline repeats', { repeats: 1 }, () => { + expect('foo').toMatchInlineSnapshot() +}) + +test('inline retry', { retry: 1 }, (ctx) => { + expect('foo').toMatchInlineSnapshot() + if (ctx.task.result?.retryCount === 0) { + throw new Error('boom') + } +}) diff --git a/test/snapshots/test/fixtures/test-update/same-title-file.test.js b/test/snapshots/test/fixtures/test-update/same-title-file.test.js new file mode 100644 index 000000000000..cf4955532f88 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/same-title-file.test.js @@ -0,0 +1,20 @@ +import { expect, test } from 'vitest' + +test('same title exist', () => { + // correct entry exists in .snap + expect('a').toMatchSnapshot() +}) + +test('same title exist', () => { + // wrong entry exists in .snap + expect('b').toMatchSnapshot() +}) + +test('same title new', () => { + expect('a').toMatchSnapshot() +}) + +test('same title new', () => { + expect('b').toMatchSnapshot() + expect('c').toMatchSnapshot() +}) diff --git a/test/snapshots/test/fixtures/test-update/same-title-inline.test.js b/test/snapshots/test/fixtures/test-update/same-title-inline.test.js new file mode 100644 index 000000000000..27726affdd57 --- /dev/null +++ b/test/snapshots/test/fixtures/test-update/same-title-inline.test.js @@ -0,0 +1,16 @@ +import { expect, test } from 'vitest' + +test('same title', () => { + expect('new').toMatchInlineSnapshot() + expect('new').toMatchInlineSnapshot() +}) + +test('same title', () => { + expect('a').toMatchInlineSnapshot(`"a"`) + expect('a').toMatchInlineSnapshot(`"a"`) +}) + +test('same title', () => { + expect('b').toMatchInlineSnapshot(`"wrong"`) + expect('b').toMatchInlineSnapshot(`"wrong"`) +}) diff --git a/test/snapshots/test/snapshots.test.ts b/test/snapshots/test/snapshots.test.ts index 13556156b4c2..1f07adf0d2ec 100644 --- a/test/snapshots/test/snapshots.test.ts +++ b/test/snapshots/test/snapshots.test.ts @@ -1,5 +1,3 @@ -import fs from 'node:fs/promises' -import pathe from 'pathe' import { expect, test } from 'vitest' import { editFile, runVitest } from '../../test-utils' @@ -14,18 +12,6 @@ test('non default snapshot format', () => { `) }) -test('js snapshots generated correctly', async () => { - const path = pathe.resolve(__dirname, '../test-update/snapshots-inline-js.test.js') - const content = await fs.readFile(path, 'utf8') - expect(content).toMatchSnapshot() -}) - -test('concurrent snapshot update', async () => { - const path = pathe.resolve(__dirname, '../test-update/inline-test-template-concurrent.test.js') - const content = await fs.readFile(path, 'utf8') - expect(content).toMatchSnapshot() -}) - test('--update works for workspace project', async () => { // setup wrong snapshot value editFile( diff --git a/test/snapshots/test/summary.test.ts b/test/snapshots/test/summary.test.ts new file mode 100644 index 000000000000..5a438afba53c --- /dev/null +++ b/test/snapshots/test/summary.test.ts @@ -0,0 +1,42 @@ +import fs from 'node:fs' +import { join } from 'node:path' +import { expect, test } from 'vitest' +import { runVitest } from '../../test-utils' + +function fsUpdate(file: string, updateFn: (data: string) => string) { + fs.writeFileSync(file, updateFn(fs.readFileSync(file, 'utf-8'))) +} + +test('summary', async () => { + // cleanup snapshot + const dir = join(import.meta.dirname, 'fixtures/summary') + const testFile = join(dir, 'basic.test.ts') + const snapshotFile = join(dir, '__snapshots__/basic.test.ts.snap') + fsUpdate(testFile, s => s.replace(/`"@SNAP\d"`/g, '')) + fs.rmSync(snapshotFile, { recursive: true, force: true }) + + // write everything + let vitest = await runVitest({ + root: 'test/fixtures/summary', + update: true, + }) + expect(vitest.stdout).toContain('Snapshots 12 written') + + // write partially + fsUpdate(testFile, s => s.replace('`"@SNAP2"`', '')) + fsUpdate(snapshotFile, s => s.replace('exports[`file repeats 1`] = `"@SNAP5"`;', '')) + vitest = await runVitest({ + root: 'test/fixtures/summary', + update: true, + }) + expect(vitest.stdout).toContain('Snapshots 2 written') + + // update partially + fsUpdate(testFile, s => s.replace('`"@SNAP2"`', '`"@WRONG"`')) + fsUpdate(snapshotFile, s => s.replace('`"@SNAP5"`', '`"@WRONG"`')) + vitest = await runVitest({ + root: 'test/fixtures/summary', + update: true, + }) + expect(vitest.stdout).toContain('Snapshots 2 updated') +}) diff --git a/test/snapshots/test/test-update.test.ts b/test/snapshots/test/test-update.test.ts new file mode 100644 index 000000000000..7e76b777cc37 --- /dev/null +++ b/test/snapshots/test/test-update.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest' + +const entries = import.meta.glob('../test-update/*inline*', { eager: true, query: 'raw' }) +for (const [file, mod] of Object.entries(entries)) { + test(file.split('/').at(-1)!, () => { + expect((mod as any).default).toMatchSnapshot() + }) +}