playwright-e2e

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Playwright E2E & Click-Verification Guide

Playwright端到端测试与点击验证指南

Drive a real browser to prove a web change works — not just that it compiles. Use this to verify flows end-to-end, reproduce UI bugs, and screenshot pages. Pairs with
nextjs-app
.
Core principle: a green build is not a working feature. Before calling an interactive change done, click through the actual rendered page in a browser. Especially for authed routes, modals, and forms — those are exactly where "looks fine in code" silently breaks.
驱动真实浏览器来验证Web变更确实生效——而不只是代码编译通过。使用本指南可验证端到端流程、复现UI bug并为页面截图。可与
nextjs-app
搭配使用。
核心原则:构建成功不代表功能可用。在宣布交互式变更完成前,务必在浏览器中点击实际渲染的页面进行验证。对于已认证路由、模态框和表单尤其重要——这些正是“代码看起来没问题”但实际会悄悄出问题的地方。

Install

安装

bash
npm install -D @playwright/test
npx playwright install chromium      # or: --with-deps on CI / fresh boxes
bash
npm install -D @playwright/test
npx playwright install chromium      # or: --with-deps on CI / fresh boxes

Minimal config

最简配置

ts
// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry' },
  webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: true },
})
ts
// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry' },
  webServer: { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: true },
})

A first spec

首个测试规范

ts
// e2e/home.spec.ts
import { test, expect } from '@playwright/test'

test('home renders and CTA navigates', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible()
  await page.getByRole('link', { name: 'Get started' }).click()
  await expect(page).toHaveURL(/.*dashboard/)
})
Run:
bash
npx playwright test                     # headless, all specs
npx playwright test --headed            # watch it
npx playwright test home.spec.ts -g CTA # filter
npx playwright show-report              # HTML report after a run
ts
// e2e/home.spec.ts
import { test, expect } from '@playwright/test'

test('home renders and CTA navigates', async ({ page }) => {
  await page.goto('/')
  await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible()
  await page.getByRole('link', { name: 'Get started' }).click()
  await expect(page).toHaveURL(/.*dashboard/)
})
运行命令:
bash
npx playwright test                     # 无头模式,运行所有测试规范
npx playwright test --headed            # 可视模式运行
npx playwright test home.spec.ts -g CTA # 过滤测试用例
npx playwright show-report              # 运行后生成HTML报告

Selectors — prefer user-facing, in this order

选择器——优先使用面向用户的方式,优先级如下

  1. page.getByRole('button', { name: 'Save' })
    — accessible role + name (best)
  2. page.getByLabel('Email')
    ,
    page.getByPlaceholder(…)
    ,
    page.getByText(…)
  3. page.getByTestId('submit')
    — add
    data-testid
    for fragile/ambiguous nodes
  4. CSS/XPath — last resort; brittle
Playwright auto-waits for elements to be actionable — avoid manual
waitForTimeout
. If you must wait on state, wait on a condition:
await expect(locator).toBeVisible()
or
page.waitForURL(…)
.
  1. page.getByRole('button', { name: 'Save' })
    —— 可访问角色+名称(最佳选择)
  2. page.getByLabel('Email')
    ,
    page.getByPlaceholder(…)
    ,
    page.getByText(…)
  3. page.getByTestId('submit')
    —— 为易失效/模糊的节点添加
    data-testid
  4. CSS/XPath —— 最后选择;易失效
Playwright会自动等待元素可交互——避免手动使用
waitForTimeout
。如果必须等待状态,请等待特定条件:
await expect(locator).toBeVisible()
page.waitForURL(…)

Testing authenticated pages

测试已认证页面

Most real apps gate the interesting pages behind login. Two reliable patterns:
大多数真实应用会将关键页面设置为登录后才可访问。以下两种可靠方案:

A. Log in once, reuse the storage state

A. 登录一次,复用存储状态

ts
// e2e/auth.setup.ts  (run as a setup project)
import { test as setup } from '@playwright/test'

setup('authenticate', async ({ page }) => {
  await page.goto('/sign-in')
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL!)
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()
  await page.waitForURL('**/dashboard')
  await page.context().storageState({ path: 'e2e/.auth/user.json' })
})
ts
// playwright.config.ts — wire the setup + reuse the session
projects: [
  { name: 'setup', testMatch: /auth\.setup\.ts/ },
  { name: 'app', use: { storageState: 'e2e/.auth/user.json' }, dependencies: ['setup'] },
]
ts
// e2e/auth.setup.ts  (作为前置项目运行)
import { test as setup } from '@playwright/test'

