React 19 Test Guardian 实战指南:从 act 导入修复到零失败测试套件
【免费下载链接】awesome-copilotCommunity-contributed instructions, agents, skills, and configurations to help you make the most of GitHub Copilot.项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-copilot
导读
react19-test-guardian是当前仓库中 React 19 迁移套件(react19-upgrade 插件)里负责测试文件迁移与验证的专项子代理。它的使命只有一条:把所有测试文件迁移到 React 19 兼容形态,并持续运行测试套件直到npm test报出0 失败。本文以 agents/react19-test-guardian.agent.md 为核心骨架,结合仓库中同套件的 react19-commander、react19-auditor、react19-migrator 以及 react19-test-patterns 技能 等源码证据,系统讲解测试迁移的 8 个标准改造点(T1–T8)、执行循环、错误分类表与完成门槛。读完本文,你将掌握一套可直接照做的"测试套件修复 + 验证"方法论,以及每条修复背后的 React 19 行为变更原理。
一、Agent 定位:迁移管线中的"守门员"
1.1 在 React 19 迁移管线中的角色
在 react19-commander.agent.md 定义的迁移管线中,整个 React 18 → React 19 升级被拆成四个串行阶段:
- Audit(审计)— 由
react19-auditor全库扫描破坏性变更与弃用模式,产出.github/react19-audit.md报告; - Deps(依赖手术)— 由
react19-dep-surgeon升级 react@19、@testing-library/react@16+ 并清零 peer 依赖冲突; - Migrate(源码迁移)— 由
react19-migrator重写所有非测试源码中的废弃 API; - Tests(测试修复与验证)— 由
react19-test-guardian修复全部测试文件,并把测试套件跑到全绿。
react19-test-guardian的元数据(frontmatter)写得很清楚:
name: react19-test-guardian description: 'Test suite fixer and verification specialist. Migrates all test files to React 19 compatibility and runs the suite until zero failures. Uses memory to track per-file fix progress and failure history. Does not stop until npm test reports 0 failures. Invoked as a subagent by react19-commander.' user-invocable: false关键信息:user-invocable: false表明它不面向用户直接调用,而是由react19-commander通过#tool:agent机制以子代理方式激活(见 react19-commander.agent.md 中 PHASE 4 的调用指令)。这保证了迁移管线的"串行 + 门禁"纪律——只有测试全绿,commander 才会把迁移状态推进到done。
1.2 与 React 18 Test Guardian 的对照
仓库中还存在 react18-test-guardian.agent.md,两者同为"测试守护者",但关注点不同:
| 维度 | React 18 Test Guardian | React 19 Test Guardian |
|---|---|---|
| 核心痛点 | Enzyme 无 React 18 支持、RTL v14userEvent异步化、自动批处理回归 | react-dom/test-utils被移除、StrictMode 不再双调用 effect、错误日志行为变化 |
| 关键 API | act()异步语义、waitFor | act从react导入、fireEvent替换Simulate |
| 相同点 | 都是"跑到 0 失败才停"、都用记忆协议断点续传 | 同左 |
这种"先升到 18.3.1,再升到 19"的两步走策略,在 plugins/react19-upgrade/README.md 的 Prerequisite 一节有明确说明。
二、记忆协议(Memory Protocol):断点续传的关键
测试修复往往跨多个会话。react19-test-guardian使用vscode/memory工具把进度持久化,保证会话中断后可以从上次位置继续:
1. 会话开始时读取既往状态:
#tool:memory read repository "react19-test-state"2. 每修复一个文件后写入检查点:
#tool:memory write repository "react19-test-state" "fixed:[filename]"3. 每完成一轮完整测试运行后记录失败数:
#tool:memory write repository "react19-test-state" "run-[N]:failures:[count]"该协议与同套件其他代理一脉相承:react19-commander用react19-migration-state记录phase/auditComplete/depsComplete/migrateComplete/testsComplete/reactVersion/failedTests/lastRun构成的 JSON 状态(见 react19-commander.agent.md);react19-auditor用react19-audit-progress记录各扫描阶段进度。整个套件因此具备"中断可恢复"能力——这是大规模迁移工程里极其实用的设计。
三、启动序列(Boot Sequence):先摸底,再动手
代理开工的第一步是两条命令:
# 1. 列出全部测试文件,掌握改造范围 find src/ \( -name "*.test.js" -o -name "*.test.jsx" -o -name "*.spec.js" -o -name "*.spec.jsx" \) | sort # 2. 基线运行——先跑一遍,捕获当前失败数量 npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | tail -30然后把基线失败数写入记忆:baseline: [N] failures。
这里的命令参数值得逐一理解:
--watchAll=false:关闭 Jest 监听模式,跑完即退出,适配 CI 与脚本场景;--passWithNoTests:仓库中暂时没有测试文件时也不报错,避免干扰迁移前期流程;--forceExit:强制进程退出,防止有异步句柄未关闭导致脚本挂起(迁移期测试本身不稳定时很实用)。
与 react19-auditor.agent.md 的 PHASE 4 相呼应,auditor 会预先用以下 grep 扫描出测试侧的雷区,test-guardian 启动后可直接对照react19-audit.md里"Test Files Requiring Changes"清单逐文件开工:
# act import 位置错误 grep -rn "from 'react-dom/test-utils'" src/ --include="*.test.*" --include="*.spec.*" # Simulate 使用(已移除) grep -rn "Simulate\." src/ --include="*.test.*" --include="*.spec.*" # react-test-renderer(已弃用) grep -rn "react-test-renderer" src/ --include="*.test.*" --include="*.spec.*" # 间谍调用次数断言(可能受 StrictMode 变化影响) grep -rn "toHaveBeenCalledTimes" src/ --include="*.test.*" --include="*.spec.*" | head -20四、测试迁移参考:8 个标准改造点(T1–T8)
T1:act() 导入修复 —— 最高优先级
变更事实:act不再从react-dom/test-utils导出,必须改为从react导入。
扫描:
grep -rn "from 'react-dom/test-utils'" src/ --include="*.test.*"改前 / 改后:
// Before import { act } from 'react-dom/test-utils' // After import { act } from 'react'在 skills/react19-test-patterns/SKILL.md 中,act导入被列为第一优先级,原因很直接:"fix first, it unblocks everything else"——act是 React 测试同步渲染的基石,导入位置不对,后面几乎所有交互类断言都会连锁失败。若原导入混有其他 test-utils 导出,需要拆分导入:
// Before(混合导入) import { act, Simulate, renderIntoDocument } from 'react-dom/test-utils'; // After(拆分导入) import { act } from 'react'; import { fireEvent, render } from '@testing-library/react';T2:Simulate → fireEvent
变更事实:Simulate已从react-dom/test-utils移除,交互触发统一改用 Testing Library 的fireEvent。
扫描:
grep -rn "Simulate\." src/ --include="*.test.*"改前 / 改后:
// Before import { Simulate } from 'react-dom/test-utils'; Simulate.click(element); Simulate.change(input, { target: { value: 'hello' } }); // After import { fireEvent } from '@testing-library/react'; fireEvent.click(element); fireEvent.change(input, { target: { value: 'hello' } });react19-test-patterns 补充了更多常见映射:Simulate.submit(form)→fireEvent.submit(form);Simulate.keyDown(element, { key: 'Enter', keyCode: 13 })→fireEvent.keyDown(element, { key: 'Enter', keyCode: 13 })。
T3:react-dom/test-utils 全量导出清理
React 19 移除了react-dom/test-utils的大部分导出,官方映射如下:
| 旧 API(react-dom/test-utils) | 新方案 |
|---|---|
act | import { act } from 'react' |
Simulate | fireEventfrom@testing-library/react |
renderIntoDocument | renderfrom@testing-library/react |
findRenderedDOMComponentWithTag | RTL 查询(getByRole、getByTestId等) |
scryRenderedDOMComponentsWithTag | RTL 查询(getAllByRole等) |
isElement、isCompositeComponent | 直接删除——RTL 下不再需要 |
react19-test-patterns 中还有两条补充映射:findRenderedDOMComponentWithClass→getByRole或container.querySelector;isDOMComponent→ 直接删除。
这里体现的是 RTL 的哲学:测试"行为与输出"而非"实现细节",旧的手工 DOM 断言大多可以被语义化查询替代。
T4:StrictMode 间谍调用次数更新
行为变更事实:React 19 的 StrictMode 在开发环境下不再双调用 effect。
- React 18:StrictMode dev 下 effect 执行两次 → 相关 spy 被调用 ×2 / ×4;
- React 19:effect 只执行一次 → spy 调用 ×1 / ×2。
核心策略:跑测试、读实际次数、按实际值更新断言,绝不靠猜。文档明确要求用如下命令从失败信息中提取真实计数:
# 只跑失败的那个测试,抓取实际计数 npm test -- --watchAll=false --testPathPattern="ComponentName" --forceExit 2>&1 | grep -E "Expected|Received|toHaveBeenCalled"react19-test-patterns 给出了精细化的判断口径,值得注意:
// effect 类调用:React 18 双调用 ×2 → React 19 单次 ×1 expect(mockFn).toHaveBeenCalledTimes(2); // 改为 1 // 渲染阶段调用(组件函数体)在 React 19 StrictMode 下仍然双调用: expect(renderSpy).toHaveBeenCalledTimes(2); // 保持 2 不变也就是说:并不是所有 spy 计数都减半,只有 effect 相关的调用才受"不再双调用"影响,渲染函数体依旧双调用。这正是文档强调"measure, don't guess"的原因。
T5:测试中的 useRef 形态
React 19 中未初始化useRef()的current初值从undefined变为null。凡是在测试里直接构造 ref 或断言 ref 形态的代码都需要同步:
// Before const ref = { current: undefined }; // After const ref = { current: null };T6:自定义 render 助手校验
很多项目会封装test-utils.js/renderWithProviders/customRender之类的渲染助手。React 19 下必须确认这些助手基于 RTL 的render实现(其内部使用createRoot),而不是过时的ReactDOM.render。
定位:
find src/ -name "test-utils.js" -o -name "renderWithProviders*" -o -name "custom-render*" 2>/dev/null grep -rn "customRender\|renderWith" src/ --include="*.js" | head -10若发现旧实现,用 RTL 的render+wrapper改写。这一步"每个代码库只需校验一次,而不是每个测试文件各查一遍",性价比极高(该要点同样出现在 react19-test-patterns 的优先级顺序第 6 条)。
T7:错误边界(Error Boundary)测试更新
行为变更事实:React 19 改变了错误日志行为——错误不再被重复上报。
// Before(React 18):console.error 被调用两次(React 记录 + 重新抛出) expect(console.error).toHaveBeenCalledTimes(2); // After(React 19):只调用一次 expect(console.error).toHaveBeenCalledTimes(1);扫描:
grep -rn "ErrorBoundary\|console\.error" src/ --include="*.test.*"T8:异步 act() 包裹
若运行测试时看到Warning: An update to X inside a test was not wrapped in act(...),说明存在异步状态更新未包裹进act。React 19 对act纪律的要求与 React 18 一脉相承,交互触发后应立即包裹:
// Before fireEvent.click(button); expect(screen.getByText('loaded')).toBeInTheDocument(); // After await act(async () => { fireEvent.click(button); }); expect(screen.getByText('loaded')).toBeInTheDocument();五、执行循环(Execution Loop):批次修复 + 单文件确认
5.1 Round 1:按审计报告逐文件修复
第一轮严格依据.github/react19-audit.md中"Test Files Requiring Changes"清单,对每个文件套用 T1–T8 对应改造,每完成一个文件写入记忆检查点(fixed:[filename])。
5.2 批次后整体回归
每完成一批,跑一次完整套件并只抓汇总行,快速判断进展:
npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | grep -E "Tests:|Test Suites:|FAIL" | tail -155.3 Round 2+:逐个击破剩余失败
对每个 FAIL 文件执行标准化五步:
打开失败测试文件;
读取确切错误信息;
应用对应修复;
只重跑该文件确认修复生效:
npm test -- --watchAll=false --testPathPattern="FailingFile" --forceExit 2>&1 | tail -20写入记忆检查点。
循环往复,直到FAIL行数为零。
这种"整批跑 + 单文件验"的双循环节奏,与 react19-commander.agent.md 中"子代理说完成不算数,必须用命令验证门禁"的规则完全一致。
六、错误分类表(Error Triage Table):一线排障速查
文档给出了 7 种高频错误的即查即用对照表:
| 错误 | 原因 | 修复 |
|---|---|---|
act is not a function | 导入位置错误 | import { act } from 'react' |
Simulate is not defined | 导出被移除 | 替换为fireEvent |
Expected N received M(调用次数) | StrictMode 行为差异 | 跑测试,用实际次数更新 |
Cannot find module react-dom/test-utils | 包已被掏空 | 切换全部相关导入 |
cannot read .current of undefined | useRef()形态变化 | 初始值设为null |
not wrapped in act(...) | 异步状态更新 | 包裹进await act(async () => {...}) |
Warning: ReactDOM.render is no longer supported | 设置/助手中残留旧渲染 API | 改为createRoot |
最后一条虽然报错出现在测试运行期,根源往往是源码或测试助手中残留ReactDOM.render——这在 react19-migrator.agent.md 的 M1 中已有标准改写(import { createRoot } from 'react-dom/client'+root.render(<App />))。若测试侧命中此错误,应回头检查第 T6 步的渲染助手是否已更新。
七、完成门槛(Completion Gate):什么才算"真正完成"
7.1 最终验证命令
echo "=== FINAL TEST SUITE RUN ===" npm test -- --watchAll=false --passWithNoTests --forceExit --verbose 2>&1 | tail -30 # 抽取结果行 npm test -- --watchAll=false --passWithNoTests --forceExit 2>&1 | grep -E "^Tests:"并向记忆写入最终状态:
#tool:memory write repository "react19-test-state" "complete:0-failures:all-tests-green"7.2 返回 commander 的硬性条件
只有同时满足以下全部条件才允许向上汇报:
Tests: X passed, X total,失败数为 0;- 没有删除任何测试(删除 = 隐藏问题,不是修复);
- 没有新增任何
.skip测试; - 既有的
.skip测试必须按名字逐一记录在案。
7.3 三次修复失败的处理
若某个测试连续尝试 3 次仍无法修复,规则是:不静默跳过,而是把该测试写入.github/react19-audit.md的 "Blocked Tests" 章节,注明导致问题的具体 React 19 行为变更,并把这份清单交还 commander 决策。这与 react18-test-guardian.agent.md 末尾"失败必须上报数量与组件名,不得悄悄略过"的纪律一脉相承——整个套件对"假装完成"是零容忍的。
八、从源码证据看:为什么这 8 个改造点缺一不可
把 test-guardian 的修复项与套件内其他文档对照,可以看到每条修复背后都有 React 19 真实变更支撑,绝非凭空设计:
- 移除类变更(T1/T2/T3):
react-dom/test-utils的导出在 React 19 中被大量移除,auditor 的 PHASE 2 明确把from 'react-dom/test-utils'列为"破坏性、必须修复"(见 react19-auditor.agent.md); - 行为类变更(T4/T7):StrictMode 不再双调用 effect、错误日志只记录一次,都会直接冲击
toHaveBeenCalledTimes断言,而 plugins/react19-upgrade/README.md 的 "Behavioral Changes" 章节对这两条有明文确认; - 形态类变更(T5):
useRef初值变化与源码迁移的 M9(useRef()→useRef(null))互为表里(见 react19-migrator.agent.md); - 环境类变更(T6/T8):测试助手的渲染底座必须切换到基于
createRoot的 RTL v16+,异步更新必须遵守act纪律。
换言之,test-guardian 的 T1–T8 覆盖了 React 19 对测试代码的全部已知冲击面:导入路径、交互 API、调用计数、ref 形态、渲染底座、错误日志、异步纪律。按此清单执行,配合记忆协议断点续传与"零失败才放行"的门禁,即可把 React 19 升级的最后一道关卡——测试全绿——稳妥拿下。
九、快速上手路径
若你正面临 React 18 → React 19 升级,可以直接以本仓库的 react19-upgrade 插件为参考模板(详见 plugins/react19-upgrade/README.md):
- 按顺序激活管线:
react19-commander会依次调度 auditor → dep-surgeon → migrator → test-guardian,任何阶段不达门禁不前进; - 让 test-guardian 接管测试侧:把 T1–T3 的导入清理放在最前(它们是其他一切修复的前提),T4 务必"实测计数而非猜测",T6 只需全库校验一次;
- 用记忆协议保底:任何一步中断,下个会话都能从检查点续跑,不会重复劳动。
整套方法不仅适用于 React 19,其"批次修复 + 单文件验证 + 错误分类速查 + 零失败门禁"的工程范式,对任何大型框架升级中的测试迁移都同样有效。
【免费下载链接】awesome-copilotCommunity-contributed instructions, agents, skills, and configurations to help you make the most of GitHub Copilot.项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-copilot
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考