前端 UI 测试全景:Vitest、Playwright、视觉回归与 Agent 契约

This article is extracted from the chat log with AI. Please identify it with caution.

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-react 2.2.0 及对应 Chromium 实际执行。Vitest Component Test、Playwright Interaction、ARIA Snapshot 与 Visual Baseline 均通过复跑;Storybook、Pact 和 Cucumber 部分只做官方资料核验,未在同一临时工程中执行。

结论#

前端 UI 不只有“能不能点击”这一种正确性。一个可信的测试体系至少要分别验证:

  1. Component 的输入、状态、输出和错误分支是否正确。
  2. DOM Semantics、Focus、Keyboard 与真实 Event Propagation 是否符合预期。
  3. CSS Layout、Responsive State 和视觉外观是否发生非预期变化。
  4. Authentication、Routing、Backend、Cookie、Download 等完整 Browser Flow 是否连得起来。
  5. 前端 Consumer 与 Backend Provider 交换的 Request、Response 或 Message 是否仍兼容。
  6. 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 时,至少要问清七件事:

  1. 测的是 Function、Component、Page,还是完整 User Journey?
  2. 运行在 Compiler、Node.js、模拟 DOM、真实 Browser,还是已部署系统?
  3. API、Storage 与 Authentication 是 Mock、Local Fake,还是真实 Service?
  4. 用什么刺激系统:Function Input、API Response、Clock,还是 Click / Keyboard?
  5. 观察的是 Type、DOM、Accessibility Tree、Geometry、Pixel,还是 API Message?
  6. 用什么 Matcher / Comparator 判定,允许多大的 Tolerance?
  7. 覆盖了哪些 Browser、Viewport、Locale、角色、数据和错误状态?
要证明的事实首选层级推荐工具主要产物不能单独证明什么
价格、权限、解析与状态转换Unit / ModuleVitest nodeAssertion、CoverageBrowser 与 CSS 行为
Component Loading、Error、Form 与 CallbackComponent BehaviorVitest Browser ModeDOM / Role Assertion完整部署链路
Focus、Keyboard、Event、Browser APIBrowser ComponentVitest Browser ModeInteraction Assertion跨页面 User Journey
Component 外观和局部 LayoutComponent VisualVitest toMatchScreenshot 或 Storybook Visual TestsBaseline、Actual、Diff需求本身是否合理
登录、路由、支付跳转、下载、多页面流程Application / E2EPlaywright TestAssertion、HTML Report、Trace独立 Service 的所有 Contract
Page 的 Accessible StructureSemantic SnapshotPlaywright ARIA SnapshotYAML Snapshot完整 WCAG 合规与真实辅助技术体验
前端调用的 HTTP / Message 是否兼容Integration ContractPactPact 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 TestSignature、Assignability、Inference当前 Compiler 与 Config 下的 Type Relation 成立Runtime Input 有效或真实数据兼容
Runtime SchemaJSON 等 Runtime Value样本满足已编码的 Shape 与 Constraint数据的业务含义正确
DOM / Role AssertionText、Role、Accessible Name、Attribute、StateUI 暴露了指定结构和语义CSS Layout、Pixel 或 WCAG Conformance
Geometry AssertionBounding Box、Overflow、Viewport Intersection指定 Browser 与 Viewport 下的显式几何约束成立其他 Viewport 正确或设计美观
ARIA SnapshotAccessibility Tree选定 Role、Name、State 与 Hierarchy 没有未批准变化Screen Reader 全流程正确或满足全部 WCAG
Screenshot BaselinePixel / Image Diff当前渲染与已批准 Reference 在容差内一致Reference 本身正确、可用或符合需求
Pact InteractionConsumer / 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 使用 FeatureRuleScenarioGivenWhenThen 等 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 RegressionTypography、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 ModePlaywright Test
默认心智模型Vite / Vitest Test Suite 中的 Browser Project独立 Browser Automation 与 Test Runner
最合适的 ScopeComponent、Module Integration、靠近 Source 的行为Page、Application、User Journey、部署系统
Transformation复用 Vite Plugin、Alias 与 Module GraphE2E 直接访问 App;当前 Component Testing 由项目自己的 Dev Server / Story Gallery 提供构建链路
反馈方式Watch、Related Test、Vitest Assertion / MockProject、Retry、HTML Report、Trace Viewer
Visual APItoMatchScreenshottoHaveScreenshot
Semantic SnapshotBrowser 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 的 PageBrowserContext 等对象位于 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 --update

Vitest 的 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 devhttp://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-report

Playwright 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。

