Playwright 入门指南:从零跑通第一个 E2E 测试

Playwright 入门指南:从零跑通第一个 E2E 测试

Playwright 是微软开源的端到端(E2E)测试框架,支持 Chromium / Firefox / WebKit 三大浏览器引擎,跨 Windows、Linux、macOS,本地和 CI 都能跑。它最大的卖点就一句话:自动等待 + Web-first 断言,不需要手写 sleep,测试也不容易 flaky。这篇教程带你在 5 分钟内置好环境、写出第一个测试、看懂输出报告,Node.js 和 Python 两条线都覆盖。

目录

一、Playwright 是什么,为什么选它

传统 Selenium 测试最大的痛点是显式等待——你得自己写 WebDriverWait 或者 time.sleep(),写少了就 flaky,写多了就慢。Playwright 换了个思路:

  • 自动等待(Actionability):每次 click() / fill() 之前,框架自动检查元素是否可见、稳定、可交互,通过后才执行,不需要手动 sleep。
  • Web-first 断言:expect(locator).toBeVisible() 这类断言是异步重试的,会一直等到条件满足或超时,而不是”检查一次就 fail”。
  • 测试隔离:每个测试拿到一个全新的 BrowserContext,相当于独立浏览器 profile,测试之间 cookie / storage 互不干扰。
  • 多引擎支持:同一套测试跑 Chromium、Firefox、WebKit,跨浏览器覆盖在每次提交都有。
适合场景:Web 应用 E2E 验收测试、CI 流水线冒烟测试、浏览器自动化脚本(爬虫 / 截图 / 表单填充)。不适合单元测试——单元测试用 Jest / Vitest / pytest 就行。

二、Node.js 版:安装与初始化

前置要求

  • Node.js:最新 22.x / 24.x / 26.x
  • 系统:Windows 11+ / macOS 14+ / Debian 12-13 / Ubuntu 22.04+

初始化项目

# 在新项目目录下初始化(会生成 playwright.config.ts + tests/)
npm init playwright@latest

交互式提示会让你选:

  1. TypeScript 还是 JavaScript(默认 TS)
  2. 测试文件夹名(默认 tests,若已存在则 e2e)
  3. 是否加 GitHub Actions 工作流(CI 推荐选是)
  4. 是否安装浏览器二进制(默认是)

初始化完成后,项目结构大致如下:

playwright.config.ts     # 测试配置(浏览器、超时、重试、reporter)
package.json
tests/
  example.spec.ts        # 示例测试
注意:npm init playwright@latest 下载浏览器二进制可能需要几分钟(约 200 MB+),首次安装请耐心等待,不要中途 Ctrl+C。

三、Node.js 版:写第一个测试

打开 tests/example.spec.ts,Playwright 默认生成一个最小示例。下面是一个稍完整的版本,演示导航 + 点击 + 断言的完整流程:

import { test, expect } from '@playwright/test';

test('页面标题包含 Playwright', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  // 异步断言:自动等待标题包含 "Playwright",最多等默认 5s
  await expect(page).toHaveTitle(/Playwright/);
});

test('点击 Get started 链接后看到 Installation 标题', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  // 用 getByRole 定位,比 CSS 选择器稳定得多
  await page.getByRole('link', { name: 'Get started' }).click();
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

常用 Locators 速查

Playwright 推荐用角色(role)/ 标签 / 占位符定位,而不是脆弱的 CSS 路径。最常用几个:

// 按 ARIA 角色
page.getByRole('button', { name: 'Submit' });
page.getByRole('link',   { name: 'Get started' });
page.getByRole('heading', { name: 'Installation' });

// 按表单标签
page.getByLabel('Username');

// 按 placeholder
page.getByPlaceholder('Search');

// 按 data-testid(最稳定,推荐生产环境用)
page.getByTestId('submit-btn');

常用 Action 速查

await loc.click();              // 点击
await loc.fill('hello');        // 输入文本(会先清空)
await loc.press('Enter');       // 按键
await loc.hover();              // 悬停
await loc.check();              // 勾选 checkbox
await loc.selectOption('zh');   // 下拉框选值
await loc.setInputFiles('a.png'); // 上传文件

四、Python 版:pytest-playwright

Python 用户用 pytest-playwright 插件,API 几乎和 Node 版一一对应:

pip install pytest-playwright
playwright install

创建 test_example.py:

import re
from playwright.sync_api import Page, expect

def test_has_title(page: Page):
    page.goto("https://playwright.dev/")
    # 同步版 assert,自动重试直到标题匹配
    expect(page).to_have_title(re.compile("Playwright"))

def test_get_started_link(page: Page):
    page.goto("https://playwright.dev/")
    page.get_by_role("link", name="Get started").click()
    expect(page.get_by_role("heading", name="Installation")).to_be_visible()

运行:

# 默认只跑 chromium
pytest

# 指定浏览器
pytest --browser firefox

# 有头模式(可以看到浏览器窗口)
pytest --browser chromium --headed
Python 版 fixture 说明:page 是 pytest-playwright 自动注入的 fixture,每个测试拿到隔离的 BrowserContext,不需要手动 playwright.chromium.launch()。

五、运行、看报告、常用命令速查

Node.js 版常用命令

# 跑全部测试(默认并行,三引擎)
npx playwright test

# 只跑 chromium
npx playwright test --project=chromium

# 有头模式
npx playwright test --headed

# 跑单个文件
npx playwright test tests/example.spec.ts

# 按测试名过滤
npx playwright test -g "页面标题"

# UI 模式(watch + 时间旅行调试)
npx playwright test --ui

# 看 HTML 报告
npx playwright show-report

Python 版常用命令

# 默认 chromium,headless
pytest

# 指定 firefox
pytest --browser firefox

# 有头模式
pytest --browser chromium --headed

# 按文件跑
pytest test_example.py

# 按测试名
pytest -k "test_has_title"

六、核心概念:Locators、自动等待、隔离

自动等待 vs 显式等待

Playwright 的所有 action(click、fill 等)和 web-first 断言(expect(...).toBeVisible())都内置自动重试,默认超时 5 秒(可在 playwright.config.ts 里改)。你几乎永远不需要写 await page.waitForTimeout()。

什么时候才需要显式等待:

  1. 等待网络请求完成:await page.waitForResponse(url => ...)
  2. 等待某个 URL 跳转:await page.waitForURL(/dashboard/)
  3. 等待 Web API 状态(如 WebRTC 连接建立):这类 Playwright 自动等待覆盖不到,需要手动 waitForFunction

测试隔离(BrowserContext)

每个测试拿到一个独立的 BrowserContext,相当于开一个全新浏览器 profile:cookie、localStorage、sessionStorage 全部隔离。这意味着:

  • 测试 A 里登录了,测试 B 里是未登录状态——符合预期,互不污染
  • 想共享登录态:用 storageState 把登录后的 cookie 存下来,后续测试注入
// 保存登录态
test('login', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Username').fill('admin');
  await page.getByLabel('Password').fill('pass');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByText('Dashboard')).toBeVisible();
  await page.context().storageState({ path: 'auth.json' });
});

