mcpField
T
testing-webapps

testing-webapps

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

关于 testing-webapps

使用原生 Python Playwright 脚本测试本地 Web 应用的技能。内容涵盖服务管理、浏览器交互、无障碍与响应式检查、网络状态测试及调试方法。

主要功能

  1. 01使用 Python Playwright 进行浏览器测试
  2. 02管理一个或多个本地服务
  3. 03检查页面并通过定位器交互
  4. 04检查无障碍和响应式布局
  5. 05模拟网络及应用状态
  6. 06用跟踪记录和 Inspector 调试

适用场景

  1. 01发布前测试本地 Web 应用
  2. 02验证表单和交互控件
  3. 03检查不同视口尺寸下的布局
  4. 04测试错误、空数据和离线状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/osmontero/opencode-skills --skill testing-webapps

安装后在 Claude Code 中输入/testing-webapps确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE.txt
examples/
console_logging.py
element_discovery.py
static_html_automation.py
scripts/
with_server.py

详细介绍


测试 Web 应用

要测试本地 Web 应用,请编写原生 Python Playwright 脚本。

前提条件

运行任何 Python 脚本之前,请激活 opencode 虚拟环境:

source ~/.local/opencode-venv/bin/activate

然后照常使用 python3 scripts/...。如果尚未安装 Playwright 浏览器二进制文件:

playwright install chromium

可用的辅助脚本:

  • scripts/with_server.py - 管理服务生命周期(支持多个服务)

务必先使用 --help 运行脚本,查看用法。除非你先尝试运行脚本,并确认确实需要定制解决方案,否则不要阅读源码。这些脚本可能非常庞大,会占用上下文窗口。它们的设计用途是作为黑盒脚本直接调用,而不是载入上下文窗口。

决策树:选择你的方法

User task → Is it static HTML?
    ├─ Yes → Read HTML file directly to identify selectors
    │         ├─ Success → Write Playwright script using selectors
    │         └─ Fails/Incomplete → Treat as dynamic (below)
    │
    └─ No (dynamic webapp) → Is the server already running?
        ├─ No → Run: python scripts/with_server.py --help
        │        Then use the helper + write simplified Playwright script
        │
        └─ Yes → Reconnaissance-then-action:
            1. Navigate and wait for networkidle
            2. Take screenshot or inspect DOM
            3. Identify selectors from rendered state
            4. Execute actions with discovered selectors

示例:使用 with_server.py

启动服务时,先运行 --help,然后使用辅助脚本:

单个服务:

python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py

多个服务(例如,后端 + 前端):

python scripts/with_server.py \
  --server "cd backend && python server.py" --port 3000 \
  --server "cd frontend && npm run dev" --port 5173 \
  -- python your_automation.py

创建自动化脚本时,只需包含 Playwright 逻辑(服务会自动管理):

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
    page = browser.new_page()
    page.goto('http://localhost:5173') # Server already running and ready
    page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
    # ... your automation logic
    browser.close()

先侦察,再行动模式

  1. 检查渲染后的 DOM:

    page.screenshot(path='/tmp/inspect.png', full_page=True)
    content = page.content()
    page.locator('button').all()
    
  2. 根据检查结果确定选择器

  3. 使用找到的选择器执行操作

常见陷阱

❌ 不要在动态应用上等待 networkidle 之前检查 DOM ✅ 应当先等待 page.wait_for_load_state('networkidle'),再进行检查

最佳实践

  • 将打包脚本作为黑盒使用 - 为完成任务,请考虑 scripts/ 中的脚本是否能提供帮助。这些脚本可以可靠地处理常见的复杂工作流,同时避免占用上下文窗口。使用 --help 查看用法,然后直接调用。
  • 同步脚本使用 sync_playwright()
  • 完成后务必关闭浏览器
  • 使用描述性选择器:text=、role=、CSS 选择器或 ID
  • 添加适当的等待:page.wait_for_selector() 或 page.wait_for_timeout()

正确等待

首次加载时,networkidle 是合适的默认选择;但在断言特定事件是否发生时,它并不适用。优先等待你实际关心的条件。

page.wait_for_selector("[data-testid=results]", state="visible")
page.wait_for_function("() => document.querySelectorAll('.row').length > 0")
page.get_by_role("button", name="Save").click()
expect(page.get_by_text("Saved")).to_be_visible()      # auto-retries

切勿用 wait_for_timeout() 代替条件等待。 固定时长的等待要么太短(导致不稳定),要么太长(导致缓慢),而且总会掩盖真正的竞态。唯一合理的用途是在截图前等待已知时长的动画结束。

优先使用基于角色的定位器,而非 CSS——它们不易受样式调整影响,也能同时验证可访问性:

page.get_by_role("button", name="Delete account")
page.get_by_label("Email address")
page.get_by_role("heading", level=1)

如果 get_by_role 找不到控件,通常说明存在真正的可访问性缺陷,而不是测试问题。请参阅 building-accessible-interfaces。

断言控制台无错误

页面虽然正确渲染,但同时抛出错误,仍然说明页面有问题。请捕获并断言。

errors, warnings = [], []
page.on("console", lambda m: (errors if m.type == "error" else warnings).append(m.text)
        if m.type in ("error", "warning") else None)
