From 6c61baa76e366a9a7bfb351108a2d4ca97820bf9 Mon Sep 17 00:00:00 2001 From: Christophe Porteneuve Date: Thu, 12 Jul 2018 08:27:07 +0200 Subject: [PATCH] Feature: broader uses for watch plugins (#6473) * Allow WatchPlugins access to a broader list of global config options Refs #6467 * Tests for whitelisting config options in WatchPlugins * Docs on allowed config options in WatchPlugins * chore: Changelog * tests: leverage it.each to clean up code * refactor: centralize config update logic * Update plugin test to use new signature for configs * Refactor watch extensions as per PR review (#6473) --- CHANGELOG.md | 4 + docs/WatchPlugins.md | 21 ++++ .../watch_filename_pattern_mode.test.js.snap | 10 ++ packages/jest-cli/src/__tests__/watch.test.js | 102 +++++++++++++++++- .../watch_filename_pattern_mode.test.js | 9 +- .../jest-cli/src/lib/update_global_config.js | 94 ++++++++++++---- packages/jest-cli/src/watch.js | 48 +++++---- 7 files changed, 239 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b65f07552de..7e147b7289c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## master +### Features + +- `[jest-cli]` Watch plugins now have access to a broader range of global configuration options in their `updateConfigAndRun` callbacks, so they can provide a wider set of extra features ([#6473](https://github.com/facebook/jest/pull/6473)) + ## 23.4.0 ### Features diff --git a/docs/WatchPlugins.md b/docs/WatchPlugins.md index 7e731cdb53fc..d2f6bd2d54f2 100644 --- a/docs/WatchPlugins.md +++ b/docs/WatchPlugins.md @@ -149,6 +149,27 @@ class MyWatchPlugin { } ``` +**Note**: If you do call `updateConfigAndRun`, your `run` method should not resolve to a truthy value, as that would trigger a double-run. + +#### Authorized configuration keys + +For stability and safety reasons, only part of the global configuration keys can be updated with `updateConfigAndRun`. The current white list is as follows: + +- [`bail`](configuration.html#bail-boolean) +- [`collectCoverage`](configuration.html#collectcoverage-boolean) +- [`collectCoverageFrom`](configuration.html#collectcoveragefrom-array) +- [`collectCoverageOnlyFrom`](configuration.html#collectcoverageonlyfrom-array) +- [`coverageDirectory`](configuration.html#coveragedirectory-string) +- [`coverageReporters`](configuration.html#coveragereporters-array) +- [`notify`](configuration.html#notify-boolean) +- [`notifyMode`](configuration.html#notifymode-string) +- [`onlyFailures`](configuration.html#onlyfailures-boolean) +- [`reporters`](configuration.html#reporters-array-modulename-modulename-options) +- [`testNamePattern`](cli.html#testnamepattern-regex) +- [`testPathPattern`](cli.html#testpathpattern-regex) +- [`updateSnapshot`](cli.html#updatesnapshot) +- [`verbose`](configuration.html#verbose-boolean) + ## Customization Plugins can be customized via your Jest configuration. 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 96fd5eb1a402..219ebf926ca9 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 @@ -80,6 +80,16 @@ exports[`Watch mode flows Pressing "P" enters pattern mode 8`] = ` [MOCK - cursorRestorePosition]" `; +exports[`Watch mode flows Pressing "P" enters pattern mode 9`] = ` +Object { + "onlyChanged": false, + "passWithNoTests": true, + "testPathPattern": "p.*3", + "watch": true, + "watchAll": false, +} +`; + exports[`Watch mode flows Pressing "c" clears the filters 1`] = ` "[MOCK - cursorHide] [MOCK - clearScreen] diff --git a/packages/jest-cli/src/__tests__/watch.test.js b/packages/jest-cli/src/__tests__/watch.test.js index 0107973cbd31..189129eb080e 100644 --- a/packages/jest-cli/src/__tests__/watch.test.js +++ b/packages/jest-cli/src/__tests__/watch.test.js @@ -86,6 +86,11 @@ jest.doMock( {virtual: true}, ); +const regularUpdateGlobalConfig = require('../lib/update_global_config') + .default; +const updateGlobalConfig = jest.fn(regularUpdateGlobalConfig); +jest.doMock('../lib/update_global_config', () => updateGlobalConfig); + const watch = require('../watch').default; const nextTick = () => new Promise(res => process.nextTick(res)); @@ -435,6 +440,101 @@ describe('Watch mode flows', () => { }); }); + it.each` + ok | option + ✔︎ | bail + ✖︎ | changedFilesWithAncestor + ✖︎ | changedSince + ✔︎ | collectCoverage + ✔︎ | collectCoverageFrom + ✔︎ | collectCoverageOnlyFrom + ✔︎ | coverageDirectory + ✔︎ | coverageReporters + ✖︎ | coverageThreshold + ✖︎ | detectLeaks + ✖︎ | detectOpenHandles + ✖︎ | enabledTestsMap + ✖︎ | errorOnDeprecated + ✖︎ | expand + ✖︎ | filter + ✖︎ | findRelatedTests + ✖︎ | forceExit + ✖︎ | globalSetup + ✖︎ | globalTeardown + ✖︎ | json + ✖︎ | lastCommit + ✖︎ | listTests + ✖︎ | logHeapUsage + ✖︎ | maxWorkers + ✖︎ | nonFlagArgs + ✖︎ | noSCM + ✖︎ | noStackTrace + ✔︎ | notify + ✔︎ | notifyMode + ✖︎ | onlyChanged + ✔︎ | onlyFailures + ✖︎ | outputFile + ✖︎ | passWithNoTests + ✖︎ | projects + ✖︎ | replname + ✔︎ | reporters + ✖︎ | rootDir + ✖︎ | runTestsByPath + ✖︎ | silent + ✖︎ | skipFilter + ✖︎ | testFailureExitCode + ✔︎ | testNamePattern + ✔︎ | testPathPattern + ✖︎ | testResultsProcessor + ✔︎ | updateSnapshot + ✖︎ | useStderr + ✔︎ | verbose + ✖︎ | watch + ✖︎ | watchAll + ✖︎ | watchman + ✖︎ | watchPlugins + `( + 'allows WatchPlugins to modify only white-listed global config keys', + async ({ok, option}) => { + const pluginPath = `${__dirname}/__fixtures__/plugin_path_config_updater`; + const config = Object.assign({}, globalConfig, { + rootDir: __dirname, + watchPlugins: [{config: {}, path: pluginPath}], + }); + + jest.doMock( + pluginPath, + () => + class WatchPlugin { + getUsageInfo() { + return {key: 'x', prompt: 'test option white-listing'}; + } + + run(globalConfig, updateConfigAndRun) { + updateConfigAndRun({[option]: '__JUST_TRYING__'}); + return Promise.resolve(); + } + }, + {virtual: true}, + ); + + watch(config, contexts, pipe, hasteMapInstances, stdin); + await nextTick(); + + stdin.emit('x'); + await nextTick(); + + const lastCall = updateGlobalConfig.mock.calls.slice(-1)[0]; + let expector = expect(lastCall[0]); + if (!ok) { + expector = expector.not; + } + expector.toMatchObject({ + [option]: '__JUST_TRYING__', + }); + }, + ); + it('triggers enter on a WatchPlugin when its key is pressed', async () => { const run = jest.fn(() => Promise.resolve()); const pluginPath = `${__dirname}/__fixtures__/plugin_path`; @@ -588,7 +688,6 @@ describe('Watch mode flows', () => { expect(runJestMock.mock.calls[0][0].globalConfig).toMatchObject({ testNamePattern: 'test', - testPathPattern: '', watch: true, watchAll: false, }); @@ -606,7 +705,6 @@ describe('Watch mode flows', () => { await nextTick(); expect(runJestMock.mock.calls[0][0].globalConfig).toMatchObject({ - testNamePattern: '', testPathPattern: 'file', watch: true, watchAll: false, 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 157809b2155d..f85aeaf65e54 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 @@ -125,14 +125,7 @@ describe('Watch mode flows', () => { expect(runJestMock).toBeCalled(); // globalConfig is updated with the current pattern - expect(runJestMock.mock.calls[0][0].globalConfig).toEqual({ - onlyChanged: false, - passWithNoTests: true, - testNamePattern: '', - testPathPattern: 'p.*3', - watch: true, - watchAll: false, - }); + expect(runJestMock.mock.calls[0][0].globalConfig).toMatchSnapshot(); }); it('Pressing "c" clears the filters', async () => { diff --git a/packages/jest-cli/src/lib/update_global_config.js b/packages/jest-cli/src/lib/update_global_config.js index 153e2a1e1a8b..94ea02f139c6 100644 --- a/packages/jest-cli/src/lib/update_global_config.js +++ b/packages/jest-cli/src/lib/update_global_config.js @@ -7,16 +7,31 @@ * @flow */ -import type {GlobalConfig, SnapshotUpdateState} from 'types/Config'; +import {replacePathSepForRegex} from 'jest-regex-util'; -type Options = { - testNamePattern?: string, - testPathPattern?: string, - noSCM?: boolean, - updateSnapshot?: SnapshotUpdateState, +import type {GlobalConfig} from 'types/Config'; + +export type Options = { + bail?: $PropertyType, + collectCoverage?: $PropertyType, + collectCoverageFrom?: $PropertyType, + collectCoverageOnlyFrom?: $PropertyType< + GlobalConfig, + 'collectCoverageOnlyFrom', + >, + coverageDirectory?: $PropertyType, + coverageReporters?: $PropertyType, mode?: 'watch' | 'watchAll', - passWithNoTests?: boolean, - onlyFailures?: boolean, + noSCM?: $PropertyType, + notify?: $PropertyType, + notifyMode?: $PropertyType, + onlyFailures?: $PropertyType, + passWithNoTests?: $PropertyType, + reporters?: $PropertyType, + testNamePattern?: $PropertyType, + testPathPattern?: $PropertyType, + updateSnapshot?: $PropertyType, + verbose?: $PropertyType, }; export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => { @@ -27,10 +42,6 @@ export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => { options = {}; } - if (options.updateSnapshot) { - newConfig.updateSnapshot = options.updateSnapshot; - } - if (options.mode === 'watch') { newConfig.watch = true; newConfig.watchAll = false; @@ -39,12 +50,13 @@ export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => { newConfig.watchAll = true; } - if ('testPathPattern' in options) { - newConfig.testPathPattern = options.testPathPattern || ''; + if (options.testNamePattern !== undefined) { + newConfig.testNamePattern = options.testNamePattern || ''; } - if ('testNamePattern' in options) { - newConfig.testNamePattern = options.testNamePattern || ''; + if (options.testPathPattern !== undefined) { + newConfig.testPathPattern = + replacePathSepForRegex(options.testPathPattern) || ''; } newConfig.onlyChanged = false; @@ -53,17 +65,61 @@ export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => { !newConfig.testNamePattern && !newConfig.testPathPattern; + if (options.bail !== undefined) { + newConfig.bail = options.bail || false; + } + + if (options.collectCoverage !== undefined) { + newConfig.collectCoverage = options.collectCoverage || false; + } + + if (options.collectCoverageFrom !== undefined) { + newConfig.collectCoverageFrom = options.collectCoverageFrom; + } + + if (options.collectCoverageOnlyFrom !== undefined) { + newConfig.collectCoverageOnlyFrom = options.collectCoverageOnlyFrom; + } + + if (options.coverageDirectory !== undefined) { + newConfig.coverageDirectory = options.coverageDirectory; + } + + if (options.coverageReporters !== undefined) { + newConfig.coverageReporters = options.coverageReporters; + } + if (options.noSCM) { newConfig.noSCM = true; } - if (options.passWithNoTests) { - newConfig.passWithNoTests = true; + if (options.notify !== undefined) { + newConfig.notify = options.notify || false; + } + + if (options.notifyMode !== undefined) { + newConfig.notifyMode = options.notifyMode; } - if ('onlyFailures' in options) { + if (options.onlyFailures !== undefined) { newConfig.onlyFailures = options.onlyFailures || false; } + if (options.passWithNoTests !== undefined) { + newConfig.passWithNoTests = true; + } + + if (options.reporters !== undefined) { + newConfig.reporters = options.reporters; + } + + if (options.updateSnapshot !== undefined) { + newConfig.updateSnapshot = options.updateSnapshot; + } + + if (options.verbose !== undefined) { + newConfig.verbose = options.verbose || false; + } + return Object.freeze(newConfig); }; diff --git a/packages/jest-cli/src/watch.js b/packages/jest-cli/src/watch.js index 1ec6de1b1cad..00d1254d6a06 100644 --- a/packages/jest-cli/src/watch.js +++ b/packages/jest-cli/src/watch.js @@ -7,15 +7,15 @@ * @flow */ -import type {GlobalConfig, SnapshotUpdateState} from 'types/Config'; +import type {GlobalConfig} from 'types/Config'; import type {Context} from 'types/Context'; import type {WatchPlugin} from './types'; +import type {Options as UpdateGlobalConfigOptions} from './lib/update_global_config'; import ansiEscapes from 'ansi-escapes'; import chalk from 'chalk'; import getChangedFilesPromise from './get_changed_files_promise'; import exit from 'exit'; -import {replacePathSepForRegex} from 'jest-regex-util'; import HasteMap from 'jest-haste-map'; import isValidPath from './lib/is_valid_path'; import {isInteractive} from 'jest-util'; @@ -68,31 +68,39 @@ export default function watch( }); const updateConfigAndRun = ({ + bail, + collectCoverage, + collectCoverageFrom, + collectCoverageOnlyFrom, + coverageDirectory, + coverageReporters, mode, + notify, + notifyMode, + onlyFailures, + reporters, testNamePattern, testPathPattern, updateSnapshot, - }: { - mode?: 'watch' | 'watchAll', - testNamePattern?: string, - testPathPattern?: string, - updateSnapshot?: SnapshotUpdateState, - } = {}) => { + verbose, + }: UpdateGlobalConfigOptions = {}) => { const previousUpdateSnapshot = globalConfig.updateSnapshot; globalConfig = updateGlobalConfig(globalConfig, { + bail, + collectCoverage, + collectCoverageFrom, + collectCoverageOnlyFrom, + coverageDirectory, + coverageReporters, mode, - testNamePattern: - testNamePattern !== undefined - ? testNamePattern - : globalConfig.testNamePattern, - testPathPattern: - testPathPattern !== undefined - ? replacePathSepForRegex(testPathPattern) - : globalConfig.testPathPattern, - updateSnapshot: - updateSnapshot !== undefined - ? updateSnapshot - : globalConfig.updateSnapshot, + notify, + notifyMode, + onlyFailures, + reporters, + testNamePattern, + testPathPattern, + updateSnapshot, + verbose, }); startRun(globalConfig);