Skip to content

Commit 96f00c6

Browse files
fix(expect, jest-snapshot): Pass test.failing tests when containing failing snapshot matchers (#14313)
1 parent 813f231 commit 96f00c6

File tree

21 files changed

+220
-5
lines changed

21 files changed

+220
-5
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- `[babel-plugin-jest-hoist]` Use `denylist` instead of the deprecated `blacklist` for Babel 8 support ([#14109](https://github.com/jestjs/jest/pull/14109))
2020
- `[expect]` Check error instance type for `toThrow/toThrowError` ([#14576](https://github.com/jestjs/jest/pull/14576))
2121
- `[jest-circus]` [**BREAKING**] Prevent false test failures caused by promise rejections handled asynchronously ([#14315](https://github.com/jestjs/jest/pull/14315))
22+
- `[jest-circus, jest-expect, jest-snapshot]` Pass `test.failing` tests when containing failing snapshot matchers ([#14313](https://github.com/jestjs/jest/pull/14313))
2223
- `[jest-config]` Make sure to respect `runInBand` option ([#14578](https://github.com/facebook/jest/pull/14578))
2324
- `[@jest/expect-utils]` Fix comparison of `DataView` ([#14408](https://github.com/jestjs/jest/pull/14408))
2425
- `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`test.failing doesnt update or remove snapshots 1`] = `
4+
"// Jest Snapshot v1, https://goo.gl/fbAQLP
5+
6+
exports[\`snapshots not updated nor removed 1\`] = \`"1"\`;
7+
8+
exports[\`snapshots not updated nor removed 2\`] = \`"1"\`;
9+
10+
exports[\`snapshots not updated nor removed 3\`] = \`"1"\`;
11+
"
12+
`;
13+
14+
exports[`test.failing doesnt update or remove snapshots 2`] = `
15+
"/**
16+
* Copyright (c) Meta Platforms, Inc. and affiliates.
17+
*
18+
* This source code is licensed under the MIT license found in the
19+
* LICENSE file in the root directory of this source tree.
20+
*/
21+
22+
test.failing('inline snapshot not updated', () => {
23+
// eslint-disable-next-line quotes
24+
expect('0').toMatchInlineSnapshot(\`"1"\`);
25+
});
26+
"
27+
`;
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import * as path from 'path';
9+
import * as fs from 'graceful-fs';
10+
import {skipSuiteOnJasmine} from '@jest/test-utils';
11+
import runJest from '../runJest';
12+
13+
skipSuiteOnJasmine();
14+
15+
describe('test.failing', () => {
16+
describe('should pass when', () => {
17+
test.failing('snapshot matchers fails', () => {
18+
expect('0').toMatchSnapshot();
19+
});
20+
21+
test.failing('snapshot doesnt exist', () => {
22+
expect('0').toMatchSnapshot();
23+
});
24+
25+
test.failing('inline snapshot matchers fails', () => {
26+
expect('0').toMatchInlineSnapshot('0');
27+
});
28+
29+
test.failing('at least one snapshot fails', () => {
30+
expect('1').toMatchSnapshot();
31+
expect('0').toMatchSnapshot();
32+
});
33+
});
34+
35+
describe('should fail when', () => {
36+
test.each([
37+
['snapshot', 'snapshot'],
38+
['inline snapshot', 'inlineSnapshot'],
39+
])('%s matchers pass', (_, fileName) => {
40+
const dir = path.resolve(__dirname, '../test-failing-snapshot-all-pass');
41+
const result = runJest(dir, [`./__tests__/${fileName}.test.js`]);
42+
expect(result.exitCode).toBe(1);
43+
});
44+
});
45+
46+
it('doesnt update or remove snapshots', () => {
47+
const dir = path.resolve(__dirname, '../test-failing-snapshot');
48+
const result = runJest(dir, ['-u']);
49+
expect(result.exitCode).toBe(0);
50+
expect(result.stdout).not.toMatch(/snapshots? (written|removed|obsolete)/);
51+
52+
const snapshot = fs
53+
.readFileSync(
54+
path.resolve(dir, './__tests__/__snapshots__/snapshot.test.js.snap'),
55+
)
56+
.toString();
57+
expect(snapshot).toMatchSnapshot();
58+
59+
const inlineSnapshot = fs
60+
.readFileSync(path.resolve(dir, './__tests__/inlineSnapshot.test.js'))
61+
.toString();
62+
expect(inlineSnapshot).toMatchSnapshot();
63+
});
64+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`snapshots not updated 1`] = `"1"`;
4+
5+
exports[`snapshots not updated 2`] = `"1"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test.failing('inline snapshot not updated', () => {
9+
// eslint-disable-next-line quotes
10+
expect('1').toMatchInlineSnapshot(`"1"`);
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test.failing('snapshots not updated', () => {
9+
expect('1').toMatchSnapshot();
10+
expect('1').toMatchSnapshot();
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`snapshots not updated nor removed 1`] = `"1"`;
4+
5+
exports[`snapshots not updated nor removed 2`] = `"1"`;
6+
7+
exports[`snapshots not updated nor removed 3`] = `"1"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test.failing('inline snapshot not updated', () => {
9+
// eslint-disable-next-line quotes
10+
expect('0').toMatchInlineSnapshot(`"1"`);
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
test.failing('snapshots not updated nor removed', () => {
9+
expect('1').toMatchSnapshot();
10+
expect('0').toMatchSnapshot();
11+
expect('0').toMatchSnapshot();
12+
});
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,16 @@ const _addSnapshotData = (
109109
results: TestResult,
110110
snapshotState: SnapshotState,
111111
) => {
112-
for (const {fullName, status} of results.testResults) {
113-
if (status === 'pending' || status === 'failed') {
114-
// if test is skipped or failed, we don't want to mark
112+
for (const {fullName, status, failing} of results.testResults) {
113+
if (
114+
status === 'pending' ||
115+
status === 'failed' ||
116+
(failing && status === 'passed')
117+
) {
118+
// If test is skipped or failed, we don't want to mark
115119
// its snapshots as obsolete.
120+
// When tests called with test.failing pass, they've thrown an exception,
121+
// so maintain any snapshots after the error.
116122
snapshotState.markSnapshotsAsCheckedForTest(fullName);
117123
}
118124
}

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export const runAndTransformResultsToJestFormat = async ({
179179
return {
180180
ancestorTitles,
181181
duration: testResult.duration,
182+
failing: testResult.failing,
182183
failureDetails: testResult.errorsDetailed,
183184
failureMessages: testResult.errors,
184185
fullName: title
@@ -242,7 +243,10 @@ const handleSnapshotStateAfterRetry =
242243
const eventHandler = async (event: Circus.Event) => {
243244
switch (event.name) {
244245
case 'test_start': {
245-
jestExpect.setState({currentTestName: getTestID(event.test)});
246+
jestExpect.setState({
247+
currentTestName: getTestID(event.test),
248+
testFailing: event.test.failing,
249+
});
246250
break;
247251
}
248252
case 'test_done': {

packages/jest-circus/src/utils.ts

+2
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export const makeSingleTestResult = (
377377
duration: test.duration,
378378
errors: errorsDetailed.map(getErrorStack),
379379
errorsDetailed,
380+
failing: test.failing,
380381
invocations: test.invocations,
381382
location,
382383
numPassingAsserts: test.numPassingAsserts,
@@ -502,6 +503,7 @@ export const parseSingleTestResult = (
502503
return {
503504
ancestorTitles,
504505
duration: testResult.duration,
506+
failing: testResult.failing,
505507
failureDetails: testResult.errorsDetailed,
506508
failureMessages: Array.from(testResult.errors),
507509
fullName,

packages/jest-expect/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ type PromiseMatchers<T = unknown> = {
5252
declare module 'expect' {
5353
interface MatcherState {
5454
snapshotState: SnapshotState;
55+
/** Whether the test was called with `test.failing()` */
56+
testFailing?: boolean;
5557
}
5658
interface BaseExpect {
5759
addSnapshotSerializer: typeof addSerializer;

packages/jest-snapshot/src/State.ts

+19
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type SnapshotMatchOptions = {
3636
readonly inlineSnapshot?: string;
3737
readonly isInline: boolean;
3838
readonly error?: Error;
39+
readonly testFailing?: boolean;
3940
};
4041

4142
type SnapshotReturnOptions = {
@@ -197,6 +198,7 @@ export default class SnapshotState {
197198
inlineSnapshot,
198199
isInline,
199200
error,
201+
testFailing = false,
200202
}: SnapshotMatchOptions): SnapshotReturnOptions {
201203
this._counters.set(testName, (this._counters.get(testName) || 0) + 1);
202204
const count = Number(this._counters.get(testName));
@@ -230,6 +232,23 @@ export default class SnapshotState {
230232
this._snapshotData[key] = receivedSerialized;
231233
}
232234

235+
// In pure matching only runs, return the match result while skipping any updates
236+
// reports.
237+
if (testFailing) {
238+
if (hasSnapshot && !isInline) {
239+
// Retain current snapshot values.
240+
this._addSnapshot(key, expected, {error, isInline});
241+
}
242+
return {
243+
actual: removeExtraLineBreaks(receivedSerialized),
244+
count,
245+
expected:
246+
expected === undefined ? undefined : removeExtraLineBreaks(expected),
247+
key,
248+
pass,
249+
};
250+
}
251+
233252
// These are the conditions on when to write snapshots:
234253
// * There's no snapshot file in a non-CI environment.
235254
// * There is a snapshot file and we decided to update the snapshot.

packages/jest-snapshot/src/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,13 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
278278
config;
279279
let {received} = config;
280280

281-
context.dontThrow && context.dontThrow();
281+
/** If a test was ran with `test.failing`. Passed by Jest Circus. */
282+
const {testFailing = false} = context;
283+
284+
if (!testFailing && context.dontThrow) {
285+
// Supress errors while running tests
286+
context.dontThrow();
287+
}
282288

283289
const {currentConcurrentTestName, isNot, snapshotState} = context;
284290
const currentTestName =
@@ -360,6 +366,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
360366
inlineSnapshot,
361367
isInline,
362368
received,
369+
testFailing,
363370
testName: fullTestName,
364371
});
365372
const {actual, count, expected, pass} = result;

packages/jest-snapshot/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type SnapshotState from './State';
1313

1414
export interface Context extends MatcherContext {
1515
snapshotState: SnapshotState;
16+
testFailing?: boolean;
1617
}
1718

1819
// This is typically implemented by `jest-haste-map`'s `HasteFS`, but we

packages/jest-test-result/src/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export type TestResult = {
9191
console?: ConsoleBuffer;
9292
coverage?: CoverageMapData;
9393
displayName?: Config.DisplayName;
94+
/**
95+
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
96+
* was used.
97+
*/
98+
failing?: boolean;
9499
failureMessage?: string | null;
95100
leaks: boolean;
96101
memoryUsage?: number;

packages/jest-types/src/Circus.ts

+5
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ export type TestResult = {
203203
duration?: number | null;
204204
errors: Array<FormattedError>;
205205
errorsDetailed: Array<MatcherResults | unknown>;
206+
/**
207+
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
208+
* was used.
209+
*/
210+
failing?: boolean;
206211
invocations: number;
207212
status: TestStatus;
208213
location?: {column: number; line: number} | null;

packages/jest-types/src/TestResult.ts

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ type Callsite = {
2323
export type AssertionResult = {
2424
ancestorTitles: Array<string>;
2525
duration?: number | null;
26+
/**
27+
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
28+
* was used.
29+
*/
30+
failing?: boolean;
2631
failureDetails: Array<unknown>;
2732
failureMessages: Array<string>;
2833
fullName: string;

0 commit comments

Comments
 (0)