首页
/ 用 Cypress 复用 Storybook Story:基于 iframe.html 的组件端到端测试实战指南

用 Cypress 复用 Storybook Story:基于 iframe.html 的组件端到端测试实战指南

2026-09-07 15:32:21作者:裘晴惠Vivianne

本篇技术指南聚焦 Storybook 官方测试体系中的「Stories in end-to-end tests」主题,以仓库内 component-cypress-test.md 代码片段为骨架,讲解如何把 CSF 中定义的 Story 直接复用到 Cypress 测试中,对运行在 Storybook 隔离 iframe 里的组件做真实 DOM 断言。读完本文,你将理解 Storybook iframe.html 预览页的加载机制、story id(如 components-login-form--example)的生成规则,并掌握一套可直接落地到登录表单等场景的 Cypress + Storybook 端到端测试写法。

背景:CSF 让 Story 天然成为 E2E 测试素材

端到端测试(E2E)要求在完整运行环境中模拟真实用户行为。Cypress 是典型的端到端测试框架,但它的用例对象可以是整站,也可以是「跑在隔离环境里的单个组件」——这正是 Storybook 的用武之地。

在 Storybook 中,一个 CSF 文件的每个命名导出(named export)即一个 Story。官方文档 stories-in-end-to-end-tests.mdx 明确指出:借助 Component Story Format(CSF),开发者可以编写模拟用户交互、验证单个组件行为的测试用例,从而在 Storybook 环境中对组件的功能、响应式与视觉表现进行跨场景验证。也就是说,同一个 LoginForm.stories.ts 里写好的 Story,既能用于 Storybook 浏览,也能被 Cypress 直接渲染并断言,实现「一次编写、多处复用」。

本文关联文档的出处

本文讲解的核心代码片段 component-cypress-test.md 是一个被文档引擎动态嵌入的 snippet 文件,它的宿主页面是官方 E2E 测试指南中的 With Cypress 小节,相关配套片段还包括:

Cypress 测试用例逐行剖析

下面是从原文档继承的完整 Cypress 测试文件(文件落在 /cypress/integration/Login.spec.js,对应经典 Cypress 布局):

/// <reference types="cypress" />

describe('Login Form', () => {
  it('Should contain valid login information', () => {
    cy.visit('/iframe.html?id=components-login-form--example');
    cy.get('#login-form').within(() => {
      cy.log('**enter the email**');
      cy.get('#email').should('have.value', 'email@provider.com');
      cy.log('**enter password**');
      cy.get('#password').should('have.value', 'a-random-password');
    });
  });
});

逐部分拆解如下。

1. 类型提示声明

/// <reference types="cypress" />

/// 三斜线指令将 Cypress 的类型声明引入该 JS 文件,让 cy 全局对象获得完整的 IntelliSense 与类型检查支持。当你的 Cypress 工程使用较新版本并默认采用 cypress/e2e 目录时,只要把该 spec 放入与 cypress 配置一致的位置并保持同样的类型声明即可。

2. 用例骨架

describe('Login Form', () => {
  it('Should contain valid login information', () => {
    ...
  });
});

外层 describe 按「登录表单」归组,it 描述断言意图是「表单应包含有效的登录信息」。测试对象不是页面而是组件,这正是 Storybook 与 Cypress 结合的典型形态。

3. 访问 Storybook 的隔离 iframe

cy.visit('/iframe.html?id=components-login-form--example');

