Playwright测试偶发失败原因及稳定化方案咨询
Playwright测试用例偶发失败排查与修复
问题背景
我有一组Playwright测试用例,存在偶发失败的情况(原问题基于开发中网站,已适配公开URL复现)。文件结构如下:
├── Dockerfile ├── package.json ├── tests │ ├── 00.spec.ts │ ├── 01.spec.ts │ ├── 02.spec.ts │ ├── 03.spec.ts │ ├── 04.spec.ts
其中tests/00.spec.ts整合了01-04的测试用例,因此4个测试场景各会运行两次(一次在00.spec.ts,一次在对应0x.spec.ts)。
执行命令:
docker build -t playwright-test . && docker run --rm -it playwright-test
连续执行10次的结果:
- 全部成功
- 04失败,其余成功
- 全部成功
- 00(league form 1)失败,其余成功
- 04失败,其余成功
- 03失败,其余成功
- 02失败,其余成功
- 00(league form 1)失败,其余成功
- 全部成功
- 全部成功
失败错误信息
测试失败时抛出超时错误,日志如下:
1) tests/00.spec.ts:54:7 › league form 1 ──────────────────────────────────────────────────────── Test timeout of 30000ms exceeded. Error: locator.click: Test timeout of 30000ms exceeded. Call log: - waiting for locator('#news-sidebar .sidebar-teaser form').locator('.form-wrapper').locator('.select-wrapper').nth(5).locator('a').filter({ hasText: /^Deutschland$/ }) - locator resolved to <a class="ng-binding" data-ng-click="wamData.areas.…>Deutschland</a> - attempting click action - waiting for element to be visible, enabled and stable - element is not visible - waiting... 58 | for (const div of await form_wrapper.locator('.select-wrapper').all()) { 59 | await div.locator('span.icon-angle-down').click(); // show dropdown options > 60 | await div.locator('a', {hasText: new RegExp('^' + leagues[url][i] + '$') }).click(); // select option | ^ 61 | i++; 62 | } 63 | await form.locator('button:has-text("anzeigen")').click(); at /usr/src/app/tests/00.spec.ts:60:83
原因分析
从错误日志和测试行为来看,偶发失败的核心原因是交互时机不对,具体包括:
- 点击下拉按钮后,下拉选项未完全渲染/展开就尝试点击,导致Playwright等待元素稳定超时
- Cookie弹窗处理不严谨:未等待弹窗元素完全可见就执行点击,可能导致后续页面交互受影响
- DOM动态变化影响:一次性获取所有
.select-wrapper元素后遍历,若页面在交互过程中DOM发生变化,会导致已获取的元素引用失效 - Playwright版本不兼容:Docker镜像使用v1.41.1,但package.json依赖的
@playwright/test是v1.16.1,版本差异可能导致API行为不一致
解决方案
1. 同步Playwright版本
Docker镜像和package.json中的Playwright版本保持一致,避免兼容性问题:
- 修改package.json的
@playwright/test版本为^1.41.1 - Dockerfile保持原镜像版本即可
2. 优化Cookie弹窗处理
等待弹窗元素可见后再执行点击,确保操作时机正确:
test.beforeEach(async ({ page }) => { await page.goto('http://www.fussball.de'); // 等待Cookie弹窗加载完成并可见 const ucr = page.locator('#usercentrics-root'); await ucr.waitFor({ state: 'visible' }); await ucr.locator('button:has-text("Akzeptiere alle")').click(); // 等待弹窗消失,避免遮挡后续元素 await ucr.waitFor({ state: 'hidden' }); });
3. 优化下拉选项点击逻辑
点击下拉按钮后,等待目标选项可见再执行点击,确保元素处于可交互状态:
for (let i = 0; i < leagues[url].length; i++) { // 按索引定位下拉框,避免DOM变化导致的元素失效 const selectWrapper = form_wrapper.locator('.select-wrapper').nth(i); await selectWrapper.locator('span.icon-angle-down').click(); // 等待目标选项可见后再点击 const targetOption = selectWrapper.locator('a', { hasText: new RegExp('^' + leagues[url][i] + '$') }); await targetOption.waitFor({ state: 'visible' }); await targetOption.click(); }
4. 调整Playwright配置(可选)
在playwright.config.ts中增加超时时间或调整重试策略,应对极端慢加载场景:
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ timeout: 60000, // 全局超时调整为60秒 retries: 2, // 失败测试自动重试2次 use: { actionTimeout: 30000, // 单个操作超时 baseURL: 'http://www.fussball.de', }, });
优化后代码示例(以00.spec.ts为例)
import { test, expect } from '@playwright/test'; test.beforeEach(async ({ page }) => { await page.goto('/'); // 处理Cookie弹窗 const ucr = page.locator('#usercentrics-root'); await ucr.waitFor({ state: 'visible' }); await ucr.locator('button:has-text("Akzeptiere alle")').click(); await ucr.waitFor({ state: 'hidden' }); }); const leagues = { 'https://www.fussball.de/spieltagsuebersicht/a-junioren-bundesliga-west-deutschland-a-junioren-bundesliga-a-junioren-saison2324-deutschland/-/staffel/02LNP13DU800000FVS5489B4VUAB0UC4-G#!/': [ 'Deutschland', '23/24', 'Meisterschaften', 'A-Junioren', 'Bundesliga', 'Deutschland', 'A-Junioren-Bundesliga West' ], 'https://www.fussball.de/spieltagsuebersicht/bfv-landesliga-odenwald-baden-landesliga-herren-saison2324-baden/-/staffel/02M5LMHKRK00000FVS5489B4VSAUO6GA-G#!/': [ 'Baden', '23/24', 'Meisterschaften', 'Herren', 'Landesliga', 'Baden', 'bfv-Landesliga Odenwald' ], 'https://www.fussball.de/spieltagsuebersicht/landesliga-mitte-bayern-landesliga-herren-saison2324-bayern/-/staffel/02LS70U7C4000007VS5489B4VUAB0UC4-G#!/': [ 'Bayern', '23/24', 'Meisterschaften', 'Herren', 'Landesliga', 'Bayern', 'Landesliga Mitte' ], 'https://www.fussball.de/spieltagsuebersicht/herren-landesliga-st1-berlin-landesliga-herren-saison2324-berlin/-/staffel/02M7480S2O000006VS5489B4VSAUO6GA-G#!/': [ 'Berlin', '23/24', 'Meisterschaften', 'Herren', 'Landesliga', 'Berlin', 'Herren Landesliga St.1' ], }; let j = 1; for (const url of Object.keys(leagues)) { test(`league form ${j}`, async ({ page }) => { const form = page.locator('#news-sidebar .sidebar-teaser form'); const form_wrapper = form.locator('.form-wrapper'); for (let i = 0; i < leagues[url].length; i++) { const selectWrapper = form_wrapper.locator('.select-wrapper').nth(i); await selectWrapper.locator('span.icon-angle-down').click(); const targetOption = selectWrapper.locator('a', { hasText: new RegExp(`^${leagues[url][i]}$`) }); await targetOption.waitFor({ state: 'visible' }); await targetOption.click(); } await form.locator('button:has-text("anzeigen")').click(); await expect(page).toHaveURL(url); }); j++; }
修改后的package.json片段
{ "devDependencies": { "@playwright/test": "^1.41.1", "jest": "^27.3.1" } }
内容的提问来源于stack exchange,提问作者veronikan
相关产品推荐
相关产品推荐

