Skip to content

Commit

Permalink
feat: add support for snapshot matchers in concurrent tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitri-gb committed Jun 4, 2023
1 parent 6460335 commit 564944c
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-circus, jest-snapshot]` Add support for snapshot matchers in concurrent tests
- `[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))

Expand Down
16 changes: 16 additions & 0 deletions e2e/__tests__/snapshot-concurrent.test.ts
@@ -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);
});
21 changes: 21 additions & 0 deletions e2e/snapshot-concurrent/__tests__/__snapshots__/works.test.js.snap
@@ -0,0 +1,21 @@
// 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[`B 1`] = `"B1"`;

exports[`B 2`] = `"B2"`;

exports[`C 1`] = `"C1"`;

exports[`C 2`] = `"C2"`;
40 changes: 40 additions & 0 deletions e2e/snapshot-concurrent/__tests__/works.test.js
@@ -0,0 +1,40 @@
/**
* 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.concurrent('B', async () => {
await sleep(10);
expect('B1').toMatchSnapshot();
expect('B2').toMatchSnapshot();
});

it.concurrent('C', async () => {
expect('C1').toMatchSnapshot();
expect('C2').toMatchSnapshot();
});
5 changes: 5 additions & 0 deletions e2e/snapshot-concurrent/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"testEnvironment": "node"
}
}
1 change: 1 addition & 0 deletions packages/expect/package.json
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"@jest/expect-utils": "workspace:^",
"@types/node": "*",
"jest-get-type": "workspace:^",
"jest-matcher-utils": "workspace:^",
"jest-message-util": "workspace:^",
Expand Down
2 changes: 2 additions & 0 deletions packages/expect/src/types.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -57,6 +58,7 @@ export interface MatcherUtils {

export interface MatcherState {
assertionCalls: number;
currentConcurrentTestName?: AsyncLocalStorage<string>;
currentTestName?: string;
error?: Error;
expand?: boolean;
Expand Down
50 changes: 32 additions & 18 deletions packages/jest-circus/src/run.ts
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 [];
}
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/jest-snapshot/src/index.ts
Expand Up @@ -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(
Expand Down

0 comments on commit 564944c

Please sign in to comment.