Jest Watch Plugins 实战指南:扩展 watch 模式菜单与测试生命周期钩子
2026/9/19 19:50:12 网站建设 项目流程

Jest Watch Plugins 实战指南:扩展 watch 模式菜单与测试生命周期钩子

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

Jest 的 watch 插件系统允许开发者挂钩测试运行的特定生命周期事件,并在 watch 模式菜单中注册自定义按键与提示,从而把 Jest 的 watch 模式改造成贴合自身工作流的交互式开发工具。本文以 Jest 30.4 文档 WatchPlugins.md 为主体,结合仓库源码与测试用例,完整讲解插件接口、生命周期钩子、菜单集成、配置定制与按键冲突处理,读完即可动手编写自己的 watch 插件。

一、认识 Watch 插件系统

Jest 的 watch 插件系统提供了一种把自定义逻辑挂接到 Jest 特定环节的机制:一方面可以监听测试运行的生命周期事件(文件变更、测试运行完成、某条测试是否该运行),另一方面可以在 watch 模式菜单中注册「按键 + 提示文字」,按下按键即执行对应代码。二者组合起来,就能开发出完全贴合个人工作流的交互式体验。

从源码结构看,插件系统由packages/jest-watcher包支撑,其中:

  • types.ts 定义了WatchPlugin接口、JestHookSubscriber订阅者类型、UsageDatakey+prompt)等核心类型;
  • JestHooks.ts 实现了钩子的订阅(subscriber)与触发(emitter)机制;
  • BaseWatchPlugin.ts 提供抽象基类,内置applygetUsageInfoonKeyrun的空实现,方便快速继承。

Watch Plugin 接口

一个最小的插件就是一个拥有三个方法的类:

