Playwright自动化测试入门:从核心原理到实战应用

Playwright自动化测试入门:从核心原理到实战应用

1. 项目概述:为什么是 Playwright?

如果你正在寻找一个能帮你搞定现代 Web 应用自动化测试和脚本编写的工具,那么 Playwright 这个名字你肯定绕不开。它早已不是那个“后起之秀”,而是成为了许多团队在构建可靠、高效自动化流程时的首选。我最初接触它,是因为厌倦了处理那些因页面动态加载、元素异步渲染而频繁失败的脚本。Playwright 的出现,几乎完美地解决了这些痛点。

简单来说,Playwright 是一个由微软开源的 Node.js 库,它提供了一个统一的 API 来驱动 Chromium、Firefox 和 WebKit 三大浏览器引擎。这意味着,你用一套代码,就能在几乎所有现代浏览器环境下运行你的自动化脚本或测试用例。这听起来可能和 Selenium 很像,但 Playwright 在设计之初就瞄准了现代 Web 应用(SPA、PWA 等)的复杂性,其内置的“自动等待”机制、强大的定位器策略以及对网络请求的拦截能力,让它在实际项目中表现出了惊人的稳定性和开发效率。

这个“初识”系列,我会从一个实战者的角度,带你从零开始,一步步拆解 Playwright。我们不会只停留在“Hello World”,而是直接深入到那些你在真实项目中一定会遇到的问题:如何应对动态内容?如何编写健壮的定位器?如何管理测试状态?以及,如何将 Playwright 无缝集成到你的开发工作流中,甚至让 AI 助手帮你写脚本?让我们开始吧。

2. 核心设计理念:超越传统自动化框架

在深入代码之前,理解 Playwright 的设计哲学至关重要。这能帮你明白,为什么在某些场景下,它的选择会如此不同,以及这些选择如何最终转化为你的开发体验和脚本的稳定性。

2.1 架构对比:Playwright vs. Selenium vs. Puppeteer

很多开发者会问,已经有了 Selenium 和 Puppeteer,为什么还需要 Playwright?我们可以从几个关键维度来对比:

特性维度Selenium WebDriverPuppeteerPlaywright
浏览器支持多浏览器(通过各浏览器驱动)仅 Chromium/ChromeChromium, Firefox, WebKit(统一 API)
通信协议基于 JSON Wire Protocol / W3C WebDriverChrome DevTools Protocol专有协议(基于 CDP 扩展,更高效)
自动等待需要手动设置显式/隐式等待部分 API 支持,不完整内置自动等待(几乎所有操作都等待元素可交互)
测试隔离会话级,隔离成本高上下文(Context)级浏览器上下文(Context)级,轻量且快速
网络拦截有限支持,复杂强大强大,且 API 更直观
多页面/标签支持,但管理稍复杂支持原生支持,并行处理能力强
移动端模拟需要附加配置良好优秀,内置大量设备描述符

核心差异解读

  1. 协议层优势:Playwright 没有使用标准的 WebDriver 协议,而是为每个浏览器引擎开发了定制化的协议。这听起来像“不兼容”,但带来的好处是巨大的:更快的执行速度、更稳定的连接,以及能够实现 WebDriver 协议无法支持的底层操作(比如精确的输入模拟、可靠的页面生命周期监听)。
  2. “Web-First”断言与自动等待:这是 Playwright 稳定性的基石。在 Selenium 中,你点击一个按钮的代码element.click()可能瞬间执行,即使按钮还没渲染出来或不可点击,导致失败。Playwright 的几乎所有操作(click, fill, check 等)都内置了等待。它会自动等待元素满足一系列可操作性条件(如可见、启用、稳定等)后才执行动作。同样,它的断言(如expect(page).toHaveTitle(...))也是“Web-First”的,会自动重试直到条件满足或超时,彻底告别了“Flaky Tests”(不稳定的测试)中常见的sleep语句。
  3. 浏览器上下文(Browser Context):这是 Playwright 中一个核心且高效的概念。你可以把它理解为一个完全独立的浏览器会话,它拥有独立的 cookies、localStorage、会话历史,但共享同一个浏览器进程。创建和销毁一个 Context 的代价远低于启动一个全新的浏览器实例。这使得为每个测试用例提供干净的隔离环境变得非常廉价,是实现并行测试和状态隔离的关键。

