mcpField
F
fixing-flaky-e2e-tests

fixing-flaky-e2e-tests

github.com/streamlit测试与质量46.2k45 次安装今天更新
代码仓库

关于 fixing-flaky-e2e-tests

用于诊断和修复可在本地复现的 Playwright E2E 不稳定测试。流程强调先复现、采用最小改动,并通过多次运行验证修复。

主要功能

  1. 01根据 CI 数据查找不稳定测试
  2. 02要求本地复现
  3. 03检查跟踪记录与快照
  4. 04提供时序和浏览器问题指导
  5. 05通过多次运行验证修复

适用场景

  1. 01调查间歇性 Playwright 测试失败
  2. 02调试特定浏览器的快照不匹配
  3. 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 很重要)

完整工作流

  1. 获取不稳定测试:uv run scripts/fetch_flaky_tests.py --top 10

  2. 过滤掉已标记的测试:跳过带有 @pytest.mark.flaky 的测试

  3. 对于每个剩余测试:

    • 阅读测试代码以了解它测试的内容
    • 在本地复现不稳定性(最多 25 次运行)
    • 如果无法复现则跳过:如果 25 次运行全部通过,则继续下一个测试
    • 确定根本原因(时序、浏览器特定等)
    • 应用最小修复
    • 在受影响的浏览器上通过 10+ 次运行进行验证
  4. 运行检查:提交前运行 make check

规则

  • 先在本地复现:只修复你能在本地复现的测试(最多 25 次运行)
  • 最小修复:能修复问题的最小改动
  • 无正当理由不要禁用测试:绝不要仅仅为了“修复”不稳定性而跳过测试。仅在以下情况下 skip_browser 才可接受:
    1. 你已经用尽了所有时序/等待修复
    2. 不稳定性是由于有文档记录的浏览器限制(而非测试缺陷)
    3. 你附带了清晰的 reason 说明原因
  • 彻底验证:修复后在受影响的浏览器上运行 10+ 次
  • 保留测试意图:理解测试正在验证的内容
  • 记录原因:添加注释说明为什么需要等待/超时

评论

支持 Markdown