news 2026/9/7 19:03:28

Playwright 组件测试迁移指南:从 @playwright/experimental-ct-react / -vue 平滑升级到 Story Gallery 模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright 组件测试迁移指南:从 @playwright/experimental-ct-react / -vue 平滑升级到 Story Gallery 模式

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.htmlctViteConfigctPort等)的原因。
  • Story Gallery 模式:测试中的"场景"被改写为story 导出,在浏览器里原生运行——结构(用哪个组件、它的 children、providers)与行为(状态与回调,被记录进一个隐藏表单供测试断言)全部落在 story 内;纯数据型 props 通过mount(storyId, props)传入。该模式不再需要任何独立测试运行时:Story 就是组件源码旁的普通导出,Gallery 页面运行在应用自己的 dev server 上,测试仅用@playwright/test内置的mountfixture 驱动它。

二者对比如下:

维度旧 CTGallery 模式
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/afterMountgallery 的window.mount函数体(全局级),或 story decorator(单 story 级)
按测试变化的hooksConfigprops:mount('App/Routing', { route: '/dashboard' })——由 story/decorator 解释这些 props
Node 侧的routerfixture / MSW handlers测试里的page.route(),或 story/decorator 内启动 MSWsetupWorker
playwright/index.html(样式、字体、主题)gallery 的index.html/ 入口模块里的 imports
ctViteConfigctPortctTemplateDirctCacheDir全部废弃——gallery 运行在应用自己的 dev server 上;端口配置在webServer+baseURL;目录固定在playwright/gallery/
@playwright/experimental-ct-react导入defineConfig改从@playwright/test导入普通defineConfig,配baseURL= gallery 地址、serviceWorkers: 'block'reuseContext: true(详见 SKILL.md)

读这张表的要点是三个思维转换:

  1. 回调从"测试侧传入"变为"story 侧记录"。跨进程传闭包是旧 CT 的根源性复杂,Gallery 模式的约定是"一切组件需要的东西都在 story 里搭好,一切测试要断言的东西都通过页面可观测"。story 创建状态、提供回调、把状态写进隐藏表单,测试再做 web-first 断言。
  2. "结构"与"数据"分离。测试只负责"选哪个 story + 传什么纯数据 props",不再负责拼组件树。
  3. "参数化"让位给"导出化"。同一组件的不同形态优先各自成为一个命名导出,而不是通过参数在测试里展开——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/testimport { 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 模式下通过后,一次性执行清理:

  1. 从 Playwright 配置中删除 CT project
  2. 移除@playwright/experimental-ct-*依赖;
  3. 删除playwright/index.htmlplaywright/index.ts*playwright/.cache
  4. 解除 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 idcomponents/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 19:01:37

百度编辑器上传Word合同图片自动归档与分类的落地实践

做金融行业合同管理系统这几年&#xff0c;我几乎每天都要面对“百度编辑器批量上传WORD合同”这个场景。运营同事把签好字的合同Word拖到后台&#xff0c;点击粘贴&#xff0c;过一会儿后台图片目录就变成了一堆随机命名的文件&#xff0c;谁是哪份合同的哪一页&#xff0c;完…

作者头像 李华
网站建设 2026/9/7 18:59:53

单片机毕设项目:基于 STM32 或 51 单片机的步进电机驱动智能摇床控制系统设计 基于 STM32 或 51 单片机的分贝采集婴儿哭闹识别监护装置设计

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/7 18:59:18

基于ThinkPHP+Vue的中药仓库管理系统设计与实践

做药店中药仓库管理系统这件事&#xff0c;是我帮一个做医药流通的朋友处理库存管理需求时真正动起来的。当时他们还在用Excel记录几百种中药饮片的进销存&#xff0c;效期、批次、养护记录全靠人工翻台账&#xff0c;一到盘点就头大。我调研了一圈之后&#xff0c;定了thinkph…

作者头像 李华