// 后续测试复用登录态(在 playwright.config.ts 里配置 storageState: 'auth.json')

test hooks

test.describe('navigation', () => {
  // 每个测试前执行
  test.beforeEach(async ({ page }) => {
    await page.goto('https://playwright.dev/');
  });

  // 所有测试前执行一次(worker 级别)
  test.beforeAll(async () => {
    // 初始化数据库、启动 mock server 等
  });

  test('main navigation', async ({ page }) => {
    await expect(page).toHaveURL('https://playwright.dev/');
  });
});

七、小结与下一步

五步上手口诀:

  1. 装 → npm init playwright@latest(或 pip install pytest-playwright)
  2. 写 → 用 getByRole / getByLabel / getByTestId 定位,用 expect(...).toBe... 断言,不写 sleep
  3. 跑 → npx playwright test(Node)/ pytest(Python),默认 headless + 并行
  4. 看 → 失败时跑 npx playwright show-report 看 HTML 报告 + trace
  5. 接 CI → 加 GitHub Actions 工作流,每次提交自动跑,跨浏览器覆盖

下一步可以深入的内容:

  • Trace Viewer:失败后回放每一步的 DOM 快照、网络请求、控制台日志
  • Codegen:录制操作自动生成测试代码
  • Test Fixtures:自定义 fixture 封装共享逻辑
  • CI 集成:GitHub Actions / GitLab CI 配置

有问题去 Playwright Discord 或 Stack Overflow 搜,社区响应很快。

YI IoT 云蚁物联 b091qp 摄像头救砖教程:分区备份 + reset 按钮法(yi-hack 刷砖处理)

其实这台摄像头在今年 3 月份的时候就不慎刷成砖了,当时反复研究都没能救回来,只能搁在一边落灰许久。直到最近村里有些事需要调监控,结果又碰上另一台摄像头无法识别内存卡的状况,回放也看不了。

今天下单了一台说得过去的品牌摄像头,就想着还是看看能不能把这台刷坏的救回来——哪怕就当个红灯亮着的摆设也好。于是决定试试:不拆硬件、不焊接的前提下能不能修好。

由于访问仓库的网络卡得转圈圈,且仓库文档又全是英文的,干脆将仓库地址丢给 AI 进行分析,按它给的命令一条一条跑,没想到真的就成了。

继续阅读YI IoT 云蚁物联 b091qp 摄像头救砖教程:分区备份 + reset 按钮法(yi-hack 刷砖处理)

站点维护记录:切换 PHP 版本并更新插件

Debian 12 混装跨版本包导致依赖崩盘:一次完整的抢救记录(从装不上 tmux 到 Python 全恢复)

这台 Debian 12 (bookworm) 上原本想装个 tmux,结果一路排下去发现系统被手动塞进了一批 testing/trixie 的新版本包(libtinfo6、ncurses、systemd、openssh…),跟 bookworm 的依赖矩阵彻底对不上,最后连 python3、openssh-server、cmake 都被拖下水卸掉了。

这篇记录完整的排查与修复过程:怎么把整批”跨版本包”一次性降回原生版本、再把被卸掉的关键包装回来,全程不留坑。

继续阅读Debian 12 混装跨版本包导致依赖崩盘:一次完整的抢救记录(从装不上 tmux 到 Python 全恢复)

使用 FNM 更新 Node 版本

最近有太多漏洞了……运行着本站的 PHP 环境在发布补丁之后就匆匆忙的进行更新,刚更新 PHP 的版本没多久后又看到了 Node 有高危漏洞需要修复。

虽然当前没有 Node.js 的程序对外提供服务,但还是将自己本地的 Node 进行一番更新升级才为妙……

继续阅读使用 FNM 更新 Node 版本