关于 fixing-flaky-e2e-tests
用于诊断和修复可在本地复现的 Playwright E2E 不稳定测试。流程强调先复现、采用最小改动,并通过多次运行验证修复。
主要功能
- 01根据 CI 数据查找不稳定测试
- 02要求本地复现
- 03检查跟踪记录与快照
- 04提供时序和浏览器问题指导
- 05通过多次运行验证修复
适用场景
- 01调查间歇性 Playwright 测试失败
- 02调试特定浏览器的快照不匹配
- 03修复时序问题以稳定测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ streamlit/ streamlit --skill fixing-flaky-e2e-tests安装后在 Claude Code 中输入/fixing-flaky-e2e-tests确认已加载。
详细介绍
修复不稳定的 E2E 测试
诊断并修复 e2e_playwright/ 中不稳定的 Playwright E2E 测试。
何时使用
- 测试间歇性失败(有时通过,有时失败)
- 超时错误(
TimeoutError: wait_until timed out) - 快照不匹配,存在像素差异
- 特定浏览器的失败(firefox、webkit、chromium)
- 用户要求修复 CI 中排名靠前的不稳定测试
查找排名靠前的不稳定测试
运行脚本以从最近的 CI 运行中识别最不稳定的测试:
uv run scripts/fetch_flaky_tests.py
选项:
--days N:回溯 N 天(默认:4)--top N:返回排名前 N 的不稳定测试(默认:10)--min-reruns N:纳入统计的最小总重跑次数(默认:2)--json:以 JSON 格式输出,供程序化使用
该脚本会从成功的 playwright.yml 运行中下载 playwright_test_stats 工件,并汇总需要重跑的测试。
筛选要修复的测试
跳过已标记为 @pytest.mark.flaky 的测试——这些是已知的不稳定测试,正在单独跟踪。
# Check if a test file has the flaky marker
grep -l "pytest.mark.flaky" e2e_playwright/<test_file>.py
排查工作流
1. 在本地复现不稳定问题(必需)
重要:只尝试修复在本地失败的测试。如果运行 25 次后仍无法复现不稳定问题,请勿尝试修复——该测试的不稳定可能由本地无法解决的 CI 环境因素导致。
使用受影响的浏览器运行测试最多 25 次。循环会在首次失败时中断并捕获完整输出:
for i in {1..25}; do
result=$(make run-e2e-test e2e_playwright/test_file.py::test_name -- --browser firefox 2>&1)
if echo "$result" | grep -q "FAILED"; then
echo "=== FAILURE ON RUN $i ==="
echo "$result"
break
fi
echo "Run $i: PASSED"
done
如果 25 次运行全部通过,则跳过此测试并继续下一个。
2. 检查测试工件
失败后,检查:
e2e_playwright/test-results/- 追踪、截图、视频e2e_playwright/test-results/snapshot-updates/- 实际与预期快照
针对持续性的快照不稳定:如果某个测试因快照不匹配而反复失败,请比较 e2e_playwright/test-results/snapshot-updates/ 中的实际与预期图像。查找:
- 像素级差异(使用图像差异工具或叠加对比)
- 细微的布局偏移、字体渲染变化或时序伪影
- 特定浏览器的渲染怪癖(尤其是 Firefox 的亚像素问题)
这有助于判断不稳定是由时序(内容未加载)、动画状态还是浏览器渲染差异引起的。
常见原因和修复方法
时序问题(最常见)
症状:在元素完全渲染之前或动画未完成时截图。
修复:在交互或截图前添加显式等待:
# Before
element.click()
assert_snapshot(element, name="snapshot")
# After
element.click()
expect(element).to_be_visible() # Wait for visibility
assert_snapshot(element, name="snapshot")
对于有动画的弹出框/模态框/日历:
calendar = page.get_by_test_id("stDateInputCalendar").first
expect(calendar).to_be_visible() # Wait for animation to complete
assert_snapshot(calendar, name="calendar-snapshot")
浏览器重试导致额外事件
症状:断言期望精确计数但得到更多(例如 assert 44 == 41)。
修复:当浏览器可能重试失败的操作时,使用 >= 代替 ==:
# Before
assert error_count == expected_count
# After - browsers may retry failed image loads
assert error_count >= expected_count
超时过短
症状:在较慢的浏览器上出现 TimeoutError。
修复:为可能较慢的操作增加超时:
# Before
wait_until(app, lambda: check_condition(), timeout=10000)
# After
wait_until(app, lambda: check_condition(), timeout=20000)
时序导致的快照不匹配
症状:Snapshot mismatch for ... (X pixels difference)。
原因:
- 截图时元素仍在动画中
- 字体渲染未完成
- 异步内容未加载
- 图像未完全加载/解码(尤其是在 webkit 中)
修复:确保截图前元素已稳定:
element = page.locator(".my-element")
expect(element).to_be_visible()
# For elements with animations, wait for specific CSS state:
expect(element).to_have_css("opacity", "1")
assert_snapshot(element, name="snapshot")
对于包含图像的的元素,等待图像完全加载并解码:
from e2e_playwright.shared.app_utils import wait_for_images_loaded
element = page.locator(".my-element")
wait_for_images_loaded(element) # Waits for load + decode
assert_snapshot(element, name="snapshot")
特定浏览器的注意事项
| 浏览器 | 常见问题 |
|---|---|
| Firefox | 控制台日志记录较慢,可能重试失败的请求,亚像素渲染差异 |
| Webkit | 布局可能存在时序差异 |
| Chromium | 通常最可靠,可作为基准 |
Firefox 亚像素渲染不稳定
现象:由于亚像素渲染的变化,Firefox 截图会出现 1 像素差异的不稳定情况。
修复:在被测试元素上方添加一个单行 markdown 元素。这会将亚像素位置移动到一个更稳定的值:
# In the test app (.py file)
st.markdown("---") # Stabilizes subpixel rendering for elements below
st.date_input("Pick a date")
这是针对 Firefox 亚像素渲染行为的变通方案,当其他时序修复都无济于事时,它可以减少快照的不稳定性。
如果你已经用尽了时序修复手段,并且这种不稳定性仅在特定浏览器上由于已知的浏览器限制(而非测试缺陷)而持续存在,那么 skip_browser 可能适合作为最后手段:
# Only use after confirming this is a browser-level limitation, not a fixable timing issue
@pytest.mark.skip_browser("webkit", reason="Webkit has known layout timing issues with this element")
def test_problematic_on_webkit(app: Page):
...
重要: 使用 skip_browser 需要提供理由。优先修复底层的时序问题。关于何时可以接受跳过,请参阅“规则”部分。
验证
应用修复后,通过多次运行进行验证:
# Run 10+ times to ensure stability
for i in {1..10}; do
make run-e2e-test e2e_playwright/test_file.py::test_name -- --browser firefox 2>&1 | grep -E "(PASSED|FAILED)"
done
目标:在认为修复完成之前达到 10/10 通过。
关键实用工具
来自 e2e_playwright.conftest:
wait_for_app_run(page)- 等待 Streamlit 脚本执行wait_for_app_loaded(page)- 等待应用初始加载wait_until(page, fn, timeout)- 轮询直到条件为真
来自 e2e_playwright.shared.app_utils:
expect_no_skeletons(element)- 等待加载骨架屏消失reset_focus(page)- 点击外部以触发失焦事件reset_hovering(locator)- 将鼠标移开元素wait_for_images_loaded(element)- 等待图片加载并解码完成(对 webkit 很重要)
完整工作流
-
获取不稳定测试:
uv run scripts/fetch_flaky_tests.py --top 10 -
过滤掉已标记的测试:跳过带有
@pytest.mark.flaky的测试 -
对于每个剩余测试:
- 阅读测试代码以了解它测试的内容
- 在本地复现不稳定性(最多 25 次运行)
- 如果无法复现则跳过:如果 25 次运行全部通过,则继续下一个测试
- 确定根本原因(时序、浏览器特定等)
- 应用最小修复
- 在受影响的浏览器上通过 10+ 次运行进行验证
-
运行检查:提交前运行
make check
规则
- 先在本地复现:只修复你能在本地复现的测试(最多 25 次运行)
- 最小修复:能修复问题的最小改动
- 无正当理由不要禁用测试:绝不要仅仅为了“修复”不稳定性而跳过测试。仅在以下情况下
skip_browser才可接受:- 你已经用尽了所有时序/等待修复
- 不稳定性是由于有文档记录的浏览器限制(而非测试缺陷)
- 你附带了清晰的
reason说明原因
- 彻底验证:修复后在受影响的浏览器上运行 10+ 次
- 保留测试意图:理解测试正在验证的内容
- 记录原因:添加注释说明为什么需要等待/超时