关于 testing-webapps
使用原生 Python Playwright 脚本测试本地 Web 应用的技能。内容涵盖服务管理、浏览器交互、无障碍与响应式检查、网络状态测试及调试方法。
主要功能
- 01使用 Python Playwright 进行浏览器测试
- 02管理一个或多个本地服务
- 03检查页面并通过定位器交互
- 04检查无障碍和响应式布局
- 05模拟网络及应用状态
- 06用跟踪记录和 Inspector 调试
适用场景
- 01发布前测试本地 Web 应用
- 02验证表单和交互控件
- 03检查不同视口尺寸下的布局
- 04测试错误、空数据和离线状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ osmontero/ opencode-skills --skill testing-webapps安装后在 Claude Code 中输入/testing-webapps确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.txtexamples/console_logging.pyelement_discovery.pystatic_html_automation.pyscripts/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()
先侦察,再行动模式
-
检查渲染后的 DOM:
page.screenshot(path='/tmp/inspect.png', full_page=True) content = page.content() page.locator('button').all() -
根据检查结果确定选择器
-
使用找到的选择器执行操作
常见陷阱
❌ 不要在动态应用上等待 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:// URLconsole_logging.py- 在自动化过程中捕获控制台日志
相关技能
- reviewing-interface-quality — 对渲染后的 UI 进行全面的视觉/UX/无障碍审查
- building-accessible-interfaces — axe 结果的含义及修复方法
- designing-user-experience — 这些网络模拟旨在测试的状态矩阵
- systematic-debugging — 当测试失败是由真实缺陷而非测试缺陷导致时