react-spectrum test-utils RC API 迁移指南:test-utils-rc-update Codemod 深度解析
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
在 react-spectrum 仓库中,@react-aria/test-utils与@react-spectrum/test-utils的 tester API 从 beta 升级到 RC 时发生了大规模重命名:属性式访问器变成了带get前缀的方法,部分参数键也随之统一。这篇指南以仓库中官方提供的迁移工具test-utils-rc-updatecodemod 为主体,结合其完整源码实现,讲解它的重命名规则全集、命令行用法与选项,以及它如何安全地识别 tester 变量并完成多轮 AST 改写——读完后你可以直接用它批量升级测试文件,并理解每一步改写的底层原理。
一、这个 codemod 解决什么问题
@react-spectrum/codemods是 react-spectrum 仓库自带的迁移工具包(package.json),它基于 jscodeshift 构建,随包发布一个可执行 CLI(bin: dist/index.js),内置多个 codemod,其中test-utils-rc-update专用于把使用旧版 beta tester API 的测试文件改写为 RC 版本 API(见 README)。
官方 README 给出的核心变更示例如下:
- tester.listbox; + tester.getListbox(); - tester.options(); + tester.getOptions(); - await tester.selectOption({option: 'Save'}); + await tester.toggleOptionSelection({option: 'Save'}); - tester.findOption({optionIndexOrText: 'Save'}); + tester.findOption({indexOrText: 'Save'});可以归纳为三类变更:
- 属性访问改为方法调用:
tester.listbox这类属性(getter)统一改为tester.getListbox()形式的方法调用; - 方法重命名:如
options()→getOptions()、selectOption()→toggleOptionSelection(); - 查找方法的参数键统一:
findOption({optionIndexOrText: ...})、findRow({rowIndexOrText: ...})等按组件名区分的参数键,统一改为indexOrText。
该 codemod 覆盖全部 tester 类型:ListBox、ComboBox、Select、Menu、Table、GridList、Tree、Tabs、RadioGroup、CheckboxGroup和Dialog。
二、完整重命名映射表
README 只列了部分示例,而 codemod.ts 中定义的RENAME_MAP(L5-L37)才是规则全集。下表即 beta → RC 的完整 getter/方法映射:
| 组件 | beta 写法 | RC 写法 |
|---|---|---|
| CheckboxGroup | checkboxgroup/checkboxes/selectedCheckboxes | getCheckboxGroup()/getCheckboxes()/getSelectedCheckboxes() |
| ComboBox | combobox/trigger/listbox/sections/options()/focusedOption | getCombobox()/getTrigger()/getListbox()/getSections()/getOptions()/getFocusedOption() |
| Dialog | trigger/dialog | getTrigger()/getDialog() |
| GridList | gridlist/rows/selectedRows/cells() | getGridlist()/getRows()/getSelectedRows()/getCells() |
| ListBox | listbox/selectedOptions/sections/options() | getListbox()/getSelectedOptions()/getSections()/getOptions() |
| Menu | trigger/menu/submenuTriggers | getTrigger()/getMenu()/getSubmenuTriggers() |
| RadioGroup | radiogroup/radios/selectedRadio | getRadioGroup()/getRadios()/getSelectedRadio() |
| Select | trigger/listbox/sections/options() | getTrigger()/getListbox()/getSections()/getOptions() |
| Table | table/rowGroups/columns/rows/selectedRows/rowHeaders/cells() | getTable()/getRowGroups()/getColumns()/getRows()/getSelectedRows()/getRowHeaders()/getCells() |
| Tabs | tablist/tabs/tabpanels/selectedTab/activeTabpanel | getTablist()/getTabs()/getTabpanels()/getSelectedTab()/getActiveTabpanel() |
| Tree | tree/rows/selectedRows/cells() | getTree()/getRows()/getSelectedRows()/getCells() |
| 通用 | selectOption() | toggleOptionSelection() |
find*系列的参数键映射则由FIND_PARAM_KEY_MAP(codemod.ts L39-L45)定义,将各组件专属键统一为indexOrText:
| find 方法 | 旧参数键 | 新参数键 |
|---|---|---|
findCheckbox | checkboxIndexOrText | indexOrText |
findOption | optionIndexOrText | indexOrText |
findRow | rowIndexOrText | indexOrText |
findRadio | radioIndexOrText | indexOrText |
findTab | tabIndexOrText | indexOrText |
这些参数名可以直接在@react-aria/test-utils的 RC 源码中验证:例如 listbox.ts 的findOption(opts: {indexOrText: number | string})、combobox.ts、gridlist.ts、checkboxgroup.ts,签名均为indexOrText: number | string,支持传索引(数字)或文本(字符串)定位元素。
三、使用方法与命令行选项
在你的项目目录(即需要更新测试文件的目录)中运行:
npx @react-spectrum/codemods test-utils-rc-updateREADME 文档列出的选项如下:
--ignore-pattern:要忽略文件的 glob 模式,默认值为**/node_modules/**;--dry:试运行模式,只报告变更、不实际修改文件;--path:要运行 codemod 的目录路径,默认为当前目录。
从 CLI 入口 src/index.ts 可以看到更完整的实际行为:
- 选项定义中还注册了
--dry的短选项-d,以及--parser、--components、--agent(后两者主要服务于 s1-to-s2 codemod); - 所有 codemod 强制注入默认值:
parser: 'tsx'、ignorePattern: '**/node_modules/**'、path: '.',以及扫描扩展名extensions: 'js,jsx,mjs,cjs,ts,tsx'——也就是说 JavaScript 与 TypeScript 测试文件都会被处理,命令行传入的值会覆盖这些默认值; - CLI 通过
node:util的parseArgs解析参数,第一个位置参数为 codemod 名称(s1-to-s2、use-monopackages、use-subpaths、test-utils-rc-update),传入未知名称会报错并打印可用列表(index.ts L107-L124); - 运行环境要求 Node.js >= 22.14.0(见 package.json 的
engines字段)。
推荐的稳妥流程是先跑一次 dry run 检查命中范围,确认无误后再正式执行:
npx @react-spectrum/codemods test-utils-rc-update --path ./src --dry npx @react-spectrum/codemods test-utils-rc-update --path ./src四、源码级原理:tester 变量识别与多轮 AST 改写
理解这个 codemod 的实现(codemod.ts)有助于判断它的适用边界。整体流程分为“识别 tester 变量”和“四轮改写”两个阶段。
4.1 解析器配置:保留格式并容忍语法
transformer 入口(L50-L83)通过api.jscodeshift.withParser自定义了解析器:用 recast 包裹@babel/parser,开启jsx、typescript、topLevelAwait、optionalChaining、nullishCoalescingOperator等 14 个插件,并设置tokens: true与errorRecovery: true。这意味着它能完整解析 TypeScript 测试文件中常见的await、可选链、非空断言等语法,且借助 recast 尽量保留未改动代码的原始格式。
4.2 初始识别:只有 createTester 产物才会被改写
第一遍扫描(L102-L119)找出所有形如const tester = createTester(...)或const tester = await xxx.createTester(...)(含user.createTester('Menu', {root: el})这类成员调用写法)的变量声明,并把变量名收集进testerVarNames集合。unwrapAwait辅助函数(L87-L92)会先剥掉AwaitExpression和TSNonNullExpression包装层再判断调用。
关键安全边界:如果文件中一个 tester 变量都没找到,直接原样返回源文件(L121-L123)。因此someOtherObject.rows、config.options()这类与 tester 无关的同名属性不会被误伤——这一保证由 codemod.test.ts 中 “does not rename accessors on non-tester variables” 用例专门验证。
4.3 传播跟踪:openSubmenu 返回的新 tester
Menu tester 的openSubmenu()会返回一个新的子菜单 tester,代码中常见这样的写法:
const menuTester = user.createTester('Menu', {root: el}); let submenuUtil = (await menuTester.openSubmenu({submenuTrigger: 'Share…'}))!; expect(submenuUtil.options()).toHaveLength(2);为了让submenuUtil也被纳入改写范围,codemod 用TESTER_RETURNING_METHODS(L48,目前只含openSubmenu)标记“返回 tester 的方法”,然后做定点迭代传播:每轮扫描变量声明,若某变量被赋值为已知 tester 调用openSubmenu的结果,就加入集合并置位继续下一轮,直到没有新增(L125-L149)。这能正确处理嵌套子菜单的多级传播(见测试用例 “handles variables assigned from openSubmenu and optional chaining on them”,codemod.test.ts L311-L333,其中nestedSubmenu由submenuTester?.openSubmenu(...)得来,其上的?.menu、?.trigger、?.options()全部被正确改写)。
4.4 四轮改写:覆盖普通调用、属性访问与可选链
改写阶段由四个 pass 组成,renamePropInCallee(L153-L168)是共享的核心逻辑——校验调用者对象是否为已识别的 tester 变量,再查RENAME_MAP替换方法名:
- Pass 1(L170-L178):改写方法调用
tester.options()→tester.getOptions();Pass 1B(L180-L185):同理处理可选调用tester?.options()→tester?.getOptions(); - Pass 2(L187-L209):把纯属性访问改写为方法调用,
tester.rows→tester.getRows()。此处有一个重要的防重逻辑(L201-L204):若该属性访问正是某个调用的 callee(即tester.options后面紧跟()),则跳过,避免与 Pass 1 重复改写;Pass 2B(L211-L237):处理tester?.rows这类可选成员访问,生成tester?.getRows()可选调用形式,并同样跳过作为 callee 的场景; - Pass 3(L239-L271):改写
find*调用的第一个对象参数中的键名,{optionIndexOrText: x}→{indexOrText: x}。仅当第一个参数是对象字面量(ObjectExpression)时才处理,键通过Identifier精确匹配,值不做改动。
最后(L273)仅在有实际变更(didChange)时用单引号风格输出新源码,否则原样返回——对未命中的文件零 diff 副作用。
4.5 测试用例矩阵
codemod.test.ts 基于 jscodeshift 的defineInlineTest建立了完整的“输入/输出”快照测试,覆盖了全部 11 种 tester 类型以及若干边界场景:
- 每个 tester 类型一个用例(checkboxgroup、combobox、dialog、gridlist、listbox、menu、radiogroup、select、table、tabs、tree),同时覆盖 getter 重命名、
selectOption→toggleOptionSelection和find*参数键改名; - 内联使用(L261-L277):
expect(tester.tree)、expect(tester.rows.length)这类不用中间变量的写法也能改写; - 可选链(L279-L293):
tester?.menu、tester?.submenuTriggers[0]正确生成tester?.getMenu()、tester?.getSubmenuTriggers()[0]; - 非 tester 变量不误伤(L243-L259):与第 4.2 节的安全边界对应;
- openSubmenu 传播 + 非空断言 + 可选链组合(L295-L333)。
五、升级后的测试代码长什么样
以 Menu 为例,codemod 之前的测试与之后(与 codemod.test.ts L109-L129 用例一致):
// 之前(beta API) const tester = user.createTester('Menu', {root: el}); tester.trigger; tester.menu; tester.sections; tester.options(); tester.submenuTriggers; await tester.selectOption({option: 'Bar', menuSelectionMode: 'single'}); // 之后(RC API,codemod 自动生成) const tester = user.createTester('Menu', {root: el}); tester.getTrigger(); tester.getMenu(); tester.getSections(); tester.getOptions(); tester.getSubmenuTriggers(); await tester.toggleOptionSelection({option: 'Bar', menuSelectionMode: 'single'});注意toggleOptionSelection的其余参数(如menuSelectionMode)完全不受影响,codemod 只改名不动参数。
六、适用前提与注意事项
- 适用前提:测试文件必须通过
createTester(...)(含user.createTester(...)成员形式)创建 tester 变量。codemod 从源码结构看依赖这一模式做变量识别,若你封装了自己的 helper 间接返回 tester 且封装点不在被扫描范围内,链路上的新变量可能不会被传播跟踪(传播仅针对openSubmenu)。 - 扫描范围:默认扫描
js,jsx,mjs,cjs,ts,tsx五种扩展名,忽略**/node_modules/**,可用--ignore-pattern与--path调整; - 试运行优先:大仓库升级建议先
--dry(或-d)观察报告,diff 确认后落盘; - 环境要求:Node.js >= 22.14.0(见 codemods package.json);
- 运行方式:
npx @react-spectrum/codemods test-utils-rc-update在你的业务项目中执行,它修改的是你的项目文件而非本仓库;本仓库内该 codemod 的源码位于 packages/dev/codemods/src/test-utils-rc-update/,可直接阅读实现与测试以核对行为。
完成 codemod 运行后,建议再全局搜索一遍旧键名(如optionIndexOrText、selectOption)确认没有遗漏在动态构造或字符串拼接里的写法,然后运行测试套件验证改写结果。
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考