Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: broader uses for watch plugins #6473

Merged
merged 11 commits into from
Jul 12, 2018
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))

### Fixes

- `[jest-runner]` Force parallel runs for watch mode, to avoid TTY freeze ([#6647](https://github.com/facebook/jest/pull/6647))
Expand Down
21 changes: 21 additions & 0 deletions docs/WatchPlugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @SimenB!

  • --changedSince and --lastCommit do not have any direct equivalent in the global configuration object. They're used by getChangedFilesForRoots in jest-changed-files, and are also ignored when --onlyChanged (which is the default watch mode), so they don't seem to make sense in that mode. At any rate, the watch architecture would not let us change that on the fly right now, this is a bootstrap-time computation.
  • --onlyChanged is already togglable in watch mode: these are the a key (all tests, same as --watchAll) vs. o key (only changed tests, same as --onlyChanged).
  • --logHeapUsage does have a matching boolean in the global config, so I'm open to adding it, what do others think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the same way toggling onlyChanged is useful, I think changedSince would be useful. It would then overwrite onlyChanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimenB I agree it would be nice, but Jest's current architecture doesn't let us do it. It computes the watched list at boot time, then keeps it in a closure where specific global config options may amend it (such as the watch mode). There's no stuff in there right now for changedSince or lastCommit, and I feel the change would be significant.

This specific PR is bound by what's in the global config. Should this feature set become config-based, I'll be glad to expand the white list, but it's a different feature scope IMHO.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rogeliog sounds like we need another of your magical refactor runs 😀

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we would need to change a couple of things in order to make that happen 🙃


- [`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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
102 changes: 100 additions & 2 deletions packages/jest-cli/src/__tests__/watch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ jest.doMock(
{virtual: true},
);

const regularUpdateGlobalConfig = require('../lib/update_global_config')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can import it to avoid the .default call

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but as I'm mocking it right the next line, it seemed more interesting to keep it close by?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jest.doMock should be called prior to require call

.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));
Expand Down Expand Up @@ -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`;
Expand Down Expand Up @@ -588,7 +688,6 @@ describe('Watch mode flows', () => {

expect(runJestMock.mock.calls[0][0].globalConfig).toMatchObject({
testNamePattern: 'test',
testPathPattern: '',
watch: true,
watchAll: false,
});
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
94 changes: 75 additions & 19 deletions packages/jest-cli/src/lib/update_global_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<GlobalConfig, 'bail'>,
collectCoverage?: $PropertyType<GlobalConfig, 'collectCoverage'>,
collectCoverageFrom?: $PropertyType<GlobalConfig, 'collectCoverageFrom'>,
collectCoverageOnlyFrom?: $PropertyType<
GlobalConfig,
'collectCoverageOnlyFrom',
>,
coverageDirectory?: $PropertyType<GlobalConfig, 'coverageDirectory'>,
coverageReporters?: $PropertyType<GlobalConfig, 'coverageReporters'>,
mode?: 'watch' | 'watchAll',
passWithNoTests?: boolean,
onlyFailures?: boolean,
noSCM?: $PropertyType<GlobalConfig, 'noSCM'>,
notify?: $PropertyType<GlobalConfig, 'notify'>,
notifyMode?: $PropertyType<GlobalConfig, 'notifyMode'>,
onlyFailures?: $PropertyType<GlobalConfig, 'onlyFailures'>,
passWithNoTests?: $PropertyType<GlobalConfig, 'passWithNoTests'>,
reporters?: $PropertyType<GlobalConfig, 'reporters'>,
testNamePattern?: $PropertyType<GlobalConfig, 'testNamePattern'>,
testPathPattern?: $PropertyType<GlobalConfig, 'testPathPattern'>,
updateSnapshot?: $PropertyType<GlobalConfig, 'updateSnapshot'>,
verbose?: $PropertyType<GlobalConfig, 'verbose'>,
};

export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => {
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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);
};
Loading