Skip to content

Commit 4ecf91c

Browse files
authored
feat: add support for snapshot matchers in concurrent tests (#14139)
1 parent 372d6c5 commit 4ecf91c

File tree

10 files changed

+140
-19
lines changed

10 files changed

+140
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Features
44

5+
- `[jest-circus, jest-snapshot]` Add support for snapshot matchers in concurrent tests ([#14139](https://github.com/jestjs/jest/pull/14139))
56
- `[jest-cli]` Include type definitions to generated config files ([#14078](https://github.com/facebook/jest/pull/14078))
67
- `[jest-snapshot]` Support arrays as property matchers ([#14025](https://github.com/facebook/jest/pull/14025))
78
- `[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))
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 {skipSuiteOnJasmine} from '@jest/test-utils';
9+
import runJest from '../runJest';
10+
11+
skipSuiteOnJasmine();
12+
13+
test('Snapshots get correct names in concurrent tests', () => {
14+
const result = runJest('snapshot-concurrent', ['--ci']);
15+
expect(result.exitCode).toBe(0);
16+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`A a 1`] = `"Aa1"`;
4+
5+
exports[`A a 2`] = `"Aa2"`;
6+
7+
exports[`A b 1`] = `"Ab1"`;
8+
9+
exports[`A b 2`] = `"Ab2"`;
10+
11+
exports[`A c 1`] = `"Ac1"`;
12+
13+
exports[`A c 2`] = `"Ac2"`;
14+
15+
exports[`A d 1`] = `"Ad1"`;
16+
17+
exports[`A d 2`] = `"Ad2"`;
18+
19+
exports[`B 1`] = `"B1"`;
20+
21+
exports[`B 2`] = `"B2"`;
22+
23+
exports[`C 1`] = `"C1"`;
24+
25+
exports[`C 2`] = `"C2"`;
26+
27+
exports[`D 1`] = `"D1"`;
28+
29+
exports[`D 2`] = `"D2"`;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
'use strict';
9+
10+
const sleep = ms => new Promise(r => setTimeout(r, ms));
11+
12+
describe('A', () => {
13+
it.concurrent('a', async () => {
14+
await sleep(100);
15+
expect('Aa1').toMatchSnapshot();
16+
expect('Aa2').toMatchSnapshot();
17+
});
18+
19+
it.concurrent('b', async () => {
20+
await sleep(10);
21+
expect('Ab1').toMatchSnapshot();
22+
expect('Ab2').toMatchSnapshot();
23+
});
24+
25+
it.concurrent('c', async () => {
26+
expect('Ac1').toMatchSnapshot();
27+
expect('Ac2').toMatchSnapshot();
28+
});
29+
30+
it('d', () => {
31+
expect('Ad1').toMatchSnapshot();
32+
expect('Ad2').toMatchSnapshot();
33+
});
34+
});
35+
36+
it.concurrent('B', async () => {
37+
await sleep(10);
38+
expect('B1').toMatchSnapshot();
39+
expect('B2').toMatchSnapshot();
40+
});
41+
42+
it('C', () => {
43+
expect('C1').toMatchSnapshot();
44+
expect('C2').toMatchSnapshot();
45+
});
46+
47+
it.concurrent('D', async () => {
48+
expect('D1').toMatchSnapshot();
49+
expect('D2').toMatchSnapshot();
50+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}

packages/expect/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
},
2121
"dependencies": {
2222
"@jest/expect-utils": "workspace:^",
23+
"@types/node": "*",
2324
"jest-get-type": "workspace:^",
2425
"jest-matcher-utils": "workspace:^",
2526
"jest-message-util": "workspace:^",

packages/expect/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*
77
*/
88

9+
import type {AsyncLocalStorage} from 'async_hooks';
910
import type {EqualsFunction, Tester} from '@jest/expect-utils';
1011
import type * as jestMatcherUtils from 'jest-matcher-utils';
1112
import {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';
@@ -57,6 +58,7 @@ export interface MatcherUtils {
5758

5859
export interface MatcherState {
5960
assertionCalls: number;
61+
currentConcurrentTestName?: AsyncLocalStorage<string>;
6062
currentTestName?: string;
6163
error?: Error;
6264
expand?: boolean;

packages/jest-circus/src/run.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {AsyncLocalStorage} from 'async_hooks';
89
import pLimit = require('p-limit');
10+
import {jestExpect} from '@jest/expect';
911
import type {Circus} from '@jest/types';
1012
import shuffleArray, {RandomNumberGenerator, rngBuilder} from './shuffleArray';
1113
import {dispatch, getState} from './state';
@@ -19,6 +21,10 @@ import {
1921
makeRunResult,
2022
} from './utils';
2123

24+
type ConcurrentTestEntry = Omit<Circus.TestEntry, 'fn'> & {
25+
fn: Circus.ConcurrentTestFn;
26+
};
27+
2228
const run = async (): Promise<Circus.RunResult> => {
2329
const {rootDescribeBlock, seed, randomize} = getState();
2430
const rng = randomize ? rngBuilder(seed) : undefined;
@@ -49,20 +55,8 @@ const _runTestsForDescribeBlock = async (
4955

5056
if (isRootBlock) {
5157
const concurrentTests = collectConcurrentTests(describeBlock);
52-
const mutex = pLimit(getState().maxConcurrency);
53-
for (const test of concurrentTests) {
54-
try {
55-
const promise = mutex(test.fn);
56-
// Avoid triggering the uncaught promise rejection handler in case the
57-
// test errors before being awaited on.
58-
// eslint-disable-next-line @typescript-eslint/no-empty-function
59-
promise.catch(() => {});
60-
test.fn = () => promise;
61-
} catch (err) {
62-
test.fn = () => {
63-
throw err;
64-
};
65-
}
58+
if (concurrentTests.length > 0) {
59+
startTestsConcurrently(concurrentTests);
6660
}
6761
}
6862

@@ -120,7 +114,7 @@ const _runTestsForDescribeBlock = async (
120114

121115
function collectConcurrentTests(
122116
describeBlock: Circus.DescribeBlock,
123-
): Array<Omit<Circus.TestEntry, 'fn'> & {fn: Circus.ConcurrentTestFn}> {
117+
): Array<ConcurrentTestEntry> {
124118
if (describeBlock.mode === 'skip') {
125119
return [];
126120
}
@@ -135,13 +129,33 @@ function collectConcurrentTests(
135129
child.mode === 'skip' ||
136130
(hasFocusedTests && child.mode !== 'only') ||
137131
(testNamePattern && !testNamePattern.test(getTestID(child)));
138-
return skip
139-
? []
140-
: [child as Circus.TestEntry & {fn: Circus.ConcurrentTestFn}];
132+
return skip ? [] : [child as ConcurrentTestEntry];
141133
}
142134
});
143135
}
144136

137+
function startTestsConcurrently(concurrentTests: Array<ConcurrentTestEntry>) {
138+
const mutex = pLimit(getState().maxConcurrency);
139+
const testNameStorage = new AsyncLocalStorage<string>();
140+
jestExpect.setState({currentConcurrentTestName: testNameStorage});
141+
for (const test of concurrentTests) {
142+
try {
143+
const promise = testNameStorage.run(getTestID(test), () =>
144+
mutex(test.fn),
145+
);
146+
// Avoid triggering the uncaught promise rejection handler in case the
147+
// test fails before being awaited on.
148+
// eslint-disable-next-line @typescript-eslint/no-empty-function
149+
promise.catch(() => {});
150+
test.fn = () => promise;
151+
} catch (err) {
152+
test.fn = () => {
153+
throw err;
154+
};
155+
}
156+
}
157+
}
158+
145159
const _runTest = async (
146160
test: Circus.TestEntry,
147161
parentSkipped: boolean,

packages/jest-snapshot/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,9 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
279279

280280
context.dontThrow && context.dontThrow();
281281

282-
const {currentTestName, isNot, snapshotState} = context;
282+
const {currentConcurrentTestName, isNot, snapshotState} = context;
283+
const currentTestName =
284+
currentConcurrentTestName?.getStore() ?? context.currentTestName;
283285

284286
if (isNot) {
285287
throw new Error(

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9697,6 +9697,7 @@ __metadata:
96979697
"@jest/expect-utils": "workspace:^"
96989698
"@jest/test-utils": "workspace:^"
96999699
"@tsd/typescript": ^5.0.4
9700+
"@types/node": "*"
97009701
chalk: ^4.0.0
97019702
immutable: ^4.0.0
97029703
jest-get-type: "workspace:^"

0 commit comments

Comments
 (0)