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

[Proposal] Watch plugins API #5399

Merged
merged 26 commits into from
Feb 6, 2018
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
396cd18
Start adding tapable
rogeliog Jan 17, 2018
c04edc7
Use test_path_pattern as a plugin
rogeliog Jan 19, 2018
1f74aa1
Use test_name_pattern as a plugin
rogeliog Jan 19, 2018
03b53b1
Use quit as a plugin
rogeliog Jan 19, 2018
1aeafb8
Fix test interruption
rogeliog Jan 19, 2018
b40f604
Use update snapshot as a plugin
rogeliog Jan 19, 2018
6e97a6e
Use update snapshot interactive as a plugin
rogeliog Jan 25, 2018
ef14ade
Change API to use a class instance
rogeliog Jan 26, 2018
8b999ed
Merge branch 'master' into simpler-plugins
rogeliog Jan 26, 2018
2604965
A bit of clean up and make tests pass
rogeliog Jan 26, 2018
ffc85f1
Change plugin implementation to not use tapable
rogeliog Feb 2, 2018
053d9e4
Better sorting implementation
rogeliog Feb 2, 2018
2036be9
Add back third party plugin functionality
rogeliog Feb 2, 2018
40c4265
Fix flow
rogeliog Feb 2, 2018
44c82ba
Merge branch 'master' into simpler-plugins
rogeliog Feb 2, 2018
7767b71
Fix ESLint
rogeliog Feb 2, 2018
2b084f8
Reset file to state of master
rogeliog Feb 2, 2018
58f1717
Update failing snapshot
rogeliog Feb 2, 2018
13f08e5
Merge branch 'master' into simpler-plugins
rogeliog Feb 6, 2018
d4ecb92
Remove hasSnapshotFailure and hasSnapshotFailureInteractive
rogeliog Feb 6, 2018
8f6513e
Async await for showPrompt and clear active plugin on file change
rogeliog Feb 6, 2018
401e44b
Fix snapshot failure
rogeliog Feb 6, 2018
9ac7d94
Reenable tests
rogeliog Feb 6, 2018
25e0c4b
Implement shouldRunTestSuite
rogeliog Feb 6, 2018
4a68617
Add changelog
rogeliog Feb 6, 2018
3dc8c06
Clean up watch.js a bit
rogeliog Feb 6, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

