Skip to content

Commit

Permalink
feat(jest-core): add support for watchPlugins written in ESM (#11315)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahnpnl committed Apr 22, 2021
1 parent 6fcdb1e commit 0decd46
Show file tree
Hide file tree
Showing 17 changed files with 220 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -17,6 +17,7 @@
- `[jest-core]` more `TestSequencer` methods can be async ([#10980](https://github.com/facebook/jest/pull/10980))
- `[jest-core]` Add support for `testSequencer` written in ESM ([#11207](https://github.com/facebook/jest/pull/11207))
- `[jest-core]` Add support for `globalSetup` and `globalTeardown` written in ESM ([#11267](https://github.com/facebook/jest/pull/11267))
- `[jest-core]` Add support for `watchPlugins` written in ESM ([#11315](https://github.com/facebook/jest/pull/11315))
- `[jest-environment-node]` Add AbortController to globals ([#11182](https://github.com/facebook/jest/pull/11182))
- `[@jest/fake-timers]` Update to `@sinonjs/fake-timers` to v7 ([#11198](https://github.com/facebook/jest/pull/11198))
- `[jest-haste-map]` Handle injected scm clocks ([#10966](https://github.com/facebook/jest/pull/10966))
Expand Down
45 changes: 45 additions & 0 deletions e2e/__tests__/watch-plugins.test.ts
@@ -0,0 +1,45 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
import {onNodeVersions} from '@jest/test-utils';
import {runContinuous} from '../runJest';

const testCompletedRE = /Ran all test suites./g;
const numberOfTestRuns = (stderr: string): number => {
const matches = stderr.match(testCompletedRE);
return matches ? matches.length : 0;
};

test.each(['js', 'cjs'])('supports %s watch plugins', async watchPluginDir => {
const testRun = runContinuous(`watch-plugins/${watchPluginDir}`, [
'--watchAll',
'--no-watchman',
]);

await testRun.waitUntil(({stderr}) => numberOfTestRuns(stderr) === 1);

expect(testRun.getCurrentOutput().stdout.trim()).toBe('getUsageInfo');

await testRun.end();
});

onNodeVersions('^12.17.0 || >=13.2.0', () => {
test.each(['mjs', 'js-type-module'])(
'supports %s watch plugins',
async watchPluginDir => {
const testRun = runContinuous(`watch-plugins/${watchPluginDir}`, [
'--watchAll',
'--no-watchman',
]);

await testRun.waitUntil(({stderr}) => numberOfTestRuns(stderr) === 1);

expect(testRun.getCurrentOutput().stdout.trim()).toBe('getUsageInfo');

await testRun.end();
},
);
});
11 changes: 11 additions & 0 deletions e2e/watch-plugins/cjs/__tests__/index.js
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
'use strict';

test('load watch plugin cjs', () => {
expect(42).toEqual(42);
});
22 changes: 22 additions & 0 deletions e2e/watch-plugins/cjs/my-watch-plugin.cjs
@@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
class MyWatchPlugin {
// Add hooks to Jest lifecycle events
apply(jestHooks) {
}

// Get the prompt information for interactive plugins
getUsageInfo(globalConfig) {
console.log('getUsageInfo');
}

// Executed when the key from `getUsageInfo` is input
run(globalConfig, updateConfigAndRun) {
}
}

module.exports = MyWatchPlugin;
5 changes: 5 additions & 0 deletions e2e/watch-plugins/cjs/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"watchPlugins": ["./my-watch-plugin.cjs"]
}
}
11 changes: 11 additions & 0 deletions e2e/watch-plugins/js-type-module/__tests__/index.js
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
'use strict';

test('load watch plugin js type module', () => {
expect(42).toEqual(42);
});
20 changes: 20 additions & 0 deletions e2e/watch-plugins/js-type-module/my-watch-plugin.js
@@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
class MyWatchPlugin {
// Add hooks to Jest lifecycle events
apply(jestHooks) {}

// Get the prompt information for interactive plugins
getUsageInfo(globalConfig) {
console.log('getUsageInfo');
}

// Executed when the key from `getUsageInfo` is input
run(globalConfig, updateConfigAndRun) {}
}

export default MyWatchPlugin;
6 changes: 6 additions & 0 deletions e2e/watch-plugins/js-type-module/package.json
@@ -0,0 +1,6 @@
{
"type": "module",
"jest": {
"watchPlugins": ["./my-watch-plugin.js"]
}
}
11 changes: 11 additions & 0 deletions e2e/watch-plugins/js/__tests__/index.js
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
'use strict';

test('load watch plugin js', () => {
expect(42).toEqual(42);
});
20 changes: 20 additions & 0 deletions e2e/watch-plugins/js/my-watch-plugin.js
@@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
class MyWatchPlugin {
// Add hooks to Jest lifecycle events
apply(jestHooks) {}

// Get the prompt information for interactive plugins
getUsageInfo(globalConfig) {
console.log('getUsageInfo');
}

// Executed when the key from `getUsageInfo` is input
run(globalConfig, updateConfigAndRun) {}
}

module.exports = MyWatchPlugin;
5 changes: 5 additions & 0 deletions e2e/watch-plugins/js/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"watchPlugins": ["./my-watch-plugin.js"]
}
}
11 changes: 11 additions & 0 deletions e2e/watch-plugins/mjs/__tests__/index.js
@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
'use strict';

test('load watch plugin mjs', () => {
expect(42).toEqual(42);
});
22 changes: 22 additions & 0 deletions e2e/watch-plugins/mjs/my-watch-plugin.mjs
@@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. 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.
*/
class MyWatchPlugin {
// Add hooks to Jest lifecycle events
apply(jestHooks) {
}

// Get the prompt information for interactive plugins
getUsageInfo(globalConfig) {
console.log('getUsageInfo');
}

// Executed when the key from `getUsageInfo` is input
run(globalConfig, updateConfigAndRun) {
}
}

export default MyWatchPlugin;
5 changes: 5 additions & 0 deletions e2e/watch-plugins/mjs/package.json
@@ -0,0 +1,5 @@
{
"jest": {
"watchPlugins": ["./my-watch-plugin.mjs"]
}
}
28 changes: 14 additions & 14 deletions packages/jest-core/src/__tests__/watch.test.js
Expand Up @@ -205,7 +205,7 @@ describe('Watch mode flows', () => {
});

it('shows prompts for WatchPlugins in alphabetical order', async () => {
watch(
await watch(
{
...globalConfig,
rootDir: __dirname,
Expand Down Expand Up @@ -377,7 +377,7 @@ describe('Watch mode flows', () => {
${'i'} | ${'UpdateSnapshotsInteractive'}
`(
'forbids WatchPlugins overriding reserved internal plugins',
({key, plugin}) => {
async ({key}) => {
const run = jest.fn(() => Promise.resolve());
const pluginPath = `${__dirname}/__fixtures__/plugin_bad_override_${key}`;
jest.doMock(
Expand All @@ -397,7 +397,7 @@ describe('Watch mode flows', () => {
{virtual: true},
);

expect(() => {
await expect(
watch(
{
...globalConfig,
Expand All @@ -408,8 +408,8 @@ describe('Watch mode flows', () => {
pipe,
hasteMapInstances,
stdin,
);
}).toThrowError(
),
).rejects.toThrowError(
new RegExp(
`Watch plugin OffendingWatchPlugin attempted to register key <${key}>,\\s+that is reserved internally for .+\\.\\s+Please change the configuration key for this plugin\\.`,
'm',
Expand All @@ -426,7 +426,7 @@ describe('Watch mode flows', () => {
${'p'} | ${'TestPathPattern'}
`(
'allows WatchPlugins to override non-reserved internal plugins',
({key, plugin}) => {
({key}) => {
const run = jest.fn(() => Promise.resolve());
const pluginPath = `${__dirname}/__fixtures__/plugin_valid_override_${key}`;
jest.doMock(
Expand Down Expand Up @@ -460,7 +460,7 @@ describe('Watch mode flows', () => {
},
);

it('forbids third-party WatchPlugins overriding each other', () => {
it('forbids third-party WatchPlugins overriding each other', async () => {
const pluginPaths = ['Foo', 'Bar'].map(ident => {
const run = jest.fn(() => Promise.resolve());
const pluginPath = `${__dirname}/__fixtures__/plugin_bad_override_${ident.toLowerCase()}`;
Expand All @@ -486,7 +486,7 @@ describe('Watch mode flows', () => {
return pluginPath;
});

expect(() => {
await expect(
watch(
{
...globalConfig,
Expand All @@ -497,8 +497,8 @@ describe('Watch mode flows', () => {
pipe,
hasteMapInstances,
stdin,
);
}).toThrowError(
),
).rejects.toThrowError(
/Watch plugins OffendingFooThirdPartyWatchPlugin and OffendingBarThirdPartyWatchPlugin both attempted to register key <!>\.\s+Please change the key configuration for one of the conflicting plugins to avoid overlap\./m,
);
});
Expand Down Expand Up @@ -526,7 +526,7 @@ describe('Watch mode flows', () => {
{virtual: true},
);

watch(
await watch(
{
...globalConfig,
rootDir: __dirname,
Expand Down Expand Up @@ -560,7 +560,7 @@ describe('Watch mode flows', () => {
{virtual: true},
);

watch(
await watch(
{
...globalConfig,
rootDir: __dirname,
Expand Down Expand Up @@ -717,7 +717,7 @@ describe('Watch mode flows', () => {
{virtual: true},
);

watch(
await watch(
{
...globalConfig,
rootDir: __dirname,
Expand Down Expand Up @@ -778,7 +778,7 @@ describe('Watch mode flows', () => {
{virtual: true},
);

watch(
await watch(
{
...globalConfig,
rootDir: __dirname,
Expand Down
13 changes: 10 additions & 3 deletions packages/jest-core/src/watch.ts
Expand Up @@ -18,7 +18,12 @@ import type {
import {formatExecError} from 'jest-message-util';
import Resolver from 'jest-resolve';
import type {Context} from 'jest-runtime';
import {isInteractive, preRunMessage, specialChars} from 'jest-util';
import {
isInteractive,
preRunMessage,
requireOrImportModule,
specialChars,
} from 'jest-util';
import {ValidationError} from 'jest-validate';
import {
AllowedConfigOptions,
Expand Down Expand Up @@ -85,7 +90,7 @@ const RESERVED_KEY_PLUGINS = new Map<
[QuitPlugin, {forbiddenOverwriteMessage: 'quitting watch mode'}],
]);

export default function watch(
export default async function watch(
initialGlobalConfig: Config.GlobalConfig,
contexts: Array<Context>,
outputStream: NodeJS.WriteStream,
Expand Down Expand Up @@ -187,7 +192,9 @@ export default function watch(
for (const pluginWithConfig of globalConfig.watchPlugins) {
let plugin: WatchPlugin;
try {
const ThirdPartyPlugin = require(pluginWithConfig.path);
const ThirdPartyPlugin = await requireOrImportModule<WatchPluginClass>(
pluginWithConfig.path,
);
plugin = new ThirdPartyPlugin({
config: pluginWithConfig.config,
stdin,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-watcher/src/types.ts
Expand Up @@ -83,6 +83,7 @@ export interface WatchPlugin {
}
export interface WatchPluginClass {
new (options: {
config: Record<string, unknown>;
stdin: NodeJS.ReadStream;
stdout: NodeJS.WriteStream;
}): WatchPlugin;
Expand Down

0 comments on commit 0decd46

Please sign in to comment.