playwright-e2e
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePlaywright 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-appCore 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 boxesbash
npm install -D @playwright/test
npx playwright install chromium # or: --with-deps on CI / fresh boxesMinimal 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 runts
// 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
选择器——优先使用面向用户的方式,优先级如下
- — accessible role + name (best)
page.getByRole('button', { name: 'Save' }) - ,
page.getByLabel('Email'),page.getByPlaceholder(…)page.getByText(…) - — add
page.getByTestId('submit')for fragile/ambiguous nodesdata-testid - CSS/XPath — last resort; brittle
Playwright auto-waits for elements to be actionable — avoid manual . If you
must wait on state, wait on a condition: or
.
waitForTimeoutawait expect(locator).toBeVisible()page.waitForURL(…)- —— 可访问角色+名称(最佳选择)
page.getByRole('button', { name: 'Save' }) - ,
page.getByLabel('Email'),page.getByPlaceholder(…)page.getByText(…) - —— 为易失效/模糊的节点添加
page.getByTestId('submit')data-testid - CSS/XPath —— 最后选择;易失效
Playwright会自动等待元素可交互——避免手动使用。如果必须等待状态,请等待特定条件: 或 。
waitForTimeoutawait 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 — use
networkidleand wait on a concrete element instead.waitUntil: 'domcontentloaded' - 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
undefinedyaml
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 }
undefinedDebugging
调试
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 runbash
npx playwright test --debug # 使用调试器逐步执行
npx playwright codegen localhost:3000 # 录制点击操作→生成测试规范
npx playwright show-trace trace.zip # 回溯失败的测试运行