// prettier-ignore
test('escape strings', () => expect('one: \\\'').toMatchSnapshot());
test('escape strings two', () => expect('two: \'"').toMatchSnapshot());
1 change: 1 addition & 0 deletions packages/jest-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"slash": "^1.0.0",
"string-length": "^2.0.0",
"strip-ansi": "^4.0.0",
"tapable": "1.0.0-beta.5",
"which": "^1.2.12",
"yargs": "^10.0.3"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Watch mode flows Failing snapshot 1`] = `1`;

exports[`Watch mode flows Runs Jest in a non-interactive environment not showing usage 1`] = `
Array [
"
Expand All @@ -14,8 +16,8 @@ Watch Usage
› Press a to run all tests.
› 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 t to filter by a test regex pattern.
› Press Enter to trigger a test run.
",
]
Expand All @@ -29,10 +31,10 @@ Watch Usage
› Press a to run all tests.
› 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 t to filter by a test regex pattern.
› Press u to do something else.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
",
],
Expand Down
22 changes: 14 additions & 8 deletions packages/jest-cli/src/__tests__/watch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jest.doMock(
jest.doMock(
watchPluginPath,
() => ({
enter: jest.fn(),
apply: jest.fn(),
key: 's'.codePointAt(0),
prompt: 'do nothing',
}),
Expand All @@ -46,13 +46,15 @@ jest.doMock(
jest.doMock(
watchPlugin2Path,
() => ({
enter: jest.fn(),
apply: jest.fn(),
key: 'u'.codePointAt(0),
prompt: 'do something else',
}),
{virtual: true},
);

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

const watch = require('../watch').default;
afterEach(runJestMock.mockReset);

Expand Down Expand Up @@ -175,7 +177,11 @@ describe('Watch mode flows', () => {
expect(pipeMockCalls.slice(determiningTestsToRun + 1)).toMatchSnapshot();
});

it('triggers enter on a WatchPlugin when its key is pressed', () => {
it('Failing snapshot', () => {
expect(1).toMatchSnapshot();
});

xit('triggers enter on a WatchPlugin when its key is pressed', () => {
const plugin = require(watchPluginPath);

watch(
Expand All @@ -191,10 +197,10 @@ describe('Watch mode flows', () => {

stdin.emit(plugin.key.toString(16));

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

it('prevents Jest from handling keys when active and returns control when end is called', () => {
xit('prevents Jest from handling keys when active and returns control when end is called', () => {
Copy link
Member

Choose a reason for hiding this comment

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

seems like something we'd want, isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, this is one of the pending items that I have at the top.

const plugin = require(watchPluginPath);
const plugin2 = require(watchPlugin2Path);

Expand Down Expand Up @@ -256,14 +262,14 @@ 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,
Expand All @@ -273,7 +279,7 @@ describe('Watch mode flows', () => {
// updateSnapshot is not sticky after a run.
expect(runJestMock.mock.calls[1][0].globalConfig).toMatchObject({
updateSnapshot: 'new',
watch: false,
watch: true,
});
});
it('passWithNoTest should be set to true in watch mode', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ jest.doMock('../lib/terminal_utils', () => ({
getTerminalWidth: () => terminalWidth,
}));

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

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

const toHex = char => Number(char.charCodeAt(0)).toString(16);
Expand All @@ -105,12 +107,13 @@ describe('Watch mode flows', () => {
stdin = new MockStdin();
});

it('Pressing "P" enters pattern mode', () => {
it('Pressing "P" enters pattern mode', async () => {
contexts[0].config = {rootDir: ''};
watch(globalConfig, contexts, pipe, hasteMapInstances, stdin);

// Write a enter pattern mode
stdin.emit(KEYS.P);
await nextTick();
expect(pipe.write).toBeCalledWith(' pattern › ');

const assertPattern = hex => {
Expand All @@ -129,6 +132,7 @@ describe('Watch mode flows', () => {
// Runs Jest again
runJestMock.mockReset();
stdin.emit(KEYS.ENTER);
await nextTick();
expect(runJestMock).toBeCalled();

// globalConfig is updated with the current pattern
Expand All @@ -142,27 +146,33 @@ 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));
await nextTick();

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
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const watch = require('../watch').default;

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

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

const globalConfig = {
watch: true,
};
Expand All @@ -121,12 +123,13 @@ describe('Watch mode flows', () => {
stdin = new MockStdin();
});

it('Pressing "T" enters pattern mode', () => {
it('Pressing "T" enters pattern mode', async () => {
contexts[0].config = {rootDir: ''};
watch(globalConfig, contexts, pipe, hasteMapInstances, stdin);

// Write a enter pattern mode
stdin.emit(KEYS.T);
await nextTick();
expect(pipe.write).toBeCalledWith(' pattern › ');

const assertPattern = hex => {
Expand All @@ -145,6 +148,7 @@ describe('Watch mode flows', () => {
// Runs Jest again
runJestMock.mockReset();
stdin.emit(KEYS.ENTER);
await nextTick();
expect(runJestMock).toBeCalled();

// globalConfig is updated with the current pattern
Expand Down
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)
Copy link
Member

Choose a reason for hiding this comment

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

.filter(Boolean)? Or just remove the double bang, not really needed (as you check against null)

Copy link
Contributor Author

@rogeliog rogeliog Feb 6, 2018

Choose a reason for hiding this comment

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

Sorry, I just moved that from watch.js, changing it now

.join(', ');

const messages = ['\n' + chalk.bold('Active Filters: ') + filters];
Copy link
Member

Choose a reason for hiding this comment

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

Why in an array? Did you mean to concat with filters instead of joining on line 27?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this doesn't seem to be needed, same as above just moved to from watch.js


return messages.filter(message => !!message).join(delimiter);
Copy link
Member

Choose a reason for hiding this comment

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

this is still weird - messages will always be an array of size 1

}

return '';
};

export default activeFilters;
12 changes: 8 additions & 4 deletions packages/jest-cli/src/lib/watch_plugin_registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import getType from 'jest-get-type';

const RESERVED_KEYS = [
0x03, // Jest should handle ctrl-c interrupt
'q'.codePointAt(0), // 'q' is reserved for quit
];

export default class WatchPluginRegistry {
Expand All @@ -26,7 +25,7 @@ export default class WatchPluginRegistry {

loadPluginPath(pluginModulePath: string) {
// $FlowFixMe dynamic require
const maybePlugin = require(pluginModulePath);
let maybePlugin = require(pluginModulePath);

// Since we're loading the module from a dynamic path, assert its shape
// before assuming it's a valid watch plugin.
Expand All @@ -35,6 +34,11 @@ export default class WatchPluginRegistry {
`Jest watch plugin ${pluginModulePath} must be an ES Module or export an object`,
);
}

if (getType(maybePlugin.default) === 'object') {
maybePlugin = maybePlugin.default;
}

if (getType(maybePlugin.key) !== 'number') {
throw new Error(
`Jest watch plugin ${pluginModulePath} must export 'key' as a number`,
Expand All @@ -45,9 +49,9 @@ export default class WatchPluginRegistry {
`Jest watch plugin ${pluginModulePath} must export 'prompt' as a string`,
);
}
if (getType(maybePlugin.enter) !== 'function') {
if (getType(maybePlugin.apply) !== 'function') {
throw new Error(
`Jest watch plugin ${pluginModulePath} must export 'enter' as a function`,
`Jest watch plugin ${pluginModulePath} must export 'apply' as a function`,
);
}

Expand Down
26 changes: 26 additions & 0 deletions packages/jest-cli/src/plugins/quit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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 WatchPlugin from '../watch_plugin';

class QuitPlugin extends WatchPlugin {
showPrompt(): Promise<void> {
this._stdout.write('\n');
process.exit(0);
return Promise.resolve();
Copy link
Member

Choose a reason for hiding this comment

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

can just mark the function as async and drop the return, although the transpiled code is pretty ugly. Up to to you 🙂

(would be cool if babel just wrapped the return value in Promise.resolve if there were no awaits in an async function)

}

getUsageRow() {
return {
key: 'q'.codePointAt(0),
prompt: 'quit watch mode',
};
}
}

export default QuitPlugin;
62 changes: 62 additions & 0 deletions packages/jest-cli/src/plugins/test_name_pattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* 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 {JestHooks} from '../types';
import type {GlobalConfig} from 'types/Config';
import WatchPlugin from '../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 {
_prompt: Prompt;

constructor(options: {
stdin: stream$Readable | tty$ReadStream,
stdout: stream$Writable | tty$WriteStream,
}) {
super(options);
this._prompt = new Prompt();
}

getUsageRow() {
return {
key: 't'.codePointAt(0),
prompt: 'filter by a test regex pattern',
};
}

onData(key: string) {
this._prompt.put(key);
}

showPrompt(
globalConfig: GlobalConfig,
updateConfigAndRun: Function,
): Promise<void> {
return new Promise((res, rej) => {
const testPathPatternPrompt = new TestNamePatternPrompt(
this._stdout,
this._prompt,
);

testPathPatternPrompt.run(
Copy link
Member

Choose a reason for hiding this comment

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

would be great if this was promise-friendly by default. Not for this PR though

Copy link
Member

Choose a reason for hiding this comment

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

It is sorta odd to reject on cancel though...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree

(value: string) => {
updateConfigAndRun({testNamePattern: value});
res();
},
rej,
{
header: activeFilters(globalConfig),
},
);
});
}
}

export default TestNamePatternPlugin;
Loading