快马平台与Playwright结合:打造高效电商E2E自动化测试方案

快马平台与Playwright结合:打造高效电商E2E自动化测试方案

1. 项目概述:当电商测试遇上“快马”与Playwright

最近在团队里搞自动化测试基建,一个绕不开的痛点就是电商这类复杂业务系统的端到端(E2E)测试。页面多、流程长、状态复杂,靠人工点点点,回归一次就得大半天,还容易漏。手动维护测试脚本?开发同学写起来费劲,测试同学维护起来头疼,脚本还动不动就“脆断”。所以,当“快马平台”这个号称能低代码生成测试用例的工具,和我们正在评估的Playwright这个新兴的自动化框架碰到一起时,我决定来一次深度的“实战演练”,看看它们结合后,能否真的为电商全流程测试带来质变。

简单来说,这个项目就是:利用快马平台的智能录制与用例生成能力,快速创建覆盖电商核心流程(如登录、浏览、加购、下单、支付)的测试场景,然后将其转化为可维护、可扩展、高稳定性的Playwright测试脚本,最终形成一个完整的、自动化的回归测试套件。这不仅仅是两个工具的简单拼接,更涉及到如何将平台生成的“草图”打磨成工业级的“成品”,其中关于元素定位策略、等待机制、数据驱动、异常处理等细节,才是决定成败的关键。无论你是正在被繁重E2E测试困扰的测试工程师,还是希望提升交付质量的前后端开发者,这套组合拳都值得你深入了解。

2. 核心思路与方案选型:为什么是“快马”+Playwright?

在决定技术栈时,我们对比了市面上几种常见的方案。传统的“Selenium + 手动编码”模式,自由度最高,但对编写者的编程和测试框架设计能力要求也高,且脚本稳定性维护成本不小。而纯粹的“录制/回放”工具,虽然上手快,但生成的脚本往往像“胶水代码”,可读性差、复用性低,页面稍一改动就“全军覆没”。

快马平台在这里扮演了一个“智能脚手架”的角色。它通过录制用户操作,不仅能生成操作步骤,更能尝试理解页面结构,生成带有语义化定位(如根据按钮文字、角色属性)的测试步骤。这比单纯录制XPath或CSS选择器要健壮得多。更重要的是,它提供了一个可视化的用例编辑和管理界面,产品、测试、开发都可以基于同一份“用例描述”进行协作,降低了沟通成本。

Playwright则是我们选定的“执行引擎”。相比于Selenium,它的优势太明显了:原生支持多浏览器(Chromium, Firefox, WebKit)且无需额外驱动、自动等待机制极大地减少了“flaky tests”(不稳定的测试)、强大的网络请求拦截与模拟能力、以及媲美原生开发工具的调试体验。对于电商测试中常见的弹窗、iframe、文件上传、地理位置模拟等场景,Playwright都提供了优雅的API。

所以,我们的核心思路是:用快马平台降低用例设计与生成的“门槛”和“初稿”成本,然后用Playwright的强大与严谨,对初稿进行“精装修”,将其转化为结构清晰、易于维护、执行稳定的自动化资产。这个分工,让工具各司其职,人则专注于更重要的测试场景设计与异常逻辑处理。

注意:快马平台生成的是“步骤描述”和“定位建议”,并非直接可用的Playwright脚本。我们需要一个“翻译”或“适配”的过程,这个过程正是注入我们测试智慧和工程化思想的关键环节。

3. 环境准备与工具链搭建

工欲善其事,必先利其器。在开始生成和编写用例之前,一个可靠的本地开发与调试环境是基础。

3.1 Playwright测试环境搭建

首先,我们需要一个Node.js环境(建议版本16+)。然后,在项目目录下初始化并安装Playwright。

# 初始化npm项目(如果已有package.json可跳过) npm init -y # 安装Playwright测试库 npm install --save-dev @playwright/test # 安装Playwright支持的浏览器(Chromium, Firefox, WebKit) npx playwright install

这里有个小技巧:如果网络环境导致npx playwright install下载缓慢或失败,可以采用手动安装。先去Playwright的GitHub Releases页面下载对应操作系统的浏览器套件压缩包,然后解压到~/.cache/ms-playwright目录(Linux/macOS)或%USERPROFILE%\AppData\Local\ms-playwright目录(Windows)下对应的浏览器文件夹内。不过,直接使用命令安装是最推荐的方式,它能处理好所有依赖。

