Skip to content

组件行为、浏览器模式与 CI ​

组件测试的对象是用户能观察到的行为:可访问名称、键盘操作、错误消息、提交结果和焦点变化。DOM Testing Library 的原则是让查询接近用户使用方式;因此优先 getByRole、getByLabelText,而不是依赖 CSS 类名、组件实例或内部 state。模拟 DOM 适合快速验证状态转换,但不能代替真实浏览器对布局、原生导航、cookie、焦点策略和浏览器 API 的证据。

先写一个可观察的 DOM 契约 ​

不依赖框架的登录表单可以直接用 jsdom 练习:

ts
// login.ts
export function renderLogin(root: HTMLElement, submit: (email: string) => Promise<void>) {
  root.innerHTML = `<form aria-label="登录"><label>邮箱<input name="email" type="email" required></label><button>登录</button><p role="alert" tabindex="-1" hidden></p></form>`
  const form = root.querySelector('form')!
  const alert = root.querySelector('[role=alert]') as HTMLElement
  form.addEventListener('submit', async (event) => {
    event.preventDefault()
    const input = form.elements.namedItem('email') as HTMLInputElement
    if (!input.value.includes('@')) {
      alert.hidden = false
      alert.textContent = '请输入有效邮箱'
      input.focus()
      return
    }
    try { await submit(input.value); alert.hidden = true }
    catch { alert.hidden = false; alert.textContent = '登录失败'; alert.focus() }
  })
}

测试从用户路径开始:

ts
// login.test.ts
// @vitest-environment jsdom
import { expect, it, vi } from 'vitest'
import { renderLogin } from './login'

it('无效邮箱显示错误并把焦点留在输入框', async () => {
  const root = document.createElement('main')
  document.body.append(root)
  renderLogin(root, vi.fn())
  const input = root.querySelector('input[aria-label="邮箱"]')
    ?? root.querySelector('input[name="email"]')!
  input.dispatchEvent(new Event('input', { bubbles: true }))
  (root.querySelector('form') as HTMLFormElement).dispatchEvent(new SubmitEvent('submit', { bubbles: true, cancelable: true }))
  expect(root.querySelector('[role=alert]')?.textContent).toBe('请输入有效邮箱')
  expect(document.activeElement).toBe(input)
})

真实项目中可用 @testing-library/dom 的 screen.getByRole('button', { name: '登录' }) 与 userEvent 触发完整键盘流程。关键不是 API 名字,而是断言不依赖 innerHTML 的完整字符串;表单改 CSS 或从原生 DOM 换成组件实现时,行为测试仍然有意义。

哪些问题必须提升到 Browser Mode ​

Browser Mode 项目会在真实浏览器执行测试,适用于:

  • 焦点移动、原生约束校验、beforeunload、ResizeObserver 等宿主行为;
  • 真实 URL、cookie、localStorage、跨页面导航;
  • 依赖浏览器渲染或 CSS 布局的断言。

Vitest 的 browser 不是 Node 的 environment;应在配置中用独立 project,把 jsdom 测试和浏览器测试分开运行。Browser Mode 仍不等于完整后端 E2E:它需要服务可访问、路由和数据隔离,真实认证流程仍应在端到端层覆盖。

CI 是执行策略,不是新断言 ​

本地开发可用 vitest watch;CI 要使用明确的 vitest run,并在需要时启用 coverage。先运行稳定的 Node/jsdom project,再运行浏览器 project;失败日志要保留 Node、浏览器、依赖锁文件和重试设置。--retry 只能帮助收集 flaky 证据,不能把随机失败变成通过。覆盖率阈值也只是风险信号:未覆盖的失败路径仍可能藏在高数字背后。

练习:同一个登录表单的三层证据 ​

为上面的表单各写一条断言:

  1. 纯函数层:空邮箱和无 @ 输入返回同一个错误码。
  2. jsdom 组件层:按 label 输入无效值,提交后出现 role=alert 并聚焦输入。
  3. Browser/E2E 层:真实路由 /login 提交成功后 URL 变为 /dashboard,刷新后会话仍按产品策略存在或被明确清除。

说明上一层为什么不能证明下一层:纯函数没有 DOM,jsdom 没有真实布局/导航,Browser 测试又不应替代每次快速的边界测试。

返回Vitest 课程总览。

用心记录每一步成长