如果你还停留在“自动化测试 = Selenium + WebDriver + 各种隐式等待”的旧时代,那 Playwright 这个来自微软的开源自动化测试框架,很可能会刷新你对浏览器自动化的所有认知。它现在是我日常做端到端测试的第一选择,没有之一。这篇文章我想从项目本身出发,把它的设计思路、核心细节、实际落地的步骤、以及我这两年用它在真实项目中踩过的坑和摸索出来的技巧,一块儿打包分享给你。内容会比较扎实,适合正打算在团队里引入 Playwright、或者想把手头已经有点乱了的自动化项目彻底重构一遍的朋友。
1. 项目整体设计与核心思路拆解
1.1 Playwright 是谁做出来的,它到底想解决什么
我先简单说一下背景。Playwright 由微软持续维护,核心代码开源,GitHub 上活跃度和关注度都相当高。它诞生的直接原因是微软在做 Edge 浏览器和 Azure 相关的云测试服务时,发现市面上已有的浏览器自动化方案,在处理现代单页应用、多标签交互、复杂网络请求拦截、以及跨浏览器一致性这些问题上,总是差那么一口气。于是干脆自己造了一套更“现代”的工具。
这里的“现代”两个字,是理解 Playwright 设计思想的关键。过去的自动化测试框架,比如老牌的 Selenium WebDriver,核心思路是“模拟用户操作”:定位元素、点击、输入、断言,本质上是在模仿一个人类坐在电脑前干活。这套思路没有问题,但它有几个积重难返的痛点:一个是等待时序控制,你永远要在“元素还没加载完成”和“元素已经被 DOM 移除”之间反复调试;另一个是浏览器的兼容性差异,每个浏览器厂商都实现一套自己的 WebDriver 协议,行为不一致,排查起来特别痛苦。
Playwright 换了个底层思路,它直接通过 Chrome DevTools Protocol(CDP)和浏览器进行原生通信。这意味着它不是在浏览器外面“遥控”,而是直接住进了浏览器引擎的核心。所有操作都基于浏览器内部的真实状态,时序问题、协议兼容问题,在架构层面就被消除了一大半。
1.2 项目选型背后的技术优势:为什么根本不虚 Selenium
很多朋友问我,既然 Selenium 用了这么多年、社区里教程一堆堆的,团队里也早就跑着基于它写的用例,我为什么要带着大家冒险换到 Playwright。我的答案一直很直接:你不是在冒险,你是在清债。Selenium 的项目越跑越久,维护成本是成指数增长的,而 Playwright 从根上把你最头疼的几类问题直接炸掉了。
首先是自动等待机制。Playwright 里所有的操作,比如点击、输入、查找元素,默认都会在一段可配置的超时时间内持续重试,直到元素出现、可见、可交互。你在脚本里基本上不需要写sleep(3000),也不需要用WebDriverWait配上一堆expected_conditions。我用 Selenium 写脚本时,大概三分之一的代码都是等待逻辑,到了 Playwright 这边,这部分工作量直接归零。
其次是多标签页和弹窗的处理。传统 WebDriver 的模式,你需要先拿到 window handle,然后switch_to.window,再干别的事。而 Playwright 用了浏览器上下文(Browser Context)这个概念,它和浏览器窗口完全解耦。每个上下文都是一套独立的存储、Cookie、会话状态。你在一个上下文里打开多少个 Tab,每个 Tab 都是一个独立的 Page 对象,随时可以切过去操作,不需要焦头烂额地换 handle。这在测试多标签、第三方登录回调这些场景的时候,幸福感非常明显。
还有测试隔离。Playwright 对每个测试用例默认提供全新的上下文环境,相当于每一个用例都从一个“干净饥饿”的浏览器状态开始。这让并发执行变得非常安全,你不用担心用例 A 的登录状态跑到用例 B 里去。这一点在搭建大型自动化测试体系时是决定性的优势。
1.3 这套方案的整体架构和执行模型
我要特别说一个容易被人忽略的架构特性:Playwright 的测试执行是通过一个单独的 Test Runner 来驱动的,它不是单纯意义上的“库”。Playwright 官方推荐的用法是用@playwright/test,它自带断言、测试组织、并行执行、失败重试、截图对比、以及 HTML 测试报告等能力。
整个执行链路大概是这样的:
- 你用
test('用例名称', async ({ page }) => { ... })定义用例 - Runner 启动浏览器进程,通常是 Chromium、Firefox、WebKit 三个引擎之一
- 每个测试启动时,Runner 自动创建 Browser Context,隔离状态
- 操作和断言通过 CDP 层的网络通道直接下发到浏览器
- 测试结束后,Runner 自动关闭浏览器进程,回收资源
这个架构让单机并发变得非常轻松。我之前用 Selenium 搭过分布式测试环境,需要额外部署 Selenium Grid 节点,还得处理节点之间的负载和注册问题。Playwright 默认就是多进程并行的,一个 runner 进程对应一个浏览器实例,CPU 利用率直接拉满。我在平时项目中跑几百条用例,按 CPU 核数开并行,分分钟跑完,这体感是完全不同的。
2. 核心功能与关键技术细节解析
2.1 选择器引擎:你以后很可能不用再写脆弱的 XPath 了
Playwright 的定位系统是它另一个特别值得学习的设计。它支持 CSS 选择器、XPath,但更推荐的是它自己封装的自定义选择器策略。比如getByRole、getByText、getByLabel、getByPlaceholder。这套 API 的设计哲学是:不要依赖 DOM 结构,要站在“用户怎么看待这个页面”的角度去找元素。
举个例子,传统 XPath 写法可能是:
//*[@id="login-form"]/div[2]/inputPlaywright 推荐的是:
page.get_by_label("用户名").fill("test_user") page.get_by_role("button", name="登录").click()你可以感受一下,这两者之间的区别不仅仅是代码更易读了。getByRole背后用的是无障碍树(Accessibility Tree),是浏览器已经解析渲染好的语义结构。只要按钮上的文字还叫“登录”,你根本不在乎它是button标签还是什么自定义组件。这就是“面向用户语义操作页面”,而不是“面向 HTML 标签操作页面”。这套思路在实际项目里,至少能减少一半因为前端改样式导致的用例挂掉。
2.2 auto-waiting:你不用再关心页面到底加载完了没有
我刚提到 Playwright 内置了自动等待,但这背后的机制值得说清楚。它不是简单地在操作前加一个固定延时,而是对元素做多条件轮询:元素是否附着在 DOM 中、是否可见、是否稳定不抖动、是否被其他元素覆盖遮挡、是否处于可接收事件的“稳定态”。只要其中一个条件不满足,操作就会持续等待,直到超时。
这个特性的价值,唯有在真实项目里被 Selenium 的花式 time.sleep 折磨过的人才能体会。特别是现代前端框架渲染太快,页面骨架和数据加载是异步的,你永远不知道某个按钮为什么就诡异地点不中。用了 Playwright 之后,这类问题就是根本不用去查,因为框架层帮你把状态处理干净了。
当然,auto-waiting不是万能的,它主要关心的是“这个元素此刻能否被操作”。如果你的业务逻辑有更复杂的异步流程,比如两个接口返回后某个变量才正确渲染,那还是需要显式地使用expect(locator).to_have_text()这类断言式等待。Playwright 把这些等待断言做成了 Web 一等公民,名副其实的“向用户真实看到的东西进行断言”。
2.3 网络拦截与 Mock:把后端不稳定因素彻底屏蔽掉
现代复杂系统的测试最怕什么?前端逻辑跑起来很顺,后端接口时不时抽风返回个 500,然后用例就红了,排查半天发现压根不是代码 bug。Playwright 强大的网络层接口可以让你直接拦截请求、修改响应、模拟异常场景。
比如我可以很轻松地对一个接口做 mock:
page.route("**/api/login", lambda route: route.fulfill( status=200, content_type="application/json", body='{"code": 0, "msg": "ok", "token": "fake_token"}' ))这让我可以在完全不依赖测试环境的情况下,把前端在“登录成功 / 登录失败 / 接口超时 / 返回数据缺失”等不同状态下的表现全部覆盖掉。这是其他框架很难做到的精致控制。
我还特别喜欢用page.route来做前端性能分析,把脚本里所有请求的耗时统计拉出来,用少量代码拿到每个请求的响应时间、资源大小。这在纯手工的无谓调试阶段,确实帮了很多忙。
2.4 脚本录制与追踪:降低上手门槛但不止于新手
Playwright 提供了一款叫playwright codegen的命令行工具,可以把你手工在浏览器里的操作自动生成对应的测试代码。很多初学者把 codegen 当成“不写代码光点点点就能完成自动化”的傻瓜工具,这就有点误解了。
codegen 更合理的定位是原型快速生成器。我个人的建议是,用 codegen 先把核心路径操作录一遍,生成的代码作为骨架,然后自己再手工改写选择器、补断言、处理异常分支。这让建脚本的前期体力活轻松不少,但真正的价值——把用例写得稳定、精准、面向业务语义——还是得靠人来设计。
还有一点值得强调:Playwright 的测试报告天然带着每个步骤的trace 文件,录制了所有 DOM 快照、网络请求、控制台日志以及调用栈。用例失败的时候,打开 trace 就能看到操作前后的完整现场。这种问题定位能力,比干看日志和截图效率高出太多了。
3. 实操过程:从零到一跑通一个真实自动化项目
3.1 项目初始化与安装细节
我这部分会以 Node.js 环境为例跑一遍,因为@playwright/test在 JavaScript/TypeScript 生态里是最顺滑的。当然 Python 版也一样成熟,你可以在安装时按语言偏好选。
先准备一个空目录,初始化 npm 项目:
mkdir my-e2e-project cd my-e2e-project npm init -y然后安装 Playwright 测试库和对应浏览器:
npm install -D @playwright/test npx playwright installnpx playwright install这个命令是把 Chromium、Firefox、WebKit 的二进制都下载到本地缓存目录。默认下载三个浏览器会有点费时间,你可以按需加参数,比如只装 Chromium:
npx playwright install chromium如果你是在服务器或者容器环境里跑,可能还需要装一些系统依赖:
npx playwright install-deps此命令会帮你搞定常见的 Linux 环境问题,省去一堆人工排障。
3.2 用配置文件的几个核心参数实际控制测试规模
在项目根目录创建playwright.config.ts:
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests', timeout: 30_000, expect: { timeout: 5_000, }, fullyParallel: true, retries: process.env.CI ? 2 : 0, reporter: [ ['html', { outputFolder: 'playwright-report' }], ['json', { outputFolder: 'test-results', outputFile: 'results.json' }], ], use: { baseURL: 'https://staging.example.com', trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, ], });配置里几个比较容易忽略的点:
timeout是单个测试用例的总体超时时间,expect.timeout是每个断言的等待时间。两者配合好,用例才不会因为单个接口卡死而拖垮整个 suite。retries在 CI 环境里设成 2 是一个很务实的策略,因为线上流水线远比本地环境不稳定。本地调试建议 0,可以及时发现新鲜问题。trace建议设置为on-first-retry,这样既不会每一次运行都记录(文件挺大),又能保证重试失败时有现场资料。video: retain-on-failure会让你拿到失败用例的完整视频回放。对于排查那些“偶尔红一下,再跑一遍就好了”的 flaky 用例,这个视频功不可没。
你没看错,Playwright 默认就是并行跑多个浏览器内核。同一个用例同时验证三种浏览器,虽然耗时多了一点,但查出来的兼容性问题确实值得。
3.3 写第一个可落地的端到端用例
我下面写一个登录加建单的完整用例,它基本覆盖了 Playwright 日常使用最常见的能力:
import { test, expect } from '@playwright/test'; test('用户登录后可以成功创建订单', async ({ page }) => { // 准备:拦截创建订单的接口,记录请求数据 let createOrderRequestPayload: any; await page.route('**/api/orders**', async (route) => { createOrderRequestPayload = route.request().postDataJSON(); await route.continue(); }); // 登录 await page.goto('/login'); await page.getByLabel('用户名').fill('tester'); await page.getByLabel('密码').fill('p@ssw0rd'); await page.getByRole('button', { name: '登 录' }).click(); // 等待跳转成功,而不是傻等 await expect(page).toHaveURL(/\/dashboard/); // 进入“新建订单”页面,表单信息通过用户可见文本定位 await page.getByText('新建订单').click(); await page.getByLabel('订单名称').fill(`测试订单-${Date.now()}`); await page.getByLabel('金额').fill('188.00'); await page.getByRole('button', { name: '提交' }).click(); // 在网络层校验请求体,而不只是表面上提交成功了 await expect.poll(() => createOrderRequestPayload).not.toBeNull(); // 断言页面出现成功提示 await expect(page.getByText('创建成功')).toBeVisible(); const orderNo = await page.locator('.order-number').innerText(); expect(orderNo).toMatch(/^ORD\d{12}$/); });这里有几个我特意写进去的细节,新手照着抄很有价值:
- 用
page.route拦截请求,并把请求体数据抓出来,这样不但可以断言前端 UI,还能校验最终发给后端的参数是否符合预期。这是很多测 UI 的人压根没想过的深度。 - 用
toHaveURL等待路由跳转,比page.wait_for_url更优雅,它本身就是断言,语义清楚。 - 用
Date.now()生成不重复的订单名,不然二次运行同一用例会撞上唯一性约束。 - 最后断言订单号格式,用正则校验后端返回业务编排是否符合规范。
3.4 用 UI 状态和响应状态对上号
端到端测试最容易出现的问题是:UI 明明显示成功了,但实际后端可能根本没收到数据;或者 UI 报错,但不是真的业务失败,而是前端校验拦截了。Playwright 的网络层能力帮我们把这两个层次串成了一个闭环。
我常用的另一个模式是直接监听失败的请求:
page.on('requestfailed', (request) => { console.log(`请求失败: ${request.url()}, 原因: ${request.failure()?.errorText}`); });别小看这段代码,它基本上让所有“静默失败的接口调用”都无处遁形。我在很多老项目的自动化改造里加了这么一行,立刻揪出了大量前端吞掉错误后没有反馈的隐藏 bug。这个技巧我强烈建议每一个用 Playwright 的人放进自己的通用 fixture 里。
3.5 跑起来:命令行与报告结果解读
用例写好了,执行方式相当灵活:
# 运行 tests 目录下全部用例 npx playwright test # 只跑某个文件 npx playwright test tests/login.spec.ts # 带标题模糊匹配 npx playwright test -g "创建订单" # 带有标签的用例 npx playwright test --grep "@smoke"我习惯在项目里用@smoke和@regression标签把用例分级。提交代码后的快速冒烟测试只跑 smoke,每夜回归则跑全量,这样能在效率和质量之间取得很好的平衡。
报告输出非常直观:npx playwright test --reporter=html,跑完自动生成一个 HTML 站点,里面包含所有用例的执行时间、通过失败状态、步骤截图、网络请求详情、trace 回放入口。我给团队做代码评审的时候,直接拿报告出来指给前端同事看:“你这里的按钮在 firefox 下定位不到,具体 trace 我给你放这了。”这种体验比贴一堆控制台日志让人自己猜要高效太多。
4. 高级场景:优雅处理多标签、认证状态和移动端模拟
4.1 多标签页与独立上下文
在一个真实的后台管理系统里,我经常需要验证“订单详情在 A 标签打开,然后在 B 标签对同一个订单做操作,然后回到 A 标签刷新页面看到状态变了”这种跨页交互流程。在 Playwright 里,这个过程用一个 Promise 就能优雅地等待新页面出现:
const [newPage] = await Promise.all([ page.waitForEvent('popup'), page.getByText('在新标签中打开订单').click(), ]); await newPage.waitForLoadState(); await newPage.getByRole('button', { name: '审核通过' }).click(); await newPage.close(); // 回到原页面,刷新后断言状态更新 await page.reload(); await expect(page.getByText('审核通过')).toBeVisible();Promise.all是为了避免点击操作还没触发弹窗,就过早去监听 popup 事件从而导致的竞态问题。这个是 Playwright 官方推荐的写法,也是我在实际项目中验证过无数次的可靠模式。类似地,如果你要同时验证两个用户之间的消息流转,最简单的方式就是开两个 context,各自独立登录,然后一边发、一边收,完全互不干扰。
4.2 登录态的复用,让它不要重复登录
端到端测试最耗时的地方,往往是每个用例都得走一遍完整的登录流程。如果登录过程还涉及短信验证码或者扫码,那就更是灾难。Playwright 提供了storageState机制,可以把登录后的存储状态(Cookie、LocalStorage、IndexedDB)直接保存下来,之后每个测试都直接复用,十行以内的代码就能让整个 suite 跑得飞快。
第一次执行时,我先做一次快速登录并保存状态:
test('收集登录态', async ({ page }) => { await page.goto('/login'); await page.getByLabel('用户名').fill('tester'); await page.getByLabel('密码').fill('p@ssw0rd'); await page.getByRole('button', { name: '登 录' }).click(); await page.waitForURL('**/dashboard'); await page.context().storageState({ path: 'auth/user.json' }); });然后在配置文件里:
use: { storageState: 'auth/user.json', },所有用例自动带着登录态开启。这里要注意两个坑:一是如果登录态的 Cookie 有效期短,跨天执行时必须重新生成这份 storage 文件;二是不同角色的测试(管理员、普通用户、只读访客)需要分别跑一遍并保存各自的 json,不能全项目共用一份。
4.3 移动端 Web 测试与设备仿真
很多团队说 Playwright 只适合做桌面端浏览器,这是不对的。它内置的设备描述符覆盖了典型的移动浏览器规格,包括 iPhone 和主流安卓设备的视口尺寸、UA、设备比例因子等。它支持模拟触摸手势、地理定位、多倍像素密度,甚至硬件加速能力。
在配置里直接声明一个移动设备项目:
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'], baseURL: 'https://staging.example.com', }, }对于 H5 页面、小程序内嵌 WebView 的前端代码,用这套移动端模拟跑一下,往往可以提前揪出一堆桌面环境压根不会暴露的排版与点击区域问题。当然,模拟终究不是真机,遇到强依赖设备 API 或网络状况的情景,依然要配合云真机平台验证。但在日常迭代开发里,这个能力已经能覆盖百分之八十的移动端回归需求了。
5. 部署 CI 与测试稳定性的工程实践
5.1 在标准流水线里跑起 Playwright
绝大部分云 CI 平台对 Playwright 都有现成的镜像或者安装指引。核心做法是在流水线上拉代码、装依赖、启动被测服务、跑测试。
我这里给一个 GitHub Actions 常见的配置参考:
name: e2e-tests on: push: branches: [main] pull_request: branches: [main] jobs: e2e: timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install --with-deps chromium - run: npx playwright test --project=chromium - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/很多流水线失败后不知道怎么定位,因为默认步骤失败就停了。加一个if: always()的 artifact 上传步骤,保证失败时也能拿到测试报告和 trace。这个细节是我觉得必须提醒的。
5.2 sharding:跟踪多台机器的并行规模
当你的用例量级上千以后,单台机器并行已经不够用了,因为 CPU 资源已经被榨干。Playwright 提供了原生 sharding 机制,把测试集按一定规则切分到多台机器上跑:
npx playwright test --shard=1/4 npx playwright test --shard=2/4 npx playwright test --shard=3/4 npx playwright test --shard=4/4四台机器各拿四分之一用例并行执行,总耗时可压缩到之前的四分之一左右。这个方案极其平滑,不需要自己去实现分发调度逻辑。只要流水线或者自建的 runner 支持多任务并发,就能直接把 Playwright 的 sharding 接进去。
要注意的是 shard 数量不要设置成超过用例总数,否则会出现某一台机器拿到空切片,白白浪费资源。另外切分逻辑默认是基于文件粒度的,所以文件内的用例数量最好保持相对均匀,避免某一台机器被一个特大文件拖成木桶短板。
5.3 减少 flaky 的实战指导意见
可以说,每个团队做 E2E 测试,坚持不下去的根源不是框架选型,而是用例动不动就莫名其妙红一下。稳定性问题是工程化最大的敌人。根据我的经验,控制 flaky 要从三个层面同时下手:
第一,所有数据尽可能自洽且隔离。测试用例里能创建的测试数据就现场创建,不要共享一套全局数据。别人改动数据导致你的用例失败,是最常见的 flaky 来源,也是团队协作里最容易相互甩锅的场景。
第二,把“外部依赖”挡在网络层之外。凡是第三方 SDK、支付网关、短信平台等接口,统统用page.routemock 掉。你测的是你的前端和你的后端之间的链路,外部服务偶尔抖动不该让用例负责。
第三,统一断言的重试语义。很多 flaky 是因为代码写成了固定等待后再断言,而固定等待永远是在跟环境赛跑。我见过太多await page.waitForTimeout(2000)的用法,它表面上有用,实际只是为特定一次运行“量身定做”。正确姿势永远是使用expect(locator).to_xxx(),让框架层的重试机制帮你对抗延迟抖动。
5.4 测试报告和失败定位的一条龙方案
Playwright 默认 HTML 报告会把所有失败的用例在最前面展示,并且每一个失败用例都带四样东西:错误堆栈、失败瞬间的页面截图、录制好的视频回放、以及可以逐步重现的 trace 文件。这四样信息用来排查问题,效率极高。
尤其是 trace 文件,它本质是一个可回放的会话记录,里面记录了整个测试周期内浏览器的 DOM 快照、网络请求、控制台日志、以及每一步操作对应的源代码。在排查隐性异步问题时,我经常把 trace 文件丢给开发同事,对方双击打开,沿着时间轴拖动,很快就能看出哪一步出现了与预期不符的状态。你现在问前端同事要 debug log 的时候,人家可能还要花半天加日志重新跑一遍。有了 trace,整个沟通成本直接降一个量级。
6. 常见问题速查表与排查技巧实录
我在维护这套框架的过程中,把大家问得最多的问题整理了一份速查表,贴在下面:
| 现象 | 最可能的原因 | 排查方式 |
|---|---|---|
| 选择器定位不到元素 | 元素在 iframe 里,或 shadow DOM 内部 | 检查页面结构,定位到 frame 后再查找 |
| 点击事件没生效 | 元素被覆盖、透明或动画未结束 | 改用click(force=True)前先思考是否合理,同时看自动等待是否已就绪 |
| 测试超时 | 页面有无限轮询或未捕获的弹窗 | 检查控制台与网络层,配合 trace 定位卡点 |
| 浏览器启动失败 | 服务器内存不足或沙箱权限问题 | 容器里加--no-sandbox,或调整资源限制 |
| storageState 失效 | Cookie 过期或域名不一致 | 重新录制登录态,并确认授权域名覆盖完整 |
| 相对路径 404 | baseURL 未配置或配置错误 | 检查配置文件中的 baseURL,确认环境正确 |
| Playwright 与 WebDriver 并存冲突 | 某些内网代理转发 CDP 请求异常 | 关掉不必要的代理或单独指定浏览器启动参数 |
| 新窗口 popup 监听不到 | 事件绑定与点击动作分离 | 用Promise.all同时处理 waitForEvent 和点击 |
实际上,我们在这套框架上踩过最大的一个坑,是某次升级 Electron 版本后,系统自带的 Chromium 内核和 Playwright 自带的浏览器二进制产生了兼容性问题。这类底层问题普遍存在,排查思路就是第一时间看官方 issue 列表。框架再成熟也免不了和你的特定环境打架,别慌着改业务代码,先检查版本信息、浏览器二进制路径和依赖树。
还有一个小 trick:page.pause()。在很多需要我肉眼确认当前状态的关键节点,我习惯直接插入一个page.pause(),然后在调试模式里手动操作浏览器、观察当前 DOM 状态,再选择下一步。这种交互式调试远比不停地打印一堆 log 来定位自然。
根据我个人的经验,如果你现在正打算为团队搭建一套浏览器端自动化体系,Playwright 几乎是综合成本最低、上限最高、学习曲线最友好的选择。它把太多过去要靠人力手工弥补的细节,在框架层面彻底抹平。把基本的骨架搭起来以后,你会发现码测试用例的节奏,其实和写业务代码一样顺手。当然,工具再好用,也别忘了自动化测试的核心永远是一份分得清楚职责、扛得住变化的用例设计。那些能把自动化项目长期运营下去、真正创造价值的团队,靠的从来不是某个多厉害的框架,而是一套能够持续稳定运行的工程习惯。这套习惯的第一步,往往就是从把 Playwright 用对、用好、用扎实开始的。