Skip to content

Commit

Permalink
[Proposal] Watch plugins API (#5399)
Browse files Browse the repository at this point in the history
* Start adding tapable

* Use test_path_pattern as a plugin

* Use test_name_pattern as a plugin

* Use quit as a plugin

* Fix test interruption

* Use update snapshot as a plugin

* Use update snapshot interactive as a plugin

* Change API to use a class instance

* A bit of clean up and make tests pass

* Change plugin implementation to not use tapable

* Better sorting implementation

* Add back third party plugin functionality

* Fix flow

* Fix ESLint

* Reset file to state of master

* Update failing snapshot

* Remove hasSnapshotFailure and hasSnapshotFailureInteractive

* Async await for showPrompt and clear active plugin on file change

* Fix snapshot failure

* Reenable tests

* Implement shouldRunTestSuite

* Add changelog

* Clean up watch.js a bit
  • Loading branch information
rogeliog authored and cpojer committed Feb 6, 2018
1 parent bda02e6 commit 4561959
Show file tree
Hide file tree
Showing 16 changed files with 694 additions and 316 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
to the absolute path. Additionally, this fixes functionality in Windows OS.
([#5398](https://github.com/facebook/jest/pull/5398))

### Chore & Maintenance

* `[jest-util]` Implement watch plugins
([#5399](https://github.com/facebook/jest/pull/5399))

## jest 22.1.4

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ Watch Usage
› 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.
› Press s to do nothing.
› Press u to do something else.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
",
],
Expand All @@ -46,10 +46,10 @@ Array [
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press u to update failing snapshots.
› Press i to update failing snapshots interactively.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press u to update failing snapshots.
› Press i to update failing snapshots interactively.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
",
Expand All @@ -64,9 +64,9 @@ Array [
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press u to update failing snapshots.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press u to update failing snapshots.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
",
Expand Down
129 changes: 100 additions & 29 deletions packages/jest-cli/src/__tests__/watch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,36 @@ jest.doMock(

jest.doMock(
watchPluginPath,
() => ({
enter: jest.fn(),
key: 's'.codePointAt(0),
prompt: 'do nothing',
}),
() =>
class WatchPlugin1 {
getUsageRow() {
return {
key: 's'.codePointAt(0),
prompt: 'do nothing',
};
}
},
{virtual: true},
);

jest.doMock(
watchPlugin2Path,
() => ({
enter: jest.fn(),
key: 'u'.codePointAt(0),
prompt: 'do something else',
}),
() =>
class WatchPlugin2 {
getUsageRow() {
return {
key: 'u'.codePointAt(0),
prompt: 'do something else',
};
}
},
{virtual: true},
);

const watch = require('../watch').default;

const nextTick = () => new Promise(res => process.nextTick(res));

afterEach(runJestMock.mockReset);

describe('Watch mode flows', () => {
Expand Down Expand Up @@ -231,50 +242,108 @@ describe('Watch mode flows', () => {
expect(pipeMockCalls.slice(determiningTestsToRun + 1)).toMatchSnapshot();
});

it('triggers enter on a WatchPlugin when its key is pressed', () => {
const plugin = require(watchPluginPath);
it('triggers enter on a WatchPlugin when its key is pressed', async () => {
const showPrompt = jest.fn(() => Promise.resolve());
const pluginPath = `${__dirname}/__fixtures__/plugin_path`;
jest.doMock(
pluginPath,
() =>
class WatchPlugin1 {
constructor() {
this.showPrompt = showPrompt;
}
getUsageRow() {
return {
key: 's'.codePointAt(0),
prompt: 'do nothing',
};
}
},
{virtual: true},
);

watch(
Object.assign({}, globalConfig, {
rootDir: __dirname,
watchPlugins: [watchPluginPath],
watchPlugins: [pluginPath],
}),
contexts,
pipe,
hasteMapInstances,
stdin,
);

stdin.emit(plugin.key.toString(16));
stdin.emit(Number('s'.charCodeAt(0)).toString(16));

await nextTick();

expect(plugin.enter).toHaveBeenCalled();
expect(showPrompt).toHaveBeenCalled();
});

it('prevents Jest from handling keys when active and returns control when end is called', () => {
const plugin = require(watchPluginPath);
const plugin2 = require(watchPlugin2Path);
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 pluginPath = `${__dirname}/__fixtures__/plugin_path_1`;
jest.doMock(
pluginPath,
() =>
class WatchPlugin1 {
constructor() {
this.showPrompt = showPrompt;
}
onData() {}
getUsageRow() {
return {
key: 's'.codePointAt(0),
prompt: 'do nothing',
};
}
},
{virtual: true},
);

let pluginEnd;
plugin.enter = jest.fn((globalConfig, end) => (pluginEnd = end));
const showPrompt2 = jest.fn(() => Promise.resolve());
const pluginPath2 = `${__dirname}/__fixtures__/plugin_path_2`;
jest.doMock(
pluginPath2,
() =>
class WatchPlugin1 {
constructor() {
this.showPrompt = showPrompt2;
}
onData() {}
getUsageRow() {
return {
key: 'z'.codePointAt(0),
prompt: 'also do nothing',
};
}
},
{virtual: true},
);

watch(
Object.assign({}, globalConfig, {
rootDir: __dirname,
watchPlugins: [watchPluginPath, watchPlugin2Path],
watchPlugins: [pluginPath, pluginPath2],
}),
contexts,
pipe,
hasteMapInstances,
stdin,
);

stdin.emit(plugin.key.toString(16));
expect(plugin.enter).toHaveBeenCalled();
stdin.emit(plugin2.key.toString(16));
expect(plugin2.enter).not.toHaveBeenCalled();
pluginEnd();
stdin.emit(plugin2.key.toString(16));
expect(plugin2.enter).toHaveBeenCalled();
stdin.emit(Number('s'.charCodeAt(0)).toString(16));
await nextTick();
expect(showPrompt).toHaveBeenCalled();
stdin.emit(Number('z'.charCodeAt(0)).toString(16));
await nextTick();
expect(showPrompt2).not.toHaveBeenCalled();
await resolveShowPrompt();
stdin.emit(Number('z'.charCodeAt(0)).toString(16));
expect(showPrompt2).toHaveBeenCalled();
});

it('Pressing "o" runs test in "only changed files" mode', () => {
Expand Down Expand Up @@ -312,20 +381,22 @@ describe('Watch mode flows', () => {
expect(runJestMock).toHaveBeenCalledTimes(2);
});

it('Pressing "u" reruns the tests in "update snapshot" mode', () => {
it('Pressing "u" reruns the tests in "update snapshot" mode', async () => {
globalConfig.updateSnapshot = 'new';

watch(globalConfig, contexts, pipe, hasteMapInstances, stdin);
runJestMock.mockReset();

stdin.emit(KEYS.U);
await nextTick();

expect(runJestMock.mock.calls[0][0].globalConfig).toMatchObject({
updateSnapshot: 'all',
watch: true,
});

stdin.emit(KEYS.A);
await nextTick();
// updateSnapshot is not sticky after a run.
expect(runJestMock.mock.calls[1][0].globalConfig).toMatchObject({
updateSnapshot: 'new',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ jest.doMock('../lib/terminal_utils', () => ({

const watch = require('../watch').default;

const nextTick = () => new Promise(res => process.nextTick(res));

const toHex = char => Number(char.charCodeAt(0)).toString(16);

const globalConfig = {watch: true};
Expand Down Expand Up @@ -142,27 +144,35 @@ describe('Watch mode flows', () => {
});
});

it('Pressing "c" clears the filters', () => {
it('Pressing "c" clears the filters', async () => {
contexts[0].config = {rootDir: ''};
watch(globalConfig, contexts, pipe, hasteMapInstances, stdin);

stdin.emit(KEYS.P);
await nextTick();

['p', '.', '*', '1', '0']
.map(toHex)
.concat(KEYS.ENTER)
.forEach(key => stdin.emit(key));

stdin.emit(KEYS.T);
await nextTick();

['t', 'e', 's', 't']
.map(toHex)
.concat(KEYS.ENTER)
.forEach(key => stdin.emit(key));

await nextTick();

stdin.emit(KEYS.C);
await nextTick();

pipe.write.mockReset();
stdin.emit(KEYS.P);
await nextTick();

expect(pipe.write.mock.calls.join('\n')).toMatchSnapshot();
});
});
Expand Down
65 changes: 65 additions & 0 deletions packages/jest-cli/src/jest_hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* 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 {AggregatedResult} from 'types/TestResult';

type ShouldRunTestSuite = (testPath: string) => Promise<boolean>;
type TestRunComplete = (results: AggregatedResult) => void;

export type JestHookSubscriber = {
shouldRunTestSuite: (fn: ShouldRunTestSuite) => void,
testRunComplete: (fn: TestRunComplete) => void,
};

export type JestHookEmitter = {
shouldRunTestSuite: (testPath: string) => Promise<boolean>,
testRunComplete: (results: AggregatedResult) => void,
};

class JestHooks {
_listeners: {
shouldRunTestSuite: Array<ShouldRunTestSuite>,
testRunComplete: Array<TestRunComplete>,
};

constructor() {
this._listeners = {
shouldRunTestSuite: [],
testRunComplete: [],
};
}

getSubscriber(): JestHookSubscriber {
return {
shouldRunTestSuite: fn => {
this._listeners.shouldRunTestSuite.push(fn);
},
testRunComplete: fn => {
this._listeners.testRunComplete.push(fn);
},
};
}

getEmitter(): JestHookEmitter {
return {
shouldRunTestSuite: async testPath =>
Promise.all(
this._listeners.shouldRunTestSuite.map(listener =>
listener(testPath),
),
).then(result =>
result.every(shouldRunTestSuite => shouldRunTestSuite),
),
testRunComplete: results =>
this._listeners.testRunComplete.forEach(listener => listener(results)),
};
}
}

export default JestHooks;
37 changes: 37 additions & 0 deletions packages/jest-cli/src/lib/active_filters_message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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 {GlobalConfig} from 'types/Config';
import chalk from 'chalk';

const activeFilters = (
globalConfig: GlobalConfig,
delimiter: string = '\n',
) => {
const {testNamePattern, testPathPattern} = globalConfig;
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 '';
};

export default activeFilters;
Loading

0 comments on commit 4561959

Please sign in to comment.