Playwright Async 异步编程:解锁现代 Web 自动化的核心利器

📅 2026/7/18 13:00:04
Playwright Async 异步编程:解锁现代 Web 自动化的核心利器
1. 项目概述为什么是 Playwright Async如果你正在寻找一个能稳定、高效地驾驭现代复杂 Web 应用的自动化工具那么 Playwright 绝对是你绕不开的选择。而当你深入使用它时async/await异步编程模式就成了解锁其全部潜力的关键钥匙。这不仅仅是一个技术选型更是一种应对现代 Web 开发异步本质的必然策略。简单来说Playwright 是一个由微软开源的浏览器自动化库它支持 Chromium、Firefox 和 WebKit 三大内核能做到真正的跨浏览器、跨平台。而async/await是 JavaScript以及 Python 等语言中处理异步操作的语法糖它让原本需要回调或 Promise 链的异步代码写得像同步代码一样清晰易读。将两者结合你就能以近乎“人类”的思维去编排一系列浏览器操作比如点击、输入、导航、等待元素而不用担心复杂的异步流程控制把自己绕晕。这个组合解决了什么痛点现代 Web 应用充斥着动态加载、懒加载、SPA单页应用路由一个简单的点击可能触发多个网络请求和 DOM 更新。传统的同步或基于回调的自动化脚本在这种环境下极其脆弱常常因为等待时间不足或事件顺序错乱而失败。Playwright Async 模式通过清晰的异步等待机制让你可以精确地控制操作时序确保脚本的稳定性和可维护性。无论你是做自动化测试、数据抓取RPA、还是监控巡检掌握 Playwright Async就意味着你拥有了在复杂 Web 世界中从容穿梭的能力。2. 核心设计Playwright 的异步架构与优势解析要玩转 Playwright Async首先得理解其底层的设计哲学。它并非简单地在同步 API 上套一层异步外壳而是从骨子里就是为异步操作而生的。2.1 事件驱动与无头浏览器通信Playwright 的核心工作原理是你的脚本Node.js/Python 等作为一个控制端通过 WebSocket 或管道等协议与一个或多个真正的浏览器进程可能是无头的也可能是有界面的进行通信。你的每一条指令如page.goto()或page.click()都会被序列化后发送给浏览器浏览器执行完毕后再将结果返回。这个过程天生就是异步的——发送指令后你不会阻塞主线程干等着而是可以继续处理其他逻辑或者等待这个操作完成。这种设计带来了几个关键优势高并发性你可以轻松创建多个浏览器上下文Context和页面Page让它们并行执行任务充分利用多核 CPU 资源。例如同时爬取十个不同页面的数据互不干扰。精准控制Playwright 提供了一组丰富的等待wait条件如waitForSelector,waitForLoadState这些都可以用await来优雅地调用确保在元素出现、网络空闲、甚至自定义条件满足后才执行下一步。这是对抗动态内容加载最有力的武器。避免“魔法等待”很多初级脚本依赖time.sleep(5)这种固定等待既低效又不稳定。Async 模式鼓励你使用基于事件的等待脚本执行速度更快且对网络波动不敏感。2.2 Async/Await 如何简化 Playwright 脚本在没有async/await的时代处理一连串的异步操作可能需要陷入“回调地狱”或复杂的 Promise 链。Playwright 的 API 返回的都是 Promise 对象。async/await语法让你能够线性地编写这些操作。举个例子一个典型的登录流程// 同步思维但用异步语法写出来 async function login(page, username, password) { await page.goto(https://example.com/login); // 等待导航完成 await page.fill(#username, username); // 等待输入框出现并填充 await page.fill(#password, password); // 同上 await page.click(button[typesubmit]); // 等待按钮可点击并点击 // 等待登录成功后的跳转或某个元素出现 await page.waitForSelector(#user-profile, { timeout: 10000 }); }这段代码读起来就像操作手册“先去登录页然后填用户名再填密码接着点提交按钮最后等用户资料页面加载出来。”await关键字确保了每一步都稳稳地完成后再进行下一步代码意图一目了然调试和维护的难度大大降低。3. 环境搭建与核心 API 异步化实战理论说再多不如动手搭一个。这里我们以 Node.js 环境为例从零开始构建一个 Playwright Async 项目。3.1 项目初始化与浏览器安装首先确保你的系统已安装 Node.js (建议 LTS 版本)。然后创建一个新目录并初始化项目mkdir playwright-async-demo cd playwright-async-demo npm init -y接下来安装 Playwright 库。强烈建议安装到项目本地而非全局这有利于依赖管理和版本控制。npm install playwright安装完成后Playwright 需要下载它要控制的浏览器内核Chromium, Firefox, WebKit。你可以通过npx运行其 CLI 工具来完成安装npx playwright install注意安装浏览器慢的解决方案这是新手常遇到的问题因为需要从海外服务器下载几百兆的浏览器二进制文件。有两个常用技巧使用镜像源在安装前设置环境变量。对于playwright install可以尝试设置PLAYWRIGHT_DOWNLOAD_HOST。但更通用的方法是如果你身处网络环境不佳的地区可以考虑在npm install阶段就使用国内 npm 镜像如淘宝镜像但浏览器二进制文件下载的镜像源设置较为复杂官方支持有限。跳过部分浏览器安装如果你只使用 Chromium可以运行npx playwright install chromium仅安装它这会快很多。Firefox 和 WebKit 同理。手动下载进阶从 Playwright 的 GitHub Releases 页面找到对应版本的浏览器包手动下载并放置到特定缓存目录。但这过程繁琐不推荐新手。3.2 编写第一个 Async 脚本创建一个demo.js文件我们将编写一个打开网页并截图的简单脚本。const { chromium } require(playwright); // 引入 chromium 浏览器对象 (async () { // 使用一个立即执行的异步函数包裹主逻辑 const browser await chromium.launch({ headless: false }); // 启动浏览器非无头模式方便观察 const page await browser.newPage(); // 创建一个新页面 await page.goto(https://example.com); // 导航到目标网址 await page.screenshot({ path: example.png }); // 截图并保存 await browser.close(); // 关闭浏览器 })();运行这个脚本node demo.js你会看到一个浏览器窗口打开访问 example.com然后截图保存后关闭。所有await确保了“启动-打开页面-导航-截图-关闭”这个流程顺序执行。3.3 核心异步 API 深度解析Playwright 中几乎所有涉及与浏览器交互的方法都是异步的。以下是一些最核心的异步 API 及其使用要点browser.newContext()/browser.newPage()创建新的上下文或页面。上下文隔离了 cookies、本地存储等非常适合并行多账号操作或测试隔离。page.goto(url[, options])页面导航。options里waitUntil参数至关重要。默认是load等待 load 事件但对于 SPA更推荐使用networkidle等待网络基本空闲或domcontentloaded。await page.goto(https://app.com, { waitUntil: networkidle });元素选择与操作page.click(selector),page.fill(selector, text),page.type(...)等。这些操作内部会自动等待元素可交互可见、启用、稳定。显式等待这是稳定性的核心。page.waitForSelector(selector[, options])等待特定选择器的元素出现在 DOM 中。page.waitForLoadState(state[, options])等待页面达到特定加载状态。page.waitForFunction(predicate[, arg, options])等待一个自定义的 JavaScript 函数在页面上下文中返回真值。功能极其强大可以用来等待任何复杂条件。// 等待一个动态生成的、内容为“加载完成”的元素出现 await page.waitForSelector(.status-message:has-text(加载完成)); // 等待页面某个变量的值变化 await page.waitForFunction(() window.dataLoaded true);处理弹窗和对话框page.on(dialog, dialog dialog.accept())用于监听并处理alert,confirm,prompt。注意这是一个事件监听通常与 Promise 结合使用。4. 应对动态内容异步等待策略与实战技巧“录制脚本最常见的失败原因就是动态内容。” 这句话一针见血。现代 Web 应用大量使用 JavaScript 异步加载内容元素可能稍纵即逝也可能姗姗来迟。依赖录制工具生成的固定等待和绝对选择器在动态内容面前不堪一击。我们必须主动出击制定等待策略。4.1 动态内容的主要挑战与应对原则内容懒加载Lazy Load滚动到视口才加载图片或模块。应对模拟滚动page.evaluate(() window.scrollTo(0, document.body.scrollHeight))然后结合waitForSelector等待目标元素。SPA 路由切换点击链接后页面 URL 变化但浏览器不刷新内容通过 JS 替换。应对使用page.waitForNavigation()或更通用的page.waitForURL(**/new-path)来等待导航完成。注意在点击触发导航的按钮前就要启动这个等待。// 经典模式先承诺等待导航再执行触发导航的动作 const navigationPromise page.waitForNavigation(); // 或 waitForURL await page.click(#next-page-link); await navigationPromise; // 等待导航完成数据驱动的 UI 更新点击按钮后发起 API 请求数据回来后更新 DOM。应对这是最复杂的情况。最佳实践是等待网络请求和等待 DOM 更新相结合。监听网络请求page.waitForResponse(urlOrPredicate)可以等待特定的 API 响应完成。等待 DOM 稳定使用waitForSelector等待代表数据加载完成的新元素出现或者使用waitForFunction检查页面 JS 变量或特定 DOM 状态。4.2 实战抓取一个动态加载的商品列表假设我们要抓取一个电商网站的商品列表该列表是滚动到底部自动加载的。const { chromium } require(playwright); (async () { const browser await chromium.launch({ headless: true }); const page await browser.newPage(); await page.goto(https://dynamic-ecom-site.com/products); const products []; const maxScrolls 10; // 防止无限滚动设置最大滚动次数 let scrollCount 0; let previousProductCount 0; while (scrollCount maxScrolls) { // 1. 滚动到底部 await page.evaluate(() window.scrollTo(0, document.body.scrollHeight)); // 2. 等待可能的新产品加载出来。这里假设新产品会带有 .product-item 类 // 设置一个合理的超时比如 3 秒如果3秒内没有新产品则认为加载完毕 try { await page.waitForFunction( (prevCount) document.querySelectorAll(.product-item).length prevCount, { timeout: 3000 }, // 等待3秒 previousProductCount // 将之前的数量作为参数传入函数 ); } catch (error) { // 如果超时说明可能没有新商品了或者网络慢。这里我们选择跳出循环 console.log(未检测到新商品加载可能已到底部或网络延迟。); break; } // 3. 更新商品计数 const currentProductCount await page.$$eval(.product-item, items items.length); if (currentProductCount previousProductCount) { // 数量没变可能真的到底部了 break; } previousProductCount currentProductCount; scrollCount; console.log(已滚动 ${scrollCount} 次发现 ${currentProductCount} 个商品。); } // 4. 提取所有商品信息 const productElements await page.$$(.product-item); for (const element of productElements) { const name await element.$eval(.product-name, node node.innerText); const price await element.$eval(.product-price, node node.innerText); products.push({ name, price }); } console.log(总共抓取到 ${products.length} 个商品。); console.log(products.slice(0, 5)); // 打印前5个看看 await browser.close(); })();这段代码的要点解析page.evaluate()在浏览器环境中执行 JavaScript这里用来滚动。page.waitForFunction()这是应对动态内容的王牌。我们传入一个函数和之前的商品数量作为参数函数在浏览器上下文执行只有当新商品数量增加时这个等待才会结束。page.$$()和element.$eval()用于批量获取元素和在特定元素内执行查询。错误处理waitForFunction设置了超时并用try...catch捕获超时错误优雅地处理加载完成或网络不佳的情况。实操心得选择器的稳定性高于一切对抗动态内容除了等待策略稳健的选择器是另一条生命线。避免使用基于索引如:nth-child(3)、绝对路径或依赖易变样式的选择器。优先选择>const { chromium } require(playwright); async function checkWebsite(url, pageName) { const browser await chromium.launch({ headless: true }); const page await browser.newPage(); try { console.log([${pageName}] 开始检查: ${url}); const response await page.goto(url, { waitUntil: domcontentloaded, timeout: 30000 }); const title await page.title(); const status response.status(); console.log([${pageName}] 状态: ${status}, 标题: ${title}); return { url, status, title, ok: status 200 }; } catch (error) { console.error([${pageName}] 检查失败:, error.message); return { url, status: 0, title: Error, ok: false, error: error.message }; } finally { await browser.close(); // 确保每个任务结束后都关闭浏览器 } } (async () { const websites [ { url: https://example.com, name: Example }, { url: https://httpbin.org/status/404, name: Test404 }, { url: https://google.com, name: Google } ]; // 使用 Promise.all 并行执行所有检查任务 const results await Promise.all( websites.map(site checkWebsite(site.url, site.name)) ); console.log(\n--- 所有检查结果 ---); console.log(results); })();关键点Promise.all()接收一个 Promise 数组并等待它们全部完成或有一个被拒绝。它让并行操作变得非常简单。资源管理每个任务函数内部都创建并关闭了自己的 browser 实例。这确保了资源隔离但创建浏览器实例开销较大。对于大量并行任务更好的模式是复用浏览器实例但创建独立的上下文Context。错误处理每个任务用try...catch...finally包裹确保即使某个网站访问失败也不会影响其他任务并且资源能被正确释放。5.2 使用 Async Queue 控制并发直接使用Promise.all发起上百个任务可能会导致系统资源内存、网络连接耗尽。我们需要一个任务队列来控制并发度。const { chromium } require(playwright); const AsyncQueue require(async/queue); // 需要安装 async 库: npm install async (async () { const browser await chromium.launch({ headless: true }); const concurrency 3; // 同时最多有3个页面在运行 const results []; // 定义一个处理单个URL的worker函数 const worker async (task) { const context await browser.newContext(); // 为每个任务创建独立的上下文 const page await context.newPage(); try { console.log(开始处理: ${task.url}); await page.goto(task.url, { waitUntil: networkidle, timeout: 60000 }); const screenshotBuffer await page.screenshot({ type: png }); // 这里可以处理截图比如保存或上传 console.log(完成处理: ${task.url}); results.push({ url: task.url, success: true }); } catch (error) { console.error(处理失败 ${task.url}:, error.message); results.push({ url: task.url, success: false, error: error.message }); } finally { await context.close(); // 关闭上下文释放资源但保留浏览器进程 } }; // 创建队列 const queue AsyncQueue(worker, concurrency); // 监听队列事件 queue.drain(() { console.log(所有任务已完成。); console.log(results); browser.close(); // 所有任务完成后才关闭主浏览器 }); // 添加任务模拟100个URL const urls Array.from({ length: 10 }, (_, i) ({ url: https://httpbin.org/delay/${i % 3} })); // 模拟不同延迟的URL urls.forEach(task queue.push(task)); // 也可以等待所有任务完成另一种方式 // await queue.drain(); })();模式优势控制并发无论有多少任务同时运行的只有concurrency个。资源友好复用浏览器实例但为每个任务创建隔离的上下文平衡了性能和隔离性。优雅停机queue.drain()回调确保了所有任务完成后才执行清理工作。5.3 增强鲁棒性重试与超时机制网络不稳定、目标网站临时过载都会导致脚本失败。一个健壮的自动化脚本必须包含重试逻辑。async function robustOperation(operation, maxRetries 3, delay 1000) { let lastError; for (let attempt 1; attempt maxRetries; attempt) { try { return await operation(); // 执行传入的异步函数 } catch (error) { lastError error; console.warn(操作失败第 ${attempt} 次重试。错误: ${error.message}); if (attempt maxRetries) { await new Promise(resolve setTimeout(resolve, delay * attempt)); // 指数退避延迟 } } } throw lastError; // 所有重试都失败抛出最后的错误 } // 使用示例 (async () { const browser await chromium.launch(); const page await browser.newPage(); try { // 用一个可能会失败的导航操作来包装 await robustOperation( () page.goto(https://sometimes-flaky-site.com, { timeout: 15000 }), 3, // 最多重试3次 2000 // 初始延迟2秒 ); console.log(导航成功); } catch (error) { console.error(经过多次重试后仍然失败:, error); } finally { await browser.close(); } })();策略解析指数退避每次重试前等待的时间逐渐增加delay * attempt避免在服务端临时故障时加重其负担。封装复用robustOperation函数可以包装任何可能失败的 Playwright 异步调用如click,fill,waitForSelector等极大提升脚本的整体稳定性。6. 集成与进阶测试框架、CI/CD 与 AI 辅助Playwright Async 不仅用于写独立脚本更是现代自动化测试和流程自动化的基石。6.1 与测试框架结合Playwright TestPlaywright 官方提供了强大的测试运行器playwright/test。它内置了异步处理、断言、夹具Fixtures和并行执行支持让你能用更结构化的方式编写异步测试。npm init playwrightlatest按照引导完成安装后你会得到一个预设好的项目结构。一个典型的测试用例文件如下// tests/example.spec.js const { test, expect } require(playwright/test); test(异步登录测试, async ({ page }) { // page fixture 由框架自动提供和管理 await page.goto(/login); await page.fill(#username, testuser); await page.fill(#password, password123); // 使用框架的断言它们也是异步友好的 await expect(page.locator(button[typesubmit])).toBeEnabled(); await page.click(button[typesubmit]); // 等待导航并断言新页面 await expect(page).toHaveURL(/\/dashboard/); await expect(page.locator(.welcome-message)).toContainText(Welcome, testuser); }); test(并行处理多个页面, async ({ browser }) { // 创建两个独立的上下文来模拟两个用户 const context1 await browser.newContext(); const context2 await browser.newContext(); const page1 await context1.newPage(); const page2 await context2.newPage(); // 两个页面并行操作 const [response1, response2] await Promise.all([ page1.goto(https://example.com), page2.goto(https://example.org) ]); expect(response1.ok()).toBeTruthy(); expect(response2.ok()).toBeTruthy(); await context1.close(); await context2.close(); });框架优势自动并行化测试运行器可以自动将测试文件分配到多个 worker 进程并行执行。内置夹具自动管理browser,context,page的生命周期无需手动launch和close。智能等待像expect(locator).toBeVisible()这样的断言内部已经包含了等待逻辑。报告与追踪生成丰富的 HTML 报告、视频录制和追踪文件便于调试。6.2 在 CI/CD 中运行在持续集成环境如 GitHub Actions, GitLab CI, Jenkins中运行 Playwright 脚本关键是处理好浏览器依赖和可能需要的显示服务器对于无头模式现代 Playwright 通常不需要但某些情况可能需要。一个简单的 GitHub Actions 工作流示例name: Playwright Tests on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps chromium # 只安装Chromium及其系统依赖 - name: Run Playwright tests run: npx playwright test - uses: actions/upload-artifactv4 if: always() # 即使测试失败也上传报告 with: name: playwright-report path: playwright-report/ retention-days: 306.3 探索 AI 辅助与 MCP网络热词中提到了“playwright mcp”、“playwright ai”。这指向了一个有趣的趋势将 Playwright 与 AI 智能体Agent结合。MCPModel Context Protocol是一种让 AI 模型使用工具如浏览器的协议。虽然直接集成 AI 模型如 OpenAI到 Playwright 脚本中属于更前沿的用法但核心思想是利用 AI 来处理非结构化的、需要“理解”的交互。例如智能选择器生成让 AI 根据页面描述推荐更稳定的选择器。处理非预期弹窗AI 可以分析弹窗内容决定是点击“确认”还是“取消”。自然语言指令将“找到价格最便宜的商品并加入购物车”这样的指令分解成一系列 Playwright 操作。一个非常初级的示例可能是调用一个文本分析 API 来决定下一步操作const { analyzePageIntent } require(./some-ai-helper); // 假设的AI分析函数 async function handleDynamicModal(page) { const modalText await page.locator(.modal-content).innerText(); const intent await analyzePageIntent(modalText); // 调用AI分析文本意图 if (intent prompt-for-confirmation) { await page.click(button:has-text(确定)); } else if (intent prompt-for-input) { await page.fill(.modal-input, 默认值); await page.click(button:has-text(提交)); } else { // 默认关闭 await page.click(.modal-close); } }这打开了自动化更新的可能性让脚本能适应一些微小的 UI 变化。当然这需要额外的 AI 服务成本并且目前成熟度有限更多是探索方向。7. 性能调优与调试技巧编写出能跑的脚本只是第一步写出高效、稳定的脚本才是终极目标。7.1 性能优化要点复用浏览器与上下文创建浏览器实例 (browser.launch()) 是昂贵的操作。在可能的情况下复用同一个浏览器实例通过browser.newContext()创建隔离的上下文来执行不同任务。避免不必要的等待精确使用waitFor条件而不是简单的page.waitForTimeout(5000)。过度等待会显著拉长执行时间。禁用无关资源如果不需要图片、样式、字体等来执行你的操作例如只检查 API 响应可以在上下文中设置拦截大幅提升页面加载速度。const context await browser.newContext(); await context.route(**/*.{png,jpg,jpeg,svg,gif,css,woff2}, route route.abort()); // 拦截并中止图片、字体等请求 const page await context.newPage();并行化如前所述使用Promise.all或任务队列处理独立任务。选择轻量级浏览器如果功能足够优先使用 Chromium 而非 Chrome。在无头模式下运行也通常更快。7.2 调试技巧实录慢动作与可视化在调试时使用headless: false并设置slowMo参数让操作慢下来方便观察。await chromium.launch({ headless: false, slowMo: 200 }); // 每个操作后暂停200毫秒录制与代码生成Playwright 的 Codegen 工具是强大的起点。运行npx playwright codegen https://example.com它会打开浏览器并记录你的操作实时生成对应代码。但切记生成的代码需要优化特别是选择器和等待逻辑。丰富的日志启动浏览器时开启详细日志。const browser await chromium.launch({ headless: true, logger: { isEnabled: (name, severity) name api || severity error, log: (name, severity, message, args) console.log(${name} ${message}) } });使用 Playwright Inspector通过设置PWDEBUG1环境变量运行脚本或在代码中await page.pause()会启动一个交互式调试器可以单步执行、查看 DOM 快照、检查选择器等。PWDEBUG1 node your_script.js追踪Trace对于复杂的失败用例启用追踪可以记录操作每一步的截图、网络请求、控制台日志生成一个 ZIP 文件可以用 Playwright Trace Viewer (npx playwright show-trace trace.zip) 可视化回放是定位偶发问题的神器。await context.tracing.start({ screenshots: true, snapshots: true }); // ... 执行你的测试 ... await context.tracing.stop({ path: trace.zip });8. 常见问题排查与避坑指南即使掌握了所有技巧在实际操作中依然会遇到各种“坑”。这里记录一些高频问题及其解决方案。问题现象可能原因排查与解决思路page.goto超时网络慢、页面过重、waitUntil条件永不满足1. 增加timeout值。2. 检查waitUntil策略对于 SPA 尝试networkidle或domcontentloaded。3. 监听request和response事件看关键请求是否失败。4. 禁用无关资源加载见性能优化。page.click失败元素不可见、被遮挡、未启用、选择器匹配到多个元素1. 使用page.locator(selector).click()它提供更好的错误信息。2. 点击前先用waitForSelector确保元素可见且可交互 ({ state: visible })。3. 使用page.screenshot()截图确认页面状态是否符合预期。4. 检查是否有弹窗、遮罩层挡住了目标元素。脚本在 CI 环境失败本地却成功CI 环境缺少依赖、无头模式差异、资源限制、时区/语言环境不同1. 确保 CI 镜像安装了 Playwright 所需系统依赖 (npx playwright install-deps)。2. 在 CI 脚本中增加--headed参数运行一次并配置虚拟显示服务器如xvfb来查看发生了什么。3. 检查 CI 环境的内存和 CPU 限制是否足够。4. 在浏览器启动参数中固定视口大小和时区args: [--window-size1920,1080, --timezoneAsia/Shanghai]。动态元素抓不到等待时间不足、选择器不对、元素在 iframe 内1.首选waitForSelector并设置足够长的超时。2. 使用page.waitForFunction等待更复杂的 JS 条件。3. 使用 Playwright Inspector 检查元素的实际选择器注意是否有 Shadow DOM。4. 如果元素在 iframe 里需要先定位到 iframe 对象const frame page.frame({ name: iframe-name }); await frame.click(...)。内存泄漏运行久了崩溃页面、上下文未关闭、监听器未移除、大量截图未清理1.确保每个newPage()和newContext()都有对应的close()最好放在try...finally块中。2. 避免在循环中无限制创建页面。3. 如果注册了事件监听器如page.on(request, ...)在不需要时记得移除。4. 对于长期运行的脚本定期重启浏览器实例。最后再分享一个关于选择器的心得Playwright 提供了多种定位器LocatorAPI如getByText(),getByRole(),getByTestId()。这些语义化的定位器比原始的 CSS 或 XPath 选择器更稳定因为它们更贴近用户的感知方式如通过文本内容或 ARIA 角色。在编写自动化脚本时应优先考虑使用这些定位器。例如page.getByRole(button, { name: Submit })比page.click(button.btn-primary)更能抵抗 CSS 类名的变化。这需要前端开发有一定的配合例如添加>