实操心得:从 Selenium 迁移到 Playwright,最大的思维转变就是要信任框架的自动等待。初期你可能会不自觉地加上page.waitForTimeout(3000)这类语句,请尽量戒掉这个习惯。转而使用page.waitForSelector配合状态,或者更优雅地,直接依赖 Playwright 操作自身的等待能力。你的代码会简洁得多,也稳定得多。

2.2 核心组件生态:不止于“测试”

Playwright 不仅仅是一个测试库,它已经发展成一个围绕浏览器自动化的工具生态。根据你的使用场景,可以选择不同的入口:

  1. Playwright Test (测试运行器):这是进行端到端(E2E)测试的推荐方式。它是一个功能完整的测试框架,提供测试运行、并行化、报告生成、追踪记录等功能。npm init playwright@latest这个命令会帮你搭建好一个最佳实践的测试项目结构。
  2. Playwright Library (核心库):如果你不需要测试运行器的功能,只是想写一个自动化的脚本(比如爬虫、截图工具、PDF生成器),那么直接安装playwright库即可。它提供了所有驱动浏览器的底层 API。
  3. Playwright CLI (命令行工具):这是一个为AI 编程助手(如 Claude Code, GitHub Copilot)优化的工具。它通过命令行提供浏览器自动化能力,让 AI 可以更高效地理解和执行网页操作任务,避免了将复杂的浏览器状态灌入 AI 上下文的开销。
  4. Playwright MCP (模型上下文协议服务器):这是让 AI 智能体(如 Claude Desktop 中的助手)直接控制浏览器的桥梁。AI 通过结构化的可访问性树来“看到”网页,并使用工具进行交互,实现了确定性的自动化,而非依赖容易出错的视觉模型。
  5. VS Code 扩展:提供了图形化的测试运行、调试、代码录制(CodeGen)和定位器拾取功能,极大提升了开发体验。

对于大多数从零开始的开发者,我建议的路径是:先通过Playwright Test来学习和实践,因为它提供了最全面的脚手架和最佳实践。当你熟悉了核心 API 后,再根据需求切换到 Library 模式或探索 CLI/MCP 与 AI 的结合。

3. 环境搭建与核心配置详解

理论说再多,不如动手跑一遍。我们来一步步搭建一个坚实的 Playwright 开发环境。

3.1 安装:避开网络与依赖的坑

官方推荐的一站式安装命令是:

npm init playwright@latest

这个命令会交互式地引导你:

  1. 选择使用 JavaScript 还是 TypeScript(强烈推荐 TypeScript,Playwright 对其支持极佳)。
  2. 选择测试目录位置。
  3. 是否添加 GitHub Actions 工作流。
  4. 是否立即安装浏览器。

命令执行后,它会自动完成以下工作:

  • 创建一个新的 npm 项目或更新现有项目。
  • 安装@playwright/test包(测试运行器)和playwright核心库。
  • 在项目根目录生成playwright.config.ts配置文件。
  • 生成一个基础的tests/目录和示例测试文件。
  • 运行playwright install来下载所需的浏览器二进制文件(Chromium, Firefox, WebKit)。

常见问题与解决方案实录

问题1:playwright install下载浏览器极慢或失败。这是国内开发者最常遇到的问题,因为二进制文件托管在谷歌存储上。

  • 最佳解决方案:配置镜像源。Playwright 支持通过环境变量指定下载镜像。
    # 在命令行中设置,仅对当前命令生效 PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright npx playwright install
    • 常用镜像源:https://npmmirror.com/mirrors/playwright(阿里云镜像) 或https://cdn.npmmirror.com/binaries/playwright
  • 持久化配置:你可以将环境变量写入 shell 配置文件(如~/.bashrc~/.zshrc),一劳永逸。
    # 添加到 ~/.bashrc 或 ~/.zshrc export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright
  • 手动下载:如果镜像也不行,可以尝试从镜像站手动下载对应版本的压缩包,放到 Playwright 的缓存目录中(通常位于~/.cache/ms-playwright),再运行安装命令。

