Skip to content

Commit

Permalink
Rewrite snapshot types to be a discriminated union in preparation for…
Browse files Browse the repository at this point in the history
… adding a new variant.
  • Loading branch information
seansfkelley committed Aug 1, 2023
1 parent 0fd5b1c commit ffaaa2d
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 76 deletions.
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

0 comments on commit ffaaa2d

Please sign in to comment.