Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Implement new "standalone" snapshots. #14380

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 37 additions & 29 deletions packages/jest-snapshot/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
import * as fs from 'graceful-fs';
import type {Config} from '@jest/types';
import {getStackTraceLines, getTopFrame} from 'jest-message-util';
import {InlineSnapshot, saveInlineSnapshots} from './InlineSnapshots';
import type {SnapshotData, SnapshotFormat} from './types';
import {saveInlineSnapshots} from './InlineSnapshots';
import type {
FilePersistedSnapshotData,
SnapshotData,
SnapshotFormat,
SnapshotKind,
} from './types';
import {
addExtraLineBreaks,
getSnapshotData,
Expand All @@ -33,8 +38,7 @@ export type SnapshotMatchOptions = {
readonly testName: string;
readonly received: unknown;
readonly key?: string;
readonly inlineSnapshot?: string;
readonly isInline: boolean;
readonly kind: SnapshotKind;
readonly error?: Error;
};

Expand All @@ -58,9 +62,8 @@ export default class SnapshotState {
private _index: number;
private readonly _updateSnapshot: Config.SnapshotUpdateState;
private _snapshotData: SnapshotData;
private readonly _initialData: SnapshotData;
private readonly _initialData: FilePersistedSnapshotData;
private readonly _snapshotPath: string;
private _inlineSnapshots: Array<InlineSnapshot>;
private readonly _uncheckedKeys: Set<string>;
private readonly _prettierPath: string | null;
private readonly _rootDir: string;
Expand All @@ -80,11 +83,10 @@ export default class SnapshotState {
options.updateSnapshot,
);
this._initialData = data;
this._snapshotData = data;
this._snapshotData = {...data, inline: []};
this._dirty = dirty;
this._prettierPath = options.prettierPath ?? null;
this._inlineSnapshots = [];
this._uncheckedKeys = new Set(Object.keys(this._snapshotData));
this._uncheckedKeys = new Set(Object.keys(this._snapshotData.grouped));
this._counters = new Map();
this._index = 0;
this.expand = options.expand || false;
Expand All @@ -108,11 +110,12 @@ export default class SnapshotState {
private _addSnapshot(
key: string,
receivedSerialized: string,
options: {isInline: boolean; error?: Error},
kind: SnapshotKind,
error: Error | undefined,
): void {
this._dirty = true;
if (options.isInline) {
const error = options.error || new Error();
if (kind.kind === 'inline') {
error ||= new Error();
const lines = getStackTraceLines(
removeLinesBeforeExternalMatcherTrap(error.stack || ''),
);
Expand All @@ -122,18 +125,17 @@ export default class SnapshotState {
"Jest: Couldn't infer stack frame for inline snapshot.",
);
}
this._inlineSnapshots.push({
this._snapshotData.inline.push({
frame,
snapshot: receivedSerialized,
});
} else {
this._snapshotData[key] = receivedSerialized;
this._snapshotData.grouped[key] = receivedSerialized;
}
}

clear(): void {
this._snapshotData = this._initialData;
this._inlineSnapshots = [];
this._snapshotData = {...this._initialData, inline: []};
this._counters = new Map();
this._index = 0;
this.added = 0;
Expand All @@ -143,8 +145,8 @@ export default class SnapshotState {
}

save(): SaveStatus {
const hasExternalSnapshots = Object.keys(this._snapshotData).length;
const hasInlineSnapshots = this._inlineSnapshots.length;
const hasExternalSnapshots = Object.keys(this._snapshotData.grouped).length;
const hasInlineSnapshots = this._snapshotData.inline.length;
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots;

const status: SaveStatus = {
Expand All @@ -158,7 +160,7 @@ export default class SnapshotState {
}
if (hasInlineSnapshots) {
saveInlineSnapshots(
this._inlineSnapshots,
this._snapshotData.inline,
this._rootDir,
this._prettierPath,
);
Expand All @@ -185,7 +187,9 @@ export default class SnapshotState {
removeUncheckedKeys(): void {
if (this._updateSnapshot === 'all' && this._uncheckedKeys.size) {
this._dirty = true;
this._uncheckedKeys.forEach(key => delete this._snapshotData[key]);
this._uncheckedKeys.forEach(
key => delete this._snapshotData.grouped[key],
);
this._uncheckedKeys.clear();
}
}
Expand All @@ -194,8 +198,7 @@ export default class SnapshotState {
testName,
received,
key,
inlineSnapshot,
isInline,
kind,
error,
}: SnapshotMatchOptions): SnapshotReturnOptions {
this._counters.set(testName, (this._counters.get(testName) || 0) + 1);
Expand All @@ -208,26 +211,31 @@ export default class SnapshotState {
// 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
// removed with `--updateSnapshot`.
if (!(isInline && this._snapshotData[key] !== undefined)) {
if (
kind.kind === 'grouped' ||
this._snapshotData.grouped[key] === undefined
) {
this._uncheckedKeys.delete(key);
}

const receivedSerialized = addExtraLineBreaks(
serialize(received, undefined, this.snapshotFormat),
);
const expected = isInline ? inlineSnapshot : this._snapshotData[key];
const expected =
kind.kind === 'inline' ? kind.value : this._snapshotData.grouped[key];
const pass = expected === receivedSerialized;
const hasSnapshot = expected !== undefined;
const snapshotIsPersisted = isInline || fs.existsSync(this._snapshotPath);
const snapshotIsPersisted =
kind.kind === 'inline' || fs.existsSync(this._snapshotPath);

if (pass && !isInline) {
if (pass && kind.kind === 'grouped') {
// Executing a snapshot file as JavaScript and writing the strings back
// when other snapshots have changed loses the proper escaping for some
// characters. Since we check every snapshot in every test, use the newly
// generated formatted string.
// Note that this is only relevant when a snapshot is added and the dirty
// flag is set.
this._snapshotData[key] = receivedSerialized;
this._snapshotData.grouped[key] = receivedSerialized;
}

// These are the conditions on when to write snapshots:
Expand All @@ -249,12 +257,12 @@ export default class SnapshotState {
} else {
this.added++;
}
this._addSnapshot(key, receivedSerialized, {error, isInline});
this._addSnapshot(key, receivedSerialized, kind, error);
} else {
this.matched++;
}
} else {
this._addSnapshot(key, receivedSerialized, {error, isInline});
this._addSnapshot(key, receivedSerialized, kind, error);
this.added++;
}

Expand Down
9 changes: 5 additions & 4 deletions packages/jest-snapshot/src/__tests__/printSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
printPropertiesAndReceived,
printSnapshotAndReceived,
} from '../printSnapshot';
import type {InlineSnapshotKind} from '../types';
import {serialize} from '../utils';

const aOpenForeground1 = styles.magenta.open;
Expand Down Expand Up @@ -517,11 +518,11 @@ describe('pass false', () => {
promise: '',
snapshotState: {
expand: false,
match({inlineSnapshot, received}) {
match({kind, received}) {
return {
actual: format(received),
count: 1,
expected: inlineSnapshot,
expected: (kind as InlineSnapshotKind).value,
pass: false,
};
},
Expand Down Expand Up @@ -711,11 +712,11 @@ describe('pass false', () => {
promise: '',
snapshotState: {
expand: false,
match({inlineSnapshot, received}) {
match({kind, received}) {
return {
actual: format(received),
count: 1,
expected: inlineSnapshot,
expected: (kind as InlineSnapshotKind).value,
pass: false,
};
},
Expand Down
14 changes: 7 additions & 7 deletions packages/jest-snapshot/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,27 @@ test('testNameToKey', () => {
expect(testNameToKey('abc cde ', 12)).toBe('abc cde 12');
});

test('saveSnapshotFile() works with \r\n', () => {
test('saveSnapshotFile() works with \\r\\n', () => {
const filename = path.join(__dirname, 'remove-newlines.snap');
const data = {
const grouped = {
myKey: '<div>\r\n</div>',
};

saveSnapshotFile(data, filename);
saveSnapshotFile({grouped}, filename);
expect(fs.writeFileSync).toHaveBeenCalledWith(
filename,
`// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` +
'exports[`myKey`] = `<div>\n</div>`;\n',
);
});

test('saveSnapshotFile() works with \r', () => {
test('saveSnapshotFile() works with \\r', () => {
const filename = path.join(__dirname, 'remove-newlines.snap');
const data = {
const grouped = {
myKey: '<div>\r</div>',
};

saveSnapshotFile(data, filename);
saveSnapshotFile({grouped}, filename);
expect(fs.writeFileSync).toHaveBeenCalledWith(
filename,
`// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` +
Expand Down Expand Up @@ -170,7 +170,7 @@ test('escaping', () => {
const writeFileSync = jest.mocked(fs.writeFileSync);

writeFileSync.mockReset();
saveSnapshotFile({key: data}, filename);
saveSnapshotFile({grouped: {key: data}}, filename);
const writtenData = writeFileSync.mock.calls[0][1];
expect(writtenData).toBe(
`// Jest Snapshot v1, ${SNAPSHOT_GUIDE_LINK}\n\n` +
Expand Down
40 changes: 20 additions & 20 deletions packages/jest-snapshot/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export const toMatchSnapshot: MatcherFunctionWithContext<
return _toMatchSnapshot({
context: this,
hint,
isInline: false,
kind: {kind: 'grouped'},
matcherName,
properties,
received,
Expand Down Expand Up @@ -261,20 +261,21 @@ export const toMatchInlineSnapshot: MatcherFunctionWithContext<

return _toMatchSnapshot({
context: this,
inlineSnapshot:
inlineSnapshot !== undefined
? stripAddedIndentation(inlineSnapshot)
: undefined,
isInline: true,
kind: {
kind: 'inline',
value:
inlineSnapshot !== undefined
? stripAddedIndentation(inlineSnapshot)
: undefined,
},
matcherName,
properties,
received,
});
};

const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
const {context, hint, inlineSnapshot, isInline, matcherName, properties} =
config;
const {context, hint, kind, matcherName, properties} = config;
let {received} = config;

context.dontThrow && context.dontThrow();
Expand Down Expand Up @@ -356,8 +357,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {

const result = snapshotState.match({
error: context.error,
inlineSnapshot,
isInline,
kind,
received,
testName: fullTestName,
});
Expand Down Expand Up @@ -420,7 +420,7 @@ export const toThrowErrorMatchingSnapshot: MatcherFunctionWithContext<
{
context: this,
hint,
isInline: false,
kind: {kind: 'grouped'},
matcherName,
received,
},
Expand Down Expand Up @@ -453,11 +453,13 @@ export const toThrowErrorMatchingInlineSnapshot: MatcherFunctionWithContext<
return _toThrowErrorMatchingSnapshot(
{
context: this,
inlineSnapshot:
inlineSnapshot !== undefined
? stripAddedIndentation(inlineSnapshot)
: undefined,
isInline: true,
kind: {
kind: 'inline',
value:
inlineSnapshot !== undefined
? stripAddedIndentation(inlineSnapshot)
: undefined,
},
matcherName,
received,
},
Expand All @@ -469,8 +471,7 @@ const _toThrowErrorMatchingSnapshot = (
config: MatchSnapshotConfig,
fromPromise?: boolean,
) => {
const {context, hint, inlineSnapshot, isInline, matcherName, received} =
config;
const {context, hint, kind, matcherName, received} = config;

context.dontThrow && context.dontThrow();

Expand Down Expand Up @@ -521,8 +522,7 @@ const _toThrowErrorMatchingSnapshot = (
return _toMatchSnapshot({
context,
hint,
inlineSnapshot,
isInline,
kind,
matcherName,
received: error.message,
});
Expand Down
6 changes: 3 additions & 3 deletions packages/jest-snapshot/src/printSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const matcherHintFromConfig = (
{
context: {isNot, promise},
hint,
inlineSnapshot,
kind,
matcherName,
properties,
}: MatchSnapshotConfig,
Expand All @@ -117,7 +117,7 @@ export const matcherHintFromConfig = (
if (typeof hint === 'string' && hint.length !== 0) {
options.secondArgument = HINT_ARG;
options.secondArgumentColor = BOLD_WEIGHT;
} else if (typeof inlineSnapshot === 'string') {
} else if (kind.kind === 'inline' && kind.value !== undefined) {
options.secondArgument = SNAPSHOT_ARG;
if (isUpdatable) {
options.secondArgumentColor = aSnapshotColor;
Expand All @@ -129,7 +129,7 @@ export const matcherHintFromConfig = (
if (typeof hint === 'string' && hint.length !== 0) {
expectedArgument = HINT_ARG;
options.expectedColor = BOLD_WEIGHT;
} else if (typeof inlineSnapshot === 'string') {
} else if (kind.kind === 'inline' && kind.value !== undefined) {
expectedArgument = SNAPSHOT_ARG;
if (isUpdatable) {
options.expectedColor = aSnapshotColor;
Expand Down