Playwright 组件测试迁移指南:从 @playwright/experimental-ct-react / -vue 平滑升级到 Story Gallery 模式
【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright
Playwright 自 1.63 起移除了@playwright/experimental-ct-react/@playwright/experimental-ct-vue等旧组件测试(CT)运行时,组件测试改由"Story Gallery(故事画廊)"模式承接:被测组件以 story 导出形式运行在真实浏览器中,测试则退化为普通的@playwright/test端到端用例。本文基于 migration.md 展开,面向正在使用旧 CT 包的项目,提供从概念映射、分步迁移到完整代码对照的实操方案,读完即可按图索骥把最后一个 CT spec 平滑移植到 Gallery 模式下。
为什么需要迁移:两种组件测试架构的本质差异
理解迁移动机要先看清旧 CT 与新 Gallery 模式的架构分界:
- 旧 CT(
@playwright/experimental-ct-*):测试文件中的 JSX 在 Node 进程里被编译,再把组件结构"marshalling(序列化搬运)"进浏览器渲染。这意味着组件树、children、回调闭包都需要跨越进程边界传递,这也是它必须自带一套独立运行时(playwright/index.html、ctViteConfig、ctPort等)的原因。 - Story Gallery 模式:测试中的"场景"被改写为story 导出,在浏览器里原生运行——结构(用哪个组件、它的 children、providers)与行为(状态与回调,被记录进一个隐藏表单供测试断言)全部落在 story 内;纯数据型 props 通过
mount(storyId, props)传入。该模式不再需要任何独立测试运行时:Story 就是组件源码旁的普通导出,Gallery 页面运行在应用自己的 dev server 上,测试仅用@playwright/test内置的mountfixture 驱动它。
二者对比如下:
| 维度 | 旧 CT | Gallery 模式 |
|---|---|---|
| JSX 编译位置 | Node 进程内编译后 marshall 进浏览器 | story 在浏览器内原生执行 |
| 结构 + 行为 | 由测试文件内联 JSX 提供 | 由 story 导出承载(组件、children、providers + 状态/回调) |
| 运行时 | 独立 CT 运行时(playwright/index.ts、.cache等) | 应用自身的 dev server + 一个 gallery 页面 |
| 测试形态 | 专用mountfixture 的特殊测试 | 普通@playwright/teste2e 测试 |
版本前提:请在 1.62 完成迁移
文档明确提示:CT 相关包已在 Playwright 1.63 被移除且不再发布,因此迁移窗口是"钉在 Playwright 1.62 上逐个移植 spec,移植完成后一次性解除版本锁定并升级"。本仓库当前的 packages/playwright-core/package.json 版本号为1.64.0-next,仓库根目录packages/下也确已不再包含任何@playwright/experimental-ct-*实现——这从侧面印证了 1.63 已是"过去式",停留在旧 CT 上的项目只能主动迁移,不能指望依赖升级。
核心概念映射表:把每条 CT 用法翻译成 Gallery 用法
迁移的本质是逐条翻译。文档给出的映射表是移植工作的"总纲",这里完整保留并逐条解读:
@playwright/experimental-ct-*用法 | Gallery 模式对应写法 |
|---|---|
mount(<Button title="…" onClick={spy} />) | 有状态 story:story 提供onClick,把效果记录进一个隐藏表单 input;测试用toHaveValue()断言 |
| 测试里传入的纯数据 props | 语义不变:mount(id, props) |
| 测试里传入的 JSX children / 插槽 | 无法跨进程边界——每种组合各烘焙成一个 story 导出(Vue 中插槽密集的场景推荐.story.vue) |
component.update(<Button count={2} />) | component.update({ count: 2 })——保留状态的重渲染,需要 gallery 复用其根实例(见 gallery-spec.md) |
component.unmount() | component.unmount()——由 gallery 的window.unmount()支撑 |
playwright/index.ts中的beforeMount/afterMount | gallery 的window.mount函数体(全局级),或 story decorator(单 story 级) |
按测试变化的hooksConfig | props:mount('App/Routing', { route: '/dashboard' })——由 story/decorator 解释这些 props |
Node 侧的routerfixture / MSW handlers | 测试里的page.route(),或 story/decorator 内启动 MSWsetupWorker |
playwright/index.html(样式、字体、主题) | gallery 的index.html/ 入口模块里的 imports |
ctViteConfig、ctPort、ctTemplateDir、ctCacheDir | 全部废弃——gallery 运行在应用自己的 dev server 上;端口配置在webServer+baseURL;目录固定在playwright/gallery/ |
从@playwright/experimental-ct-react导入defineConfig | 改从@playwright/test导入普通defineConfig,配baseURL= gallery 地址、serviceWorkers: 'block'、reuseContext: true(详见 SKILL.md) |
读这张表的要点是三个思维转换:
- 回调从"测试侧传入"变为"story 侧记录"。跨进程传闭包是旧 CT 的根源性复杂,Gallery 模式的约定是"一切组件需要的东西都在 story 里搭好,一切测试要断言的东西都通过页面可观测"。story 创建状态、提供回调、把状态写进隐藏表单,测试再做 web-first 断言。
- "结构"与"数据"分离。测试只负责"选哪个 story + 传什么纯数据 props",不再负责拼组件树。
- "参数化"让位给"导出化"。同一组件的不同形态优先各自成为一个命名导出,而不是通过参数在测试里展开——story 本身是可检索、可评审的组件状态文档。
迁移五步流程
第 1 步:按 SKILL.md 搭好 gallery 与配置,旧项目并行保留
先搭建新底座、保持旧 CT 项目继续运行,直到最后一个 spec 移植完毕。搭建工作完全遵循 SKILL.md:在<project>/playwright/gallery/下实现 gallery 页面(暴露出window.mount/window.unmount),story 文件与组件源码同目录,测试目录放普通 Playwright spec。
若应用本身跑在 Vite 上,gallery 由现有 dev server 直接服务;其他场景(Next.js、webpack 或无 dev server)则起一个独立小 dev server 服务 gallery 页面。playwright.config.ts参考配置如下(有既有配置时是合并而非覆盖):
projects: [ { name: 'components', testDir: './tests/components', use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', // 防止应用自身的 service worker 用缓存响应遮蔽 page.route() mock reuseContext: true, // worker 内复用 browser context,大幅加速组件套件 }, }, ], webServer: { command: 'npm run dev', // 或: npx vite --config playwright/vite.config.ts url: 'http://localhost:5173/playwright/gallery/index.html', // 独立 server 则指向其端口 reuseExistingServer: !process.env.CI, },其中两个关键选项:mount会导航到baseURL,因此它必须指向 gallery 地址;reuseContext: true复刻了旧 CT 运行时按 worker 复用 context 的行为,是组件套件的大幅提速来源(在 packages/playwright/src/index.ts 中它作为 worker 作用域的选项与PW_TEST_REUSE_CONTEXT环境变量一起生效)。
第 2 步:逐个拆分 CT spec 中的mount(<…/>)调用
对每个 CT spec,把每个mount(<…/>)拆成两部分:
- JSX 结构 → 组件旁的 story 导出。providers、mock 数据、状态、回调都焊进 story;
- 纯数据 props → 留在测试里,作为
mount的第二个参数。
回调 spy 转成"story 状态 + 隐藏表单记录";仅变化数据 props 的调用点通常只需要一个把 props 透传下去的通用 story:
export const Default = (props: ButtonProps) => <Button title="Submit" {...props} />;这样不同测试对同一 story 传入不同 props 即可覆盖不同数据面,story 文件本身保持极薄。
第 3 步:重写 spec 的 API 调用面
重写时遵循三条机械替换规则:
- 导入改自
@playwright/test:import { test, expect } from '@playwright/test';; mount(<X a={1}/>)→mount('X/Default', { a: 1 });update(<X a={2}/>)→update({ a: 2 });unmount()保持不变。
注意返回值的语义变化:mount返回的是gallery 根节点(#root)的 locator,因此查询必须从它作用域化出发——component.getByRole('button').click(),而不是对根节点整体click()。spy 断言改写为对 story 记录状态的toHaveValue()断言。
第 4 步:移植beforeMount钩子
beforeMount/afterMount的归宿分两层:
- 应用级全局初始化(providers、store、插件等)→ 放进 gallery 的
window.mount函数体。gallery-spec 明确说明:window.mount本身就是"你的 setup/teardown 钩子",没有独立的钩子注册表,你拥有的这个函数就是钩子——在渲染前装 providers/种子数据/启动浏览器内 mock server,渲染后做收尾,全部按测试传入的story/props分支; - 按测试变化的
hooksConfig分支→ 变为 props,由 story 或 decorator 解释执行,例如mount('App/Routing', { route: '/dashboard' })。
第 5 步:全绿后清理旧 CT 残留
当所有 spec 在 Gallery 模式下通过后,一次性执行清理:
- 从 Playwright 配置中删除 CT project;
- 移除
@playwright/experimental-ct-*依赖; - 删除
playwright/index.html、playwright/index.ts*与playwright/.cache; - 解除 Playwright 版本锁定并升级。
Before / After 完整代码对照
以"点击按钮并收集回调数据"这一最典型的旧 CT 场景为例,完整对照如下。
迁移前(CT spec):
// Before (CT) import { test, expect } from '@playwright/experimental-ct-react'; import Button from '../src/components/Button'; test('click', async ({ mount }) => { const messages: string[] = []; const component = await mount(<Button title="Submit" onClick={data => messages.push(data)} />); await component.click(); expect(messages).toEqual(['hello']); });迁移后——story 文件(与组件同目录):
// After: src/components/Button.story.tsx import Button from './Button'; export const Default = (props: { onClick?: (data: string) => void }) => <Button title="Submit" {...props} />;迁移后——spec 文件:
// After: src/components/Button.spec.ts import { test, expect } from '@playwright/test'; test('click', async ({ mount }) => { const messages: string[] = []; const component = await mount('components/Button/Default', { onClick: (data: string) => messages.push(data) }); await component.click(); expect(messages).toEqual(['hello']); });对照要点:
- 测试不再 import 组件本身,改 import
@playwright/test;组件树彻底从测试文件中消失; - story id
components/Button/Default由"src/下路径去掉.story.*后缀 + 导出名"推导而来; - 组件导入、props 类型与渲染都收敛到了 story 文件内,浏览器端只有 story 需要知道组件的一切。
对回调处理,这里展示的是"props 透传"形态;文档在概念映射中更推荐另一种更稳健的形态——有状态 story:story 内部用useState持有状态、提供回调、把结果写入带data-testid的隐藏输入框,测试断言toHaveValue('true')之类的值。完整可运行的例子可直接参考模板文件 templates/react/Button.story.tsx 与 templates/react/button.spec.ts,其中CountsClicksstory 就是标准写法:
export const CountsClicks = () => { const [clicks, setClicks] = useState(0); return <> <Button title="Submit" onClick={() => setClicks(count => count + 1)} /> <form hidden><input contenteditable="false">【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
项目地址: https://gitcode.com/GitHub_Trending/pl/playwright创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考