AI 参与说明(Agent:Codex):本文由 Codex 基于 Vitest、Playwright、Storybook、Pact、Cucumber、jsdom 与 W3C WAI 官方资料辅助整理,并把这些已存在的测试能力组织成一套面向 Agent 提交代码的验收方案。工具能力与术语定义来自所列一手资料;“机器可验证契约”“自然语言验收契约”及文中的验收证据链是本文为了工程沟通提出的分层,不代表行业统一标准。
版本与验证范围:资料核验于 2026-08-26。本文按 Vitest 4.1.11、Playwright Test 1.62.1 与 Storybook 10.5 编写;最小 React / Vite 示例已使用 TypeScript 5.9.2、React 19.2.8、Vite 8.2.2、
vitest-browser-react2.2.0 及对应 Chromium 实际执行。Vitest Component Test、Playwright Interaction、ARIA Snapshot 与 Visual Baseline 均通过复跑;Storybook、Pact 和 Cucumber 部分只做官方资料核验,未在同一临时工程中执行。
结论#
前端 UI 不只有“能不能点击”这一种正确性。一个可信的测试体系至少要分别验证:
- Component 的输入、状态、输出和错误分支是否正确。
- DOM Semantics、Focus、Keyboard 与真实 Event Propagation 是否符合预期。
- CSS Layout、Responsive State 和视觉外观是否发生非预期变化。
- Authentication、Routing、Backend、Cookie、Download 等完整 Browser Flow 是否连得起来。
- 前端 Consumer 与 Backend Provider 交换的 Request、Response 或 Message 是否仍兼容。
- Agent 的代码提交是否满足最初的自然语言需求,并留下可以复查的执行证据。
直接回答“Vitest 有没有解决 UI 测试”:解决了一部分,而且不只限于模拟 DOM。Vitest Browser Mode 可以在真实 Browser 中 Mount Component,验证 Interaction、Focus、Browser API、Accessibility Structure 和局部 Screenshot;但它不会自动判断设计是否美观,也不应替代 Login、Routing、Popup、Download 等完整 Application Flow。后者通常交给 Playwright Test。
这些目标不应被压进一个巨大的 End-to-End Test:
- Vitest
node负责 Pure Logic、State Reducer、Formatter 与便宜的 Unit Test。 - Vitest Browser Mode 负责靠近 Source 的 Component Behavior、真实 Browser Semantics,以及小范围 Visual Regression。
- Playwright Test 负责 Page / Application Flow、跨 Browser Project、部署链路和失败后的 Trace。
- Storybook 负责枚举并共享 Component State,让 Interaction、Accessibility 与 Visual Test 复用同一批 Story。
- Pact 一类 Contract Testing 工具负责独立 Application 之间的 Message Contract,不负责 UI Layout。
- 自然语言需求负责表达 Intent;只有映射到 Assertion、Snapshot、Contract Verification 和人工审阅证据后,才能成为可重复验收的约束。
先分清被测范围、运行环境与判定面#
理解前端测试最容易卡住的地方,是把不同维度混成同一条“测试层级”。Component 与 End-to-End 描述的是被测范围;jsdom 与真实 Browser 描述的是运行环境;Click、Keyboard 与 Network Response 可以是输入或刺激;DOM、Accessibility Tree、Layout 与 Pixel 则是观察面。Matcher / Comparator 与 Threshold 构成判定器;Snapshot / Baseline 是 Expected Artifact,Diff 是失败后的 Diagnostic Artifact。这些维度彼此正交,但只能在环境和工具能力允许时组合,并非任意组合都成立。
更严谨地说,一次 Test 通过只能支持下面这个有限结论:
对版本 V,在环境 E、状态与输入 I、依赖边界 D 下,
观察面 S 的结果满足判定器 O 与容差 T。因此评审一个 Test 时,至少要问清七件事:
- 测的是 Function、Component、Page,还是完整 User Journey?
- 运行在 Compiler、Node.js、模拟 DOM、真实 Browser,还是已部署系统?
- API、Storage 与 Authentication 是 Mock、Local Fake,还是真实 Service?
- 用什么刺激系统:Function Input、API Response、Clock,还是 Click / Keyboard?
- 观察的是 Type、DOM、Accessibility Tree、Geometry、Pixel,还是 API Message?
- 用什么 Matcher / Comparator 判定,允许多大的 Tolerance?
- 覆盖了哪些 Browser、Viewport、Locale、角色、数据和错误状态?
| 要证明的事实 | 首选层级 | 推荐工具 | 主要产物 | 不能单独证明什么 |
|---|---|---|---|---|
| 价格、权限、解析与状态转换 | Unit / Module | Vitest node | Assertion、Coverage | Browser 与 CSS 行为 |
| Component Loading、Error、Form 与 Callback | Component Behavior | Vitest Browser Mode | DOM / Role Assertion | 完整部署链路 |
| Focus、Keyboard、Event、Browser API | Browser Component | Vitest Browser Mode | Interaction Assertion | 跨页面 User Journey |
| Component 外观和局部 Layout | Component Visual | Vitest toMatchScreenshot 或 Storybook Visual Tests | Baseline、Actual、Diff | 需求本身是否合理 |
| 登录、路由、支付跳转、下载、多页面流程 | Application / E2E | Playwright Test | Assertion、HTML Report、Trace | 独立 Service 的所有 Contract |
| Page 的 Accessible Structure | Semantic Snapshot | Playwright ARIA Snapshot | YAML Snapshot | 完整 WCAG 合规与真实辅助技术体验 |
| 前端调用的 HTTP / Message 是否兼容 | Integration Contract | Pact | Pact Interaction、Provider Verification | 页面交互和视觉外观 |
| Agent 是否满足一段需求 | Acceptance Evidence Chain | 本文提出的分层组合 | Acceptance ID、Tests、Diff、Trace、Review | 不能只凭 Agent 的完成声明证明 |
一个实用判断是:能用精确 Behavior Assertion 表达的,不先用 Screenshot;只在真实 Browser 才成立的,不用 jsdom 结果代替;涉及完整 User Journey 的,不把 Component Test 当作 E2E。
jsdom 很适合低成本验证 DOM 与 Component Behavior,但其官方 README 明确把 Navigation 与 Layout 列为范围之外的能力,许多 Layout Property 只能返回零等占位结果。因此 jsdom Test 通过不能写成“真实布局已验证”。jsdom:Unimplemented parts of the web platform
术语边界:哪些是已有概念,哪些是本文分层#
Pact 语境中的 Contract Testing 指 Integration Contract#
“Contract Testing”在更广泛的行业语境中也可能指 Provider 对 OpenAPI 等已发布 Specification 的符合性检查。本文讨论 Pact 时采用更窄的 Integration 含义:分别测试 Integration Point 两端,确认 Application 发送或接收的 Message 符合双方记录在 Contract 中的共同理解。HTTP 场景中的 Message 是 Request / Response,Queue 场景中则是 Message。Pact 是 Code-first、Consumer-driven 的 Contract Testing 工具,其 Contract 由 Consumer Test 的 Interaction Example 生成,再由 Provider Verification 验证。Pact Introduction How Pact works
因此,以下说法需要区分:
- “Frontend 与 Quote API 的 Response Shape 仍兼容”可以是 Pact Contract Test。
- “Button 必须位于 Card 右下角”是 Visual / Layout Requirement,不是 Pact 意义上的 Contract Test。
- “点击 Button 后出现 Dialog”是 Component 或 E2E Behavior,不应为了使用“Contract”一词而改写成 Pact Test。
“机器可验证契约”是本文使用的工程总称#
本文把 TypeScript Type、Schema、具体 Test Assertion、ARIA Snapshot、Screenshot Baseline、Pact Interaction 等能被工具比较并产生 Pass / Fail 的 Artifact,统称为“机器可验证契约”。这些子项各自有成熟定义,但这个总称及其分层是本文的组织方式,不是某个标准或 Test Runner 的正式分类。
机器可验证不等于可靠:过宽的 Screenshot Threshold、只检查 Status Code 的 API Test、可被随手更新的 Snapshot,都会形成“能够运行但约束很弱”的契约。
| 契约载体 | 工具判定的对象 | 通过后能支持的结论 | 仍不能说明 |
|---|---|---|---|
| TypeScript Type / Type Test | Signature、Assignability、Inference | 当前 Compiler 与 Config 下的 Type Relation 成立 | Runtime Input 有效或真实数据兼容 |
| Runtime Schema | JSON 等 Runtime Value | 样本满足已编码的 Shape 与 Constraint | 数据的业务含义正确 |
| DOM / Role Assertion | Text、Role、Accessible Name、Attribute、State | UI 暴露了指定结构和语义 | CSS Layout、Pixel 或 WCAG Conformance |
| Geometry Assertion | Bounding Box、Overflow、Viewport Intersection | 指定 Browser 与 Viewport 下的显式几何约束成立 | 其他 Viewport 正确或设计美观 |
| ARIA Snapshot | Accessibility Tree | 选定 Role、Name、State 与 Hierarchy 没有未批准变化 | Screen Reader 全流程正确或满足全部 WCAG |
| Screenshot Baseline | Pixel / Image Diff | 当前渲染与已批准 Reference 在容差内一致 | Reference 本身正确、可用或符合需求 |
| Pact Interaction | Consumer / Provider Message | 已验证版本对所用 Request、Response 或 Message 有共同理解 | Provider Side Effect 与完整 User Journey 正确 |
自然语言 Acceptance Criteria 没有列入这张“机器可验证”表:它由人解释 Intent 与边界,自身没有确定的自动 Pass / Fail Oracle。它位于证据链上游,必须映射到表中的一个或多个载体,才能成为自动门禁。
W3C WAI 也明确指出,没有任何单一工具能够判定一个站点是否满足 Accessibility Standard;自动化结果必须与有经验的人工评估结合。W3C WAI:Evaluating Web Accessibility
Gherkin 是结构化 Executable Specification,不是任意自然语言#
Gherkin 使用 Feature、Rule、Scenario、Given、When、Then 等 Keyword 给 Executable Specification 提供语法结构。每个 Step 的文本还必须匹配 Step Definition,Cucumber 才能执行对应代码。官方也明确把 Scenario / Example 描述为 Specification、Documentation 和 Test。Gherkin Reference
@AC-E2E-01
Feature: Checkout confirmation
Scenario: A customer submits the checkout form
Given the customer is on the checkout page
When the customer enters a valid email and places the order
Then the order confirmation page is visible
And the confirmation URL contains an order identifier这段 Gherkin 只有在项目提供匹配的 Step Definition 后才可执行。自由文本 PRD、Prompt 或 Markdown Checklist 不会因为使用了 Given / When / Then 的句式就自动成为 Cucumber Test。
“自然语言验收契约”不是行业统一术语#
本文用“自然语言验收契约”指向人和 Agent 共同阅读的 Requirement / Acceptance Criteria,例如:
AC-UI-01
前置:账户菜单处于关闭状态。
动作:用户点击“打开账户菜单”,随后按 Escape 关闭。
结果:显示名称为“账户”的 Dialog;Focus 进入 Dialog;Escape 可关闭并把 Focus 还给触发按钮。
视觉:390 × 844 视口下,Dialog 区域与已批准 Baseline 一致。
排除:本条不验证登录 API 或跨页面导航。它很适合表达 Intent 和边界,但本身仍有歧义。“与 Baseline 一致”还需要明确 Screenshot Region、Environment 与容差,“显示”需要明确是 DOM 存在、可见还是可被辅助技术访问。本文后续的做法是为每条 Acceptance ID 建立可执行映射,而不是把自然语言直接当作 Pass / Fail Oracle。
布局、视觉与设计意图需要不同判定器#
“UI 看起来对不对”至少包含三种不同问题,不能只用一个 Screenshot Test 回答:
| 问题 | 更合适的判定方式 | 典型示例 |
|---|---|---|
| 明确的 Layout Rule 是否成立 | Geometry Assertion、Computed Style、Overflow / Viewport 检查 | Dialog 不遮挡 Trigger;CTA 位于首屏;内容没有横向溢出 |
| 已批准的外观是否被意外改变 | Screenshot / Visual Regression | Typography、Spacing、Color、Border、Responsive Composition 没有非预期 Diff |
| 设计本身是否清晰、美观、符合产品意图 | Human Design Review;Vision Agent 可辅助检查 | Information Hierarchy 是否合理;视觉重心是否正确;文案与交互是否容易理解 |
例如,“Menu 必须出现在 Trigger 下方且不能超出 Viewport”可以写成确定的 Geometry Contract。以下是独立示意片段,假设页面已打开 Menu:
const trigger = page.getByRole("button", { name: "Account" });
const menu = page.getByRole("dialog", { name: "Account" });
const [triggerBox, menuBox] = await Promise.all([
trigger.boundingBox(),
menu.boundingBox(),
]);
const viewport = page.viewportSize();
if (!triggerBox || !menuBox || !viewport) {
throw new Error("The account menu must be visible in a fixed viewport");
}
expect(menuBox.y).toBeGreaterThanOrEqual(triggerBox.y + triggerBox.height);
expect(menuBox.x + menuBox.width).toBeLessThanOrEqual(viewport.width);Geometry Assertion 适合验证少量明确、稳定的空间约束;Screenshot 更适合发现整体 Composition 的未知变化。两者都需要固定 Viewport、Font 和 Browser Environment,而且都不能证明 Reference Design 本身是正确的。
多模态 Agent 可以读取 Requirement 与 Screenshot,协助指出遮挡、层级混乱、明显溢出或与设计说明不一致之处,但它的自然语言判断可能受 Model、Prompt 和输入压缩影响。除非团队已经针对固定 Evaluator 建立可接受的误报率、漏报率和版本策略,否则这类结果应作为 Review Signal,而不是唯一的 Merge Gate。稳定门禁仍应来自 Behavior Assertion、Geometry Constraint、ARIA / Screenshot Baseline 等可复查 Artifact;对“是否好看、是否符合品牌与产品意图”的首次批准仍需要人。
Vitest Browser Mode 与 Playwright 的职责边界#
两者都能打开真实 Browser,也都能测试 Component;区别不是“一个真浏览器、一个假浏览器”,而是默认 Workflow 和目标 Scope。
| 维度 | Vitest Browser Mode | Playwright Test |
|---|---|---|
| 默认心智模型 | Vite / Vitest Test Suite 中的 Browser Project | 独立 Browser Automation 与 Test Runner |
| 最合适的 Scope | Component、Module Integration、靠近 Source 的行为 | Page、Application、User Journey、部署系统 |
| Transformation | 复用 Vite Plugin、Alias 与 Module Graph | E2E 直接访问 App;当前 Component Testing 由项目自己的 Dev Server / Story Gallery 提供构建链路 |
| 反馈方式 | Watch、Related Test、Vitest Assertion / Mock | Project、Retry、HTML Report、Trace Viewer |
| Visual API | toMatchScreenshot | toHaveScreenshot |
| Semantic Snapshot | Browser Mode 也提供 ARIA Snapshot 能力 | toMatchAriaSnapshot,可内联或保存 .aria.yml |
| 典型边界 | 不替代关键完整 Browser Flow | 不替代便宜的 Unit / Type Test,也不自动证明 Provider Contract |
Vitest 官方把 Browser Mode 作为 Component Testing 的推荐方式,因为真实 Browser 能发现 DOM Simulator 可能遗漏的 CSS Layout、Browser API、Event Propagation、Focus 和 Accessibility 问题。Vitest Component Testing
同时,Vitest 官方仍把 Browser Mode 标记为 Early Development,并建议用 Playwright、WebdriverIO 或 Cypress 等独立 Browser-side Runner 补充。这意味着“推荐用于 Component Testing”和“关键 Flow 仍需独立 Runner”可以同时成立。Why Browser Mode
截至 Vitest 4.1.11,Browser Mode 的 ARIA Snapshots 是 4.1.4+ 才提供的 Experimental 能力;可以用于补充 Semantic Regression,但升级时需要复核 API 和 Snapshot Format,不能把它当作稳定的长期 Storage Format。Vitest ARIA Snapshots
Playwright 当前也提供稳定的 Component Testing:内置 mount() Fixture 驱动由项目 Dev Server 提供的 Story Gallery。旧的 @playwright/experimental-ct-react、@playwright/experimental-ct-vue 等 Package 已被这一 Framework-agnostic 方案替代。选择它时,团队要维护 Gallery Contract;换来的则是 Playwright 的 Project、Retry、Visual Comparison 和 Trace 能力。Playwright Component Testing
建议的默认分工是:
- 已使用 Vite / Vitest,且要高频测试大量 Component State:先用 Vitest Browser Mode。
- 测试跨 Page、Authentication、Popup、Download、Cookie、部署后的 URL 或多个 Browser Project:用 Playwright Test。
- 团队已经把所有 Component Scenario 建成 Story Gallery,并希望统一使用 Playwright Trace:可以直接采用 Playwright Component Testing。
- 不要仅因为 Provider 使用了 Playwright,就把 Vitest Browser Mode 误称为 Playwright E2E;Provider 只是驱动 Browser,Runner、Isolation 和 API 仍属于 Vitest。
Vitest 从 vitest/browser 导出的 page 也不是 Playwright Test 的 Page。Test Code 运行在 Browser,而 Playwright 的 Page、BrowserContext 等对象位于 Provider 的 Server Side;需要通过 Vitest Commands API 暴露受控命令,不能在 Browser Test 中直接混用 Playwright Test API。Vitest Commands API
用 Vitest Browser Mode 测 Component Behavior 与 Visual Regression#
下面示例假设项目已经是 React + Vite,并已安装 React Plugin。Vitest 4.1.11 的 Browser Project 可以这样准备:
pnpm add -D vitest@4.1.11 @vitest/browser-playwright@4.1.11 vitest-browser-react@2.2.0
pnpm exec playwright install chromium// vitest.config.ts
import react from "@vitejs/plugin-react";
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
test: {
projects: [
{
test: {
name: "unit",
include: ["src/**/*.unit.test.ts"],
environment: "node",
},
},
{
test: {
name: "component",
include: ["src/**/*.browser.test.tsx"],
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [
{
browser: "chromium",
viewport: { width: 390, height: 844 },
},
],
},
},
},
],
},
});Component Test 应先断言 Behavior,再为真正关心外观的稳定区域增加 Screenshot:
// src/AccountMenu.browser.test.tsx
import { useEffect, useRef, useState } from "react";
import { expect, test } from "vitest";
import { userEvent } from "vitest/browser";
import { render } from "vitest-browser-react";
function AccountMenu() {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (open) dialogRef.current?.focus();
}, [open]);
const close = () => {
setOpen(false);
requestAnimationFrame(() => triggerRef.current?.focus());
};
return (
<section>
<button ref={triggerRef} type="button" onClick={() => setOpen(true)}>
Open account menu
</button>
{open ? (
<div
ref={dialogRef}
role="dialog"
aria-label="Account"
tabIndex={-1}
onKeyDown={(event) => {
if (event.key === "Escape") close();
}}
>
<label>
Display name
<input defaultValue="Ada" />
</label>
<button type="button" onClick={close}>
Close
</button>
</div>
) : null}
</section>
);
}
test("AC-UI-01 opens the account dialog", async () => {
const screen = await render(<AccountMenu />);
const trigger = screen.getByRole("button", { name: "Open account menu" });
await trigger.click();
const dialog = screen.getByRole("dialog", { name: "Account" });
await expect.element(dialog).toBeVisible();
await expect.element(dialog).toHaveFocus();
await expect.element(screen.getByLabelText("Display name")).toHaveValue("Ada");
await expect(dialog).toMatchScreenshot("account-dialog");
await userEvent.keyboard("{Escape}");
await expect.element(dialog).not.toBeInTheDocument();
await expect.element(trigger).toHaveFocus();
});这个最小示例是 Non-modal Dialog,因此没有声明 aria-modal="true"。如果产品要求真正的 Modal Dialog,不能只添加一个 ARIA Attribute:还要让背景内容不可交互、把 Tab / Shift+Tab 限制在 Dialog 内、支持 Escape 关闭,并在关闭后合理返回 Focus。W3C WAI-ARIA APG:Modal Dialog Pattern
pnpm exec vitest run --project component第一次运行 toMatchScreenshot 时,Vitest 会创建 Reference Screenshot 并让 Test 失败,要求先审阅 Reference;Reference 默认放在 Test 相邻的 __screenshots__ 中,应提交 Version Control。只有确认 UI 变化符合 Requirement 后才运行更新:
pnpm exec vitest run --project component --updateVitest 的 Screenshot 名称包含 Browser 和 Platform。Browser Rendering 受 OS、Font、Browser Version 等因素影响,因此 CI 应固定 Browser、OS / Container、Font、Viewport、Color Scheme、Locale、Time 和 Test Data。优先截取具体 Component,避免 Whole Page 中无关区域制造 Diff;动态时间或随机内容应 Mock 或 Mask,而不是无限放宽 Threshold。Vitest Visual Regression Testing
用 Playwright 验证完整 Flow、ARIA Structure 与失败现场#
下面示例假设应用能通过 pnpm dev 在 http://127.0.0.1:5173 启动,并为 Test Data 提供稳定的 Checkout 页面:
pnpm add -D @playwright/test@1.62.1
pnpm exec playwright install chromium// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "tests/e2e",
retries: process.env.CI ? 2 : 0,
reporter: [["html", { open: "never" }]],
use: {
baseURL: "http://127.0.0.1:5173",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "pnpm dev",
url: "http://127.0.0.1:5173",
reuseExistingServer: !process.env.CI,
},
});为了让示例可复现,这里的 Place order 假定连接受控的 Local Fake 或固定 Test Fixture;它证明 Browser 中的 Form、Routing 与 Confirmation UI Journey,不证明真实 Provider 已持久化订单。若目标是覆盖真实 Backend,应改用隔离的 Test Environment、准备可控数据,并额外验证 Provider Side Effect 或对应 Contract。
// tests/e2e/checkout.spec.ts
import { expect, test } from "@playwright/test";
test("AC-E2E-01 submits an order and shows confirmation", async ({ page }) => {
await page.goto("/checkout");
await page.getByLabel("Email").fill("buyer@example.test");
await page.getByRole("button", { name: "Place order" }).click();
await expect(page).toHaveURL(/\/orders\/[^/]+\/confirmation$/u);
await expect(page.getByRole("heading", { name: "Order confirmed" })).toBeVisible();
await expect(page).toMatchAriaSnapshot(`
- main:
- heading "Order confirmed" [level=1]
`);
await expect(page.getByRole("main")).toHaveScreenshot("order-confirmation.png");
});pnpm exec playwright test --project=chromium
pnpm exec playwright show-reportPlaywright toHaveScreenshot 首次生成 Reference,后续比较;官方提醒 Baseline 和 Test 必须运行在一致环境中,因为 OS、Hardware、Headless Mode、Browser Version 等都可能改变 Rendering。Playwright Visual Comparisons
ARIA Snapshot 是 Accessibility Tree 的 YAML Representation,适合验证 Role、Accessible Name、State 与 Hierarchy。它不是完整 Accessibility Audit:它不会代替 Keyboard 操作、Focus Flow、Color Contrast、Screen Reader 或人工体验检查。Playwright ARIA Snapshots
trace: "on-first-retry" 可以在失败后的第一次 Retry 记录 Trace。Trace Viewer 能检查每个 Action 前后的 DOM Snapshot、Network、Console、Error 与 Source,这类 Failure Artifact 对远程 CI 和 Agent 提交尤其有价值。Playwright Trace Viewer
Storybook:把 Component State 变成可复用 Test Case#
Story 的价值不只是展示 Component,而是为 Loading、Empty、Error、Disabled、Long Content、Locale、Theme、Responsive 等状态命名。Storybook 10.5 的 Vitest Addon 会把 Story 自动转换为 Vitest Test,并通过 Browser Mode 运行;当前官方优先推荐 Addon,而不是手工调用 Portable Stories API。该 Addon 只支持 Vite-based Storybook Framework,并要求 Vitest 3.0 或更高版本;Webpack-based Framework 不能直接套用这条路径。Storybook Vitest Addon Portable Stories in Vitest
pnpm exec storybook add @storybook/addon-vitest
pnpm exec storybook add @storybook/addon-a11y一个 Story 的 play Function 可以表达 Interaction:
// Dialog.stories.ts
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect } from "storybook/test";
import { Dialog } from "./Dialog";
const meta = {
component: Dialog,
} satisfies Meta<typeof Dialog>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Opens: Story = {
play: async ({ canvas, userEvent }) => {
await userEvent.click(canvas.getByRole("button", { name: "Open" }));
await expect(canvas.getByRole("dialog")).toBeInTheDocument();
},
};Portable Stories 仍可通过 composeStories / composeStory 把 Args、Decorator、Loader 和 play Function 带入外部 Vitest,但需要正确应用 Project Annotation。新项目优先让 Vitest Addon 自动完成转换;只有需要在自定义 Test 中复用某个 Story 时,再直接使用 Portable API。
Storybook 官方 Visual Tests 通过由 Storybook 团队维护的 Chromatic Cloud Service 对每个 Story 建立 Baseline;这不是纯本地、无服务依赖的 Storybook Core 功能。Chromatic 默认只在 Chrome 中测试;启用其他 Browser 后,每个 Browser 都与自己的历史 Baseline 比较,而不是把不同 Browser 的 Screenshot 彼此互比。选择它之前应确认 Account、网络、Browser、价格和 Artifact 保存策略。Storybook Visual Tests Chromatic Browser Support
Storybook Accessibility Addon 基于 axe-core。把 parameters.a11y.test 设为 "error" 会让发现 Violation 的 Test 失败;要真正阻止 Merge,还必须在 CI 执行该 Test,并把对应 CI Check 配成 Required Merge Gate。"todo" 只在 Storybook UI 中保留 Warning。自动化检查只能发现一部分 WCAG 问题,仍需 Keyboard、Focus 与人工辅助技术验证。Storybook Accessibility Tests
Agent 提交代码时的验收证据链#
Playwright 官方已经提供 Planner、Generator 和 Healer Agent:Planner 生成 Human-readable Markdown Plan,Generator 将 Plan 转成 Playwright Test,Healer 执行并尝试修复失败 Test。官方同时说明 Generated Test 可能有初始错误,Healer 甚至可能在判断功能损坏时 Skip Test。因此,Agent 能生成或修复 Test,不代表 Agent 可以自行决定 Requirement 已满足。Playwright Test Agents
本文建议从下表按风险选择验收证据,而不是要求每个 Agent Task 机械交付全部层级。A0 Intent 应始终存在;A1、A2 和 A3 按改动涉及的风险选择;A4 只在 Failure 或 Retry 时产生。这个模型是工程建议,不是 Playwright、Vitest 或 Cucumber 标准:
对 Regression Fix,还应保留一个额外证据:新增 Test 在修复前的 Parent Commit 或未修复实现上确实失败,修复后才通过。否则 Agent 同时生成实现与 Test 时,可能只写出一个复述当前实现、从未捕获问题的 Assertion。
| 层 | 内容 | 必须可审阅的 Artifact | Merge Gate |
|---|---|---|---|
| A0 Intent | PRD / Prompt / Gherkin 中的 Acceptance Criteria | 带稳定 ID 的 Requirement | Reviewer 确认含义和 Scope |
| A1 Behavior | Unit、Component、E2E Assertion | Test File、JUnit / HTML Result、Expected Test Count | CI Pass;核对 Test Discovery,不得出现未解释的 Skip |
| A2a Data Boundary | Type / Runtime Schema | Type Check、Schema Validation Result | 对应 Compiler / Runtime Validator 通过 |
| A2b Integration Contract | Pact Interaction | Pact File、Provider Verification | Consumer Contract 生成且 Provider Verification 通过 |
| A3a Visual / Semantic Baseline | Screenshot、ARIA Snapshot | Reference、Actual、Diff、ARIA YAML | Baseline Change 必须人工批准 |
| A3b Accessibility Check | axe-core 等 Rule Result | Violation 与 Context | Violation 失败,或经显式、可追踪的 Waiver 处理 |
| A4 Failure Evidence | Trace、Console、Network、Error Context | 失败或 Retry 时生成的 Artifact、Trace ZIP、Report | 触发 Failure / Retry 时保留可复现的诊断 Artifact |
用 Acceptance ID 建立一对多映射#
不要要求“一条需求必须对应一个 Test”。同一条 UI Requirement 往往需要多个证据:
| Acceptance ID | 可执行映射 | 证据 |
|---|---|---|
AC-UI-01 | Vitest Component Test:Dialog 可见、Focus、Escape、Focus Return | Vitest Result |
AC-UI-01 | Component Screenshot:390 × 844 Viewport | Reference、Actual、Diff |
AC-E2E-01 | Playwright:提交订单并进入 Confirmation URL | HTML Report、Trace on Retry |
AC-API-01 | Pact:Frontend Consumer 需要的最小 Quote Response | Pact File、Provider Verification |
Test Name、Story Name 或 Annotation 中应保留 Acceptance ID。Reviewer 才能从 Requirement 找到 Test,也能从失败 Artifact 回到原始 Intent。
Agent 不应拥有的隐式权限#
以下变化都可能让 CI “重新变绿”,但不等于修复:
- 删除 Assertion、给 Test 加
.skip或缩小 Test Discovery Pattern。 - 自动执行
--update接受所有 Screenshot / ARIA Snapshot 变化。 - 放宽 Pixel Threshold、隐藏大面积 Dynamic Region。
- 把 Accessibility
error改为todo/off。 - 修改 Pact Expectation 以迎合已经破坏 Consumer 的 Provider。
- 修改 Requirement 文本,让实现反过来定义需求。
因此,Agent 可以提出这些变更,但应把它们作为显式 Contract Change 交给 Reviewer,而不是当作普通修复混入代码。
可执行的 CI 分层#
项目可以用独立 Script 保持 Failure Scope 清晰:
{
"scripts": {
"test:unit": "vitest run --project unit",
"test:component": "vitest run --project component",
"test:storybook": "vitest run --project storybook",
"test:e2e": "playwright test --project=chromium"
}
}# GitHub Actions 示例:先执行 Test
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm test:unit
- run: pnpm test:component
- run: pnpm test:storybook
- run: pnpm test:e2e
# 无论 Test 成败都上传可审阅产物
- name: Upload UI test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-test-artifacts
path: |
playwright-report/
test-results/
**/__screenshots__/
**/.vitest-attachments/**
include-hidden-files: trueVitest 的 Visual Failure Attachment 默认位于隐藏的 .vitest-attachments 目录,因此上面的 GitHub Actions 示例显式上传该目录并启用 include-hidden-files。建议把 Visual Job 与快速 Unit Job 分开:Visual Failure 需要 Diff Review,不能淹没 Behavior Failure;Reference Update 使用单独、可审计的 Workflow,不能在每次 PR 自动重建 Baseline。本文为保持示例最小,把 Behavior 与 Screenshot 放在同一个 Component Project;大型项目可将 *.visual.browser.test.tsx 放入独立 Browser Project 和 CI Job。Vitest 官方 Visual Regression Guide 同样建议隔离 Visual Test,并只在 Intentional Change 时运行 Update Workflow。Vitest Visual Regression Testing
常见错误#
- 只断言 Component 存在,不断言 User-observable Behavior。应优先通过 Role、Label、Visible State 和实际 Interaction 检查结果。
- 用
jsdom证明 CSS Layout 正确。DOM Simulator 没有真实 Layout Engine;这类风险应进入 Browser Mode 或 Playwright。 - 用 Screenshot 代替所有 Assertion。Screenshot Diff 能发现外观变化,却很难精确说明 Callback、Error Recovery 或 API Contract 是否正确。
- 把 ARIA Snapshot 当作 WCAG Conformance 结论。它只比较 Accessibility Tree 的选定结构。
- 在 Developer Laptop 生成 Baseline,在不同 OS 的 CI 比较。Font 和 Rendering 差异会造成稳定性问题。
- 把 Pact Contract Test 当成完整 Integration / E2E Test。Pact 官方建议 Contract Test 聚焦 Message,而不是 Provider 的一般 Functional Behavior。
- 把 Gherkin 当作装饰性文档。没有匹配的 Step Definition,
.feature无法执行;只在本地执行而没有接入 Required CI Check,则不会形成 Merge Gate。Cucumber API Reference - 让 Agent 同时修改实现、Test、Baseline 和 Acceptance Criteria,却没有独立 Review。此时 Agent 可以通过降低 Oracle 强度而不是修复产品来获得 Green Build。
最小落地方案#
如果团队从零开始,可以按以下顺序扩展:
- 为业务规则和已知 Regression 建立 Vitest Unit Test。
- 为关键 Component 的 Loading、Empty、Error、Disabled 与 Interaction State 建立 Vitest Browser Test。
- 只为稳定且高价值的 Component Region 增加 Screenshot Baseline。
- 为 Login、Checkout、Routing 等 Critical Flow 建立 Playwright Test,并在 CI 保存 Trace。
- 如果已有 Storybook,让 Story 成为 Component State Catalog,再接入 Vitest、Accessibility 与 Visual Test。
- 只有存在独立 Consumer / Provider Deployment Risk 时引入 Pact,不因“Contract”一词泛化使用。Pact 更适合双方可控、仍在活跃演进、Provider Test Data 可控的 Integration;无法控制的第三方或公共 API 通常更适合 Schema Validation 和少量真实 Integration Test。When to use Pact
- 给 Agent Task 的 Acceptance Criteria 分配 ID,并按风险要求相关 Test、Baseline 与 Review 回链;Trace 等 Failure Artifact 只在对应运行产生时保存。
最终目标不是让一个 Runner 覆盖所有层,而是让每一种 Failure 都能回答三个问题:什么 Requirement 被破坏、哪个最小层级首先发现、Reviewer 可以查看什么 Evidence。
站内关联阅读#
- Playwright 使用指南:场景、能力边界与工程实践:系统查看 Playwright Test、Library、CLI、MCP、Component Testing、Browser / Network / Trace 能力,以及与 Vitest、jsdom 和 Storybook 的横向比较。
- Vitest 使用指南:适用场景、核心能力与工程实践:继续查看 Vitest 的安装、Mock、Coverage、Type Testing、Projects 与 CI 用法。
- Matt Pocock Skills:从需求澄清到工程交付的 Agent 工作流:从 Specification、TDD 与双轴 Review 理解 Agent 的工程反馈闭环。
官方资料#
- Vitest Browser Mode
- Vitest Why Browser Mode
- Vitest Component Testing
- Vitest Visual Regression Testing
- Vitest ARIA Snapshots
- Vitest Commands API
- Playwright Component Testing
- Playwright Visual Comparisons
- Playwright ARIA Snapshots
- Playwright Trace Viewer
- Playwright Test Agents
- Storybook UI Testing
- Storybook Vitest Addon
- Storybook Portable Stories in Vitest
- Storybook Visual Tests
- Storybook Accessibility Tests
- Chromatic Browser Support
- Pact Introduction
- Pact How It Works
- When to use Pact
- Cucumber Gherkin Reference
- Cucumber API Reference
- Cucumber Behaviour-Driven Development
- jsdom:Unimplemented parts of the web platform
- W3C WAI:Evaluating Web Accessibility
- W3C WAI-ARIA APG:Modal Dialog Pattern