feat: add support for snapshot matchers in concurrent tests (#14139)

This commit is contained in:
Dmitri 2023-06-30 16:49:33 +03:00 committed by GitHub
parent 372d6c53bf
commit 4ecf91ccb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 140 additions and 19 deletions

View File

@ -2,6 +2,7 @@
### Features
- `[jest-circus, jest-snapshot]` Add support for snapshot matchers in concurrent tests ([#14139](https://github.com/jestjs/jest/pull/14139))
- `[jest-cli]` Include type definitions to generated config files ([#14078](https://github.com/facebook/jest/pull/14078))
- `[jest-snapshot]` Support arrays as property matchers ([#14025](https://github.com/facebook/jest/pull/14025))
- `[jest-core, jest-circus, jest-reporter, jest-runner]` Added support for reporting about start individual test cases using jest-circus ([#14174](https://github.com/jestjs/jest/pull/14174))

View File

@ -0,0 +1,16 @@
/**
* 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 {skipSuiteOnJasmine} from '@jest/test-utils';
import runJest from '../runJest';
skipSuiteOnJasmine();
test('Snapshots get correct names in concurrent tests', () => {
const result = runJest('snapshot-concurrent', ['--ci']);
expect(result.exitCode).toBe(0);
});

View File

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`A a 1`] = `"Aa1"`;
exports[`A a 2`] = `"Aa2"`;
exports[`A b 1`] = `"Ab1"`;
exports[`A b 2`] = `"Ab2"`;
exports[`A c 1`] = `"Ac1"`;
exports[`A c 2`] = `"Ac2"`;
exports[`A d 1`] = `"Ad1"`;
exports[`A d 2`] = `"Ad2"`;
exports[`B 1`] = `"B1"`;
exports[`B 2`] = `"B2"`;
exports[`C 1`] = `"C1"`;
exports[`C 2`] = `"C2"`;
exports[`D 1`] = `"D1"`;
exports[`D 2`] = `"D2"`;

View File

@ -0,0 +1,50 @@
/**
* 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.
*
*/
'use strict';
const sleep = ms => new Promise(r => setTimeout(r, ms));
describe('A', () => {
it.concurrent('a', async () => {
await sleep(100);
expect('Aa1').toMatchSnapshot();
expect('Aa2').toMatchSnapshot();
});
it.concurrent('b', async () => {
await sleep(10);
expect('Ab1').toMatchSnapshot();
expect('Ab2').toMatchSnapshot();
});
it.concurrent('c', async () => {
expect('Ac1').toMatchSnapshot();
expect('Ac2').toMatchSnapshot();
});
it('d', () => {
expect('Ad1').toMatchSnapshot();
expect('Ad2').toMatchSnapshot();
});
});
it.concurrent('B', async () => {
await sleep(10);
expect('B1').toMatchSnapshot();
expect('B2').toMatchSnapshot();
});
it('C', () => {
expect('C1').toMatchSnapshot();
expect('C2').toMatchSnapshot();
});
it.concurrent('D', async () => {
expect('D1').toMatchSnapshot();
expect('D2').toMatchSnapshot();
});

View File

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

View File

@ -20,6 +20,7 @@
},
"dependencies": {
"@jest/expect-utils": "workspace:^",
"@types/node": "*",
"jest-get-type": "workspace:^",
"jest-matcher-utils": "workspace:^",
"jest-message-util": "workspace:^",

View File

@ -6,6 +6,7 @@
*
*/
import type {AsyncLocalStorage} from 'async_hooks';
import type {EqualsFunction, Tester} from '@jest/expect-utils';
import type * as jestMatcherUtils from 'jest-matcher-utils';
import {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';
@ -57,6 +58,7 @@ export interface MatcherUtils {
export interface MatcherState {
assertionCalls: number;
currentConcurrentTestName?: AsyncLocalStorage<string>;
currentTestName?: string;
error?: Error;
expand?: boolean;

View File

@ -5,7 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import {AsyncLocalStorage} from 'async_hooks';
import pLimit = require('p-limit');
import {jestExpect} from '@jest/expect';
import type {Circus} from '@jest/types';
import shuffleArray, {RandomNumberGenerator, rngBuilder} from './shuffleArray';
import {dispatch, getState} from './state';
@ -19,6 +21,10 @@ import {
makeRunResult,
} from './utils';
type ConcurrentTestEntry = Omit<Circus.TestEntry, 'fn'> & {
fn: Circus.ConcurrentTestFn;
};
const run = async (): Promise<Circus.RunResult> => {
const {rootDescribeBlock, seed, randomize} = getState();
const rng = randomize ? rngBuilder(seed) : undefined;
@ -49,20 +55,8 @@ const _runTestsForDescribeBlock = async (
if (isRootBlock) {
const concurrentTests = collectConcurrentTests(describeBlock);
const mutex = pLimit(getState().maxConcurrency);
for (const test of concurrentTests) {
try {
const promise = mutex(test.fn);
// Avoid triggering the uncaught promise rejection handler in case the
// test errors before being awaited on.
// eslint-disable-next-line @typescript-eslint/no-empty-function
promise.catch(() => {});
test.fn = () => promise;
} catch (err) {
test.fn = () => {
throw err;
};
}
if (concurrentTests.length > 0) {
startTestsConcurrently(concurrentTests);
}
}
@ -120,7 +114,7 @@ const _runTestsForDescribeBlock = async (
function collectConcurrentTests(
describeBlock: Circus.DescribeBlock,
): Array<Omit<Circus.TestEntry, 'fn'> & {fn: Circus.ConcurrentTestFn}> {
): Array<ConcurrentTestEntry> {
if (describeBlock.mode === 'skip') {
return [];
}
@ -135,13 +129,33 @@ function collectConcurrentTests(
child.mode === 'skip' ||
(hasFocusedTests && child.mode !== 'only') ||
(testNamePattern && !testNamePattern.test(getTestID(child)));
return skip
? []
: [child as Circus.TestEntry & {fn: Circus.ConcurrentTestFn}];
return skip ? [] : [child as ConcurrentTestEntry];
}
});
}
function startTestsConcurrently(concurrentTests: Array<ConcurrentTestEntry>) {
const mutex = pLimit(getState().maxConcurrency);
const testNameStorage = new AsyncLocalStorage<string>();
jestExpect.setState({currentConcurrentTestName: testNameStorage});
for (const test of concurrentTests) {
try {
const promise = testNameStorage.run(getTestID(test), () =>
mutex(test.fn),
);
// Avoid triggering the uncaught promise rejection handler in case the
// test fails before being awaited on.
// eslint-disable-next-line @typescript-eslint/no-empty-function
promise.catch(() => {});
test.fn = () => promise;
} catch (err) {
test.fn = () => {
throw err;
};
}
}
}
const _runTest = async (
test: Circus.TestEntry,
parentSkipped: boolean,

View File

@ -279,7 +279,9 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
context.dontThrow && context.dontThrow();
const {currentTestName, isNot, snapshotState} = context;
const {currentConcurrentTestName, isNot, snapshotState} = context;
const currentTestName =
currentConcurrentTestName?.getStore() ?? context.currentTestName;
if (isNot) {
throw new Error(

View File

@ -9697,6 +9697,7 @@ __metadata:
"@jest/expect-utils": "workspace:^"
"@jest/test-utils": "workspace:^"
"@tsd/typescript": ^5.0.4
"@types/node": "*"
chalk: ^4.0.0
immutable: ^4.0.0
jest-get-type: "workspace:^"