class MyWatchPlugin { // Add hooks to Jest lifecycle events apply(jestHooks) {} // Get the prompt information for interactive plugins getUsageInfo(globalConfig) {} // Executed when the key from `getUsageInfo` is input run(globalConfig, updateConfigAndRun) {} }

对照 types.ts 中的WatchPlugin接口可以看到,实际可选的成员还包括isInternal(标记内置插件)与onKey(处理按键原始输入)。getUsageInfo允许返回null,表示该插件不注册菜单按键;run返回Promise<void | boolean>

把插件接入 Jest

在 Jest 配置中通过watchPlugins数组声明插件路径即可接入:

module.exports = { // ... watchPlugins: ['path/to/yourWatchPlugin'], };

Jest 在启动 watch 模式时会加载这些模块。加载逻辑位于 packages/jest-core/src/watch.ts:通过requireOrImportModule动态加载插件模块(因此 CJS、ESM 均可),然后以{config, stdin, stdout}为参数实例化插件类;若初始化失败,会抛出带有格式化错误上下文的异常。仓库的端到端测试 e2e/watch-plugins 分别用cjsjsmjsjs-type-module四种模块形态验证了插件的加载。

二、Hooking into Jest:生命周期钩子

自定义 watch 插件可以通过apply方法向 Jest 注册钩子。即使插件不打算注册交互式菜单按键,也可以只使用钩子来观察或干预测试运行。

apply(jestHooks)

apply方法接收一个jestHooks参数,允许插件挂接到测试运行生命周期的具体环节:

class MyWatchPlugin { apply(jestHooks) {} }

从 JestHooks.ts 的实现可以看到,JestHooks内部维护了onFileChangeonTestRunCompleteshouldRunTestSuite三组监听器数组:getSubscriber()返回的订阅者把回调推入数组,getEmitter()返回的发射器负责在事件发生时逐一调用监听器。启动 watch 模式时,Jest 会先对内置插件调用plugin.apply(hookSubscriber)(watch.ts),再对配置中的第三方插件依次调用(watch.ts)。isUsed(hook)方法还允许外部判断某个钩子是否被注册过。

jestHooks.shouldRunTestSuite(testSuiteInfo)

返回布尔值(或Promise<boolean>以处理异步操作),决定某条测试是否应该运行:

class MyWatchPlugin { apply(jestHooks) { jestHooks.shouldRunTestSuite(testSuiteInfo => { return testSuiteInfo.testPath.includes('my-keyword'); }); // or a promise jestHooks.shouldRunTestSuite(testSuiteInfo => { return Promise.resolve(testSuiteInfo.testPath.includes('my-keyword')); }); } }

testSuiteInfo的结构见 types.ts:包含config(项目配置)、testPath(测试文件路径)与可选的duration

值得注意的实现细节:该钩子是「一票否决制」。在 JestHooks.ts 中,发射器会用Promise.all并行调用所有监听器,然后result.every(Boolean)——即只要有一个监听器返回false,该测试套件就不会运行。

jestHooks.onTestRunComplete(results)

在每次测试运行结束时被调用,参数是测试结果:

class MyWatchPlugin { apply(jestHooks) { jestHooks.onTestRunComplete(results => { this._hasSnapshotFailure = results.snapshot.failure; }); } }

resultsAggregatedResult类型(来自@jest/test-result),包含汇总的断言数、失败数以及snapshot等字段。上面的例子用它在每次运行结束后记录「是否存在快照失败」,供菜单按键或后续逻辑使用。

jestHooks.onFileChange({projects})

只要文件系统发生变化就会被调用:

  • projects: Array<config: ProjectConfig, testPaths: Array<string>>:包含 Jest 正在监听的所有测试路径。
class MyWatchPlugin { apply(jestHooks) { jestHooks.onFileChange(({projects}) => { this._projects = projects; }); } }

对应类型JestHookExposedFS定义在 types.ts:每个项目包含configtestPaths数组。插件可以在文件变更后把项目清单缓存起来,供自定义菜单使用。

三、Watch Menu Integration:注册交互式菜单按键

除了生命周期钩子,插件还能通过「getUsageInfo返回按键/提示 +run执行按键逻辑」的方式为 watch 菜单增加或覆盖功能。

getUsageInfo(globalConfig)

实现该方法并返回一个keyprompt,即可向 watch 菜单添加一个按键:

class MyWatchPlugin { getUsageInfo(globalConfig) { return { key: 's', prompt: 'do something', }; } }

这会在 watch 模式菜单中新增一行(› Press s to do something.):

Watch Usage › 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 something. // <-- This is our plugin › Press Enter to trigger a test run.

菜单渲染逻辑在 watch.ts 的usage函数中:先输出内置项(ca/foptq等),再通过getSortedUsageRows(watchPlugins, globalConfig)把各插件的key/prompt拼成Press <key> to <prompt>.行。按键的获取见getPluginKey(watch.ts):调用plugin.getUsageInfo(globalConfig)并取其返回对象的key

注意:如果插件的按键与某个默认按键重复,你的插件将覆盖该默认按键(前提是该默认键允许被覆盖,见下文「Choosing a good key」)。

run(globalConfig, updateConfigAndRun)

处理getUsageInfo返回的按键对应的事件。该方法返回一个Promise<boolean>,当插件希望把控制权交还给 Jest 时 resolve;布尔值表示 Jest 拿回控制权后是否应重新运行测试:

  • globalConfig:Jest 当前全局配置的表示;
  • updateConfigAndRun:允许你在交互式插件运行期间触发一次测试运行。
class MyWatchPlugin { run(globalConfig, updateConfigAndRun) { // do something. } }

注意:如果你调用了updateConfigAndRun,那么run方法就不应 resolve 为真值,否则会触发双重运行。

Authorized configuration keys(可授权更新的配置键)

出于稳定与安全考虑,updateConfigAndRun只能更新全局配置中的部分键,当前白名单如下:

  • bail(见 Configuration.md)
  • changedSince(见 CLI.md)
  • collectCoverage(见 Configuration.md)
  • collectCoverageFrom(见 Configuration.md)
  • coverageDirectory(见 Configuration.md)
  • coverageReporters(见 Configuration.md)
  • notify(见 Configuration.md)
  • notifyMode(见 Configuration.md)
  • onlyFailures(见 Configuration.md)
  • reporters(见 Configuration.md)
  • testNamePattern(见 CLI.md)
  • testPathPatterns(见 CLI.md)
  • updateSnapshot(见 CLI.md)
  • verbose(见 Configuration.md)

白名单在源码中有两处对应物:一处是 types.ts 中的AllowedConfigOptions类型(额外还包含findRelatedTestsnonFlagArgsmodetestPathPatterns数组形式);另一处是 watch.ts 中updateConfigAndRun的实参解构与updateGlobalConfig调用。从该实现还能看到两个细节:调用后会立即startRun(globalConfig)触发一次测试运行;并且updateSnapshot不是粘性的——运行结束后会被重置为none(除非之前就是all)。

四、Customization:通过配置定制插件

插件可以通过 Jest 配置进行定制。在watchPlugins数组中使用「路径 + 配置对象」的元组形式:

module.exports = { // ... watchPlugins: [ [ 'path/to/yourWatchPlugin', { key: 'k', // <- your custom key prompt: 'show a custom prompt', }, ], ], };

推荐的配置项:

  • key:修改插件按键;
  • prompt:允许用户定制插件提示文字。

如果用户提供了自定义配置,它会作为参数传给插件构造函数。从 watch.ts 可以看到,实例化时传入的是{config: pluginWithConfig.config, stdin, stdout},其中stdin/stdout由 Jest 注入,config即你在配置里写的定制对象:

class MyWatchPlugin { constructor({config}) {} }

WatchPluginClass类型(types.ts)正式声明了构造参数为{config, stdin, stdout}。若要进一步感知按键、实现类似内置p/t那样的输入式交互,可以继承 BaseWatchPlugin.ts(它把stdin/stdout存为受保护成员)并结合 PatternPrompt.ts(内置的「Pattern Mode」交互基类,支持 Esc 退出、Enter 应用正则)或 constants.ts 中导出的按键码常量(KEYS.ENTERKEYS.ESCAPE、方向键、CONTROL_C等,Windows 与 Unix 的退格键码不同)。

五、Choosing a good key:按键选择与冲突处理

Jest 允许第三方插件覆盖部分内置功能键,但并非全部。以下按键不可覆盖(保留给 Jest 内部):

  • c(清除过滤模式)
  • i(交互式更新不匹配的快照)
  • q(退出)
  • u(更新所有不匹配的快照)
  • w(显示 watch 模式用法 / 可用操作)

以下内置功能键可以被覆盖

  • p(测试文件名模式)
  • t(测试名称模式)

任何未被内置功能使用的按键都可以声明。建议避免使用在各种键盘上难以输入的键(如é),或默认不可见的字符(很多 Mac 键盘对|\[等没有直观的键帽提示)。

当冲突发生时

从源码看,内置插件的保留信息集中在 watch.ts:INTERNAL_PLUGINS包含 6 个内置插件(FailedTestsInteractivePluginTestPathPatternPluginTestNamePatternPluginUpdateSnapshotsPluginUpdateSnapshotsInteractivePluginQuitPlugin),而RESERVED_KEY_PLUGINSuiq等键登记了forbiddenOverwriteMessage。如果插件试图覆盖保留键,Jest 会抛出带描述信息的错误:

Watch plugin YourFaultyPlugin attempted to register key `q`, that is reserved internally for quitting watch mode. Please change the configuration key for this plugin.

第三方插件同样禁止覆盖已在插件列表(watchPlugins数组)中更靠前位置注册的另一个第三方插件的键。发生这种情况时也会得到帮助定位问题的错误信息:

Watch plugins YourFaultyPlugin and TheirFaultyPlugin both attempted to register key `x`. Please change the key configuration for one of the conflicting plugins to avoid overlap.

冲突检测的完整逻辑在checkForConflicts函数(watch.ts):它维护一张「键 → 插件」映射表;若新插件要注册的键已被不可覆盖的插件占用,则按上述规则抛出ValidationError。注意内置保留键的判断依据是forbiddenOverwriteMessage是否存在——只有携带该消息的键(即uiq)绝对不可覆盖,而pt等插件虽然内置,但可以被第三方插件覆盖。

六、实战:组合钩子与菜单的完整示例

把以上知识点组合起来,一个完整的 watch 插件通常长这样:

// my-watch-plugin.js class SnapshotAwareWatchPlugin { constructor({config, stdin, stdout}) { this._config = config; this._hasSnapshotFailure = false; this._projects = []; } apply(jestHooks) { // 记录每次运行是否出现快照失败 jestHooks.onTestRunComplete(results => { this._hasSnapshotFailure = results.snapshot.failure; }); // 监听文件系统变化 jestHooks.onFileChange(({projects}) => { this._projects = projects; }); } getUsageInfo() { return { key: this._config.key || 's', prompt: this._config.prompt || 'show snapshot failure status', }; } async run() { const status = this._hasSnapshotFailure ? 'snapshot failures detected' : 'all snapshots up to date'; process.stdout.write(`\n[my-plugin] ${status}\n`); return false; // 不触发测试重跑 } } module.exports = SnapshotAwareWatchPlugin;

配置中使用(同时定制按键与提示):

module.exports = { watchPlugins: [ [ './my-watch-plugin.js', { key: 's', prompt: 'show snapshot failure status', }, ], ], };

验证方式:在项目根目录运行jest --watch(或jest --watchAll),菜单中应出现› Press s to show snapshot failure status.;仓库自身的 e2e 测试 e2e/watch-plugins/js/tests/index.js 展示了插件加载后可正常执行测试的最小验证形态。此外,Jest 主仓库的 jest.config.mjs 自己就在使用第三方插件jest-watch-typeaheadfilenametestname两个入口——这是社区插件在真实项目中的直接落地范例,可作为编写第三方插件的参考对象。

七、小结

Jest watch 插件系统由三块能力构成:生命周期钩子apply+shouldRunTestSuite/onTestRunComplete/onFileChange)、菜单集成getUsageInfo注册键位、run执行逻辑、updateConfigAndRun在受控白名单内更新配置并触发重跑)以及配置定制watchPlugins数组中的元组形式 + 构造函数config参数)。编写时注意三点:shouldRunTestSuite是「任一 false 即不运行」的与逻辑;调用了updateConfigAndRunrun不要 resolve 为真值;按键应避开c/i/q/u/w五个不可覆盖的保留键,并留意与第三方插件之间的键位重叠。

【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询