接下来,初始化Playwright的配置文件playwright.config.ts。这个文件控制着测试如何运行。

npx playwright init

初始化后,我们需要根据电商测试的特点调整配置。一个针对电商全流程测试的配置核心如下:

// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // 全局测试超时时间,电商流程长,建议设置长一些 timeout: 120 * 1000, // 全局断言超时 expect: { timeout: 30 * 1000 }, // 全局每个测试用例超时 globalTimeout: 10 * 60 * 1000, // 重复运行策略,用于排查偶发失败 retries: process.env.CI ? 2 : 1, // CI环境重试2次,本地1次 // 报告生成器 reporter: [ ['html', { outputFolder: 'playwright-report', open: 'never' }], // HTML报告 ['list'] // 控制台简洁输出 ], // 项目配置:可以定义多套环境,如桌面Chrome、移动端模拟等 projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, // 可以添加移动端测试 // { // name: 'Mobile Chrome', // use: { ...devices['Pixel 5'] }, // }, ], // 全局设置,如每个测试开始前执行的钩子 use: { // 基础URL,测试中可使用相对路径 baseURL: 'https://your-ecommerce-staging-site.com', // 自动截屏:只在失败时截屏,避免报告过大 screenshot: 'only-on-failure', // 录制视频:只在失败时录制,对诊断UI交互问题极有帮助 video: 'retain-on-failure', // 追踪文件:记录完整的测试轨迹,可用于Timeline查看 trace: 'retain-on-failure', // 忽略HTTPS错误,应对一些内部测试证书问题 ignoreHTTPSErrors: true, }, });

3.2 快马平台接入与录制准备

快马平台通常是一个SaaS服务或企业内部部署的系统。你需要有一个账号,并创建一个针对你的电商测试项目的空间。关键步骤是安装其浏览器扩展程序,这个扩展程序用于录制你的操作。

安装完成后,在浏览器中打开你的电商网站(最好是测试环境),点击快马扩展图标开始录制。这时,你就像正常用户一样去操作:登录、搜索商品、查看详情、加入购物车、进入结算页、选择地址、支付(通常模拟)等等。快马平台会在后台默默地记录你的每一个点击、输入、跳转,并分析页面DOM结构,生成带有智能定位的测试步骤。

录制完成后,在快马平台的控制台,你可以看到生成的测试用例。它通常包含:步骤列表、每个步骤对应的操作(click, fill)、以及平台推荐的元素定位器(可能结合了文本、角色、测试ID等)。此时,你可以给用例起个名字,比如“用户完整下单流程”,并将其导出。导出的格式可能是JSON、YAML或者平台自定义的格式。这就是我们的“原材料”。

4. 从快马用例到Playwright脚本的转化策略

拿到快马平台导出的用例数据后,我们面临的核心任务就是“翻译”。这不是简单的格式转换,而是需要将平台生成的、可能比较“泛化”的步骤,转化为具有工程化水准的Playwright测试代码。这里分享我的转化策略和实操心得。

4.1 解析用例结构与元素定位优化

快马生成的定位器,为了通用性,可能会优先使用text=或者比较冗长的CSS选择器。我们的首要任务就是将其优化为更稳定、更精准的Playwright定位器。

