fix(expect, jest-snapshot): Pass `test.failing` tests when containing failing snapshot matchers (#14313)

This commit is contained in:
Khaled Elmorsy 2023-10-03 11:06:22 +03:00 committed by GitHub
parent 813f23184e
commit 96f00c66a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 220 additions and 5 deletions

View File

@ -19,6 +19,7 @@
- `[babel-plugin-jest-hoist]` Use `denylist` instead of the deprecated `blacklist` for Babel 8 support ([#14109](https://github.com/jestjs/jest/pull/14109)) - `[babel-plugin-jest-hoist]` Use `denylist` instead of the deprecated `blacklist` for Babel 8 support ([#14109](https://github.com/jestjs/jest/pull/14109))
- `[expect]` Check error instance type for `toThrow/toThrowError` ([#14576](https://github.com/jestjs/jest/pull/14576)) - `[expect]` Check error instance type for `toThrow/toThrowError` ([#14576](https://github.com/jestjs/jest/pull/14576))
- `[jest-circus]` [**BREAKING**] Prevent false test failures caused by promise rejections handled asynchronously ([#14315](https://github.com/jestjs/jest/pull/14315)) - `[jest-circus]` [**BREAKING**] Prevent false test failures caused by promise rejections handled asynchronously ([#14315](https://github.com/jestjs/jest/pull/14315))
- `[jest-circus, jest-expect, jest-snapshot]` Pass `test.failing` tests when containing failing snapshot matchers ([#14313](https://github.com/jestjs/jest/pull/14313))
- `[jest-config]` Make sure to respect `runInBand` option ([#14578](https://github.com/facebook/jest/pull/14578)) - `[jest-config]` Make sure to respect `runInBand` option ([#14578](https://github.com/facebook/jest/pull/14578))
- `[@jest/expect-utils]` Fix comparison of `DataView` ([#14408](https://github.com/jestjs/jest/pull/14408)) - `[@jest/expect-utils]` Fix comparison of `DataView` ([#14408](https://github.com/jestjs/jest/pull/14408))
- `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526)) - `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526))

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test.failing doesnt update or remove snapshots 1`] = `
"// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[\`snapshots not updated nor removed 1\`] = \`"1"\`;
exports[\`snapshots not updated nor removed 2\`] = \`"1"\`;
exports[\`snapshots not updated nor removed 3\`] = \`"1"\`;
"
`;
exports[`test.failing doesnt update or remove snapshots 2`] = `
"/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
test.failing('inline snapshot not updated', () => {
// eslint-disable-next-line quotes
expect('0').toMatchInlineSnapshot(\`"1"\`);
});
"
`;

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as path from 'path';
import * as fs from 'graceful-fs';
import {skipSuiteOnJasmine} from '@jest/test-utils';
import runJest from '../runJest';
skipSuiteOnJasmine();
describe('test.failing', () => {
describe('should pass when', () => {
test.failing('snapshot matchers fails', () => {
expect('0').toMatchSnapshot();
});
test.failing('snapshot doesnt exist', () => {
expect('0').toMatchSnapshot();
});
test.failing('inline snapshot matchers fails', () => {
expect('0').toMatchInlineSnapshot('0');
});
test.failing('at least one snapshot fails', () => {
expect('1').toMatchSnapshot();
expect('0').toMatchSnapshot();
});
});
describe('should fail when', () => {
test.each([
['snapshot', 'snapshot'],
['inline snapshot', 'inlineSnapshot'],
])('%s matchers pass', (_, fileName) => {
const dir = path.resolve(__dirname, '../test-failing-snapshot-all-pass');
const result = runJest(dir, [`./__tests__/${fileName}.test.js`]);
expect(result.exitCode).toBe(1);
});
});
it('doesnt update or remove snapshots', () => {
const dir = path.resolve(__dirname, '../test-failing-snapshot');
const result = runJest(dir, ['-u']);
expect(result.exitCode).toBe(0);
expect(result.stdout).not.toMatch(/snapshots? (written|removed|obsolete)/);
const snapshot = fs
.readFileSync(
path.resolve(dir, './__tests__/__snapshots__/snapshot.test.js.snap'),
)
.toString();
expect(snapshot).toMatchSnapshot();
const inlineSnapshot = fs
.readFileSync(path.resolve(dir, './__tests__/inlineSnapshot.test.js'))
.toString();
expect(inlineSnapshot).toMatchSnapshot();
});
});

View File

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshots not updated 1`] = `"1"`;
exports[`snapshots not updated 2`] = `"1"`;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
test.failing('inline snapshot not updated', () => {
// eslint-disable-next-line quotes
expect('1').toMatchInlineSnapshot(`"1"`);
});

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
test.failing('snapshots not updated', () => {
expect('1').toMatchSnapshot();
expect('1').toMatchSnapshot();
});

View File

@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshots not updated nor removed 1`] = `"1"`;
exports[`snapshots not updated nor removed 2`] = `"1"`;
exports[`snapshots not updated nor removed 3`] = `"1"`;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
test.failing('inline snapshot not updated', () => {
// eslint-disable-next-line quotes
expect('0').toMatchInlineSnapshot(`"1"`);
});

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
test.failing('snapshots not updated nor removed', () => {
expect('1').toMatchSnapshot();
expect('0').toMatchSnapshot();
expect('0').toMatchSnapshot();
});

View File

@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}

View File

@ -109,10 +109,16 @@ const _addSnapshotData = (
results: TestResult, results: TestResult,
snapshotState: SnapshotState, snapshotState: SnapshotState,
) => { ) => {
for (const {fullName, status} of results.testResults) { for (const {fullName, status, failing} of results.testResults) {
if (status === 'pending' || status === 'failed') { if (
// if test is skipped or failed, we don't want to mark status === 'pending' ||
status === 'failed' ||
(failing && status === 'passed')
) {
// If test is skipped or failed, we don't want to mark
// its snapshots as obsolete. // its snapshots as obsolete.
// When tests called with test.failing pass, they've thrown an exception,
// so maintain any snapshots after the error.
snapshotState.markSnapshotsAsCheckedForTest(fullName); snapshotState.markSnapshotsAsCheckedForTest(fullName);
} }
} }

View File

@ -179,6 +179,7 @@ export const runAndTransformResultsToJestFormat = async ({
return { return {
ancestorTitles, ancestorTitles,
duration: testResult.duration, duration: testResult.duration,
failing: testResult.failing,
failureDetails: testResult.errorsDetailed, failureDetails: testResult.errorsDetailed,
failureMessages: testResult.errors, failureMessages: testResult.errors,
fullName: title fullName: title
@ -242,7 +243,10 @@ const handleSnapshotStateAfterRetry =
const eventHandler = async (event: Circus.Event) => { const eventHandler = async (event: Circus.Event) => {
switch (event.name) { switch (event.name) {
case 'test_start': { case 'test_start': {
jestExpect.setState({currentTestName: getTestID(event.test)}); jestExpect.setState({
currentTestName: getTestID(event.test),
testFailing: event.test.failing,
});
break; break;
} }
case 'test_done': { case 'test_done': {

View File

@ -377,6 +377,7 @@ export const makeSingleTestResult = (
duration: test.duration, duration: test.duration,
errors: errorsDetailed.map(getErrorStack), errors: errorsDetailed.map(getErrorStack),
errorsDetailed, errorsDetailed,
failing: test.failing,
invocations: test.invocations, invocations: test.invocations,
location, location,
numPassingAsserts: test.numPassingAsserts, numPassingAsserts: test.numPassingAsserts,
@ -502,6 +503,7 @@ export const parseSingleTestResult = (
return { return {
ancestorTitles, ancestorTitles,
duration: testResult.duration, duration: testResult.duration,
failing: testResult.failing,
failureDetails: testResult.errorsDetailed, failureDetails: testResult.errorsDetailed,
failureMessages: Array.from(testResult.errors), failureMessages: Array.from(testResult.errors),
fullName, fullName,

View File

@ -52,6 +52,8 @@ type PromiseMatchers<T = unknown> = {
declare module 'expect' { declare module 'expect' {
interface MatcherState { interface MatcherState {
snapshotState: SnapshotState; snapshotState: SnapshotState;
/** Whether the test was called with `test.failing()` */
testFailing?: boolean;
} }
interface BaseExpect { interface BaseExpect {
addSnapshotSerializer: typeof addSerializer; addSnapshotSerializer: typeof addSerializer;

View File

@ -36,6 +36,7 @@ export type SnapshotMatchOptions = {
readonly inlineSnapshot?: string; readonly inlineSnapshot?: string;
readonly isInline: boolean; readonly isInline: boolean;
readonly error?: Error; readonly error?: Error;
readonly testFailing?: boolean;
}; };
type SnapshotReturnOptions = { type SnapshotReturnOptions = {
@ -197,6 +198,7 @@ export default class SnapshotState {
inlineSnapshot, inlineSnapshot,
isInline, isInline,
error, error,
testFailing = false,
}: SnapshotMatchOptions): SnapshotReturnOptions { }: SnapshotMatchOptions): SnapshotReturnOptions {
this._counters.set(testName, (this._counters.get(testName) || 0) + 1); this._counters.set(testName, (this._counters.get(testName) || 0) + 1);
const count = Number(this._counters.get(testName)); const count = Number(this._counters.get(testName));
@ -230,6 +232,23 @@ export default class SnapshotState {
this._snapshotData[key] = receivedSerialized; this._snapshotData[key] = receivedSerialized;
} }
// In pure matching only runs, return the match result while skipping any updates
// reports.
if (testFailing) {
if (hasSnapshot && !isInline) {
// Retain current snapshot values.
this._addSnapshot(key, expected, {error, isInline});
}
return {
actual: removeExtraLineBreaks(receivedSerialized),
count,
expected:
expected === undefined ? undefined : removeExtraLineBreaks(expected),
key,
pass,
};
}
// These are the conditions on when to write snapshots: // These are the conditions on when to write snapshots:
// * There's no snapshot file in a non-CI environment. // * There's no snapshot file in a non-CI environment.
// * There is a snapshot file and we decided to update the snapshot. // * There is a snapshot file and we decided to update the snapshot.

View File

@ -278,7 +278,13 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
config; config;
let {received} = config; let {received} = config;
context.dontThrow && context.dontThrow(); /** If a test was ran with `test.failing`. Passed by Jest Circus. */
const {testFailing = false} = context;
if (!testFailing && context.dontThrow) {
// Supress errors while running tests
context.dontThrow();
}
const {currentConcurrentTestName, isNot, snapshotState} = context; const {currentConcurrentTestName, isNot, snapshotState} = context;
const currentTestName = const currentTestName =
@ -360,6 +366,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
inlineSnapshot, inlineSnapshot,
isInline, isInline,
received, received,
testFailing,
testName: fullTestName, testName: fullTestName,
}); });
const {actual, count, expected, pass} = result; const {actual, count, expected, pass} = result;

View File

@ -13,6 +13,7 @@ import type SnapshotState from './State';
export interface Context extends MatcherContext { export interface Context extends MatcherContext {
snapshotState: SnapshotState; snapshotState: SnapshotState;
testFailing?: boolean;
} }
// This is typically implemented by `jest-haste-map`'s `HasteFS`, but we // This is typically implemented by `jest-haste-map`'s `HasteFS`, but we

View File

@ -91,6 +91,11 @@ export type TestResult = {
console?: ConsoleBuffer; console?: ConsoleBuffer;
coverage?: CoverageMapData; coverage?: CoverageMapData;
displayName?: Config.DisplayName; displayName?: Config.DisplayName;
/**
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
* was used.
*/
failing?: boolean;
failureMessage?: string | null; failureMessage?: string | null;
leaks: boolean; leaks: boolean;
memoryUsage?: number; memoryUsage?: number;

View File

@ -203,6 +203,11 @@ export type TestResult = {
duration?: number | null; duration?: number | null;
errors: Array<FormattedError>; errors: Array<FormattedError>;
errorsDetailed: Array<MatcherResults | unknown>; errorsDetailed: Array<MatcherResults | unknown>;
/**
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
* was used.
*/
failing?: boolean;
invocations: number; invocations: number;
status: TestStatus; status: TestStatus;
location?: {column: number; line: number} | null; location?: {column: number; line: number} | null;

View File

@ -23,6 +23,11 @@ type Callsite = {
export type AssertionResult = { export type AssertionResult = {
ancestorTitles: Array<string>; ancestorTitles: Array<string>;
duration?: number | null; duration?: number | null;
/**
* Whether [`test.failing()`](https://jestjs.io/docs/api#testfailingname-fn-timeout)
* was used.
*/
failing?: boolean;
failureDetails: Array<unknown>; failureDetails: Array<unknown>;
failureMessages: Array<string>; failureMessages: Array<string>;
fullName: string; fullName: string;