setup('authenticate', async ({ page }) => {
  await page.goto('/sign-in')
  await page.getByLabel('Email').fill(process.env.TEST_EMAIL!)
  await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
  await page.getByRole('button', { name: 'Sign in' }).click()
  await page.waitForURL('**/dashboard')
  await page.context().storageState({ path: 'e2e/.auth/user.json' })
})
ts
// playwright.config.ts —— 配置前置项目并复用会话
projects: [
  { name: 'setup', testMatch: /auth\.setup\.ts/ },
  { name: 'app', use: { storageState: 'e2e/.auth/user.json' }, dependencies: ['setup'] },
]

B. Programmatic session (faster, no UI login)

B. 程序化会话(更快,无需UI登录)

Many auth providers (Clerk, Auth.js, custom JWT) let you mint a session token via their server SDK / admin API, then inject it so the browser starts already-authed:
ts
// Mint a short-lived sign-in token server-side, then exchange it in the browser.
// Exact call depends on the provider; the shape is: get a ticket → land on a URL that
// consumes it → save storageState.
await page.goto(`/sign-in#__token=${signInToken}`)   // provider-specific consume step
await page.waitForURL('**/dashboard')
await page.context().storageState({ path: 'e2e/.auth/user.json' })
Keep test creds/tokens in env vars (never commit them), scope them to a throwaway test user, and scrub any token out of logs/artifacts.
许多认证提供商(Clerk、Auth.js、自定义JWT)允许通过其服务端SDK/管理API生成会话令牌,然后注入令牌使浏览器启动时已处于认证状态:
ts
// 服务端生成短期登录令牌,然后在浏览器中兑换。
// 具体调用取决于提供商;流程为:获取凭证 → 访问消费凭证的URL → 保存存储状态。
await page.goto(`/sign-in#__token=${signInToken}`)   // 提供商专属的消费步骤
await page.waitForURL('**/dashboard')
await page.context().storageState({ path: 'e2e/.auth/user.json' })
将测试凭证/令牌存储在环境变量中(切勿提交到代码仓库),限定为一次性测试用户使用,并确保日志/产物中没有令牌信息。

Quick one-off verification (no spec file)

快速一次性验证(无需测试规范文件)

For ad-hoc "does this actually render" checks, a short script beats a full suite:
js
// verify.mjs — node verify.mjs
import { chromium } from 'playwright'
const b = await chromium.launch()
const p = await b.newPage()
await p.goto('https://your-app.example.com/', { waitUntil: 'domcontentloaded' })
await p.screenshot({ path: 'shot.png', fullPage: true })
console.log('title:', await p.title())
await b.close()
Gotchas from real runs:
  • Pages with autoplay video/streams never hit
    networkidle
    — use
    waitUntil: 'domcontentloaded'
    and wait on a concrete element instead.
  • Kill cookie/consent banners before screenshotting (
    page.getByRole('button', { name: /accept/i }).click().catch(() => {})
    ).
  • A client-inlined key (auth publishable key, analytics id) is baked at build time — if a locally-served build behaves oddly, verify against a real deployed URL instead of fighting the local server.
对于临时的“页面是否真的渲染”检查,简短脚本比完整测试套件更高效:
js
// verify.mjs —— node verify.mjs
import { chromium } from 'playwright'
const b = await chromium.launch()
const p = await b.newPage()
await p.goto('https://your-app.example.com/', { waitUntil: 'domcontentloaded' })
await p.screenshot({ path: 'shot.png', fullPage: true })
console.log('title:', await p.title())
await b.close()
实际运行中的常见问题:
  • 包含自动播放视频/流的页面永远不会达到
    networkidle
    状态——请使用
    waitUntil: 'domcontentloaded'
    并等待具体元素。
  • 截图前关闭Cookie/授权提示框(
    page.getByRole('button', { name: /accept/i }).click().catch(() => {})
    )。
  • 客户端内联密钥(认证公钥、分析ID)是在构建时嵌入的——如果本地部署的构建行为异常,请直接验证真实部署的URL,而非纠结本地服务器问题。

CI

CI配置

yaml
undefined
yaml
undefined

.github/workflows/e2e.yml

.github/workflows/e2e.yml

  • run: npm ci
  • run: npx playwright install --with-deps chromium
  • run: npx playwright test
  • uses: actions/upload-artifact@v4 if: failure() with: { name: playwright-report, path: playwright-report }
undefined
  • run: npm ci
  • run: npx playwright install --with-deps chromium
  • run: npx playwright test
  • uses: actions/upload-artifact@v4 if: failure() with: { name: playwright-report, path: playwright-report }
undefined

Debugging

调试

bash
npx playwright test --debug          # step through with the inspector
npx playwright codegen localhost:3000 # record clicks → generated spec
npx playwright show-trace trace.zip  # time-travel a failed run
bash
npx playwright test --debug          # 使用调试器逐步执行
npx playwright codegen localhost:3000 # 录制点击操作→生成测试规范
npx playwright show-trace trace.zip  # 回溯失败的测试运行