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订阅者类型、UsageData(key+prompt)等核心类型; - JestHooks.ts 实现了钩子的订阅(subscriber)与触发(emitter)机制;
- BaseWatchPlugin.ts 提供抽象基类,内置
apply、getUsageInfo、onKey、run的空实现,方便快速继承。
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 分别用cjs、js、mjs、js-type-module四种模块形态验证了插件的加载。
二、Hooking into Jest:生命周期钩子
自定义 watch 插件可以通过apply方法向 Jest 注册钩子。即使插件不打算注册交互式菜单按键,也可以只使用钩子来观察或干预测试运行。
apply(jestHooks)
apply方法接收一个jestHooks参数,允许插件挂接到测试运行生命周期的具体环节:
class MyWatchPlugin { apply(jestHooks) {} }从 JestHooks.ts 的实现可以看到,JestHooks内部维护了onFileChange、onTestRunComplete、shouldRunTestSuite三组监听器数组: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; }); } }results是AggregatedResult类型(来自@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:每个项目包含config与testPaths数组。插件可以在文件变更后把项目清单缓存起来,供自定义菜单使用。
三、Watch Menu Integration:注册交互式菜单按键
除了生命周期钩子,插件还能通过「getUsageInfo返回按键/提示 +run执行按键逻辑」的方式为 watch 菜单增加或覆盖功能。
getUsageInfo(globalConfig)
实现该方法并返回一个key和prompt,即可向 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函数中:先输出内置项(c、a/f、o、p、t、q等),再通过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类型(额外还包含findRelatedTests、nonFlagArgs、mode与testPathPatterns数组形式);另一处是 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.ENTER、KEYS.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 个内置插件(FailedTestsInteractivePlugin、TestPathPatternPlugin、TestNamePatternPlugin、UpdateSnapshotsPlugin、UpdateSnapshotsInteractivePlugin、QuitPlugin),而RESERVED_KEY_PLUGINS为u、i、q等键登记了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是否存在——只有携带该消息的键(即u、i、q)绝对不可覆盖,而p、t等插件虽然内置,但可以被第三方插件覆盖。
六、实战:组合钩子与菜单的完整示例
把以上知识点组合起来,一个完整的 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-typeahead的filename与testname两个入口——这是社区插件在真实项目中的直接落地范例,可作为编写第三方插件的参考对象。
七、小结
Jest watch 插件系统由三块能力构成:生命周期钩子(apply+shouldRunTestSuite/onTestRunComplete/onFileChange)、菜单集成(getUsageInfo注册键位、run执行逻辑、updateConfigAndRun在受控白名单内更新配置并触发重跑)以及配置定制(watchPlugins数组中的元组形式 + 构造函数config参数)。编写时注意三点:shouldRunTestSuite是「任一 false 即不运行」的与逻辑;调用了updateConfigAndRun后run不要 resolve 为真值;按键应避开c/i/q/u/w五个不可覆盖的保留键,并留意与第三方插件之间的键位重叠。
【免费下载链接】jestDelightful JavaScript Testing.项目地址: https://gitcode.com/gh_mirrors/je/jest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考