这是整段测试的关键一行:Cypress 直接访问 Storybook 预览应用提供的 iframe.html,并通过 id 查询参数定位到某个具体的 Story。

  • iframe.html 是 Storybook 渲染 Story 的隔离 iframe 页面(preview iframe),它不包含 Manager 侧的工具栏/面板,只承载组件本身,因此非常适合被外部测试框架直接驱动。这一机制在源码中有大量佐证,例如 Iframe.stories.tsx 中就以 src="/iframe.html?id=components-loader--infinite-state" 作为 iframe 入口 URL,FramesRenderer.stories.tsx 也展示了 iframe.html?id=${id} 的拼接形式。
  • URL 使用相对路径 /iframe.html,意味着 Cypress 的 baseUrl 应指向运行中的 Storybook 服务(开发模式默认 6006 端口,Playwright 对照片段 component-playwright-test.md 中显式使用了 http://localhost:6006/iframe.html?id=...,可互相印证)。

4. Story id 是怎么来的

components-login-form--example 不是随机字符串,而是由 CSF 的 标题(title)+ Story 名称 计算得到的稳定标识。查看 Storybook 构建 story index 的核心实现 StoryIndexGenerator.ts 可以看到:

const name = input.name ?? storyNameFromExport(input.exportName);
const title = input.title ?? defaultMakeTitle();
const id = input.__id ?? toId(input.metaId ?? title, storyNameFromExport(input.exportName));

即 Story 的最终 idtoId(title, storyName) 生成,其中 toId 来自 storybook/internal/csf/csf-utils(同一文件在 StoryIndexGenerator.test.ts 中被大量单测覆盖)。它的规则可概括为:把 title 与 Story 名转成小写、按连字符(kebab-case)规范化后用 -- 分隔:

  • title: 'components/LoginForm' → 前缀段 components-login-form
  • 名为 Example(或导出名 Example)的 Story → 后缀段 example
  • 拼接结果 → components-login-form--example

需要特别提醒:官方示例的 CSF 文件 login-form-with-play-function.md 中的 FilledForm 对应 story id 中的 filled-form 段。因此实测时,iframe.html?id= 后面的 id 必须与你仓库中真实 Story 的 meta 标题和导出名一致,否则 Cypress 将访问到不存在的 Story。你可以直接在浏览器打开 Storybook 对应 Story,从地址栏复制准确 id。

5. 作用域收窄与语义化日志

cy.get('#login-form').within(() => { ... });

.within() 把后续查询的作用域收窄到 #login-form 容器内部,避免与页面其他元素撞名;块内的 cy.log('**enter the email**') 会在 Cypress 命令日志里渲染成易于阅读的步骤注释,是官方文档在长流程用例中常用的可读性技巧。

6. 值断言

cy.get('#email').should('have.value', 'email@provider.com');
cy.get('#password').should('have.value', 'a-random-password');

should('have.value', ...) 断言输入框的当前值。这一步能够通过,前提是 Story 在渲染后已经带上了这些初始值——这正是下面要说的 play function 场景。

前置条件:让 Story 自带可断言的初始状态

官方文档强调:Cypress 测试跑起来时,「加载 Storybook 的隔离 iframe 并检查输入框是否与测试值匹配」。为了满足这种匹配,被访问的 Story 必须在渲染完成后进入「已填好值」的状态。

实现该状态的标准方式是在 Story 中书写 play function。参考配套片段 login-form-with-play-function.md 中的 CSF 3 写法(以 React 为例):

import { expect } from 'storybook/test';
import { LoginForm } from './LoginForm';

export default { component: LoginForm };

export const FilledForm = {
  play: async ({ canvas, userEvent }) => {
    // 👇 模拟与组件的交互
    await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');
    await userEvent.type(canvas.getByTestId('password'), 'a-random-password');
    await userEvent.click(canvas.getByRole('button'));
    // 👇 断言 DOM 结构
    await expect(
      canvas.getByText('Everything is perfect. Your account is ready and we should probably get you started!'),
    ).toBeInTheDocument();
  },
};

play function 是理解两种测试协同的关键概念:它是一段在 Story 渲染完成后自动执行的小代码,允许你编排组件内的交互序列(键入、点击等)。当 Cypress 通过 iframe.html?id=... 加载该 Story 时,play function 已经先一步把邮箱与密码填好,因此 Cypress 侧只需做纯读取式断言即可验证最终 DOM 状态,无需重复模拟整段交互。仓库为 React、Angular、Vue 3、Svelte、Web Components 等渲染器均提供了相同语义的多框架示例,同一 Story 定义同时服务于 Storybook 交互测试与外部 E2E 框架。

与 Playwright 版本对照

同一主题的 Playwright 示例 component-playwright-test.md 呈现了「相同思路、不同 API」的实现,适合对比阅读:

import { test, expect } from '@playwright/test';

test('Login Form inputs', async ({ page }) => {
  await page.goto('http://localhost:6006/iframe.html?id=components-login-form--example');
  const email = await page.inputValue('#email');
  const password = await page.inputValue('#password');
  await expect(email).toBe('email@provider.com');
  await expect(password).toBe('a-random-password');
});

两者的共同点是:都直接访问同一份 iframe.html?id=components-login-form--example。差异体现在 API 风格上:

维度 Cypress Playwright
URL 访问 cy.visit('/iframe.html?id=...')(相对路径,走 baseUrl) page.goto('http://localhost:6006/iframe.html?id=...')
取值 cy.get('#email').should('have.value', ...) 链式断言 await page.inputValue('#email') 后配合 expect 断言
作用域 .within() 收窄选择范围 通过具体选择器直接定位

选择哪个取决于团队既有基建:Cypress 提供命令日志与时间旅行调试,Playwright 则强调跨浏览器与移动端模拟能力。无论选择哪一个,Story 定义与 iframe.html 加载模型都是完全复用的。

运行前提与适用边界

要让上述测试真正跑通,请先确认以下前提,它们都以本仓库文档内容与代码行为为准:

  1. Storybook 服务可用:先用 storybook dev(开发预览,默认端口 6006)启动项目;若已 storybook build 产出静态文件,也可用任意静态服务器托管 dist 目录,使 /iframe.html 可访问。
  2. Story id 真实存在:访问前先在浏览器打开目标 Story,确认地址栏 id(含 -- 分隔符)与测试中书写的一致。
  3. Story 状态可被观察:目标 Story 渲染后应携带确定性内容(如示例中的邮箱、密码值),通常通过 play function 或 args 提供;否则 have.value 断言将无从比较。

同时要认清适用边界:这种模式适合对「运行在 Storybook 隔离环境中的组件/功能模块」做组件级 E2E 验证;若要验证跨页面路由、真实鉴权或后端联调的整体业务链路,仍需针对完整应用实例编写独立的端到端测试。它与其他测试形态是互补关系而非替代关系。

延伸阅读与配套测试体系

围绕「用 Story 驱动测试」,本仓库官方文档还提供了一组相互衔接的指南,可按需深入:

小结

本文以官方 E2E 指南中的 Cypress snippet 为线索,梳理出一条完整链路:CSF 命名的 Story → Storybook 为每个 Story 生成稳定 id → 外部测试框架通过 iframe.html?id=<title>--<story> 加载隔离预览 → 依据 play function 已就绪的 DOM 状态做 Cypress 断言。掌握 story id 的生成规则(StoryIndexGenerator.ts)与 iframe.html 的加载语义(FramesRenderer.stories.tsxIframe.stories.tsx),你就能把 Storybook 从「组件浏览工具」无缝扩展为「组件级端到端测试基础设施」,让组件开发与验证在同一条流水线上高效闭环。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388