问题2:在 CentOS 7 等老系统上安装失败,提示 GLIBC 版本过低。错误信息可能类似playwright install chromium centos 7 + glibc 2.17 + playwright 1.54.0所描述。

  • 根本原因:Playwright 下载的 Chromium 二进制文件需要较新版本的 GLIBC(如 2.18+),而 CentOS 7 默认是 2.17。
  • 解决方案
    1. 升级系统 GLIBC:风险极高,不推荐,可能破坏系统稳定性。
    2. 使用 Docker这是最推荐的方式。在容器内运行 Playwright,完全隔离系统依赖。官方也提供了 Playwright Docker 镜像。
    3. 寻找兼容的旧版本:尝试安装稍旧版本的 Playwright,其对应的 Chromium 可能对 GLIBC 要求较低。但这不是长久之计。
    4. 使用已编译的替代包:有些社区镜像可能提供了兼容老 GLIBC 的版本,但需自行甄别安全性。

实操心得:对于企业级或长期项目,从一开始就使用 Docker是最佳实践。这保证了所有开发、测试、CI 环境的一致性。你可以使用mcr.microsoft.com/playwright官方镜像,它包含了所有依赖和浏览器。

3.2 解剖playwright.config.ts:你的项目控制中心

安装完成后,你会得到一个配置文件。别急着跳过,理解它能让你的测试能力提升一个档次。

import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // 测试文件的位置 testDir: './tests', // 并行运行所有测试文件 fullyParallel: true, // 每个测试文件内,默认禁止并行,避免状态干扰。可根据需要开启。 forbidOnly: !!process.env.CI, // 失败重试次数,在CI环境中很有用 retries: process.env.CI ? 2 : 0, // 默认每个工作进程(worker)运行的测试文件数。`null` 表示一个文件一个worker。 workers: process.env.CI ? 1 : undefined, // 报告器配置 reporter: 'html', // 共享的“use”配置,适用于所有项目(浏览器) use: { // 基础URL,这样测试中可以用相对路径:`await page.goto('/admin')` baseURL: 'http://localhost:3000', // 收集失败时的追踪信息,'on-first-retry' 表示只在第一次重试时收集(节省资源) trace: 'on-first-retry', // 录制失败测试的视频 video: 'retain-on-failure', // 截图配置,仅在失败时截图 screenshot: 'only-on-failure', }, // 配置不同的“项目”,即在不同浏览器/环境下运行测试 projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, // 模拟移动端 { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, }, { name: 'Mobile Safari', use: { ...devices['iPhone 12'] }, }, // 一个专门用于登录并保存认证状态的项目 { name: 'setup', testMatch: /.*\.setup\.ts/, // 只运行 setup 文件 teardown: 'chromium', // 指定 setup 完成后,哪个项目依赖它 }, { name: 'chromium with auth', use: { ...devices['Desktop Chrome'], // 复用 setup 项目中保存的存储状态 storageState: 'playwright/.auth/user.json', }, dependencies: ['setup'], // 声明依赖 setup 项目 }, ], // 本地开发服务器配置,在运行测试前启动 webServer: { command: 'npm run start', // 启动你本地应用的命令 url: 'http://localhost:3000', // 等待这个URL可访问 reuseExistingServer: !process.env.CI, // CI环境中不重用 timeout: 120 * 1000, // 启动超时时间 }, });

关键配置解析

  • workers: 控制并行度。如果你的测试是纯前端且无副作用,可以设置为undefined(CPU核心数)。如果有数据库操作等,可能需要设为 1 或更少。CI 环境中通常设为 1 以保证稳定性。
  • trace: 'on-first-retry':这是调试神器。当测试失败时,会生成一个trace.zip文件。使用npx playwright show-trace trace.zip命令打开一个可视化界面,你可以回溯测试执行的每一步,查看当时的 DOM 快照、网络请求、控制台日志,极大简化了失败原因排查。
  • projects:这是 Playwright 非常强大的功能。你可以定义多套测试环境。上面的配置示例展示了:
    1. 在三大桌面浏览器运行测试。
    2. 在两种移动端模拟器运行测试。
    3. 实现全局登录状态共享:通过一个setup项目执行登录,并将storageState保存为文件。其他项目(如chromium with auth)通过dependencies依赖它,并加载这个状态文件,避免了每个测试都重复登录,提升了效率。

