Fix testPathPatterns when config is in subdirectory (#14934)

This commit is contained in:
Brandon Chinn 2024-05-29 15:41:13 +09:00 committed by GitHub
parent cb15c34c04
commit 68dca2a321
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 405 additions and 145 deletions

View File

@ -64,11 +64,11 @@
- `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526))
- `[jest-runtime]` Properly handle re-exported native modules in ESM via CJS ([#14589](https://github.com/jestjs/jest/pull/14589))
- `[jest-util]` Make sure `isInteractive` works in a browser ([#14552](https://github.com/jestjs/jest/pull/14552))
- `[jest-util]` Add missing dependency on `jest-regex-util` ([#15030](https://github.com/jestjs/jest/pull/15030))
- `[pretty-format]` [**BREAKING**] Print `ArrayBuffer` and `DataView` correctly ([#14290](https://github.com/jestjs/jest/pull/14290))
- `[jest-cli]` When specifying paths on the command line, only match against the relative paths of the test files ([#12519](https://github.com/jestjs/jest/pull/12519))
- [**BREAKING**] Changes `testPathPattern` configuration option to `testPathPatterns`, which now takes a list of patterns instead of the regex.
- [**BREAKING**] `--testPathPattern` is now `--testPathPatterns`
- [**BREAKING**] Specifying `testPathPatterns` when programmatically calling `watch` must be specified as `new TestPathPatterns(patterns)`, where `TestPathPatterns` can be imported from `@jest/pattern`
- `[jest-reporters, jest-runner]` Unhandled errors without stack get correctly logged to console ([#14619](https://github.com/jestjs/jest/pull/14619))
### Performance

View File

@ -46,7 +46,7 @@ test('Tests are executed only once even in an MPR', () => {
});
/* eslint-enable sort-keys */
const {stderr, exitCode} = runJest(DIR, ['foo/folder/my-test-bar.js']);
const {stderr, exitCode} = runJest(DIR, ['my-test-bar.js']);
expect(exitCode).toBe(0);

View File

@ -109,7 +109,7 @@ test('should not call a globalSetup of a project if there are no tests to run fr
const result = runWithJson(e2eDir, [
`--config=${configPath}`,
'--testPathPatterns=project-1',
'--testPathPatterns=setup1',
]);
expect(result.exitCode).toBe(0);

View File

@ -93,7 +93,7 @@ test('should not call a globalTeardown of a project if there are no tests to run
const result = runWithJson('global-teardown', [
`--config=${configPath}`,
'--testPathPatterns=project-1',
'--testPathPatterns=teardown1',
]);
expect(result.exitCode).toBe(0);

View File

@ -0,0 +1,17 @@
/**
* 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 {json} from '../runJest';
it('works when specifying --testPathPatterns when config is in subdir', () => {
const {
json: {numTotalTests},
} = json('test-path-patterns-subprojects', [
'--config=config/jest.config.js',
'--testPathPatterns=testA',
]);
expect(numTotalTests).toBe(1);
});

View File

@ -6,6 +6,6 @@
*/
module.exports = function (globalConfig, projectConfig) {
console.log(globalConfig.testPathPatterns);
console.log(globalConfig.testPathPatterns.patterns);
console.log(projectConfig.cache);
};

View File

@ -6,6 +6,6 @@
*/
export default function (globalConfig, projectConfig): void {
console.log(globalConfig.testPathPatterns);
console.log(globalConfig.testPathPatterns.patterns);
console.log(projectConfig.cache);
}

View File

@ -6,6 +6,6 @@
*/
module.exports = function (globalConfig, projectConfig) {
console.log(globalConfig.testPathPatterns);
console.log(globalConfig.testPathPatterns.patterns);
console.log(projectConfig.cache);
};

View File

@ -6,6 +6,6 @@
*/
export default function (globalConfig, projectConfig): void {
console.log(globalConfig.testPathPatterns);
console.log(globalConfig.testPathPatterns.patterns);
console.log(projectConfig.cache);
}

View File

@ -12,6 +12,7 @@ import dedent from 'dedent';
import execa = require('execa');
import * as fs from 'graceful-fs';
import stripAnsi = require('strip-ansi');
import {TestPathPatterns} from '@jest/pattern';
import type {FormattedTestResults} from '@jest/test-result';
import {normalizeIcons} from '@jest/test-utils';
import type {Config} from '@jest/types';
@ -285,5 +286,10 @@ export function getConfig(
throw error;
}
return JSON.parse(stdout);
const {testPathPatterns, ...globalConfig} = JSON.parse(stdout);
return {
...globalConfig,
testPathPatterns: new TestPathPatterns(testPathPatterns),
};
}

View File

@ -0,0 +1,10 @@
/**
* 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.
*/
module.exports = {
projects: [{rootDir: 'src'}],
};

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,10 @@
/**
* 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';
it('test', () => {});

View File

@ -0,0 +1,10 @@
/**
* 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';
it('test', () => {});

View File

@ -0,0 +1,10 @@
/**
* 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';
it('test', () => {});

View File

@ -655,7 +655,7 @@ export const options: {[key: string]: Options} = {
},
testPathPatterns: {
description:
'A regexp pattern string that is matched against all tests ' +
'An array of regexp pattern strings that are matched against all tests ' +
'paths before executing the test.',
requiresArg: true,
string: true,

View File

@ -32,6 +32,7 @@
},
"dependencies": {
"@babel/core": "^7.11.6",
"@jest/pattern": "workspace:*",
"@jest/test-sequencer": "workspace:*",
"@jest/types": "workspace:*",
"babel-jest": "workspace:*",

View File

@ -1601,7 +1601,7 @@ describe('testPathPatterns', () => {
it('defaults to empty', async () => {
const {options} = await normalize(initialOptions, {} as Config.Argv);
expect(options.testPathPatterns).toEqual([]);
expect(options.testPathPatterns.patterns).toEqual([]);
});
const cliOptions = [
@ -1614,14 +1614,14 @@ describe('testPathPatterns', () => {
const argv = {[opt.property]: ['a/b']} as Config.Argv;
const {options} = await normalize(initialOptions, argv);
expect(options.testPathPatterns).toEqual(['a/b']);
expect(options.testPathPatterns.patterns).toEqual(['a/b']);
});
it('ignores invalid regular expressions and logs a warning', async () => {
const argv = {[opt.property]: ['a(']} as Config.Argv;
const {options} = await normalize(initialOptions, argv);
expect(options.testPathPatterns).toEqual([]);
expect(options.testPathPatterns.patterns).toEqual([]);
expect(jest.mocked(console.log).mock.calls[0][0]).toMatchSnapshot();
});
@ -1629,7 +1629,7 @@ describe('testPathPatterns', () => {
const argv = {[opt.property]: ['a/b', 'c/d']} as Config.Argv;
const {options} = await normalize(initialOptions, argv);
expect(options.testPathPatterns).toEqual(['a/b', 'c/d']);
expect(options.testPathPatterns.patterns).toEqual(['a/b', 'c/d']);
});
});
}
@ -1638,7 +1638,7 @@ describe('testPathPatterns', () => {
const argv = {_: [1]} as Config.Argv;
const {options} = await normalize(initialOptions, argv);
expect(options.testPathPatterns).toEqual(['1']);
expect(options.testPathPatterns.patterns).toEqual(['1']);
});
it('joins multiple --testPathPatterns and <regexForTestFiles>', async () => {
@ -1646,7 +1646,7 @@ describe('testPathPatterns', () => {
_: ['a', 'b'],
testPathPatterns: ['c', 'd'],
} as Config.Argv);
expect(options.testPathPatterns).toEqual(['a', 'b', 'c', 'd']);
expect(options.testPathPatterns.patterns).toEqual(['a', 'b', 'c', 'd']);
});
it('gives precedence to --all', async () => {

View File

@ -13,6 +13,7 @@ import merge = require('deepmerge');
import {glob} from 'glob';
import {statSync} from 'graceful-fs';
import micromatch = require('micromatch');
import {TestPathPatterns} from '@jest/pattern';
import type {Config} from '@jest/types';
import {replacePathSepForRegex} from 'jest-regex-util';
import Resolver, {
@ -22,7 +23,6 @@ import Resolver, {
resolveWatchPlugin,
} from 'jest-resolve';
import {
TestPathPatterns,
clearLine,
replacePathSepForGlob,
requireOrImportModule,
@ -393,10 +393,7 @@ const normalizeReporters = ({
});
};
const buildTestPathPatterns = (
argv: Config.Argv,
rootDir: string,
): TestPathPatterns => {
const buildTestPathPatterns = (argv: Config.Argv): TestPathPatterns => {
const patterns = [];
if (argv._) {
@ -406,12 +403,9 @@ const buildTestPathPatterns = (
patterns.push(...argv.testPathPatterns);
}
const config = {rootDir};
const testPathPatterns = new TestPathPatterns(patterns, config);
const testPathPatterns = new TestPathPatterns(patterns);
try {
testPathPatterns.validate();
} catch {
if (!testPathPatterns.isValid()) {
clearLine(process.stdout);
// eslint-disable-next-line no-console
@ -422,7 +416,7 @@ const buildTestPathPatterns = (
),
);
return new TestPathPatterns([], config);
return new TestPathPatterns([]);
}
return testPathPatterns;
@ -1012,8 +1006,8 @@ export default async function normalize(
}
newOptions.nonFlagArgs = argv._?.map(arg => `${arg}`);
const testPathPatterns = buildTestPathPatterns(argv, options.rootDir);
newOptions.testPathPatterns = testPathPatterns.patterns;
const testPathPatterns = buildTestPathPatterns(argv);
newOptions.testPathPatterns = testPathPatterns;
newOptions.json = !!argv.json;
newOptions.testFailureExitCode = Number.parseInt(

View File

@ -12,6 +12,7 @@
"references": [
{"path": "../jest-environment-node"},
{"path": "../jest-get-type"},
{"path": "../jest-pattern"},
{"path": "../jest-regex-util"},
{"path": "../jest-resolve"},
{"path": "../jest-runner"},

View File

@ -15,6 +15,7 @@
},
"dependencies": {
"@jest/console": "workspace:*",
"@jest/pattern": "workspace:*",
"@jest/reporters": "workspace:*",
"@jest/test-result": "workspace:*",
"@jest/transform": "workspace:*",

View File

@ -8,6 +8,7 @@
import * as os from 'os';
import * as path from 'path';
import micromatch = require('micromatch');
import type {TestPathPatternsExecutor} from '@jest/pattern';
import type {Test, TestContext} from '@jest/test-result';
import type {Config} from '@jest/types';
import type {ChangedFiles} from 'jest-changed-files';
@ -15,7 +16,7 @@ import {replaceRootDirInPath} from 'jest-config';
import {escapePathForRegex} from 'jest-regex-util';
import {DependencyResolver} from 'jest-resolve-dependencies';
import {buildSnapshotResolver} from 'jest-snapshot';
import {TestPathPatterns, globsToMatcher} from 'jest-util';
import {globsToMatcher} from 'jest-util';
import type {Filter, Stats, TestPathCases} from './types';
export type SearchResult = {
@ -114,7 +115,7 @@ export default class SearchSource {
private _filterTestPathsWithStats(
allPaths: Array<Test>,
testPathPatterns: TestPathPatterns,
testPathPatternsExecutor: TestPathPatternsExecutor,
): SearchResult {
const data: {
stats: Stats;
@ -132,9 +133,9 @@ export default class SearchSource {
};
const testCases = [...this._testPathCases]; // clone
if (testPathPatterns.isSet()) {
if (testPathPatternsExecutor.isSet()) {
testCases.push({
isMatch: (path: string) => testPathPatterns.isMatch(path),
isMatch: (path: string) => testPathPatternsExecutor.isMatch(path),
stat: 'testPathPatterns',
});
data.stats.testPathPatterns = 0;
@ -155,10 +156,12 @@ export default class SearchSource {
return data;
}
private _getAllTestPaths(testPathPatterns: TestPathPatterns): SearchResult {
private _getAllTestPaths(
testPathPatternsExecutor: TestPathPatternsExecutor,
): SearchResult {
return this._filterTestPathsWithStats(
toTests(this._context, this._context.hasteFS.getAllFiles()),
testPathPatterns,
testPathPatternsExecutor,
);
}
@ -166,8 +169,10 @@ export default class SearchSource {
return this._testPathCases.every(testCase => testCase.isMatch(path));
}
findMatchingTests(testPathPatterns: TestPathPatterns): SearchResult {
return this._getAllTestPaths(testPathPatterns);
findMatchingTests(
testPathPatternsExecutor: TestPathPatternsExecutor,
): SearchResult {
return this._getAllTestPaths(testPathPatternsExecutor);
}
async findRelatedTests(
@ -264,6 +269,7 @@ export default class SearchSource {
private async _getTestPaths(
globalConfig: Config.GlobalConfig,
projectConfig: Config.ProjectConfig,
changedFiles?: ChangedFiles,
): Promise<SearchResult> {
if (globalConfig.onlyChanged) {
@ -292,7 +298,9 @@ export default class SearchSource {
);
} else {
return this.findMatchingTests(
TestPathPatterns.fromGlobalConfig(globalConfig),
globalConfig.testPathPatterns.toExecutor({
rootDir: projectConfig.rootDir,
}),
);
}
}
@ -321,10 +329,15 @@ export default class SearchSource {
async getTestPaths(
globalConfig: Config.GlobalConfig,
projectConfig: Config.ProjectConfig,
changedFiles?: ChangedFiles,
filter?: Filter,
): Promise<SearchResult> {
const searchResult = await this._getTestPaths(globalConfig, changedFiles);
const searchResult = await this._getTestPaths(
globalConfig,
projectConfig,
changedFiles,
);
const filterPath = globalConfig.filter;

View File

@ -7,6 +7,7 @@
*/
import * as path from 'path';
import {TestPathPatterns} from '@jest/pattern';
import type {Test} from '@jest/test-result';
import type {Config} from '@jest/types';
import {normalize} from 'jest-config';
@ -111,12 +112,14 @@ describe('SearchSource', () => {
filter?: Filter,
) => {
const {searchSource, config} = await initSearchSource(initialOptions);
const allConfig = {
...config,
...initialOptions,
testPathPatterns: new TestPathPatterns([]),
};
const {tests: paths} = await searchSource.getTestPaths(
{
...config,
...initialOptions,
testPathPatterns: [],
},
allConfig,
allConfig,
null,
filter,
);

View File

@ -93,9 +93,12 @@ Object {
"onlyChanged": false,
"passWithNoTests": true,
"rootDir": "",
"testPathPatterns": Array [
"p.*3",
],
"testPathPatterns": Object {
"patterns": Array [
"p.*3",
],
"type": "TestPathPatterns",
},
"watch": true,
"watchAll": false,
}

View File

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {TestPathPatterns} from '@jest/pattern';
import {makeGlobalConfig} from '@jest/test-utils';
import type {Config} from '@jest/types';
import getNoTestsFoundMessage from '../getNoTestsFoundMessage';
@ -18,7 +19,7 @@ describe('getNoTestsFoundMessage', () => {
function createGlobalConfig(options?: Partial<Config.GlobalConfig>) {
return makeGlobalConfig({
rootDir: '/root/dir',
testPathPatterns: ['/path/pattern'],
testPathPatterns: new TestPathPatterns(['/path/pattern']),
...options,
});
}

View File

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {TestPathPatterns} from '@jest/pattern';
import runJest from '../runJest';
jest.mock('@jest/console');
@ -23,7 +24,7 @@ describe('runJest', () => {
contexts: [],
globalConfig: {
rootDir: '',
testPathPatterns: [],
testPathPatterns: new TestPathPatterns([]),
testSequencer: require.resolve('@jest/test-sequencer'),
watch: true,
},

View File

@ -7,6 +7,7 @@
*/
import chalk from 'chalk';
import {TestPathPatterns} from '@jest/pattern';
// eslint-disable-next-line import/order
import {JestHook, KEYS, TestWatcher} from 'jest-watcher';
@ -142,7 +143,7 @@ describe('Watch mode flows', () => {
pipe = {write: jest.fn()};
globalConfig = {
rootDir: '',
testPathPatterns: [],
testPathPatterns: new TestPathPatterns([]),
watch: true,
};
hasteMapInstances = [{on: () => {}}];
@ -156,7 +157,7 @@ describe('Watch mode flows', () => {
});
it('Correctly passing test path pattern', async () => {
globalConfig.testPathPatterns = ['test-*'];
globalConfig.testPathPatterns = new TestPathPatterns(['test-*']);
await watch(globalConfig, contexts, pipe, hasteMapInstances, stdin);
@ -690,6 +691,14 @@ describe('Watch mode flows', () => {
ok = ok === '✔︎';
const pluginPath = `${__dirname}/__fixtures__/plugin_path_config_updater_${option}`;
const newVal = (() => {
if (option === 'testPathPatterns') {
return new TestPathPatterns(['a/b', 'c']);
}
return '__JUST_TRYING__';
})();
jest.doMock(
pluginPath,
() =>
@ -699,7 +708,7 @@ describe('Watch mode flows', () => {
}
run(globalConfig, updateConfigAndRun) {
updateConfigAndRun({[option]: '__JUST_TRYING__'});
updateConfigAndRun({[option]: newVal});
return Promise.resolve();
}
},
@ -726,7 +735,7 @@ describe('Watch mode flows', () => {
if (!ok) {
expector = expector.not;
}
expector.toHaveProperty(option, '__JUST_TRYING__');
expector.toHaveProperty(option, newVal);
},
);
@ -904,7 +913,7 @@ describe('Watch mode flows', () => {
await nextTick();
expect(runJestMock.mock.calls[0][0].globalConfig).toMatchObject({
testPathPatterns: ['file'],
testPathPatterns: {patterns: ['file']},
watch: true,
watchAll: false,
});
@ -928,7 +937,7 @@ describe('Watch mode flows', () => {
expect(runJestMock.mock.calls[1][0].globalConfig).toMatchObject({
testNamePattern: 'test',
testPathPatterns: ['file'],
testPathPatterns: {patterns: ['file']},
watch: true,
watchAll: false,
});

View File

@ -7,6 +7,7 @@
*/
import chalk from 'chalk';
import {TestPathPatterns} from '@jest/pattern';
// eslint-disable-next-line import/order
import {KEYS} from 'jest-watcher';
@ -72,7 +73,7 @@ const nextTick = () => new Promise(resolve => process.nextTick(resolve));
const globalConfig = {
rootDir: '',
testPathPatterns: [],
testPathPatterns: new TestPathPatterns([]),
watch: true,
};

View File

@ -7,6 +7,7 @@
*/
import chalk from 'chalk';
import {TestPathPatterns} from '@jest/pattern';
// eslint-disable-next-line import/order
import {KEYS} from 'jest-watcher';
@ -84,7 +85,7 @@ const watch = require('../watch').default;
const globalConfig = {
rootDir: '',
testPathPatterns: [],
testPathPatterns: new TestPathPatterns([]),
watch: true,
};

View File

@ -7,7 +7,7 @@
import chalk = require('chalk');
import type {Config} from '@jest/types';
import {TestPathPatterns, pluralize} from 'jest-util';
import {pluralize} from 'jest-util';
import type {TestRunData} from './types';
export default function getNoTestFound(
@ -26,9 +26,8 @@ export default function getNoTestFound(
.map(p => `"${p}"`)
.join(', ')}`;
} else {
const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig);
dataMessage = `Pattern: ${chalk.yellow(
testPathPatterns.toPretty(),
globalConfig.testPathPatterns.toPretty(),
)} - 0 matches`;
}

