diff --git a/packages/jest-cli/src/TestNamePatternPrompt.js b/packages/jest-cli/src/TestNamePatternPrompt.js index add060156fec..8087131bc6f1 100644 --- a/packages/jest-cli/src/TestNamePatternPrompt.js +++ b/packages/jest-cli/src/TestNamePatternPrompt.js @@ -19,10 +19,10 @@ const stringLength = require('string-length'); const Prompt = require('./lib/Prompt'); const formatTestNameByPattern = require('./lib/formatTestNameByPattern'); -const pluralizeTest = (total: number) => (total === 1 ? 'test' : 'tests'); +const pluralizeTest = (total: number) => total === 1 ? 'test' : 'tests'; const usage = () => - `\n ${chalk.bold('Pattern Mode Usage')}\n` + + `\n${chalk.bold('Pattern Mode Usage')}\n` + ` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` + ` ${chalk.dim('\u203A Press')} Enter ` + `${chalk.dim('to apply pattern to all tests.')}\n` + @@ -40,9 +40,12 @@ module.exports = class TestNamePatternPrompt { this._prompt = prompt; } - run(onSuccess: Function, onCancel: Function) { + run(onSuccess: Function, onCancel: Function, options?: {header: string}) { this._pipe.write(ansiEscapes.cursorHide); this._pipe.write(ansiEscapes.clearScreen); + if (options && options.header) { + this._pipe.write(options.header); + } this._pipe.write(usage()); this._pipe.write(ansiEscapes.cursorShow); @@ -121,8 +124,7 @@ module.exports = class TestNamePatternPrompt { if (regex.test(title)) { matchedTests.push(title); } - }), - ); + })); return matchedTests; } diff --git a/packages/jest-cli/src/TestPathPatternPrompt.js b/packages/jest-cli/src/TestPathPatternPrompt.js index dd1197cc6c44..42ac52fb5a43 100644 --- a/packages/jest-cli/src/TestPathPatternPrompt.js +++ b/packages/jest-cli/src/TestPathPatternPrompt.js @@ -27,10 +27,10 @@ type SearchSources = Array<{| searchSource: SearchSource, |}>; -const pluralizeFile = (total: number) => (total === 1 ? 'file' : 'files'); +const pluralizeFile = (total: number) => total === 1 ? 'file' : 'files'; const usage = () => - `\n ${chalk.bold('Pattern Mode Usage')}\n` + + `\n${chalk.bold('Pattern Mode Usage')}\n` + ` ${chalk.dim('\u203A Press')} Esc ${chalk.dim('to exit pattern mode.')}\n` + ` ${chalk.dim('\u203A Press')} Enter ` + `${chalk.dim('to apply pattern to all filenames.')}\n` + @@ -48,9 +48,12 @@ module.exports = class TestPathPatternPrompt { this._prompt = prompt; } - run(onSuccess: Function, onCancel: Function) { + run(onSuccess: Function, onCancel: Function, options?: {header: string}) { this._pipe.write(ansiEscapes.cursorHide); this._pipe.write(ansiEscapes.clearScreen); + if (options && options.header) { + this._pipe.write(options.header); + } this._pipe.write(usage()); this._pipe.write(ansiEscapes.cursorShow); @@ -109,8 +112,7 @@ module.exports = class TestPathPatternPrompt { return highlight(path, filePath, pattern, context.config.rootDir); }) .forEach(filePath => - this._pipe.write(`\n ${chalk.dim('\u203A')} ${filePath}`), - ); + this._pipe.write(`\n ${chalk.dim('\u203A')} ${filePath}`)); if (total > max) { const more = total - max; diff --git a/packages/jest-cli/src/__tests__/__snapshots__/watch-filename-pattern-mode-test.js.snap b/packages/jest-cli/src/__tests__/__snapshots__/watch-filename-pattern-mode-test.js.snap index 21d9a952f76a..ed9402f99827 100644 --- a/packages/jest-cli/src/__tests__/__snapshots__/watch-filename-pattern-mode-test.js.snap +++ b/packages/jest-cli/src/__tests__/__snapshots__/watch-filename-pattern-mode-test.js.snap @@ -208,6 +208,30 @@ exports[`Watch mode flows Pressing "P" enters pattern mode 8`] = ` [MOCK - cursorRestorePosition]" `; +exports[`Watch mode flows Pressing "c" clears the filters 1`] = ` +"[MOCK - cursorHide] +[MOCK - clearScreen] + + + +Pattern Mode Usage + › Press Esc to exit pattern mode. + › Press Enter to apply pattern to all filenames. + + +[MOCK - cursorShow] + + + + pattern › +[MOCK - cursorSavePosition] + + + Start typing to filter by a filename regex pattern. +[MOCK - cursorTo(11, 5)] +[MOCK - cursorRestorePosition]" +`; + exports[`Watch mode flows Results in pattern mode get truncated appropriately 1`] = ` " @@ -312,3 +336,128 @@ exports[`Watch mode flows Results in pattern mode get truncated appropriately 3` [MOCK - cursorTo(12, 5)] [MOCK - cursorRestorePosition]" `; + +exports[`Watch mode flows Shows the appropiate header when both filters are active 1`] = ` +"[MOCK - cursorHide] +[MOCK - clearScreen] + +Active Filters: filename /p.*10/, test name /test/ + + +Pattern Mode Usage + › Press Esc to exit pattern mode. + › Press Enter to apply pattern to all tests. + + +[MOCK - cursorShow] + + + + pattern › +[MOCK - cursorSavePosition] + + + Start typing to filter by a test name regex pattern. +[MOCK - cursorTo(11, 5)] +[MOCK - cursorRestorePosition]" +`; + +exports[`Watch mode flows Shows the appropiate header when the filename filter is active 1`] = ` +"[MOCK - cursorHide] +[MOCK - clearScreen] + +Active Filters: filename /p.*10/ + + +Pattern Mode Usage + › Press Esc to exit pattern mode. + › Press Enter to apply pattern to all filenames. + + +[MOCK - cursorShow] + + + + pattern › +[MOCK - cursorSavePosition] + + + Start typing to filter by a filename regex pattern. +[MOCK - cursorTo(11, 5)] +[MOCK - cursorRestorePosition]" +`; + +exports[`Watch mode flows Shows the appropiate header when the filename filter is active 2`] = ` +"[MOCK - cursorHide] +[MOCK - clearScreen] + +Active Filters: filename /p/ + + +Pattern Mode Usage + › Press Esc to exit pattern mode. + › Press Enter to apply pattern to all filenames. + + +[MOCK - cursorShow] + + + + pattern › +[MOCK - cursorSavePosition] + + + Start typing to filter by a filename regex pattern. +[MOCK - cursorTo(11, 5)] +[MOCK - cursorRestorePosition]" +`; + +exports[`Watch mode flows Shows the appropiate header when the test name filter is active 1`] = ` +"[MOCK - cursorHide] +[MOCK - clearScreen] + +Active Filters: test name /test/ + + +Pattern Mode Usage + › Press Esc to exit pattern mode. + › Press Enter to apply pattern to all tests. + + +[MOCK - cursorShow] + + + + pattern › +[MOCK - cursorSavePosition] + + + Start typing to filter by a test name regex pattern. +[MOCK - cursorTo(11, 5)] +[MOCK - cursorRestorePosition]" +`; + +exports[`Watch mode flows Shows the appropiate header when the test name filter is active 2`] = ` +"[MOCK - cursorHide] +[MOCK - clearScreen] + +Active Filters: test name /t/ + + +Pattern Mode Usage + › Press Esc to exit pattern mode. + › Press Enter to apply pattern to all tests. + + +[MOCK - cursorShow] + + + + pattern › +[MOCK - cursorSavePosition] + + + Start typing to filter by a test name regex pattern. +[MOCK - cursorTo(11, 5)] +[MOCK - cursorRestorePosition]" +`; 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 3d92473bbce8..7d7874e50262 100644 --- a/packages/jest-cli/src/__tests__/__snapshots__/watch-test.js.snap +++ b/packages/jest-cli/src/__tests__/__snapshots__/watch-test.js.snap @@ -6,6 +6,7 @@ Array [ Watch Usage › Press o to only run tests related to changed files. › 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. › Press Enter to trigger a test run. ", diff --git a/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js b/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js index eacf8df03a19..0709d9ca444e 100644 --- a/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js +++ b/packages/jest-cli/src/__tests__/watch-filename-pattern-mode-test.js @@ -29,42 +29,42 @@ jest.mock('ansi-escapes', () => ({ jest.mock( '../SearchSource', - () => class { - constructor(context) { - this._context = context; - } - - findMatchingTests(pattern) { - const paths = [ - './path/to/file1-test.js', - './path/to/file2-test.js', - './path/to/file3-test.js', - './path/to/file4-test.js', - './path/to/file5-test.js', - './path/to/file6-test.js', - './path/to/file7-test.js', - './path/to/file8-test.js', - './path/to/file9-test.js', - './path/to/file10-test.js', - './path/to/file11-test.js', - ].filter(path => path.match(pattern)); - - return { - tests: paths.map(path => ({ - context: this._context, - duration: null, - path, - })), - }; - } - }, + () => + class { + constructor(context) { + this._context = context; + } + + findMatchingTests(pattern) { + const paths = [ + './path/to/file1-test.js', + './path/to/file2-test.js', + './path/to/file3-test.js', + './path/to/file4-test.js', + './path/to/file5-test.js', + './path/to/file6-test.js', + './path/to/file7-test.js', + './path/to/file8-test.js', + './path/to/file9-test.js', + './path/to/file10-test.js', + './path/to/file11-test.js', + ].filter(path => path.match(pattern)); + + return { + tests: paths.map(path => ({ + context: this._context, + duration: null, + path, + })), + }; + } + }, ); jest.doMock('chalk', () => Object.assign(new chalk.constructor({enabled: false}), { stripColor: str => str, - }), -); + })); jest.doMock( '../runJest', @@ -86,6 +86,9 @@ jest.doMock('../lib/terminalUtils', () => ({ const watch = require('../watch'); + +const toHex = char => Number(char.charCodeAt(0)).toString(16); + const globalConfig = {watch: true}; afterEach(runJestMock.mockReset); @@ -120,8 +123,6 @@ describe('Watch mode flows', () => { expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); }; - const toHex = char => Number(char.charCodeAt(0)).toString(16); - // Write a pattern ['p', '.', '*', '1', '0'].map(toHex).forEach(assertPattern); @@ -157,6 +158,96 @@ describe('Watch mode flows', () => { expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); }); }); + + it('Shows the appropiate header when the filename filter is active', () => { + contexts[0].config = {rootDir: ''}; + watch(globalConfig, contexts, argv, pipe, hasteMapInstances, stdin); + + stdin.emit(KEYS.P); + + ['p', '.', '*', '1', '0'] + .map(toHex) + .concat(KEYS.ENTER) + .forEach(key => stdin.emit(key)); + + pipe.write.mockReset(); + stdin.emit(KEYS.P); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + + ['p'].map(toHex).concat(KEYS.ENTER).forEach(key => stdin.emit(key)); + + pipe.write.mockReset(); + stdin.emit(KEYS.P); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); + + it('Shows the appropiate header when the test name filter is active', () => { + contexts[0].config = {rootDir: ''}; + watch(globalConfig, contexts, argv, pipe, hasteMapInstances, stdin); + + stdin.emit(KEYS.T); + + ['t', 'e', 's', 't'] + .map(toHex) + .concat(KEYS.ENTER) + .forEach(key => stdin.emit(key)); + + pipe.write.mockReset(); + stdin.emit(KEYS.T); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + + ['t'].map(toHex).concat(KEYS.ENTER).forEach(key => stdin.emit(key)); + + pipe.write.mockReset(); + stdin.emit(KEYS.T); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); + + it('Shows the appropiate header when both filters are active', () => { + contexts[0].config = {rootDir: ''}; + watch(globalConfig, contexts, argv, pipe, hasteMapInstances, stdin); + + stdin.emit(KEYS.P); + + ['p', '.', '*', '1', '0'] + .map(toHex) + .concat(KEYS.ENTER) + .forEach(key => stdin.emit(key)); + + stdin.emit(KEYS.T); + ['t', 'e', 's', 't'] + .map(toHex) + .concat(KEYS.ENTER) + .forEach(key => stdin.emit(key)); + + pipe.write.mockReset(); + stdin.emit(KEYS.T); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); + + it('Pressing "c" clears the filters', () => { + contexts[0].config = {rootDir: ''}; + watch(globalConfig, contexts, argv, pipe, hasteMapInstances, stdin); + + stdin.emit(KEYS.P); + + ['p', '.', '*', '1', '0'] + .map(toHex) + .concat(KEYS.ENTER) + .forEach(key => stdin.emit(key)); + + stdin.emit(KEYS.T); + ['t', 'e', 's', 't'] + .map(toHex) + .concat(KEYS.ENTER) + .forEach(key => stdin.emit(key)); + + stdin.emit(KEYS.C); + + pipe.write.mockReset(); + stdin.emit(KEYS.P); + expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot(); + }); }); class MockStdin { diff --git a/packages/jest-cli/src/constants.js b/packages/jest-cli/src/constants.js index b7e24bd42c93..92c28d1c0c3b 100644 --- a/packages/jest-cli/src/constants.js +++ b/packages/jest-cli/src/constants.js @@ -21,6 +21,7 @@ const KEYS = { ARROW_RIGHT: '1b5b43', ARROW_UP: '1b5b41', BACKSPACE: isWindows ? '08' : '7f', + C: '63', CONTROL_C: '03', CONTROL_D: '04', ENTER: '0d', diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 81549ab196eb..d6a352fd57cb 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -185,6 +185,13 @@ const watch = ( }); startRun(); break; + case KEYS.C: + setState(argv, 'watch', { + testNamePattern: '', + testPathPattern: '', + }); + startRun(); + break; case KEYS.O: setState(argv, 'watch', { testNamePattern: '', @@ -193,24 +200,32 @@ const watch = ( startRun(); break; case KEYS.P: - testPathPatternPrompt.run(testPathPattern => { - setState(argv, 'watch', { - testNamePattern: '', - testPathPattern, - }); + testPathPatternPrompt.run( + testPathPattern => { + setState(argv, 'watch', { + testNamePattern: '', + testPathPattern, + }); - startRun(); - }, onCancelPatternPrompt); + startRun(); + }, + onCancelPatternPrompt, + {header: activeFilters(argv) + '\n'}, + ); break; case KEYS.T: - testNamePatternPrompt.run(testNamePattern => { - setState(argv, 'watch', { - testNamePattern, - testPathPattern: '', - }); + testNamePatternPrompt.run( + testNamePattern => { + setState(argv, 'watch', { + testNamePattern, + testPathPattern: argv.testPathPattern, + }); - startRun(); - }, onCancelPatternPrompt); + startRun(); + }, + onCancelPatternPrompt, + {header: activeFilters(argv) + '\n'}, + ); break; case KEYS.QUESTION_MARK: break; @@ -244,9 +259,34 @@ const watch = ( return Promise.resolve(); }; +const activeFilters = (argv, delimiter = '\n') => { + const {testNamePattern, testPathPattern} = argv; + if (testNamePattern || testPathPattern) { + const filters = [ + testPathPattern + ? chalk.dim('filename ') + chalk.yellow('/' + testPathPattern + '/') + : null, + testNamePattern + ? chalk.dim('test name ') + chalk.yellow('/' + testNamePattern + '/') + : null, + ] + .filter(f => !!f) + .join(', '); + + const messages = ['\n' + chalk.bold('Active Filters: ') + filters]; + + return messages.filter(message => !!message).join(delimiter); + } + + return ''; +}; + const usage = (argv, snapshotFailure, delimiter = '\n') => { - /* eslint-disable max-len */ const messages = [ + activeFilters(argv), + argv.testPathPattern || argv.testNamePattern + ? chalk.dim(' \u203A Press ') + 'c' + chalk.dim(' to clear filters.') + : null, '\n' + chalk.bold('Watch Usage'), argv.watch ? chalk.dim(' \u203A Press ') + 'a' + chalk.dim(' to run all tests.') @@ -265,12 +305,15 @@ const usage = (argv, snapshotFailure, delimiter = '\n') => { chalk.dim(' \u203A Press ') + 'p' + chalk.dim(' to filter by a filename regex pattern.'), + chalk.dim(' \u203A Press ') + + 't' + + chalk.dim(' to filter by a test name regex pattern.'), chalk.dim(' \u203A Press ') + 'q' + chalk.dim(' to quit watch mode.'), chalk.dim(' \u203A Press ') + 'Enter' + chalk.dim(' to trigger a test run.'), ]; - /* eslint-enable max-len */ + return messages.filter(message => !!message).join(delimiter) + '\n'; };