4. 编写第一个健壮的测试脚本

现在,让我们抛开简单的示例,写一个应对“动态内容”这种现代 Web 应用常见挑战的测试。

4.1 定位器策略:告别脆弱的 XPath/CSS Selector

录制脚本最常见的失败原因就是动态内容。页面上的 ID、类名可能随时变化,依赖它们编写的选择器非常脆弱。Playwright 推荐使用“面向用户”的定位器

假设我们有一个待办事项应用(如https://demo.playwright.dev/todomvc)。

传统脆弱的方式(尽量避免):

await page.click('#new-todo'); // ID 可能变 await page.fill('.todo-input', 'Buy milk'); // 类名可能变

Playwright 推荐的方式:

import { test, expect } from '@playwright/test'; test('添加并完成一个待办事项', async ({ page }) => { // 1. 导航 await page.goto('https://demo.playwright.dev/todomvc'); // 2. 使用 getByRole:这是最接近用户感知的方式(ARIA 角色) const input = page.getByRole('textbox', { name: 'What needs to be done?' }); await input.fill('Buy groceries'); await input.press('Enter'); // 3. 使用 getByText 或 getByTestId 定位新增的条目 // getByText 适用于静态文本 const newTodo = page.getByText('Buy groceries'); await expect(newTodo).toBeVisible(); // 4. 对于交互元素,getByRole 同样适用 const toggle = page.getByRole('checkbox', { name: 'Toggle Todo' }).first(); // 获取第一个 await toggle.check(); await expect(newTodo).toHaveCSS('text-decoration-line', 'line-through'); // 5. 使用 getByTestId 是最稳定的方式,需要开发配合添加>await page.click('#load-more'); await page.waitForTimeout(5000); // ❌ 糟糕!如果2秒就加载完了,浪费3秒;如果6秒才加载完,会失败。

正确示范:使用 Playwright 的等待方法

// 场景1:等待某个元素出现 await page.click('#load-more'); await page.waitForSelector('.new-item', { state: 'visible' }); // 等待类为 new-item 的元素可见 // 场景2:等待网络请求完成(非常有用!) // 点击按钮后,会发起一个获取列表的 API 请求 const [response] = await Promise.all([ page.waitForResponse(resp => resp.url().includes('/api/items') && resp.status() === 200), page.click('#load-more'), ]); // 此时可以断言响应数据 const items = await response.json(); expect(items).toHaveLength(10); // 场景3:等待页面导航完成 await page.getByRole('link', { name: 'Next Page' }).click(); await page.waitForURL('**/page-2'); // 使用通配符匹配URL // 场景4:等待元素状态变化(如按钮从禁用变为启用) const submitButton = page.getByRole('button', { name: 'Submit' }); await submitButton.waitFor({ state: 'enabled' }); await submitButton.click();

处理列表和动态内容

test('动态列表的断言', async ({ page }) => { await page.goto('/dynamic-list'); // 方法1:使用 `locator` 和 `count` const listItems = page.locator('.list-item'); await expect(listItems).toHaveCount(10); // 断言初始数量 await page.click('#load-more'); // 等待列表数量增加 await expect(listItems).toHaveCount(20); // 方法2:遍历列表项进行复杂断言 const allTexts = await listItems.allTextContents(); expect(allTexts).toContain('Expected Item'); // 方法3:使用 `filter` 定位特定条件的项 const completedItems = listItems.filter({ hasText: 'Done' }); await expect(completedItems).toHaveCount(5); });

5. 高级技巧与实战问题排查

掌握了基础,我们来看看如何让 Playwright 脚本更强大、更易维护。

5.1 页面对象模型(POM)模式

对于中大型项目,必须对页面逻辑进行封装。POM 将页面元素和操作封装成类,提高代码复用性和可读性。

示例:登录页面对象

// pages/LoginPage.ts import { Locator, Page } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.usernameInput = page.getByLabel('Username or email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Sign in' }); this.errorMessage = page.getByTestId('login-error'); } async goto() { await this.page.goto('/login'); } async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.submitButton.click(); } async getErrorMessage() { await this.errorMessage.waitFor({ state: 'visible' }); return await this.errorMessage.textContent(); } }

在测试中使用

// tests/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; test('用户登录成功', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('validUser', 'validPass'); await expect(page).toHaveURL('/dashboard'); }); test('用户登录失败显示错误信息', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalid', 'invalid'); const errorText = await loginPage.getErrorMessage(); expect(errorText).toContain('Invalid credentials'); });

5.2 夹具(Fixtures)与全局配置

Playwright Test 的夹具系统非常强大,可以用于注入页面对象、设置测试数据、处理认证等。

自定义夹具示例

// fixtures.ts import { test as baseTest } from '@playwright/test'; import { LoginPage } from './pages/LoginPage'; import { DashboardPage } from './pages/DashboardPage'; // 声明夹具类型 export type MyFixtures = { loginPage: LoginPage; dashboardPage: DashboardPage; authenticatedPage: Page; // 一个已登录的页面 }; // 扩展基础的 test 夹具 export const test = baseTest.extend<MyFixtures>({ // 一个自动创建页面对象的夹具 loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await use(loginPage); }, dashboardPage: async ({ page }, use) => { const dashboardPage = new DashboardPage(page); await use(dashboardPage); }, // 一个更复杂的夹具:自动完成登录并返回一个已认证的页面上下文 authenticatedPage: async ({ browser }, use) => { // 创建一个新的浏览器上下文,独立于默认的测试上下文 const context = await browser.newContext(); const page = await context.newPage(); const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('test-user', 'test-pass'); // 验证登录成功 await expect(page).toHaveURL('/dashboard'); // 将这个已登录的页面传递给测试使用 await use(page); // 测试结束后,清理上下文 await context.close(); }, }); export { expect } from '@playwright/test';

在测试中使用自定义夹具

// tests/dashboard.spec.ts import { test, expect } from './fixtures'; // 导入自定义的 test // 使用 authenticatedPage 夹具,测试开始时已处于登录状态 test('已登录用户可以看到仪表板', async ({ authenticatedPage }) => { // authenticatedPage 已经导航到 /dashboard 并登录 await expect(authenticatedPage.getByText('Welcome back')).toBeVisible(); }); // 使用普通的页面对象夹具 test('使用页面对象进行测试', async ({ loginPage, dashboardPage }) => { await loginPage.goto(); await loginPage.login('user', 'pass'); await dashboardPage.expectToBeLoaded(); await dashboardPage.createNewProject('My Project'); });

5.3 常见问题排查技巧实录

即使有了最佳实践,脚本仍可能失败。以下是快速排查的清单:

问题1:元素定位失败,报错TimeoutError: page.waitForSelector: Timeout 30000ms exceeded

  • 检查点1:元素真的在页面上吗?运行测试时加上--headed参数(npx playwright test --headed)在浏览器中观察。或者,在失败时自动截图的配置会帮你。
  • 检查点2:你的定位器是否唯一?使用 Playwright 的“选择器探测器”。在 VS Code 扩展中,或者运行npx playwright codegen打开浏览器,点击“拾取定位器”按钮,查看框架推荐的最佳定位器。
  • 检查点3:是否在 iframe 内?如果是,需要先切换到 iframe 上下文:const frame = page.frameLocator('iframe[name="content"]');然后frame.locator('button').click()
  • 检查点4:是否有阴影DOM?使用element.locator('>>> button'):light选择器穿透阴影边界(具体语法取决于 Playwright 版本)。

问题2:操作(如 click, fill)失败,报错Element is not visibleElement is disabled

  • 原因:Playwright 的自动等待机制发现元素不满足可操作条件。
  • 排查
    1. 使用trace功能!这是最强大的工具。查看失败前一刻的 DOM 快照,确认元素状态。
    2. 元素可能被其他元素(如弹窗、遮罩层)覆盖。尝试先关闭覆盖物。
    3. 元素可能是动态渲染的,你的操作太快。虽然 Playwright 会等待,但有时需要更明确的等待。尝试在操作前加await element.waitFor({ state: 'attached' })
    4. 对于自定义的下拉框、日期选择器等复杂组件,可能需要直接触发 JavaScript 事件:await page.evaluate(() => document.querySelector('custom-dropdown').open());

问题3:测试在 CI(如 GitHub Actions)上通过,本地却失败,或反之

  • 网络与环境差异:CI 环境可能无法访问你的本地localhost:3000。确保playwright.config.ts中的baseURL在 CI 中指向正确的测试环境地址,并使用webServer配置在 CI 中启动应用。
  • 资源加载:CI 环境可能网速慢。适当增加page.goto或等待操作的超时时间:await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 })
  • 浏览器版本:确保 CI 和本地安装了相同版本的 Playwright 和浏览器。在package.json中固定 Playwright 版本,并在 CI 脚本中运行npx playwright install --with-deps

问题4:如何调试一个卡住或失败的测试?

  1. 使用--debug标志npx playwright test --debug。这会以 headed 模式打开浏览器,并在测试开始时暂停,允许你一步步执行。
  2. 在代码中插入暂停await page.pause();。运行测试时,Playwright 会打开浏览器并停在此处,你可以打开开发者工具检查。
  3. 使用追踪(Trace):如前所述,配置trace: 'on''on-first-retry',失败后用npx playwright show-trace进行事后分析。
  4. 打印页面内容:在关键步骤后添加console.log(await page.content())console.log(await page.url())来了解测试执行状态。

6. 与 AI 协同:Playwright CLI 与 MCP 的惊艳体验

最后,让我们看看 Playwright 生态中最前沿的部分:与 AI 编程助手的结合。这能极大提升编写自动化脚本的效率。

Playwright CLI的设计目标就是成为 AI 的“手”。当你对 Claude Code 或 Copilot 说:“帮我在这个页面上填写表单并提交”,AI 可以通过调用playwright-cli命令来执行具体的浏览器操作,而不是生成可能出错的代码。你需要全局安装它:npm i -g @playwright/cli。它的命令非常直观,例如:

  • playwright-cli open https://example.com --headed
  • playwright-cli get-by-role textbox --name "Email" --fill "test@example.com"
  • playwright-cli screenshot --full-page

AI 可以将这些命令组合起来,完成一个多步骤的任务。playwright-cli show命令可以打开一个监控面板,实时查看所有自动化会话的屏幕录像,非常酷。

Playwright MCP则更进一步,它通过 Model Context Protocol 让 AI 智能体(如 Claude Desktop)直接获得浏览器的控制能力。AI 看到的不是像素截图,而是结构化的可访问性树,这比视觉识别要精确和稳定得多。AI 通过元素引用(如e5)来执行点击、输入等操作。这意味着你可以用自然语言对 AI 说:“导航到电商网站,搜索‘无线耳机’,按价格排序,把前三款产品的标题和价格列出来。” AI 可以理解并执行这一系列操作。

对于开发者而言,即使不直接使用 MCP,理解其原理也很有帮助。它揭示了未来人机交互的一种可能:用自然语言描述复杂任务,由 AI 驱动工具完成。你可以将 Playwright MCP 集成到你的 IDE 或 AI 助手中,体验这种“言出法随”的自动化。

从“初识”开始,我们梳理了 Playwright 的核心价值、环境搭建、脚本编写的最佳实践、高级模式以及问题排查。真正的精通源于在真实项目中的反复实践和踩坑。我的建议是,找一个你熟悉的、有点复杂的 Web 应用,尝试用 Playwright 为它编写一些自动化脚本。从登录开始,到完成一个核心业务流程。在这个过程中,你会遇到各种具体问题,而解决这些问题的经验,才是最有价值的。记住,多利用tracedebug工具,它们是你最好的朋友。