page.on("pageerror", lambda e: errors.append(f"uncaught: {e}"))
page.on("requestfailed", lambda r: errors.append(f"{r.method} {r.url} — {r.failure}"))

page.goto(URL); page.wait_for_load_state("networkidle")
assert not errors, errors

可访问性审查

针对实时页面运行 axe。这是可访问性检查中自动化的部分——仍需按照 building-accessible-interfaces 中的要求进行键盘遍历和屏幕阅读器检查。

page.add_script_tag(url="https://cdn.jsdelivr.net/npm/axe-core@4/axe.min.js")
result = page.evaluate("async () => await axe.run()")
violations = [(v["id"], v["impact"], len(v["nodes"])) for v in result["violations"]]
assert not violations, violations

键盘遍历——可发现缺失的焦点样式和无法访问的控件:

seq = []
for _ in range(40):
    page.keyboard.press("Tab")
    seq.append(page.evaluate("""() => {
        const el = document.activeElement;
        if (!el || el === document.body) return null;
        const s = getComputedStyle(el);
        return {tag: el.tagName, name: el.ariaLabel || el.innerText?.slice(0,30),
                outline: s.outlineStyle, ring: s.boxShadow !== 'none'};
    }"""))
invisible = [s for s in seq if s and s["outline"] == "none" and not s["ring"]]
assert not invisible, f"focusable elements with no visible focus: {invisible}"

响应式与视觉检查

for name, w, h in [("mobile", 390, 844), ("tablet", 768, 1024), ("desktop", 1440, 900)]:
    page.set_viewport_size({"width": w, "height": h})
    page.wait_for_timeout(200)                       # settle layout/animation
    overflow = page.evaluate("""() => [...document.querySelectorAll('*')]
        .filter(e => e.getBoundingClientRect().right > document.documentElement.clientWidth + 1)
        .slice(0,5).map(e => e.tagName + '.' + e.className)""")
    assert not overflow, f"{name}: horizontal overflow from {overflow}"
    page.screenshot(path=f"/tmp/{name}.png", full_page=True)

然后查看截图。 断言通过只能说明 DOM 没有溢出,无法说明页面看起来是否正确。如需进行完整的视觉审查,请使用 reviewing-interface-quality 技能。

确定性截图——禁用动画和插入符,确保重复运行得到相同结果:

page.screenshot(path="x.png", animations="disabled", caret="hide", full_page=True)

控制网络

测试错误状态和空状态需要强制触发它们。这样才能测试 designing-user-experience 中的状态矩阵。

page.route("**/api/items", lambda r: r.fulfill(status=200, json=[]))        # empty state
page.route("**/api/items", lambda r: r.fulfill(status=500))                 # error state
page.route("**/api/items", lambda r: r.abort())                             # network failure
page.context.set_offline(True)                                              # offline state

# Slow response — verifies the loading state actually appears
page.route("**/api/**", lambda r: (page.wait_for_timeout(3000), r.continue_())[1])

模拟受限条件:

ctx = browser.new_context(color_scheme="dark", reduced_motion="reduce",
                          locale="ar-EG", viewport={"width": 390, "height": 844})

reduced_motion="reduce" 可验证 prefers-reduced-motion 代码块是否存在。color_scheme="dark" 可发现仅在深色模式下出现的对比度问题。

调试失败的脚本

browser = p.chromium.launch(headless=False, slow_mo=400)   # watch it run
page.pause()                                               # opens Playwright Inspector
PWDEBUG=1 python3 your_script.py            # step through
python3 -m playwright codegen localhost:5173  # record interactions into code

失败偶尔出现时,捕获跟踪记录:

context.tracing.start(screenshots=True, snapshots=True)
# ...
context.tracing.stop(path="/tmp/trace.zip")   # then: playwright show-trace /tmp/trace.zip

常见陷阱

现象原因修复方法
找到了元素,但点击没有反应覆盖层拦截了事件page.get_by_role(...).click() 会报告拦截元素 — 先将其关闭
有头模式下通过,无头模式下失败时序问题,或字体/布局差异等待条件,而不是等待超时;设置明确的视口
strict mode violation定位器匹配了多个元素使用 .first、角色或更具体的名称缩小范围
截图为空白在绘制完成前捕获wait_for_load_state("networkidle"),并加上稳定等待超时
第一次成功,重新运行时失败测试修改了状态,却没有重置每次运行都使用新的 browser.new_context()
样式重设后选择器失效基于 CSS 类的定位器使用 get_by_role/get_by_label/data-testid

参考文件

  • examples/ - 展示常见模式的示例:
    • element_discovery.py - 在页面上发现按钮、链接和输入框
    • static_html_automation.py - 对本地 HTML 使用 file:// URL
    • console_logging.py - 在自动化过程中捕获控制台日志

相关技能

  • reviewing-interface-quality — 对渲染后的 UI 进行全面的视觉/UX/无障碍审查
  • building-accessible-interfaces — axe 结果的含义及修复方法
  • designing-user-experience — 这些网络模拟旨在测试的状态矩阵
  • systematic-debugging — 当测试失败是由真实缺陷而非测试缺陷导致时

评论

支持 Markdown