原则:优先使用显式的测试属性(如>// 优化前(依赖文本,脆弱) await page.click('text="加入购物车"'); // 优化后(使用专用测试ID,稳定) await page.click('[data-testid="add-to-cart-btn"]'); // 或者使用Playwright推荐的getByTestId方法(需要在config中配置testIdAttribute,默认为'data-testid') await page.getByTestId('add-to-cart-btn').click();

如果开发团队没有添加测试属性,我们可以退而求其次,使用Playwright强大的getByRole组合定位:

// 定位一个名为“加入购物车”的按钮 await page.getByRole('button', { name: '加入购物车' }).click(); // 或者更精确地,定位在商品卡片区域内的按钮 const productCard = page.locator('.product-card').first(); await productCard.getByRole('button', { name: '加入购物车' }).click();

实操心得:在转化初期,花时间与前端开发团队沟通,建立一套>// models/ProductPage.js class ProductPage { constructor(page) { this.page = page; this.addToCartButton = page.getByTestId('add-to-cart-btn'); this.productTitle = page.locator('.product-title'); } async addToCart() { await this.addToCartButton.click(); // 可以在这里加入一些等待或断言,比如确认商品已加入的Toast提示 await expect(this.page.locator('.toast-success')).toBeVisible(); } async getProductTitle() { return await this.productTitle.textContent(); } }

2. 显式等待与断言:Playwright虽然有自动等待,但在关键状态转换点,显式等待和断言能让测试更健壮。快马生成的步骤里通常没有这些。

  • 等待导航:点击后页面跳转,必须等待新页面加载完成。
    await Promise.all([ page.waitForURL('**/cart'), // 等待URL包含/cart page.click('[data-testid="go-to-cart"]') ]);
  • 等待元素状态:等待模态框出现、按钮变为可点击、加载动画消失。
    await page.locator('.checkout-modal').waitFor({ state: 'visible' }); await page.getByRole('button', { name: '提交订单' }).waitFor({ state: 'enabled' });
  • 关键断言:在每一个流程节点加入断言,验证业务状态。
    // 加入购物车后,断言购物车角标数量增加 await expect(page.locator('.cart-count')).toHaveText('1'); // 下单成功后,断言跳转到成功页面或出现成功提示 await expect(page).toHaveURL(/order-success/); await expect(page.locator('.order-success-msg')).toBeVisible();

4.3 实现数据驱动与配置化

电商测试经常需要用不同用户、不同商品、不同地址进行测试。硬编码在脚本里是不可行的。我们需要将测试数据外部化。

1. 使用环境变量和配置文件:将基础URL、用户凭证等敏感或易变信息放在.env文件或config.json中。

// config/test-data.json { "users": { "standard": { "username": "test_user@example.com", "password": "Password123!" }, "vip": {...} }, "products": { "sku_001": "测试商品A" } } // 在测试中引入 const testData = require('../config/test-data.json'); await loginPage.login(testData.users.standard.username, testData.users.standard.password);

2. 使用Playwright的test.describetest函数实现数据驱动:Playwright Test支持直接传入参数数组进行多组数据测试。

import { test, expect } from '@playwright/test'; const products = [ { sku: 'SKU001', name: '普通商品' }, { sku: 'SKU002', name: '秒杀商品' }, { sku: 'SKU003', name: '缺货商品' }, ]; for (const product of products) { test(`购买商品: ${product.name}`, async ({ page }) => { // ... 测试逻辑,使用 product.sku 和 product.name await page.goto(`/product/${product.sku}`); await expect(page.locator('.product-name')).toContainText(product.name); // ... }); }

5. 电商核心流程测试用例详解与脚本实现

现在,让我们聚焦电商最核心的“浏览-加购-下单”流程,看看一个完整的Playwright测试用例应该如何编写。假设我们已经从快马平台获得了这个流程的原始步骤,并按照上述策略进行了优化和重构。

5.1 用户登录与鉴权处理

登录是几乎所有流程的起点。我们不能在每个测试中都录制登录操作,那样效率太低。通常有两种处理方式:

方式一:全局前置登录(使用Storage State)这是Playwright推荐的方式。我们单独写一个setup脚本完成登录,并将浏览器上下文的状态(包括cookies、localStorage)保存下来。其他测试直接加载这个状态,就相当于已经登录了。

// global-setup.js const { chromium } = require('@playwright/test'); module.exports = async config => { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('https://your-site.com/login'); await page.fill('#username', 'testuser'); await page.fill('#password', 'testpass'); await page.click('button[type="submit"]'); // 等待登录成功,例如跳转到首页 await page.waitForURL('**/dashboard'); // 将当前上下文的状态存储到文件中 await page.context().storageState({ path: 'storage-state.json' }); await browser.close(); };

然后在playwright.config.ts中配置:

globalSetup: require.resolve('./global-setup'), use: { ...devices['Desktop Chrome'], storageState: 'storage-state.json', // 所有测试项目共用登录状态 },

方式二:在每个测试文件中使用Fixture注入已登录的Page如果你需要测试不同角色的用户(如普通用户、管理员),可以使用Playwright的Fixture功能,创建自定义的、已登录的Page对象。

// fixtures.js import { test as base, chromium } from '@playwright/test'; export const test = base.extend({ loggedInPage: async ({ }, use) => { // 启动浏览器并登录 const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); await page.goto('/login'); // ... 执行登录操作 await page.fill('input[name="email"]', 'user@example.com'); await page.fill('input[name="password"]', 'password'); await page.click('button:has-text("登录")'); await page.waitForURL('**/dashboard'); // 等待登录成功 // 将这个page传递给测试用例使用 await use(page); // 测试结束后清理 await context.close(); await browser.close(); }, }); // 在测试文件中 import { test, expect } from './fixtures'; test('测试下单流程', async ({ loggedInPage }) => { // loggedInPage 已经是一个登录状态的页面对象 await loggedInPage.goto('/products'); });

5.2 商品浏览与加入购物车

这个环节的测试要点在于:商品列表的渲染、筛选排序、商品详情页的元素完整性、加入购物车操作及反馈。

import { test, expect } from '@playwright/test'; import { ProductListingPage, ProductDetailPage } from '../pages'; // 引入Page Object test('用户浏览并添加商品到购物车', async ({ page }) => { const listingPage = new ProductListingPage(page); const detailPage = new ProductDetailPage(page); // 1. 访问商品列表页 await listingPage.goto(); await expect(page).toHaveURL(/products/); // 2. 断言列表加载成功 await expect(listingPage.productCards).toHaveCount.greaterThan(0); // 3. 进行筛选(例如按价格排序) await listingPage.sortBy('price-low-to-high'); // 可以加入断言,验证排序是否正确,例如获取前两个商品的价格进行比较 const firstPrice = await listingPage.getFirstProductPrice(); const secondPrice = await listingPage.getSecondProductPrice(); expect(parseFloat(firstPrice)).toBeLessThanOrEqual(parseFloat(secondPrice)); // 4. 进入第一个商品的详情页 await listingPage.goToFirstProductDetail(); await expect(page).toHaveURL(/product\/detail/); // 5. 断言详情页关键信息存在 await expect(detailPage.productName).toBeVisible(); await expect(detailPage.productPrice).toBeVisible(); await expect(detailPage.addToCartButton).toBeVisible(); // 6. 执行加入购物车操作 await detailPage.addToCart(); // 7. 验证加入成功的反馈 // 方式A:验证页面上的成功提示 await expect(page.locator('.notification-success')).toContainText('已加入购物车'); // 方式B:验证购物车图标上的数量变化(需要先获取初始数量) // const initialCount = await page.locator('.cart-badge').textContent() || '0'; // await expect(page.locator('.cart-badge')).toHaveText((parseInt(initialCount) + 1).toString()); });

5.3 购物车管理与结算流程

购物车页面通常涉及数量修改、商品删除、优惠券使用、价格计算等复杂交互。

test('用户管理购物车并进入结算', async ({ loggedInPage }) => { const cartPage = new CartPage(loggedInPage); // 0. 前置条件:确保购物车有商品(可以通过API或上一个测试添加) await cartPage.goto(); // 1. 验证购物车商品项 await expect(cartPage.cartItems).toHaveCount.greaterThan(0); const firstItemName = await cartPage.getItemName(0); expect(firstItemName).toBeTruthy(); // 简单断言名称存在 // 2. 修改商品数量 const initialQuantity = await cartPage.getItemQuantity(0); await cartPage.increaseItemQuantity(0); // 点击增加按钮 await expect(cartPage.getItemQuantity(0)).toHaveText((parseInt(initialQuantity) + 1).toString()); // 3. 验证价格重新计算 const unitPrice = await cartPage.getItemUnitPrice(0); const newSubtotal = await cartPage.getItemSubtotal(0); // 计算期望的小计:单价 * 新数量 const expectedSubtotal = parseFloat(unitPrice) * (parseInt(initialQuantity) + 1); expect(parseFloat(newSubtotal)).toBeCloseTo(expectedSubtotal, 2); // 允许微小浮点数误差 // 4. 应用优惠券(这是一个很好的网络拦截用例) // 先监听优惠券验证的API请求 const couponResponsePromise = loggedInPage.waitForResponse(response => response.url().includes('/api/coupon/validate') && response.status() === 200 ); await cartPage.applyCoupon('TEST2024'); const couponResponse = await couponResponsePromise; const responseBody = await couponResponse.json(); // 断言接口返回了正确的折扣信息 expect(responseBody.discount).toBeGreaterThan(0); // 再断言页面上的折扣金额显示正确 await expect(cartPage.discountAmount).toContainText(responseBody.discount.toString()); // 5. 进入结算页面 await cartPage.proceedToCheckout(); await expect(loggedInPage).toHaveURL(/checkout/); });

5.4 订单提交与支付模拟

支付环节通常需要模拟,因为不可能用真实支付进行自动化测试。Playwright的网络拦截功能在这里大放异彩。

test('用户提交订单并模拟支付成功', async ({ page }) => { const checkoutPage = new CheckoutPage(page); await checkoutPage.goto(); // 1. 填写/选择配送地址 await checkoutPage.selectAddress('默认地址'); // 2. 选择配送方式 await checkoutPage.selectShippingMethod('标准快递'); // 3. **关键:拦截创建订单和支付请求** // 拦截创建订单的POST请求,并模拟一个成功的响应 await page.route('**/api/order/create', async route => { // 可以在这里对请求体进行断言,验证传递的参数是否正确 const request = route.request(); const postData = request.postData(); // console.log('创建订单请求:', postData); // 模拟一个成功的响应,返回一个模拟的订单号 await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, orderId: 'MOCK_ORDER_123456', totalAmount: 99.9 }), }); }); // 拦截支付请求,模拟支付成功 await page.route('**/api/payment/submit', async route => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ success: true, paymentId: 'MOCK_PAY_789', status: 'paid' }), }); }); // 4. 提交订单 await checkoutPage.placeOrder(); // 5. 验证页面跳转到成功页,并显示正确的订单号 await expect(page).toHaveURL(/order-success/); await expect(page.locator('.order-id')).toContainText('MOCK_ORDER_123456'); await expect(page.locator('.success-icon')).toBeVisible(); }); // 同样,我们也需要测试支付失败的场景 test('用户支付失败流程', async ({ page }) => { const checkoutPage = new CheckoutPage(page); await checkoutPage.goto(); // ... 填写前置信息 // 拦截支付请求,模拟失败 await page.route('**/api/payment/submit', async route => { await route.fulfill({ status: 200, // 注意,接口可能本身是200,但body里success是false contentType: 'application/json', body: JSON.stringify({ success: false, code: 'INSUFFICIENT_BALANCE', message: '余额不足' }), }); }); await checkoutPage.placeOrder(); // 验证页面显示了正确的错误提示 await expect(page.locator('.error-message')).toContainText('余额不足'); // 验证页面没有跳转,仍然在结算页或支付页 await expect(page).not.toHaveURL(/order-success/); });

6. 高级技巧与稳定性提升实战

将基础流程跑通只是第一步,要让测试套件能在CI/CD流水线中稳定运行,成为团队信任的“守门员”,还需要更多工程化技巧。

6.1 处理动态内容与异步加载

电商页面充斥着动态内容:推荐商品、实时库存、倒计时、弹窗广告等。

  • 等待特定请求完成:对于依赖API加载数据的列表,可以等待对应的网络请求完成。
    // 先导航到页面,然后等待商品列表的API响应 await page.goto('/flash-sales'); const response = await page.waitForResponse(resp => resp.url().includes('/api/flash-sale/products') && resp.status() === 200 ); // 然后再去断言页面元素,此时数据已加载 await expect(page.locator('.flash-item')).toHaveCount.greaterThan(0);
  • 应对元素动态出现:使用locator.waitFor()
    // 点击按钮后,等待一个动态生成的弹窗出现 await page.click('button[data-action="show-modal"]'); const modal = page.locator('.dynamic-modal'); await modal.waitFor({ state: 'visible' }); await modal.locator('button.confirm').click(); await modal.waitFor({ state: 'hidden' }); // 等待弹窗消失

6.2 测试数据清理与隔离

测试不应该留下垃圾数据,也不应该相互影响。每条测试都应该是独立的。

  • API清理:test.beforeEachtest.afterEach钩子中,调用后端API清理测试数据(如刚创建的订单、测试用户)。
    import { test } from '@playwright/test'; test.beforeEach(async ({ request }) => { // 使用Playwright的APIRequestContext清理上一个测试可能残留的数据 // 假设有一个清理测试订单的接口,需要管理员权限 const cleanupResponse = await request.delete('/api/admin/test-orders', { headers: { 'Authorization': `Bearer ${adminToken}` } }); expect(cleanupResponse.ok()).toBeTruthy(); }); test.afterEach(async ({ request }, testInfo) => { if (testInfo.status !== 'passed') { // 如果测试失败,截图并保留更多日志,同时也可以尝试清理 console.log(`Test ${testInfo.title} failed. Attempting cleanup.`); // ... 清理逻辑 } });
  • 浏览器上下文隔离:Playwright Test默认会为每个测试用例创建一个独立的浏览器上下文(Context),这天然实现了Cookie、LocalStorage的隔离。确保不要在测试中依赖全局的、共享的浏览器状态。

6.3 集成CI/CD与生成测试报告

自动化测试的价值在于持续反馈。我们需要将其集成到GitHub Actions、GitLab CI、Jenkins等CI/CD工具中。

一个简单的GitHub Actions工作流示例:

# .github/workflows/playwright.yml name: Playwright E2E Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps chromium # CI上通常只安装一个浏览器以加快速度 - name: Run Playwright tests run: npx playwright test --project=chromium --reporter=html,github env: BASE_URL: ${{ secrets.STAGING_BASE_URL }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASS: ${{ secrets.TEST_PASS }} - name: Upload Playwright report if: always() # 即使测试失败也上传报告 uses: actions/upload-artifact@v3 with: name: playwright-report path: playwright-report/ retention-days: 7

报告分析:Playwright生成的HTML报告非常直观,可以看到每个测试的通过情况、耗时、截图、视频和追踪文件(Trace)。对于失败的测试,点击Trace文件可以像使用开发者工具时间旅行一样,一步步回放测试执行过程,查看每个步骤时的页面状态、网络请求和Console日志,这几乎是调试E2E测试最强大的功能。

7. 常见问题排查与调试技巧实录

即使有了完善的脚本,在复杂多变的电商前端环境中,测试失败仍是常事。以下是我在实践中总结的常见问题与排查思路。

问题现象可能原因排查步骤与解决方案
元素找不到 (TimeoutError)1. 页面未加载完成。
2. 元素定位器失效(前端代码更新)。
3. 元素在iframe或Shadow DOM内。
4. 动态内容尚未出现。
1.增加等待:在操作前使用page.waitForLoadState('networkidle')或等待特定元素/请求。
2.更新定位器:使用开发者工具重新检查元素,采用更稳定的定位策略(如>操作不生效 (点击/输入无效)
1. 元素被遮挡(弹窗、遮罩层)。
2. 元素状态不可交互(disabled, hidden)。
3. 页面有未处理的弹窗或脚本错误。
1.强制点击:尝试locator.click({ force: true })(谨慎使用)。
2.检查元素状态:先await expect(locator).toBeEnabled()toBeVisible()
3.监听页面错误:在config中设置use: { bypassCSP: true },并监听page.on('pageerror', ...)
测试在CI上失败,本地却通过1. CI环境与本地环境差异(数据、网络、配置)。
2. CI机器性能差,超时时间不足。
3. 测试存在竞态条件。
1.环境一致性:确保CI使用的测试环境与本地一致。使用Docker镜像。
2.调整超时:在CI配置中增加全局timeoutexpect.timeout
3.消除竞态:用Promise.all()处理并行导航,用明确的等待替代sleep
4.启用重试:在config中设置retries: 2
网络请求相关错误1. 接口响应慢或超时。
2. 拦截(mock)的请求与实际请求不匹配。
3. CORS问题。
1.Mock慢请求:使用page.route拦截并立即返回模拟数据,避免等待。
2.检查请求URL:在测试中打印拦截到的请求URL,确保模式匹配正确。
3.忽略CORS:在浏览器启动参数中添加--disable-web-security(仅测试环境)。
截图/视频显示页面空白或错位1. 在页面过渡动画期间截图。
2. CI服务器无头模式下的视图大小问题。
1.截图前等待:在截图前等待页面稳定,如await page.waitForLoadState('networkidle')
2.设置视口:在config或测试中明确设置viewport: { width: 1920, height: 1080 }

调试利器:Playwright Trace Viewer当测试失败时,第一时间打开生成的trace.zip文件(在test-results目录下)。通过命令npx playwright show-trace trace.zip可以在浏览器中打开一个强大的调试界面。你可以:

  • 时间线浏览:看到测试每一步的精确操作。
  • 查看快照:点击时间线上任意点,查看当时的页面UI、控制台日志和网络请求。
  • 性能分析:检查每个操作的耗时。 这能帮你快速定位是“页面没加载出来”、“元素定位错了”还是“接口返回异常”。

一个真实的踩坑记录:我们曾有一个测试,在本地始终通过,但在CI上随机失败。通过Trace Viewer发现,失败时页面上的一个核心JavaScript文件加载状态是(canceled)。最终排查出是因为CI服务器所在区域到某个CDN节点的网络不稳定。解决方案是在测试开始前,通过page.route拦截对该JS文件的请求,并直接返回一个我们预先下载好的本地稳定版本,彻底消除了网络波动的影响。