diff --git a/CHANGELOG.md b/CHANGELOG.md index f74e62d52ead..2f45f4ed4845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[@jest/core]` Filter API pre-filter setup hook ([#8142](https://github.com/facebook/jest/pull/8142)) - `[jest-snapshot]` Improve report when matcher fails, part 14 ([#8132](https://github.com/facebook/jest/pull/8132)) - `[@jest/reporter]` Display todo and skip test descriptions when verbose is true ([#8038](https://github.com/facebook/jest/pull/8038)) diff --git a/e2e/__tests__/filter.test.ts b/e2e/__tests__/filter.test.ts index 550330fd507f..32253ab77ed2 100644 --- a/e2e/__tests__/filter.test.ts +++ b/e2e/__tests__/filter.test.ts @@ -43,4 +43,29 @@ describe('Dynamic test filtering', () => { expect(result.stderr).toContain('did not return a valid test list'); expect(result.stderr).toContain('my-clowny-filter'); }); + + it('will call setup on filter before filtering', () => { + const result = runJest('filter', ['--filter=/my-setup-filter.js']); + + expect(result.status).toBe(0); + expect(result.stderr).toContain('1 total'); + }); + + it('will print error when filter throws', () => { + const result = runJest('filter', [ + '--filter=/my-broken-filter.js', + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Error: My broken filter error.'); + }); + + it('will return no results when setup hook throws', () => { + const result = runJest('filter', [ + '--filter=/my-broken-setup-filter.js', + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('Error: My broken setup filter error.'); + }); }); diff --git a/e2e/filter/my-broken-filter.js b/e2e/filter/my-broken-filter.js new file mode 100644 index 000000000000..a1faac8150a9 --- /dev/null +++ b/e2e/filter/my-broken-filter.js @@ -0,0 +1,9 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +'use strict'; + +module.exports = function(tests) { + return new Promise((resolve, reject) => { + reject(new Error('My broken filter error.')); + }); +}; diff --git a/e2e/filter/my-broken-setup-filter.js b/e2e/filter/my-broken-setup-filter.js new file mode 100644 index 000000000000..3a7651c0ccb9 --- /dev/null +++ b/e2e/filter/my-broken-setup-filter.js @@ -0,0 +1,17 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +'use strict'; + +module.exports = function(tests) { + return { + filtered: tests.filter(t => t.indexOf('foo') !== -1).map(test => ({test})), + }; +}; + +module.exports.setup = function() { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error('My broken setup filter error.')); + }, 0); + }); +}; diff --git a/e2e/filter/my-setup-filter.js b/e2e/filter/my-setup-filter.js new file mode 100644 index 000000000000..62861b832114 --- /dev/null +++ b/e2e/filter/my-setup-filter.js @@ -0,0 +1,24 @@ +// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + +'use strict'; + +const setupData = { + filterText: 'this will return no tests', +}; + +module.exports = function(tests) { + return { + filtered: tests + .filter(t => t.indexOf(setupData.filterText) !== -1) + .map(test => ({test})), + }; +}; + +module.exports.setup = function() { + return new Promise(resolve => { + setTimeout(() => { + setupData.filterText = 'foo'; + resolve(); + }, 1000); + }); +}; diff --git a/packages/jest-core/src/SearchSource.ts b/packages/jest-core/src/SearchSource.ts index ee28ca92bc8b..99f03b29781b 100644 --- a/packages/jest-core/src/SearchSource.ts +++ b/packages/jest-core/src/SearchSource.ts @@ -21,6 +21,7 @@ import { TestPathCases, TestPathCasesWithPathPattern, TestPathCaseWithPathPatternStats, + Filter, } from './types'; export type SearchResult = { @@ -41,11 +42,6 @@ export type TestSelectionConfig = { watch?: boolean; }; -type FilterResult = { - test: string; - message: string; -}; - const globsToMatcher = (globs?: Array | null) => { if (globs == null || globs.length === 0) { return () => true; @@ -290,18 +286,16 @@ export default class SearchSource { async getTestPaths( globalConfig: Config.GlobalConfig, changedFiles: ChangedFiles | undefined, + filter?: Filter, ): Promise { const searchResult = this._getTestPaths(globalConfig, changedFiles); const filterPath = globalConfig.filter; - if (filterPath && !globalConfig.skipFilter) { + if (filter) { const tests = searchResult.tests; - const filter = require(filterPath); - const filterResult: {filtered: Array} = await filter( - tests.map(test => test.path), - ); + const filterResult = await filter(tests.map(test => test.path)); if (!Array.isArray(filterResult.filtered)) { throw new Error( diff --git a/packages/jest-core/src/cli/index.ts b/packages/jest-core/src/cli/index.ts index a1dce1db10b9..ea14271226ee 100644 --- a/packages/jest-core/src/cli/index.ts +++ b/packages/jest-core/src/cli/index.ts @@ -16,6 +16,7 @@ import HasteMap from 'jest-haste-map'; import chalk from 'chalk'; import rimraf from 'rimraf'; import exit from 'exit'; +import {Filter} from '../types'; import createContext from '../lib/create_context'; import getChangedFilesPromise from '../getChangedFilesPromise'; import {formatHandleErrors} from '../collectHandles'; @@ -145,6 +146,36 @@ const _run = async ( // as soon as possible, so by the time we need the result it's already there. const changedFilesPromise = getChangedFilesPromise(globalConfig, configs); + // Filter may need to do an HTTP call or something similar to setup. + // We will wait on an async response from this before using the filter. + let filter: Filter | undefined; + if (globalConfig.filter && !globalConfig.skipFilter) { + const rawFilter = require(globalConfig.filter); + let filterSetupPromise: Promise | undefined; + if (rawFilter.setup) { + // Wrap filter setup Promise to avoid "uncaught Promise" error. + // If an error is returned, we surface it in the return value. + filterSetupPromise = (async () => { + try { + await rawFilter.setup(); + } catch (err) { + return err; + } + return undefined; + })(); + } + filter = async (testPaths: Array) => { + if (filterSetupPromise) { + // Expect an undefined return value unless there was an error. + const err = await filterSetupPromise; + if (err) { + throw err; + } + } + return rawFilter(testPaths); + }; + } + const {contexts, hasteMapInstances} = await buildContextsAndHasteMaps( configs, globalConfig, @@ -159,6 +190,7 @@ const _run = async ( globalConfig, outputStream, hasteMapInstances, + filter, ) : await runWithoutWatch( globalConfig, @@ -166,6 +198,7 @@ const _run = async ( outputStream, onComplete, changedFilesPromise, + filter, ); }; @@ -176,17 +209,34 @@ const runWatch = async ( globalConfig: Config.GlobalConfig, outputStream: NodeJS.WriteStream, hasteMapInstances: Array, + filter?: Filter, ) => { if (hasDeprecationWarnings) { try { await handleDeprecationWarnings(outputStream, process.stdin); - return watch(globalConfig, contexts, outputStream, hasteMapInstances); + return watch( + globalConfig, + contexts, + outputStream, + hasteMapInstances, + undefined, + undefined, + filter, + ); } catch (e) { exit(0); } } - return watch(globalConfig, contexts, outputStream, hasteMapInstances); + return watch( + globalConfig, + contexts, + outputStream, + hasteMapInstances, + undefined, + undefined, + filter, + ); }; const runWithoutWatch = async ( @@ -195,6 +245,7 @@ const runWithoutWatch = async ( outputStream: NodeJS.WritableStream, onComplete: OnCompleteCallback, changedFilesPromise?: ChangedFilesPromise, + filter?: Filter, ) => { const startRun = async (): Promise => { if (!globalConfig.listTests) { @@ -204,6 +255,7 @@ const runWithoutWatch = async ( changedFilesPromise, contexts, failedTestsCache: undefined, + filter, globalConfig, onComplete, outputStream, diff --git a/packages/jest-core/src/runJest.ts b/packages/jest-core/src/runJest.ts index 1e91cb16ec83..fad2435d8ee0 100644 --- a/packages/jest-core/src/runJest.ts +++ b/packages/jest-core/src/runJest.ts @@ -29,7 +29,7 @@ import TestSequencer from './TestSequencer'; import FailedTestsCache from './FailedTestsCache'; import collectNodeHandles from './collectHandles'; import TestWatcher from './TestWatcher'; -import {TestRunData} from './types'; +import {TestRunData, Filter} from './types'; const getTestPaths = async ( globalConfig: Config.GlobalConfig, @@ -37,9 +37,10 @@ const getTestPaths = async ( outputStream: NodeJS.WritableStream, changedFiles: ChangedFiles | undefined, jestHooks: JestHookEmitter, + filter?: Filter, ) => { const source = new SearchSource(context); - const data = await source.getTestPaths(globalConfig, changedFiles); + const data = await source.getTestPaths(globalConfig, changedFiles, filter); if (!data.tests.length && globalConfig.onlyChanged && data.noSCM) { new CustomConsole(outputStream, outputStream).log( @@ -129,6 +130,7 @@ export default (async function runJest({ changedFilesPromise, onComplete, failedTestsCache, + filter, }: { globalConfig: Config.GlobalConfig; contexts: Array; @@ -139,6 +141,7 @@ export default (async function runJest({ changedFilesPromise?: ChangedFilesPromise; onComplete: (testResults: AggregatedResult) => void; failedTestsCache?: FailedTestsCache; + filter?: Filter; }) { const sequencer = new TestSequencer(); let allTests: Array = []; @@ -168,6 +171,7 @@ export default (async function runJest({ outputStream, changedFilesPromise && (await changedFilesPromise), jestHooks, + filter, ); allTests = allTests.concat(matches.tests); diff --git a/packages/jest-core/src/types.ts b/packages/jest-core/src/types.ts index 450f8095c3b9..9e57b7039c6c 100644 --- a/packages/jest-core/src/types.ts +++ b/packages/jest-core/src/types.ts @@ -38,3 +38,14 @@ export type TestPathCases = { export type TestPathCasesWithPathPattern = TestPathCases & { testPathPattern: (path: Config.Path) => boolean; }; + +export type FilterResult = { + test: string; + message: string; +}; + +export type Filter = ( + testPaths: Array, +) => Promise<{ + filtered: Array; +}>; diff --git a/packages/jest-core/src/watch.ts b/packages/jest-core/src/watch.ts index b372d33fc04d..2efe8e05b165 100644 --- a/packages/jest-core/src/watch.ts +++ b/packages/jest-core/src/watch.ts @@ -39,6 +39,7 @@ import { filterInteractivePlugins, } from './lib/watch_plugins_helpers'; import activeFilters from './lib/active_filters_message'; +import {Filter} from './types'; type ReservedInfo = { forbiddenOverwriteMessage?: string; @@ -83,6 +84,7 @@ export default function watch( hasteMapInstances: Array, stdin: NodeJS.ReadStream = process.stdin, hooks: JestHook = new JestHook(), + filter?: Filter, ): Promise { // `globalConfig` will be constantly updated and reassigned as a result of // watch mode interactions. @@ -262,6 +264,7 @@ export default function watch( changedFilesPromise, contexts, failedTestsCache, + filter, globalConfig, jestHooks: hooks.getEmitter(), onComplete: results => {