diff --git a/packages/jest-cli/src/__tests__/__snapshots__/watch.test.js.snap b/packages/jest-cli/src/__tests__/__snapshots__/watch.test.js.snap index 298b413cdad2..73a5abba0eb3 100644 --- a/packages/jest-cli/src/__tests__/__snapshots__/watch.test.js.snap +++ b/packages/jest-cli/src/__tests__/__snapshots__/watch.test.js.snap @@ -12,6 +12,7 @@ Array [ " Watch Usage › Press a to run all tests. + › Press f to run only failed tests. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press q to quit watch mode. @@ -26,6 +27,7 @@ Array [ " Watch Usage › Press a to run all tests. + › Press f to run only failed tests. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press s to do nothing. diff --git a/packages/jest-cli/src/__tests__/failed_tests_cache.test.js b/packages/jest-cli/src/__tests__/failed_tests_cache.test.js new file mode 100644 index 000000000000..26dc193e5f75 --- /dev/null +++ b/packages/jest-cli/src/__tests__/failed_tests_cache.test.js @@ -0,0 +1,62 @@ +import FailedTestsCache from '../failed_tests_cache'; + +describe('FailedTestsCache', () => { + test('should filter tests', () => { + const failedTestsCache = new FailedTestsCache(); + failedTestsCache.setTestResults([ + { + numFailingTests: 0, + testFilePath: '/path/to/passing.js', + testResults: [ + {fullName: 'test 1', status: 'passed'}, + {fullName: 'test 2', status: 'passed'}, + ], + }, + { + numFailingTests: 2, + testFilePath: '/path/to/failed_1.js', + testResults: [ + {fullName: 'test 3', status: 'failed'}, + {fullName: 'test 4', status: 'failed'}, + ], + }, + { + numFailingTests: 1, + testFilePath: '/path/to/failed_2.js', + testResults: [ + {fullName: 'test 5', status: 'failed'}, + {fullName: 'test 6', status: 'passed'}, + ], + }, + ]); + + const result = failedTestsCache.filterTests([ + { + path: '/path/to/passing.js', + }, + { + path: '/path/to/failed_1.js', + }, + { + path: '/path/to/failed_2.js', + }, + { + path: '/path/to/unknown.js', + }, + ]); + expect(result).toMatchObject([ + { + path: '/path/to/failed_1.js', + }, + { + path: '/path/to/failed_2.js', + }, + ]); + expect(failedTestsCache.updateConfig({})).toMatchObject({ + enabledTestsMap: { + '/path/to/failed_1.js': {'test 3': true, 'test 4': true}, + '/path/to/failed_2.js': {'test 5': true}, + }, + }); + }); +}); diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index c312674a2d9e..2d7f2de1a4f2 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -349,6 +349,12 @@ export const options = { 'running tests in a git repository at the moment.', type: 'boolean', }, + onlyFailures: { + alias: 'f', + default: undefined, + description: 'Run tests that failed in the previous execution.', + type: 'boolean', + }, outputFile: { description: 'Write test results to a file when the --json option is ' + diff --git a/packages/jest-cli/src/cli/index.js b/packages/jest-cli/src/cli/index.js index 750a84494827..c4c889e26fcc 100644 --- a/packages/jest-cli/src/cli/index.js +++ b/packages/jest-cli/src/cli/index.js @@ -368,6 +368,7 @@ const runWithoutWatch = async ( return await runJest({ changedFilesPromise, contexts, + failedTestsCache: null, globalConfig, onComplete, outputStream, diff --git a/packages/jest-cli/src/constants.js b/packages/jest-cli/src/constants.js index a4a37a1e2d95..6341694c41e1 100644 --- a/packages/jest-cli/src/constants.js +++ b/packages/jest-cli/src/constants.js @@ -23,6 +23,7 @@ export const KEYS = { CONTROL_D: '04', ENTER: '0d', ESCAPE: '1b', + F: '66', O: '6f', P: '70', Q: '71', diff --git a/packages/jest-cli/src/failed_tests_cache.js b/packages/jest-cli/src/failed_tests_cache.js new file mode 100644 index 000000000000..edc0cd265795 --- /dev/null +++ b/packages/jest-cli/src/failed_tests_cache.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Test} from 'types/TestRunner'; +import type {TestResult} from 'types/TestResult'; +import type {GlobalConfig} from 'types/Config'; + +export default class FailedTestsCache { + _enabledTestsMap: ?{[key: string]: {[key: string]: boolean}}; + + filterTests(tests: Array): Array { + if (!this._enabledTestsMap) { + return tests; + } + // $FlowFixMe + return tests.filter(testResult => this._enabledTestsMap[testResult.path]); + } + + setTestResults(testResults: Array) { + this._enabledTestsMap = (testResults || []) + .filter(testResult => testResult.numFailingTests) + .reduce((suiteMap, testResult) => { + suiteMap[testResult.testFilePath] = testResult.testResults + .filter(test => test.status === 'failed') + .reduce((testMap, test) => { + testMap[test.fullName] = true; + return testMap; + }, {}); + return suiteMap; + }, {}); + this._enabledTestsMap = Object.freeze(this._enabledTestsMap); + } + + updateConfig(globalConfig: GlobalConfig): GlobalConfig { + if (!this._enabledTestsMap) { + return globalConfig; + } + // $FlowFixMe Object.assign + const newConfig: GlobalConfig = Object.assign({}, globalConfig); + newConfig.enabledTestsMap = this._enabledTestsMap; + return Object.freeze(newConfig); + } +} diff --git a/packages/jest-cli/src/get_no_test_found_failed.js b/packages/jest-cli/src/get_no_test_found_failed.js new file mode 100644 index 000000000000..737499e53f3b --- /dev/null +++ b/packages/jest-cli/src/get_no_test_found_failed.js @@ -0,0 +1,8 @@ +import chalk from 'chalk'; + +export default function getNoTestFoundFailed() { + return ( + chalk.bold('No failed test found.\n') + + chalk.dim('Press `f` to run all tests.') + ); +} diff --git a/packages/jest-cli/src/get_no_test_found_message.js b/packages/jest-cli/src/get_no_test_found_message.js index d870c144c47d..568e412a22ab 100644 --- a/packages/jest-cli/src/get_no_test_found_message.js +++ b/packages/jest-cli/src/get_no_test_found_message.js @@ -1,11 +1,15 @@ import getNoTestFound from './get_no_test_found'; import getNoTestFoundRelatedToChangedFiles from './get_no_test_found_related_to_changed_files'; import getNoTestFoundVerbose from './get_no_test_found_verbose'; +import getNoTestFoundFailed from './get_no_test_found_failed'; export default function getNoTestsFoundMessage( testRunData, globalConfig, ): string { + if (globalConfig.onlyFailures) { + return getNoTestFoundFailed(); + } if (globalConfig.onlyChanged) { return getNoTestFoundRelatedToChangedFiles(globalConfig); } diff --git a/packages/jest-cli/src/lib/update_global_config.js b/packages/jest-cli/src/lib/update_global_config.js index c994672c3dba..153e2a1e1a8b 100644 --- a/packages/jest-cli/src/lib/update_global_config.js +++ b/packages/jest-cli/src/lib/update_global_config.js @@ -16,6 +16,7 @@ type Options = { updateSnapshot?: SnapshotUpdateState, mode?: 'watch' | 'watchAll', passWithNoTests?: boolean, + onlyFailures?: boolean, }; export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => { @@ -60,5 +61,9 @@ export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => { newConfig.passWithNoTests = true; } + if ('onlyFailures' in options) { + newConfig.onlyFailures = options.onlyFailures || false; + } + return Object.freeze(newConfig); }; diff --git a/packages/jest-cli/src/run_jest.js b/packages/jest-cli/src/run_jest.js index a4a8a2a552d4..6ece917d9d75 100644 --- a/packages/jest-cli/src/run_jest.js +++ b/packages/jest-cli/src/run_jest.js @@ -22,6 +22,7 @@ import SearchSource from './search_source'; import TestScheduler from './test_scheduler'; import TestSequencer from './test_sequencer'; import {makeEmptyAggregatedTestResult} from './test_result_helpers'; +import FailedTestsCache from './failed_tests_cache'; const setConfig = (contexts, newConfig) => contexts.forEach( @@ -86,6 +87,7 @@ export default (async function runJest({ startRun, changedFilesPromise, onComplete, + failedTestsCache, }: { globalConfig: GlobalConfig, contexts: Array, @@ -94,6 +96,7 @@ export default (async function runJest({ startRun: (globalConfig: GlobalConfig) => *, changedFilesPromise: ?ChangedFilesPromise, onComplete: (testResults: AggregatedResult) => any, + failedTestsCache: ?FailedTestsCache, }) { const sequencer = new TestSequencer(); let allTests = []; @@ -138,6 +141,12 @@ export default (async function runJest({ onComplete && onComplete(makeEmptyAggregatedTestResult()); return null; } + + if (globalConfig.onlyFailures && failedTestsCache) { + allTests = failedTestsCache.filterTests(allTests); + globalConfig = failedTestsCache.updateConfig(globalConfig); + } + if (!allTests.length) { const noTestsFoundMessage = getNoTestsFoundMessage( testRunData, diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index df86bfaa0fa9..e94c02b129cf 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -27,6 +27,7 @@ import TestWatcher from './test_watcher'; import Prompt from './lib/Prompt'; import TestPathPatternPrompt from './test_path_pattern_prompt'; import TestNamePatternPrompt from './test_name_pattern_prompt'; +import FailedTestsCache from './failed_tests_cache'; import WatchPluginRegistry from './lib/watch_plugin_registry'; import {KEYS, CLEAR} from './constants'; @@ -55,6 +56,7 @@ export default function watch( } } + const failedTestsCache = new FailedTestsCache(); const prompt = new Prompt(); const testPathPatternPrompt = new TestPathPatternPrompt(outputStream, prompt); const testNamePatternPrompt = new TestNamePatternPrompt(outputStream, prompt); @@ -120,6 +122,7 @@ export default function watch( return runJest({ changedFilesPromise, contexts, + failedTestsCache, globalConfig, onComplete: results => { isRunning = false; @@ -146,7 +149,7 @@ export default function watch( } else { outputStream.write('\n'); } - + failedTestsCache.setTestResults(results.testResults); testNamePatternPrompt.updateCachedTestResults(results.testResults); }, outputStream, @@ -177,7 +180,9 @@ export default function watch( if ( isRunning && testWatcher && - [KEYS.Q, KEYS.ENTER, KEYS.A, KEYS.O, KEYS.P, KEYS.T].indexOf(key) !== -1 + [KEYS.Q, KEYS.ENTER, KEYS.A, KEYS.O, KEYS.P, KEYS.T, KEYS.F].indexOf( + key, + ) !== -1 ) { testWatcher.setState({interrupted: true}); return; @@ -230,6 +235,12 @@ export default function watch( }); startRun(globalConfig); break; + case KEYS.F: + globalConfig = updateGlobalConfig(globalConfig, { + onlyFailures: !globalConfig.onlyFailures, + }); + startRun(globalConfig); + break; case KEYS.O: globalConfig = updateGlobalConfig(globalConfig, { mode: 'watch', @@ -342,6 +353,12 @@ const usage = ( ? chalk.dim(' \u203A Press ') + 'a' + chalk.dim(' to run all tests.') : null, + globalConfig.onlyFailures + ? chalk.dim(' \u203A Press ') + 'f' + chalk.dim(' to run all tests.') + : chalk.dim(' \u203A Press ') + + 'f' + + chalk.dim(' to run only failed tests.'), + (globalConfig.watchAll || globalConfig.testPathPattern || globalConfig.testNamePattern) && diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index e14dd07b560d..19c5d2446fda 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -82,6 +82,7 @@ const getConfigs = ( coverageReporters: options.coverageReporters, coverageThreshold: options.coverageThreshold, detectLeaks: options.detectLeaks, + enabledTestsMap: options.enabledTestsMap, expand: options.expand, findRelatedTests: options.findRelatedTests, forceExit: options.forceExit, @@ -96,6 +97,7 @@ const getConfigs = ( nonFlagArgs: options.nonFlagArgs, notify: options.notify, onlyChanged: options.onlyChanged, + onlyFailures: options.onlyFailures, outputFile: options.outputFile, passWithNoTests: options.passWithNoTests, projects: options.projects, diff --git a/packages/jest-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index 9ba8d3de0055..8dc13639b70e 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -132,7 +132,14 @@ async function jasmine2( }, }); - if (globalConfig.testNamePattern) { + if (globalConfig.enabledTestsMap) { + env.specFilter = spec => { + const suiteMap = + globalConfig.enabledTestsMap && + globalConfig.enabledTestsMap[spec.result.testPath]; + return suiteMap && suiteMap[spec.result.fullName]; + }; + } else if (globalConfig.testNamePattern) { const testNameRegex = new RegExp(globalConfig.testNamePattern, 'i'); env.specFilter = spec => testNameRegex.test(spec.getFullName()); } diff --git a/test_utils.js b/test_utils.js index 49c4e5136de0..99fcdc10b9f7 100644 --- a/test_utils.js +++ b/test_utils.js @@ -21,6 +21,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { coverageReporters: [], coverageThreshold: {global: {}}, detectLeaks: false, + enabledTestsMap: null, expand: false, findRelatedTests: false, forceExit: false, @@ -35,6 +36,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { nonFlagArgs: [], notify: false, onlyChanged: false, + onlyFailures: false, outputFile: null, passWithNoTests: false, projects: [], diff --git a/types/Config.js b/types/Config.js index 067987a76ebb..44650113560c 100644 --- a/types/Config.js +++ b/types/Config.js @@ -159,6 +159,7 @@ export type GlobalConfig = {| coverageReporters: Array, coverageThreshold: {global: {[key: string]: number}}, detectLeaks: boolean, + enabledTestsMap: ?{[key: string]: {[key: string]: boolean}}, expand: boolean, findRelatedTests: boolean, forceExit: boolean, @@ -174,6 +175,7 @@ export type GlobalConfig = {| notify: boolean, outputFile: ?Path, onlyChanged: boolean, + onlyFailures: boolean, passWithNoTests: boolean, projects: Array, replname: ?string,