内容必须可审阅的 ArtifactMerge Gate
A0 IntentPRD / Prompt / Gherkin 中的 Acceptance Criteria带稳定 ID 的 RequirementReviewer 确认含义和 Scope
A1 BehaviorUnit、Component、E2E AssertionTest File、JUnit / HTML Result、Expected Test CountCI Pass;核对 Test Discovery,不得出现未解释的 Skip
A2a Data BoundaryType / Runtime SchemaType Check、Schema Validation Result对应 Compiler / Runtime Validator 通过
A2b Integration ContractPact InteractionPact File、Provider VerificationConsumer Contract 生成且 Provider Verification 通过
A3a Visual / Semantic BaselineScreenshot、ARIA SnapshotReference、Actual、Diff、ARIA YAMLBaseline Change 必须人工批准
A3b Accessibility Checkaxe-core 等 Rule ResultViolation 与 ContextViolation 失败,或经显式、可追踪的 Waiver 处理
A4 Failure EvidenceTrace、Console、Network、Error Context失败或 Retry 时生成的 Artifact、Trace ZIP、Report触发 Failure / Retry 时保留可复现的诊断 Artifact

用 Acceptance ID 建立一对多映射#

不要要求“一条需求必须对应一个 Test”。同一条 UI Requirement 往往需要多个证据:

Acceptance ID可执行映射证据
AC-UI-01Vitest Component Test:Dialog 可见、Focus、Escape、Focus ReturnVitest Result
AC-UI-01Component Screenshot:390 × 844 ViewportReference、Actual、Diff
AC-E2E-01Playwright:提交订单并进入 Confirmation URLHTML Report、Trace on Retry
AC-API-01Pact:Frontend Consumer 需要的最小 Quote ResponsePact 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: true

Vitest 的 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

常见错误#

  1. 只断言 Component 存在,不断言 User-observable Behavior。应优先通过 Role、Label、Visible State 和实际 Interaction 检查结果。
  2. jsdom 证明 CSS Layout 正确。DOM Simulator 没有真实 Layout Engine;这类风险应进入 Browser Mode 或 Playwright。
  3. 用 Screenshot 代替所有 Assertion。Screenshot Diff 能发现外观变化,却很难精确说明 Callback、Error Recovery 或 API Contract 是否正确。
  4. 把 ARIA Snapshot 当作 WCAG Conformance 结论。它只比较 Accessibility Tree 的选定结构。
  5. 在 Developer Laptop 生成 Baseline,在不同 OS 的 CI 比较。Font 和 Rendering 差异会造成稳定性问题。
  6. 把 Pact Contract Test 当成完整 Integration / E2E Test。Pact 官方建议 Contract Test 聚焦 Message,而不是 Provider 的一般 Functional Behavior。
  7. 把 Gherkin 当作装饰性文档。没有匹配的 Step Definition,.feature 无法执行;只在本地执行而没有接入 Required CI Check,则不会形成 Merge Gate。Cucumber API Reference
  8. 让 Agent 同时修改实现、Test、Baseline 和 Acceptance Criteria,却没有独立 Review。此时 Agent 可以通过降低 Oracle 强度而不是修复产品来获得 Green Build。

最小落地方案#

如果团队从零开始,可以按以下顺序扩展:

  1. 为业务规则和已知 Regression 建立 Vitest Unit Test。
  2. 为关键 Component 的 Loading、Empty、Error、Disabled 与 Interaction State 建立 Vitest Browser Test。
  3. 只为稳定且高价值的 Component Region 增加 Screenshot Baseline。
  4. 为 Login、Checkout、Routing 等 Critical Flow 建立 Playwright Test,并在 CI 保存 Trace。
  5. 如果已有 Storybook,让 Story 成为 Component State Catalog,再接入 Vitest、Accessibility 与 Visual Test。
  6. 只有存在独立 Consumer / Provider Deployment Risk 时引入 Pact,不因“Contract”一词泛化使用。Pact 更适合双方可控、仍在活跃演进、Provider Test Data 可控的 Integration;无法控制的第三方或公共 API 通常更适合 Schema Validation 和少量真实 Integration Test。When to use Pact
  7. 给 Agent Task 的 Acceptance Criteria 分配 ID,并按风险要求相关 Test、Baseline 与 Review 回链;Trace 等 Failure Artifact 只在对应运行产生时保存。

最终目标不是让一个 Runner 覆盖所有层,而是让每一种 Failure 都能回答三个问题:什么 Requirement 被破坏、哪个最小层级首先发现、Reviewer 可以查看什么 Evidence。

站内关联阅读#

官方资料#

本文共 11005 字,创建于 Aug 26, 2026

相关标签: Frontend, TypeScript, Tools, ByAI