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 d1619091990f..3cc27ce85271 100644 --- a/packages/jest-cli/src/__tests__/__snapshots__/watch.test.js.snap +++ b/packages/jest-cli/src/__tests__/__snapshots__/watch.test.js.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Watch mode flows Pressing "u" reruns the tests in "update snapshot" mode 1`] = `2`; + exports[`Watch mode flows Runs Jest in a non-interactive environment not showing usage 1`] = ` Array [ " diff --git a/packages/jest-cli/src/__tests__/watch.test.js b/packages/jest-cli/src/__tests__/watch.test.js index 01ea92f2df51..d262768b70c1 100644 --- a/packages/jest-cli/src/__tests__/watch.test.js +++ b/packages/jest-cli/src/__tests__/watch.test.js @@ -10,6 +10,7 @@ import chalk from 'chalk'; import TestWatcher from '../test_watcher'; +import JestHooks from '../jest_hooks'; import {KEYS} from '../constants'; const runJestMock = jest.fn(); @@ -37,7 +38,7 @@ jest.doMock( watchPluginPath, () => class WatchPlugin1 { - getUsageRow() { + getUsageInfo() { return { key: 's'.codePointAt(0), prompt: 'do nothing', @@ -51,7 +52,7 @@ jest.doMock( watchPlugin2Path, () => class WatchPlugin2 { - getUsageRow() { + getUsageInfo() { return { key: 'u'.codePointAt(0), prompt: 'do something else', @@ -242,17 +243,47 @@ describe('Watch mode flows', () => { expect(pipeMockCalls.slice(determiningTestsToRun + 1)).toMatchSnapshot(); }); + it('allows WatchPlugins to hook into JestHooks', async () => { + const apply = jest.fn(); + const pluginPath = `${__dirname}/__fixtures__/plugin_path_register`; + jest.doMock( + pluginPath, + () => + class WatchPlugin { + constructor() { + this.apply = apply; + } + }, + {virtual: true}, + ); + + watch( + Object.assign({}, globalConfig, { + rootDir: __dirname, + watchPlugins: [pluginPath], + }), + contexts, + pipe, + hasteMapInstances, + stdin, + ); + + await nextTick(); + + expect(apply).toHaveBeenCalled(); + }); + it('triggers enter on a WatchPlugin when its key is pressed', async () => { - const showPrompt = jest.fn(() => Promise.resolve()); + const run = jest.fn(() => Promise.resolve()); const pluginPath = `${__dirname}/__fixtures__/plugin_path`; jest.doMock( pluginPath, () => class WatchPlugin1 { constructor() { - this.showPrompt = showPrompt; + this.run = run; } - getUsageRow() { + getUsageInfo() { return { key: 's'.codePointAt(0), prompt: 'do nothing', @@ -277,24 +308,22 @@ describe('Watch mode flows', () => { await nextTick(); - expect(showPrompt).toHaveBeenCalled(); + expect(run).toHaveBeenCalled(); }); it('prevents Jest from handling keys when active and returns control when end is called', async () => { let resolveShowPrompt; - const showPrompt = jest.fn( - () => new Promise(res => (resolveShowPrompt = res)), - ); + const run = jest.fn(() => new Promise(res => (resolveShowPrompt = res))); const pluginPath = `${__dirname}/__fixtures__/plugin_path_1`; jest.doMock( pluginPath, () => class WatchPlugin1 { constructor() { - this.showPrompt = showPrompt; + this.run = run; } - onData() {} - getUsageRow() { + onKey() {} + getUsageInfo() { return { key: 's'.codePointAt(0), prompt: 'do nothing', @@ -311,10 +340,10 @@ describe('Watch mode flows', () => { () => class WatchPlugin1 { constructor() { - this.showPrompt = showPrompt2; + this.run = showPrompt2; } - onData() {} - getUsageRow() { + onKey() {} + getUsageInfo() { return { key: 'z'.codePointAt(0), prompt: 'also do nothing', @@ -337,7 +366,7 @@ describe('Watch mode flows', () => { stdin.emit(Number('s'.charCodeAt(0)).toString(16)); await nextTick(); - expect(showPrompt).toHaveBeenCalled(); + expect(run).toHaveBeenCalled(); stdin.emit(Number('z'.charCodeAt(0)).toString(16)); await nextTick(); expect(showPrompt2).not.toHaveBeenCalled(); @@ -382,11 +411,16 @@ describe('Watch mode flows', () => { }); it('Pressing "u" reruns the tests in "update snapshot" mode', async () => { + const hooks = new JestHooks(); + expect(2).toMatchSnapshot(); + globalConfig.updateSnapshot = 'new'; - watch(globalConfig, contexts, pipe, hasteMapInstances, stdin); + watch(globalConfig, contexts, pipe, hasteMapInstances, stdin, hooks); runJestMock.mockReset(); + hooks.getEmitter().testRunComplete({snapshot: {failure: true}}); + stdin.emit(KEYS.U); await nextTick(); @@ -403,6 +437,7 @@ describe('Watch mode flows', () => { watch: false, }); }); + it('passWithNoTest should be set to true in watch mode', () => { globalConfig.passWithNoTests = false; watch(globalConfig, contexts, pipe, hasteMapInstances, stdin); diff --git a/packages/jest-cli/src/watch_plugin.js b/packages/jest-cli/src/base_watch_plugin.js similarity index 73% rename from packages/jest-cli/src/watch_plugin.js rename to packages/jest-cli/src/base_watch_plugin.js index 3a666d8b05c5..e19c43def1a7 100644 --- a/packages/jest-cli/src/watch_plugin.js +++ b/packages/jest-cli/src/base_watch_plugin.js @@ -8,9 +8,9 @@ */ import type {GlobalConfig} from 'types/Config'; import type {JestHookSubscriber} from './jest_hooks'; -import type {UsageRow} from './types'; +import type {WatchPlugin, UsageData} from './types'; -class WatchPlugin { +class BaseWatchPlugin implements WatchPlugin { _stdin: stream$Readable | tty$ReadStream; _stdout: stream$Writable | tty$WriteStream; constructor({ @@ -24,15 +24,15 @@ class WatchPlugin { this._stdout = stdout; } - registerHooks(hooks: JestHookSubscriber) {} + apply(hooks: JestHookSubscriber) {} - getUsageRow(globalConfig: GlobalConfig): UsageRow { - return {hide: true, key: 0, prompt: ''}; + getUsageInfo(globalConfig: GlobalConfig): ?UsageData { + return null; } - onData(value: string) {} + onKey(value: string) {} - showPrompt( + run( globalConfig: GlobalConfig, updateConfigAndRun: Function, ): Promise { @@ -40,4 +40,4 @@ class WatchPlugin { } } -export default WatchPlugin; +export default BaseWatchPlugin; diff --git a/packages/jest-cli/src/plugins/quit.js b/packages/jest-cli/src/plugins/quit.js index f3b2957141e1..ae4e33499977 100644 --- a/packages/jest-cli/src/plugins/quit.js +++ b/packages/jest-cli/src/plugins/quit.js @@ -6,10 +6,10 @@ * * @flow */ -import WatchPlugin from '../watch_plugin'; +import BaseWatchPlugin from '../base_watch_plugin'; -class QuitPlugin extends WatchPlugin { - async showPrompt() { +class QuitPlugin extends BaseWatchPlugin { + async run() { if (typeof this._stdin.setRawMode === 'function') { this._stdin.setRawMode(false); } @@ -17,7 +17,7 @@ class QuitPlugin extends WatchPlugin { process.exit(0); } - getUsageRow() { + getUsageInfo() { return { key: 'q'.codePointAt(0), prompt: 'quit watch mode', diff --git a/packages/jest-cli/src/plugins/test_name_pattern.js b/packages/jest-cli/src/plugins/test_name_pattern.js index 70aa1e65834b..fbfc20cca6aa 100644 --- a/packages/jest-cli/src/plugins/test_name_pattern.js +++ b/packages/jest-cli/src/plugins/test_name_pattern.js @@ -7,12 +7,12 @@ * @flow */ import type {GlobalConfig} from 'types/Config'; -import WatchPlugin from '../watch_plugin'; +import BaseWatchPlugin from '../base_watch_plugin'; import TestNamePatternPrompt from '../test_name_pattern_prompt'; import activeFilters from '../lib/active_filters_message'; import Prompt from '../lib/Prompt'; -class TestNamePatternPlugin extends WatchPlugin { +class TestNamePatternPlugin extends BaseWatchPlugin { _prompt: Prompt; constructor(options: { @@ -23,21 +23,18 @@ class TestNamePatternPlugin extends WatchPlugin { this._prompt = new Prompt(); } - getUsageRow() { + getUsageInfo() { return { key: 't'.codePointAt(0), prompt: 'filter by a test name regex pattern', }; } - onData(key: string) { + onKey(key: string) { this._prompt.put(key); } - showPrompt( - globalConfig: GlobalConfig, - updateConfigAndRun: Function, - ): Promise { + run(globalConfig: GlobalConfig, updateConfigAndRun: Function): Promise { return new Promise((res, rej) => { const testPathPatternPrompt = new TestNamePatternPrompt( this._stdout, diff --git a/packages/jest-cli/src/plugins/test_path_pattern.js b/packages/jest-cli/src/plugins/test_path_pattern.js index aa27e9ac9452..2e63ac56ad23 100644 --- a/packages/jest-cli/src/plugins/test_path_pattern.js +++ b/packages/jest-cli/src/plugins/test_path_pattern.js @@ -8,12 +8,12 @@ */ import type {GlobalConfig} from 'types/Config'; -import WatchPlugin from '../watch_plugin'; +import BaseWatchPlugin from '../base_watch_plugin'; import TestPathPatternPrompt from '../test_path_pattern_prompt'; import activeFilters from '../lib/active_filters_message'; import Prompt from '../lib/Prompt'; -class TestPathPatternPlugin extends WatchPlugin { +class TestPathPatternPlugin extends BaseWatchPlugin { _prompt: Prompt; constructor(options: { @@ -24,21 +24,18 @@ class TestPathPatternPlugin extends WatchPlugin { this._prompt = new Prompt(); } - getUsageRow() { + getUsageInfo() { return { key: 'p'.codePointAt(0), prompt: 'filter by a filename regex pattern', }; } - onData(key: string) { + onKey(key: string) { this._prompt.put(key); } - showPrompt( - globalConfig: GlobalConfig, - updateConfigAndRun: Function, - ): Promise { + run(globalConfig: GlobalConfig, updateConfigAndRun: Function): Promise { return new Promise((res, rej) => { const testPathPatternPrompt = new TestPathPatternPrompt( this._stdout, diff --git a/packages/jest-cli/src/plugins/update_snapshots.js b/packages/jest-cli/src/plugins/update_snapshots.js index 46e5e8c8cdd8..c63ff6a77e9a 100644 --- a/packages/jest-cli/src/plugins/update_snapshots.js +++ b/packages/jest-cli/src/plugins/update_snapshots.js @@ -7,12 +7,12 @@ * @flow */ import type {GlobalConfig} from 'types/Config'; -import WatchPlugin from '../watch_plugin'; +import BaseWatchPlugin from '../base_watch_plugin'; import type {JestHookSubscriber} from '../jest_hooks'; -class UpdateSnapshotsPlugin extends WatchPlugin { +class UpdateSnapshotsPlugin extends BaseWatchPlugin { _hasSnapshotFailure: boolean; - showPrompt( + run( globalConfig: GlobalConfig, updateConfigAndRun: Function, ): Promise { @@ -20,18 +20,21 @@ class UpdateSnapshotsPlugin extends WatchPlugin { return Promise.resolve(false); } - registerHooks(hooks: JestHookSubscriber) { + apply(hooks: JestHookSubscriber) { hooks.testRunComplete(results => { this._hasSnapshotFailure = results.snapshot.failure; }); } - getUsageRow(globalConfig: GlobalConfig) { - return { - hide: !this._hasSnapshotFailure, - key: 'u'.codePointAt(0), - prompt: 'update failing snapshots', - }; + getUsageInfo(globalConfig: GlobalConfig) { + if (this._hasSnapshotFailure) { + return { + key: 'u'.codePointAt(0), + prompt: 'update failing snapshots', + }; + } + + return null; } } diff --git a/packages/jest-cli/src/plugins/update_snapshots_interactive.js b/packages/jest-cli/src/plugins/update_snapshots_interactive.js index 95ee20378cd3..3f2883d44d76 100644 --- a/packages/jest-cli/src/plugins/update_snapshots_interactive.js +++ b/packages/jest-cli/src/plugins/update_snapshots_interactive.js @@ -8,11 +8,11 @@ */ import type {JestHookSubscriber} from '../jest_hooks'; import type {GlobalConfig} from 'types/Config'; -import WatchPlugin from '../watch_plugin'; +import BaseWatchPlugin from '../base_watch_plugin'; import {getFailedSnapshotTests} from 'jest-util'; import SnapshotInteractiveMode from '../snapshot_interactive_mode'; -class UpdateSnapshotInteractivePlugin extends WatchPlugin { +class UpdateSnapshotInteractivePlugin extends BaseWatchPlugin { _snapshotInteractiveMode: SnapshotInteractiveMode; _failedSnapshotTestPaths: Array<*>; @@ -24,7 +24,7 @@ class UpdateSnapshotInteractivePlugin extends WatchPlugin { this._snapshotInteractiveMode = new SnapshotInteractiveMode(this._stdout); } - registerHooks(hooks: JestHookSubscriber) { + apply(hooks: JestHookSubscriber) { hooks.testRunComplete(results => { this._failedSnapshotTestPaths = getFailedSnapshotTests(results); if (this._snapshotInteractiveMode.isActive()) { @@ -33,16 +33,13 @@ class UpdateSnapshotInteractivePlugin extends WatchPlugin { }); } - onData(key: string) { + onKey(key: string) { if (this._snapshotInteractiveMode.isActive()) { this._snapshotInteractiveMode.put(key); } } - showPrompt( - globalConfig: GlobalConfig, - updateConfigAndRun: Function, - ): Promise { + run(globalConfig: GlobalConfig, updateConfigAndRun: Function): Promise { if (this._failedSnapshotTestPaths.length) { return new Promise(res => { this._snapshotInteractiveMode.run( @@ -64,14 +61,18 @@ class UpdateSnapshotInteractivePlugin extends WatchPlugin { } } - getUsageRow(globalConfig: GlobalConfig) { - return { - hide: - !this._failedSnapshotTestPaths || - this._failedSnapshotTestPaths.length === 0, - key: 'i'.codePointAt(0), - prompt: 'update failing snapshots interactively', - }; + getUsageInfo(globalConfig: GlobalConfig) { + if ( + this._failedSnapshotTestPaths && + this._failedSnapshotTestPaths.length > 0 + ) { + return { + key: 'i'.codePointAt(0), + prompt: 'update failing snapshots interactively', + }; + } + + return null; } } diff --git a/packages/jest-cli/src/types.js b/packages/jest-cli/src/types.js index 1a2070779c56..f6a90eb63324 100644 --- a/packages/jest-cli/src/types.js +++ b/packages/jest-cli/src/types.js @@ -7,30 +7,23 @@ * @flow */ import type {GlobalConfig} from 'types/Config'; +import type {JestHookSubscriber} from './jest_hooks'; -export type UsageRow = { +export type UsageData = { key: number, prompt: string, - hide?: boolean, }; export type JestHooks = { testRunComplete: any, }; -export type WatchPlugin = { - key: number, - name: string, - prompt: string, - apply: ( - jestHooks: JestHooks, - { - stdin: stream$Readable | tty$ReadStream, - stdout: stream$Writable | tty$WriteStream, - }, - ) => void, - shouldShowUsage?: ( +export interface WatchPlugin { + +apply?: (hooks: JestHookSubscriber) => void; + +getUsageInfo?: (globalConfig: GlobalConfig) => ?UsageData; + +onKey?: (value: string) => void; + +run?: ( globalConfig: GlobalConfig, - hasSnapshotFailures: boolean, - ) => boolean, -}; + updateConfigAndRun: Function, + ) => Promise; +} diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 67ab880b2ede..7b5ee1b20ec6 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -9,6 +9,7 @@ import type {GlobalConfig, SnapshotUpdateState} from 'types/Config'; import type {Context} from 'types/Context'; +import type {WatchPlugin} from './types'; import ansiEscapes from 'ansi-escapes'; import chalk from 'chalk'; @@ -27,7 +28,6 @@ import TestWatcher from './test_watcher'; import FailedTestsCache from './failed_tests_cache'; import {KEYS, CLEAR} from './constants'; import JestHooks from './jest_hooks'; -import WatchPlugin from './watch_plugin'; import TestPathPatternPlugin from './plugins/test_path_pattern'; import TestNamePatternPlugin from './plugins/test_name_pattern'; import UpdateSnapshotsPlugin from './plugins/update_snapshots'; @@ -51,13 +51,13 @@ const getSortedUsageRows = ( ) => { const internalPlugins = watchPlugins .slice(0, INTERNAL_PLUGINS.length) - .map(p => p.getUsageRow(globalConfig)) - .filter(usage => !usage.hide); + .map(p => p.getUsageInfo && p.getUsageInfo(globalConfig)) + .filter(Boolean); const thirdPartyPlugins = watchPlugins .slice(INTERNAL_PLUGINS.length) - .map(p => p.getUsageRow(globalConfig)) - .filter(usage => !usage.hide) + .map(p => p.getUsageInfo && p.getUsageInfo(globalConfig)) + .filter(Boolean) .sort((a, b) => a.key - b.key); return internalPlugins.concat(thirdPartyPlugins); @@ -69,6 +69,7 @@ export default function watch( outputStream: stream$Writable | tty$WriteStream, hasteMapInstances: Array, stdin?: stream$Readable | tty$ReadStream = process.stdin, + hooks?: JestHooks = new JestHooks(), ): Promise { // `globalConfig` will be constantly updated and reassigned as a result of // watch mode interactions. @@ -80,8 +81,6 @@ export default function watch( passWithNoTests: true, }); - const hooks = new JestHooks(); - const updateConfigAndRun = ({ testNamePattern, testPathPattern, @@ -121,14 +120,25 @@ export default function watch( ); watchPlugins.forEach((plugin: WatchPlugin) => { - plugin.registerHooks(hooks.getSubscriber()); + const hookSubscriber = hooks.getSubscriber(); + if (plugin.apply) { + plugin.apply(hookSubscriber); + } }); if (globalConfig.watchPlugins != null) { for (const pluginModulePath of globalConfig.watchPlugins) { // $FlowFixMe dynamic require - const ThirdPluginPath = require(pluginModulePath); - watchPlugins.push(new ThirdPluginPath({stdin, stdout: outputStream})); + const ThirdPartyPlugin = require(pluginModulePath); + const plugin: WatchPlugin = new ThirdPartyPlugin({ + stdin, + stdout: outputStream, + }); + const hookSubscriber = hooks.getSubscriber(); + if (plugin.apply) { + plugin.apply(hookSubscriber); + } + watchPlugins.push(plugin); } } @@ -238,10 +248,10 @@ export default function watch( return; } - if (activePlugin != null) { + if (activePlugin != null && activePlugin.onKey) { // if a plugin is activate, Jest should let it handle keystrokes, so ignore // them here - activePlugin.onData(key); + activePlugin.onKey(key); return; } @@ -261,27 +271,31 @@ export default function watch( } const matchingWatchPlugin = watchPlugins.find(plugin => { - const usageRow = plugin.getUsageRow(globalConfig) || {}; - - return usageRow.key === parseInt(key, 16); + const usageData = + (plugin.getUsageInfo && plugin.getUsageInfo(globalConfig)) || {}; + return usageData.key === parseInt(key, 16); }); if (matchingWatchPlugin != null) { // "activate" the plugin, which has jest ignore keystrokes so the plugin // can handle them activePlugin = matchingWatchPlugin; - activePlugin.showPrompt(globalConfig, updateConfigAndRun).then( - shouldRerun => { - activePlugin = null; - if (shouldRerun) { - updateConfigAndRun(); - } - }, - () => { - activePlugin = null; - onCancelPatternPrompt(); - }, - ); + if (activePlugin.run) { + activePlugin.run(globalConfig, updateConfigAndRun).then( + shouldRerun => { + activePlugin = null; + if (shouldRerun) { + updateConfigAndRun(); + } + }, + () => { + activePlugin = null; + onCancelPatternPrompt(); + }, + ); + } else { + activePlugin = null; + } } switch (key) {