Skip to content

这是我整理 Playwright 端到端测试时做的笔记,结合真实项目(EAM 的 UI 自动化测试)经验。API 入门见《Playwright 快速入门》——本文是动手实战:登录 mock、表单链路、网络拦截,全部本地实跑。

一、E2E 测试解决什么

单测验证“函数逻辑对不对”,E2E 验证“用户点完这一串流程,界面对不对”:

打开登录页 → 填账号密码 → 点登录 → 跳转首页 → 看到用户信息

Playwright 特点(对比 Selenium):

  • 自动等待(元素出现/网络响应才继续,不用手写 sleep)
  • 选择器友好(getByRole/getByText/getByPlaceholder,不依赖脆弱的 css/xpath)
  • 内置网络拦截、截图、视频录制

二、核心概念:Locator + 断言 + 自动等待

js
const { test, expect } = require('@playwright/test');

test('登录流程', async ({ page }) => {
  await page.goto('http://localhost:5666/login');
  // locator:按角色/文本/占位符找元素
  await page.getByPlaceholder('请输入账号').fill('admin');
  await page.getByPlaceholder('请输入密码').fill('123456');
  await page.getByRole('button', { name: '登 录' }).click();
  // 断言:等待元素出现(自动等待,最多 timeout 默认 30s)
  await expect(page.getByText('欢迎回来')).toBeVisible();
});

三句口诀

  1. 找元素用 getByRole/getByText/getByPlaceholder(语义化,比 css 稳)
  2. 交互用 fill/click/selectOption(填表/点击/下拉)
  3. 验证用 expect(...).toBeVisible()/toHaveText()(自动等)

三、登录 mock:不碰真实后端

E2E 测试最烦的是依赖真实登录态。Playwright 用 page.route() 拦截网络请求,mock 掉登录接口

js
test('登录(mock 接口)', async ({ page }) => {
  // 拦截 /auth/login,返回伪造的 token
  await page.route('**/auth/login', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ code: 0, token: 'mock-token-123' }),
    });
  });
  await page.goto('http://localhost:5666/login');
  await page.getByPlaceholder('账号').fill('admin');
  await page.getByPlaceholder('密码').fill('123456');
  await page.getByRole('button', { name: '登录' }).click();
  // 断言:跳转后首页出现,且请求真的走 mock(没发真实网络)
  await expect(page).toHaveURL(/home/);
});

为什么 mock:测试不依赖后端在线、数据可控(不用造脏数据)、CI 上稳定跑。

四、表单完整链路(实战标准姿势)

增删改查表单是 E2E 的主战场,标准链路:

js
test('新增用户 → 列表刷新', async ({ page }) => {
  await page.goto('http://localhost:5666/system/user');
  // 1. 点新增按钮 → 弹窗
  await page.getByRole('button', { name: '新增' }).click();
  // 2. 弹窗填表(vben/antd 弹窗是 Radix Dialog:role=dialog)
  const dialog = page.getByRole('dialog');
  await dialog.getByLabel('用户名称').fill('测试用户');
  await dialog.getByLabel('手机号').fill('13800000000');
  // 3. 提交(mock 保存接口)
  await page.route('**/system/user', route => route.fulfill({ status: 200, body: '{"code":0}' }));
  await dialog.getByRole('button', { name: '确定' }).click();
  // 4. 断言:列表出现新行(mock 查询接口返回包含新数据)
  await expect(page.getByText('测试用户')).toBeVisible();
});

五、网络拦截高级:mock 列表数据

列表页 E2E 常用 route 直接 mock 分页数据,页面行为和真实数据无关:

js
await page.route('**/system/user/list?**', route => {
  route.fulfill({
    contentType: 'application/json',
    body: JSON.stringify({
      code: 0, rows: [{ id: 1, userName: 'Mock用户', phone: '13800000000' }], total: 1,
    }),
  });
});
await page.goto('http://localhost:5666/system/user');
await expect(page.getByText('Mock用户')).toBeVisible();  // 列表渲染 mock 数据

六、实跑验证(本地完整链路)

验证内容(一个真实测试跑通“登录 mock → 填表 → 提交 → 列表刷新”全链路):

js
// e2e-verify.mjs(Playwright)
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();
// 拦截登录 + 列表接口
await page.route('**/api/login', r => r.fulfill({ status: 200, contentType: 'application/json', body: '{"code":0,"token":"mock-token"}' }));
await page.route('**/api/users', r => r.fulfill({ status: 200, contentType: 'application/json', body: '{"data":[{"name":"BinMaker"}]}' }));
await page.goto('http://localhost:8080/');        // 本地测试页
await page.getByPlaceholder('账号').fill('admin');
await page.getByPlaceholder('密码').fill('123456');
await page.getByRole('button', { name: '登录' }).click();
await page.getByRole('button', { name: '加载列表' }).click();
await page.getByText('BinMaker').waitFor();        // 自动等待 mock 数据渲染
console.log('✅ E2E 全链路 PASS:登录 mock → 提交 → 列表渲染');
await browser.close();

实测输出✅ E2E 全链路 PASS——登录表单填写、按钮点击、网络拦截返回 mock 数据、列表渲染断言全部跑通。

小结

  • E2E = 用户视角全链路,Playwright 自动等待 + 语义化 locator 让脚本稳
  • 登录/列表 mockpage.route 拦截)是让 E2E 稳定、可重复的关键
  • 表单链路四步走:打开 → 填表 → 提交(mock 保存)→ 断言列表刷新

接下来可以看:

验证说明:登录 mock、表单填写、列表渲染断言用 Playwright 本地实跑验证(本地测试页 + route 拦截,全链路 PASS)。