View File

@ -7,7 +7,7 @@
import chalk = require('chalk');
import type {Config} from '@jest/types';
import {TestPathPatterns, pluralize} from 'jest-util';
import {pluralize} from 'jest-util';
import type {Stats, TestRunData} from './types';
export default function getNoTestFoundVerbose(
@ -56,9 +56,8 @@ export default function getNoTestFoundVerbose(
.map(p => `"${p}"`)
.join(', ')}`;
} else {
const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig);
dataMessage = `Pattern: ${chalk.yellow(
testPathPatterns.toPretty(),
globalConfig.testPathPatterns.toPretty(),
)} - 0 matches`;
}

View File

@ -7,11 +7,11 @@
import chalk = require('chalk');
import type {Config} from '@jest/types';
import {TestPathPatterns, isNonNullable} from 'jest-util';
import {isNonNullable} from 'jest-util';
const activeFilters = (globalConfig: Config.GlobalConfig): string => {
const {testNamePattern} = globalConfig;
const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig);
const testPathPatterns = globalConfig.testPathPatterns;
if (testNamePattern || testPathPatterns.isSet()) {
const filters = [
testPathPatterns.isSet()

View File

@ -17,7 +17,10 @@ export default function logDebugMessages(
): void {
const output = {
configs,
globalConfig,
globalConfig: {
...globalConfig,
testPathPatterns: globalConfig.testPathPatterns.patterns,
},
version: VERSION,
};
outputStream.write(`${JSON.stringify(output, null, ' ')}\n`);

View File

@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {TestPathPatterns} from '@jest/pattern';
import type {Config} from '@jest/types';
import {TestPathPatterns} from 'jest-util';
import type {AllowedConfigOptions} from 'jest-watcher';
type ExtraConfigOptions = Partial<
@ -32,13 +32,13 @@ export default function updateGlobalConfig(
}
if (options.testPathPatterns !== undefined) {
newConfig.testPathPatterns = options.testPathPatterns;
newConfig.testPathPatterns = new TestPathPatterns(options.testPathPatterns);
}
newConfig.onlyChanged =
!newConfig.watchAll &&
!newConfig.testNamePattern &&
!TestPathPatterns.fromGlobalConfig(newConfig).isSet();
!newConfig.testPathPatterns.isSet();
if (typeof options.bail === 'boolean') {
newConfig.bail = options.bail ? 1 : 0;

View File

@ -49,7 +49,10 @@ class TestPathPatternPlugin extends BaseWatchPlugin {
testPathPatternPrompt.run(
(value: string) => {
updateConfigAndRun({mode: 'watch', testPathPatterns: [value]});
updateConfigAndRun({
mode: 'watch',
testPathPatterns: [value],
});
resolve();
},
reject,

View File

@ -38,13 +38,19 @@ import type {Filter, TestRunData} from './types';
const getTestPaths = async (
globalConfig: Config.GlobalConfig,
projectConfig: Config.ProjectConfig,
source: SearchSource,
outputStream: WriteStream,
changedFiles: ChangedFiles | undefined,
jestHooks: JestHookEmitter,
filter?: Filter,
) => {
const data = await source.getTestPaths(globalConfig, changedFiles, filter);
const data = await source.getTestPaths(
globalConfig,
projectConfig,
changedFiles,
filter,
);
if (data.tests.length === 0 && globalConfig.onlyChanged && data.noSCM) {
new CustomConsole(outputStream, outputStream).log(
@ -188,6 +194,7 @@ export default async function runJest({
const searchSource = searchSources[index];
const matches = await getTestPaths(
globalConfig,
context.config,
searchSource,
outputStream,
changedFilesPromise && (await changedFilesPromise),

View File

@ -11,12 +11,12 @@ import ansiEscapes = require('ansi-escapes');
import chalk = require('chalk');
import exit = require('exit');
import slash = require('slash');
import {TestPathPatterns} from '@jest/pattern';
import type {TestContext} from '@jest/test-result';
import type {Config} from '@jest/types';
import type {IHasteMap as HasteMap} from 'jest-haste-map';
import {formatExecError} from 'jest-message-util';
import {
TestPathPatterns,
isInteractive,
preRunMessage,
requireOrImportModule,
@ -230,11 +230,14 @@ export default async function watch(
const emitFileChange = () => {
if (hooks.isUsed('onFileChange')) {
const testPathPatterns = new TestPathPatterns([], globalConfig);
const projects = searchSources.map(({context, searchSource}) => ({
config: context.config,
testPaths: searchSource
.findMatchingTests(testPathPatterns)
.findMatchingTests(
new TestPathPatterns([]).toExecutor({
rootDir: context.config.rootDir,
}),
)
.tests.map(t => t.path),
}));
hooks.getEmitter().onFileChange({projects});
@ -533,7 +536,7 @@ const usage = (
watchPlugins: Array<WatchPlugin>,
delimiter = '\n',
) => {
const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig);
const testPathPatterns = globalConfig.testPathPatterns;
const messages = [
activeFilters(globalConfig),

View File

@ -12,6 +12,7 @@
{"path": "../jest-console"},
{"path": "../jest-haste-map"},
{"path": "../jest-message-util"},
{"path": "../jest-pattern"},
{"path": "../jest-regex-util"},
{"path": "../jest-reporters"},
{"path": "../jest-resolve"},

View File

@ -0,0 +1,3 @@
# @jest/pattern
`@jest/pattern` is a helper library for the jest library that implements the logic for parsing and matching patterns.

View File

@ -0,0 +1,31 @@
{
"name": "@jest/pattern",
"version": "30.0.0-alpha.4",
"repository": {
"type": "git",
"url": "https://github.com/jestjs/jest.git",
"directory": "packages/jest-pattern"
},
"license": "MIT",
"main": "./build/index.js",
"types": "./build/index.d.ts",
"exports": {
".": {
"types": "./build/index.d.ts",
"require": "./build/index.js",
"import": "./build/index.mjs",
"default": "./build/index.js"
},
"./package.json": "./package.json"
},
"dependencies": {
"@types/node": "*",
"jest-regex-util": "workspace:*"
},
"engines": {
"node": "^16.10.0 || ^18.12.0 || >=20.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -5,42 +5,83 @@
* LICENSE file in the root directory of this source tree.
*/
import type {Config} from '@jest/types';
import {escapePathForRegex, replacePathSepForRegex} from 'jest-regex-util';
type PatternsConfig = {
export class TestPathPatterns {
constructor(readonly patterns: Array<string>) {}
/**
* Return true if there are any patterns.
*/
isSet(): boolean {
return this.patterns.length > 0;
}
/**
* Return true if the patterns are valid.
*/
isValid(): boolean {
return this.toExecutor({
// isValid() doesn't require rootDir to be accurate, so just
// specify a dummy rootDir here
rootDir: '/',
}).isValid();
}
/**
* Return a human-friendly version of the pattern regex.
*/
toPretty(): string {
return this.patterns.join('|');
}
/**
* Return a TestPathPatternsExecutor that can execute the patterns.
*/
toExecutor(
options: TestPathPatternsExecutorOptions,
): TestPathPatternsExecutor {
return new TestPathPatternsExecutor(this, options);
}
/** For jest serializers */
toJSON(): any {
return {
patterns: this.patterns,
type: 'TestPathPatterns',
};
}
}
export type TestPathPatternsExecutorOptions = {
rootDir: string;
};
export default class TestPathPatterns {
export class TestPathPatternsExecutor {
private _regexString: string | null = null;
constructor(
readonly patterns: Array<string>,
private readonly config: PatternsConfig,
readonly patterns: TestPathPatterns,
private readonly options: TestPathPatternsExecutorOptions,
) {}
static fromGlobalConfig(globalConfig: Config.GlobalConfig): TestPathPatterns {
return new TestPathPatterns(globalConfig.testPathPatterns, globalConfig);
}
private get regexString(): string {
if (this._regexString !== null) {
return this._regexString;
}
const rootDir = this.config.rootDir.replace(/\/*$/, '/');
const rootDir = this.options.rootDir.replace(/\/*$/, '/');
const rootDirRegex = escapePathForRegex(rootDir);
const regexString = this.patterns
const regexString = this.patterns.patterns
.map(p => {
// absolute paths passed on command line should stay same
if (/^\//.test(p)) {
if (p.startsWith('/')) {
return p;
}
// explicit relative paths should resolve against rootDir
if (/^\.\//.test(p)) {
if (p.startsWith('./')) {
return p.replace(/^\.\//, rootDirRegex);
}
@ -62,14 +103,19 @@ export default class TestPathPatterns {
* Return true if there are any patterns.
*/
isSet(): boolean {
return this.patterns.length > 0;
return this.patterns.isSet();
}
/**
* Throw an error if the patterns don't form a valid regex.
* Return true if the patterns are valid.
*/
validate(): void {
this.toRegex();
isValid(): boolean {
try {
this.toRegex();
return true;
} catch {
return false;
}
}
/**
@ -85,6 +131,6 @@ export default class TestPathPatterns {
* Return a human-friendly version of the pattern regex.
*/
toPretty(): string {
return this.patterns.join('|');
return this.patterns.toPretty();
}
}

View File

@ -6,16 +6,20 @@
*/
import type * as path from 'path';
import TestPathPatterns from '../TestPathPatterns';
import {
TestPathPatterns,
TestPathPatternsExecutor,
type TestPathPatternsExecutorOptions,
} from '../TestPathPatterns';
const mockSep = jest.fn();
const mockSep: jest.Mock<() => string> = jest.fn();
jest.mock('path', () => {
return {
...(jest.requireActual('path') as typeof path),
...jest.requireActual('path'),
get sep() {
return mockSep() || '/';
},
};
} as typeof path;
});
beforeEach(() => {
jest.resetAllMocks();
@ -23,88 +27,117 @@ beforeEach(() => {
const config = {rootDir: ''};
describe('TestPathPatterns', () => {
interface TestPathPatternsLike {
isSet(): boolean;
isValid(): boolean;
toPretty(): string;
}
const testPathPatternsLikeTests = (
makePatterns: (
patterns: Array<string>,
options: TestPathPatternsExecutorOptions,
) => TestPathPatternsLike,
) => {
describe('isSet', () => {
it('returns false if no patterns specified', () => {
const testPathPatterns = new TestPathPatterns([], config);
const testPathPatterns = makePatterns([], config);
expect(testPathPatterns.isSet()).toBe(false);
});
it('returns true if patterns specified', () => {
const testPathPatterns = new TestPathPatterns(['a'], config);
const testPathPatterns = makePatterns(['a'], config);
expect(testPathPatterns.isSet()).toBe(true);
});
});
describe('validate', () => {
describe('isValid', () => {
it('succeeds for empty patterns', () => {
const testPathPatterns = new TestPathPatterns([], config);
expect(() => testPathPatterns.validate()).not.toThrow();
const testPathPatterns = makePatterns([], config);
expect(testPathPatterns.isValid()).toBe(true);
});
it('succeeds for valid patterns', () => {
const testPathPatterns = new TestPathPatterns(['abc+', 'z.*'], config);
expect(() => testPathPatterns.validate()).not.toThrow();
const testPathPatterns = makePatterns(['abc+', 'z.*'], config);
expect(testPathPatterns.isValid()).toBe(true);
});
it('fails for at least one invalid pattern', () => {
const testPathPatterns = new TestPathPatterns(
['abc+', '(', 'z.*'],
config,
);
expect(() => testPathPatterns.validate()).toThrow(
'Invalid regular expression',
);
const testPathPatterns = makePatterns(['abc+', '(', 'z.*'], config);
expect(testPathPatterns.isValid()).toBe(false);
});
});
describe('toPretty', () => {
it('renders a human-readable string', () => {
const testPathPatterns = makePatterns(['a/b', 'c/d'], config);
expect(testPathPatterns.toPretty()).toMatchSnapshot();
});
});
};
describe('TestPathPatterns', () => {
testPathPatternsLikeTests(
(patterns: Array<string>, _: TestPathPatternsExecutorOptions) =>
new TestPathPatterns(patterns),
);
});
describe('TestPathPatternsExecutor', () => {
const makeExecutor = (
patterns: Array<string>,
options: TestPathPatternsExecutorOptions,
) => new TestPathPatternsExecutor(new TestPathPatterns(patterns), options);
testPathPatternsLikeTests(makeExecutor);
describe('isMatch', () => {
it('returns true with no patterns', () => {
const testPathPatterns = new TestPathPatterns([], config);
const testPathPatterns = makeExecutor([], config);
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
});
it('returns true for same path', () => {
const testPathPatterns = new TestPathPatterns(['/a/b'], config);
const testPathPatterns = makeExecutor(['/a/b'], config);
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
});
it('returns true for same path with case insensitive', () => {
const testPathPatternsUpper = new TestPathPatterns(['/A/B'], config);
const testPathPatternsUpper = makeExecutor(['/A/B'], config);
expect(testPathPatternsUpper.isMatch('/a/b')).toBe(true);
expect(testPathPatternsUpper.isMatch('/A/B')).toBe(true);
const testPathPatternsLower = new TestPathPatterns(['/a/b'], config);
const testPathPatternsLower = makeExecutor(['/a/b'], config);
expect(testPathPatternsLower.isMatch('/A/B')).toBe(true);
expect(testPathPatternsLower.isMatch('/a/b')).toBe(true);
});
it('returns true for contained path', () => {
const testPathPatterns = new TestPathPatterns(['b/c'], config);
const testPathPatterns = makeExecutor(['b/c'], config);
expect(testPathPatterns.isMatch('/a/b/c/d')).toBe(true);
});
it('returns true for explicit relative path', () => {
const testPathPatterns = new TestPathPatterns(['./b/c'], {
const testPathPatterns = makeExecutor(['./b/c'], {
rootDir: '/a',
});
expect(testPathPatterns.isMatch('/a/b/c')).toBe(true);
});
it('returns true for partial file match', () => {
const testPathPatterns = new TestPathPatterns(['aaa'], config);
const testPathPatterns = makeExecutor(['aaa'], config);
expect(testPathPatterns.isMatch('/foo/..aaa..')).toBe(true);
expect(testPathPatterns.isMatch('/foo/..aaa')).toBe(true);
expect(testPathPatterns.isMatch('/foo/aaa..')).toBe(true);
});
it('returns true for path suffix', () => {
const testPathPatterns = new TestPathPatterns(['c/d'], config);
const testPathPatterns = makeExecutor(['c/d'], config);
expect(testPathPatterns.isMatch('/a/b/c/d')).toBe(true);
});
it('returns true if regex matches', () => {
const testPathPatterns = new TestPathPatterns(['ab*c?'], config);
const testPathPatterns = makeExecutor(['ab*c?'], config);
expect(testPathPatterns.isMatch('/foo/a')).toBe(true);
expect(testPathPatterns.isMatch('/foo/ab')).toBe(true);
@ -117,7 +150,7 @@ describe('TestPathPatterns', () => {
});
it('returns true only if matches relative path', () => {
const testPathPatterns = new TestPathPatterns(['home'], {
const testPathPatterns = makeExecutor(['home'], {
rootDir: '/home/myuser/',
});
expect(testPathPatterns.isMatch('/home/myuser/LoginPage.js')).toBe(false);
@ -125,14 +158,14 @@ describe('TestPathPatterns', () => {
});
it('matches absolute paths regardless of rootDir', () => {
const testPathPatterns = new TestPathPatterns(['/a/b'], {
const testPathPatterns = makeExecutor(['/a/b'], {
rootDir: '/foo/bar',
});
expect(testPathPatterns.isMatch('/a/b')).toBe(true);
});
it('returns true if match any paths', () => {
const testPathPatterns = new TestPathPatterns(['a/b', 'c/d'], config);
const testPathPatterns = makeExecutor(['a/b', 'c/d'], config);
expect(testPathPatterns.isMatch('/foo/a/b')).toBe(true);
expect(testPathPatterns.isMatch('/foo/c/d')).toBe(true);
@ -143,21 +176,14 @@ describe('TestPathPatterns', () => {
it('does not normalize Windows paths on POSIX', () => {
mockSep.mockReturnValue('/');
const testPathPatterns = new TestPathPatterns(['a\\z', 'a\\\\z'], config);
const testPathPatterns = makeExecutor(['a\\z', 'a\\\\z'], config);
expect(testPathPatterns.isMatch('/foo/a/z')).toBe(false);
});
it('normalizes paths for Windows', () => {
mockSep.mockReturnValue('\\');
const testPathPatterns = new TestPathPatterns(['a/b'], config);
const testPathPatterns = makeExecutor(['a/b'], config);
expect(testPathPatterns.isMatch('\\foo\\a\\b')).toBe(true);
});
});
describe('toPretty', () => {
it('renders a human-readable string', () => {
const testPathPatterns = new TestPathPatterns(['a/b', 'c/d'], config);
expect(testPathPatterns.toPretty()).toMatchSnapshot();
});
});
});

View File

@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TestPathPatterns toPretty renders a human-readable string 1`] = `"a/b|c/d"`;
exports[`TestPathPatternsExecutor toPretty renders a human-readable string 1`] = `"a/b|c/d"`;

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.
*/
export {
TestPathPatterns,
TestPathPatternsExecutor,
type TestPathPatternsExecutorOptions,
} from './TestPathPatterns';

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build"
},
"include": ["./src/**/*"],
"exclude": ["./**/__tests__/**/*"],
"references": [{"path": "../jest-regex-util"}]
}

