Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ jobs:
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
max_attempts: 5
retry_on: error
command: yarn jest-coverage --color --config jest.config.ci.mjs --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }}
- name: map coverage
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
max_attempts: 5
retry_on: error
command: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }}

Expand Down Expand Up @@ -70,6 +70,6 @@ jobs:
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
max_attempts: 5
retry_on: error
command: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ matrix.shard }}
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
max_attempts: 5
retry_on: error
command: yarn test-ci-partial:parallel --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ inputs.shard }}

Expand Down Expand Up @@ -69,6 +69,6 @@ jobs:
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
with:
timeout_minutes: 10
max_attempts: 3
max_attempts: 5
retry_on: error
command: yarn jest-jasmine-ci --max-workers ${{ steps.cpu-cores.outputs.count }} --shard=${{ inputs.shard }}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## main

### Features

- `[jest-circus, jest-cli, jest-config, jest-core, jest-jasmine2, jest-types]` Add `--collect-tests` flag to discover and list tests without executing them ([#16006](https://github.com/jestjs/jest/pull/16006))

## 30.3.0

### Features
Expand Down
76 changes: 76 additions & 0 deletions e2e/__tests__/collectTests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* 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 runJest from '../runJest';

describe('jest --collect-tests', () => {
test('lists test names without executing test bodies', () => {
const {exitCode, stdout} = runJest('each', [
'--collect-tests',
'--testPathPatterns=success',
]);

expect(exitCode).toBe(0);
expect(stdout).toContain("The word red contains the letter 'e'");
expect(stdout).toContain('passes one row expected true == true');
expect(stdout).toContain('passes all rows expected true == true');
expect(stdout).toContain('success.test.js');
});

test('produces valid JSON with --json', () => {
const {exitCode, stdout} = runJest('each', [
'--collect-tests',
'--json',
'--testPathPatterns=success',
]);

expect(exitCode).toBe(0);
const json = JSON.parse(stdout);
expect(json.success).toBe(true);
expect(json.numTotalTestSuites).toBe(1);
expect(json.numPendingTests).toBeGreaterThan(0);

const testFile = json.testResults[0];
expect(testFile.name).toContain('success.test.js');
for (const assertion of testFile.assertionResults) {
expect(assertion.status).toBe('pending');
}
});

test('does not execute tests (failing tests still exit 0)', () => {
const {exitCode, stdout} = runJest('each', [
'--collect-tests',
'--testPathPatterns=failure',
]);

expect(exitCode).toBe(0);
expect(stdout).toContain('failure.test.js');
expect(stdout).toContain('fails');
});

test('filters correctly with --testNamePattern', () => {
const {exitCode, stdout} = runJest('each', [
'--collect-tests',
'--testPathPatterns=success',
'--testNamePattern=one row',
]);

expect(exitCode).toBe(0);
expect(stdout).toContain('passes one row expected');
expect(stdout).not.toContain("The word red contains the letter 'e'");
});

test('exits 0 even when no tests match', () => {
const {exitCode, stdout} = runJest('each', [
'--collect-tests',
'--testPathPatterns=nonexistent',
]);

expect(exitCode).toBe(0);
expect(stdout).toContain('No tests found');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* 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 {beforeEach, describe, expect, it, jest} from '@jest/globals';
import {makeProjectConfig} from '@jest/test-utils';
import type {Circus} from '@jest/types';
import {getState as getRunnerState, resetState} from '../../state';
import {makeDescribe, makeTest} from '../../utils';
import {collectTestsWithoutRunning} from '../jestAdapterInit';

jest.mock('../../state', () => {
const actual =
jest.requireActual<typeof import('../../state')>('../../state');
return {...actual, dispatch: jest.fn<typeof actual.dispatch>()};
});

beforeEach(() => {
resetState();
});

const addTest = (name: string, parent: Circus.DescribeBlock) => {
const test = makeTest(
() => {},
undefined,
false,
name,
parent,
undefined,
new Error(),
false,
);
parent.children.push(test);
};

const collect = (testPath = '/test.js') =>
collectTestsWithoutRunning({config: makeProjectConfig(), testPath});

describe('collectTestsWithoutRunning', () => {
it('collects flat tests with pending status', async () => {
const root = getRunnerState().rootDescribeBlock;
addTest('test one', root);
addTest('test two', root);

const result = await collect();

expect(result.testResults.map(r => r.title)).toEqual([
'test one',
'test two',
]);
expect(result.testResults[0].status).toBe('pending');
expect(result.numPendingTests).toBe(2);
});

it('collects nested tests with correct ancestor titles', async () => {
const root = getRunnerState().rootDescribeBlock;
const outer = makeDescribe('outer', root);
root.children.push(outer);
const inner = makeDescribe('inner', outer);
outer.children.push(inner);
addTest('deep test', inner);

const result = await collect();

expect(result.testResults[0].ancestorTitles).toEqual(['outer', 'inner']);
expect(result.testResults[0].fullName).toBe('outer inner deep test');
});

it('preserves source order across describe blocks', async () => {
const root = getRunnerState().rootDescribeBlock;
const a = makeDescribe('A', root);
root.children.push(a);
addTest('first', a);
const b = makeDescribe('B', root);
root.children.push(b);
addTest('second', b);

const result = await collect();

expect(result.testResults.map(r => r.title)).toEqual(['first', 'second']);
});

it('returns empty results when no tests exist', async () => {
const result = await collect();
expect(result.testResults).toHaveLength(0);
});

it('filters tests by testNamePattern from runner state', async () => {
const state = getRunnerState();
state.testNamePattern = /one/;
addTest('test one', state.rootDescribeBlock);
addTest('test two', state.rootDescribeBlock);

const result = await collect();

expect(result.testResults.map(r => r.title)).toEqual(['test one']);
});
});
19 changes: 15 additions & 4 deletions packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ const jestAdapter = async (
testPath: string,
sendMessageToJest?: TestFileEvent,
): Promise<TestResult> => {
const {initialize, runAndTransformResultsToJestFormat} =
runtime.requireInternalModule<typeof import('./jestAdapterInit')>(
FRAMEWORK_INITIALIZER,
);
const {
collectTestsWithoutRunning,
initialize,
runAndTransformResultsToJestFormat,
} = runtime.requireInternalModule<typeof import('./jestAdapterInit')>(
FRAMEWORK_INITIALIZER,
);

const {globals, snapshotState} = await initialize({
config,
Expand Down Expand Up @@ -96,6 +99,14 @@ const jestAdapter = async (
runtime.requireModule(testPath);
}

if (globalConfig.collectTests) {
const results = await collectTestsWithoutRunning({
config,
testPath,
});
return deepCyclicCopy(results, {keepPrototype: false});
}

const setupAfterEnvPerfStats = {
setupAfterEnvEnd,
setupAfterEnvStart,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,62 @@ export const initialize = async ({
return {globals: globalsObject, snapshotState};
};

export const collectTestsWithoutRunning = async ({
config,
testPath,
}: {
config: Config.ProjectConfig;
testPath: string;
}): Promise<TestResult> => {
const {rootDescribeBlock, testNamePattern} = getRunnerState();

const assertionResults: Array<AssertionResult> = [];

const walk = (
block: Circus.DescribeBlock,
ancestors: Array<string>,
): void => {
for (const child of block.children) {
if (child.type === 'describeBlock') {
walk(child, [...ancestors, child.name]);
continue;
}

if (testNamePattern && !testNamePattern.test(getTestID(child))) {
continue;
}

const title = child.name;
assertionResults.push({
ancestorTitles: [...ancestors],
duration: null,
failing: false,
failureDetails: [],
failureMessages: [],
fullName: [...ancestors, title].join(' '),
invocations: 0,
location: null,
numPassingAsserts: 0,
retryReasons: [],
startAt: null,
status: 'pending' as Status,
title,
});
}
};
walk(rootDescribeBlock, []);

await dispatch({name: 'teardown'});

return {
...createEmptyTestResult(),
displayName: config.displayName,
numPendingTests: assertionResults.length,
testFilePath: testPath,
testResults: assertionResults,
};
};

export const runAndTransformResultsToJestFormat = async ({
config,
globalConfig,
Expand Down
6 changes: 6 additions & 0 deletions packages/jest-cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ export const options: {[key: string]: Options} = {
requiresArg: true,
type: 'string',
},
collectTests: {
description:
'Discover and report all test cases without executing them. ' +
'Prints a tree of test suites and test names.',
type: 'boolean',
},
color: {
description:
'Forces test results output color highlighting (even if ' +
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const initialOptions: Config.InitialOptions = {
clearMocks: false,
collectCoverage: true,
collectCoverageFrom: ['src', '!public'],
collectTests: false,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: [NODE_MODULES_REGEXP],
coverageProvider: 'v8',
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const groupOptions = (
ci: options.ci,
collectCoverage: options.collectCoverage,
collectCoverageFrom: options.collectCoverageFrom,
collectTests: options.collectTests,
coverageDirectory: options.coverageDirectory,
coverageProvider: options.coverageProvider,
coverageReporters: options.coverageReporters,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ export default async function normalize(
case 'changedFilesWithAncestor':
case 'clearMocks':
case 'collectCoverage':
case 'collectTests':
case 'coverageProvider':
case 'coverageReporters':
case 'coverageThreshold':
Expand Down
Loading
Loading