View File

@ -40,6 +40,7 @@
"v8-to-istanbul": "^9.0.1"
},
"devDependencies": {
"@jest/pattern": "workspace:*",
"@jest/test-utils": "workspace:*",
"@types/exit": "^0.1.30",
"@types/graceful-fs": "^4.1.3",

View File

@ -12,7 +12,6 @@ import type {
TestContext,
} from '@jest/test-result';
import type {Config} from '@jest/types';
import {TestPathPatterns} from 'jest-util';
import BaseReporter from './BaseReporter';
import getResultHeader from './getResultHeader';
import getSnapshotSummary from './getSnapshotSummary';
@ -212,7 +211,7 @@ export default class SummaryReporter extends BaseReporter {
testContexts: Set<TestContext>,
globalConfig: Config.GlobalConfig,
) {
const testPathPatterns = TestPathPatterns.fromGlobalConfig(globalConfig);
const testPathPatterns = globalConfig.testPathPatterns;
const getMatchingTestsInfo = () => {
const prefix = globalConfig.findRelatedTests

View File

@ -6,6 +6,8 @@
*/
'use strict';
import {TestPathPatterns} from '@jest/pattern';
let SummaryReporter;
const env = {...process.env};
@ -13,7 +15,7 @@ const now = Date.now;
const write = process.stderr.write;
const globalConfig = {
rootDir: 'root',
testPathPatterns: [],
testPathPatterns: new TestPathPatterns([]),
watch: false,
};

View File

@ -9,6 +9,7 @@
"references": [
{"path": "../jest-console"},
{"path": "../jest-message-util"},
{"path": "../jest-pattern"},
{"path": "../jest-resolve"},
{"path": "../jest-test-result"},
{"path": "../jest-transform"},

View File

@ -20,6 +20,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@jest/pattern": "workspace:*",
"@jest/schemas": "workspace:*",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",

View File

@ -8,6 +8,7 @@
import type {ForegroundColor} from 'chalk';
import type {ReportOptions} from 'istanbul-reports';
import type {Arguments} from 'yargs';
import type {TestPathPatterns} from '@jest/pattern';
import type {InitialOptions, SnapshotFormat} from '@jest/schemas';
export type {InitialOptions} from '@jest/schemas';
@ -305,7 +306,7 @@ export type GlobalConfig = {
errorOnDeprecated: boolean;
testFailureExitCode: number;
testNamePattern?: string;
testPathPatterns: Array<string>;
testPathPatterns: TestPathPatterns;
testResultsProcessor?: string;
testSequencer: string;
testTimeout?: number;

View File

@ -5,5 +5,5 @@
"outDir": "build"
},
"include": ["./src/**/*"],
"references": [{"path": "../jest-schemas"}]
"references": [{"path": "../jest-pattern"}, {"path": "../jest-schemas"}]
}

View File

@ -24,7 +24,6 @@
"chalk": "^4.0.0",
"ci-info": "^4.0.0",
"graceful-fs": "^4.2.9",
"jest-regex-util": "workspace:*",
"picomatch": "^4.0.0"
},
"devDependencies": {

View File

@ -21,7 +21,6 @@ export {default as deepCyclicCopy} from './deepCyclicCopy';
export {default as convertDescriptorToString} from './convertDescriptorToString';
export {specialChars};
export {default as replacePathSepForGlob} from './replacePathSepForGlob';
export {default as TestPathPatterns} from './TestPathPatterns';
export {default as globsToMatcher} from './globsToMatcher';
export {preRunMessage};
export {default as pluralize} from './pluralize';

View File

@ -6,5 +6,5 @@
},
"include": ["./src/**/*"],
"exclude": ["./**/__tests__/**/*"],
"references": [{"path": "../jest-regex-util"}, {"path": "../jest-types"}]
"references": [{"path": "../jest-types"}]
}

View File

@ -63,10 +63,12 @@ export type AllowedConfigOptions = Partial<
| 'onlyFailures'
| 'reporters'
| 'testNamePattern'
| 'testPathPatterns'
| 'updateSnapshot'
| 'verbose'
> & {mode: 'watch' | 'watchAll'}
> & {
mode: 'watch' | 'watchAll';
testPathPatterns: Array<string>;
}
>;
export type UpdateConfigCallback = (config?: AllowedConfigOptions) => void;

View File

@ -15,6 +15,7 @@
},
"dependencies": {
"@jest/globals": "workspace:*",
"@jest/pattern": "workspace:*",
"@jest/types": "workspace:*",
"@types/node": "*",
"ansi-regex": "^5.0.1",

View File

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {TestPathPatterns} from '@jest/pattern';
import type {Config} from '@jest/types';
const DEFAULT_GLOBAL_CONFIG: Config.GlobalConfig = {
@ -55,7 +56,7 @@ const DEFAULT_GLOBAL_CONFIG: Config.GlobalConfig = {
snapshotFormat: {},
testFailureExitCode: 1,
testNamePattern: '',
testPathPatterns: [],
testPathPatterns: new TestPathPatterns([]),
testResultsProcessor: undefined,
testSequencer: '@jest/test-sequencer',
testTimeout: 5000,

View File

@ -7,6 +7,7 @@
"include": ["./src/**/*"],
"references": [
{"path": "../jest-globals"},
{"path": "../jest-pattern"},
{"path": "../jest-types"},
{"path": "../pretty-format"}
]

View File

@ -50,6 +50,7 @@ const packagesNotToTest = [
'jest-matcher-utils',
'jest-message-util',
'jest-mock',
'jest-pattern',
'jest-phabricator',
'jest-regex-util',
'jest-repl',

View File

@ -2890,6 +2890,7 @@ __metadata:
resolution: "@jest/core@workspace:packages/jest-core"
dependencies:
"@jest/console": "workspace:*"
"@jest/pattern": "workspace:*"
"@jest/reporters": "workspace:*"
"@jest/test-result": "workspace:*"
"@jest/test-sequencer": "workspace:*"
@ -3113,12 +3114,22 @@ __metadata:
languageName: unknown
linkType: soft
"@jest/pattern@workspace:*, @jest/pattern@workspace:packages/jest-pattern":
version: 0.0.0-use.local
resolution: "@jest/pattern@workspace:packages/jest-pattern"
dependencies:
"@types/node": "*"
jest-regex-util: "workspace:*"
languageName: unknown
linkType: soft
"@jest/reporters@workspace:*, @jest/reporters@workspace:packages/jest-reporters":
version: 0.0.0-use.local
resolution: "@jest/reporters@workspace:packages/jest-reporters"
dependencies:
"@bcoe/v8-coverage": ^0.2.3
"@jest/console": "workspace:*"
"@jest/pattern": "workspace:*"
"@jest/test-result": "workspace:*"
"@jest/test-utils": "workspace:*"
"@jest/transform": "workspace:*"
@ -3256,6 +3267,7 @@ __metadata:
resolution: "@jest/test-utils@workspace:packages/test-utils"
dependencies:
"@jest/globals": "workspace:*"
"@jest/pattern": "workspace:*"
"@jest/types": "workspace:*"
"@types/node": "*"
"@types/semver": ^7.1.0
@ -3326,6 +3338,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@jest/types@workspace:packages/jest-types"
dependencies:
"@jest/pattern": "workspace:*"
"@jest/schemas": "workspace:*"
"@types/istanbul-lib-coverage": ^2.0.0
"@types/istanbul-reports": ^3.0.0
@ -12755,6 +12768,7 @@ __metadata:
resolution: "jest-config@workspace:packages/jest-config"
dependencies:
"@babel/core": ^7.11.6
"@jest/pattern": "workspace:*"
"@jest/test-sequencer": "workspace:*"
"@jest/types": "workspace:*"
"@types/graceful-fs": ^4.1.3
@ -13257,7 +13271,6 @@ __metadata:
chalk: ^4.0.0
ci-info: ^4.0.0
graceful-fs: ^4.2.9
jest-regex-util: "workspace:*"
picomatch: ^4.0.0
languageName: unknown
linkType: soft