testing-webapps
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Web Applications
测试Web应用
To test local web applications, write native Python Playwright scripts.
要测试本地Web应用,请编写原生Python Playwright脚本。
Prerequisites
前置条件
Before running any Python scripts, activate the opencode virtual environment:
bash
source ~/.local/opencode-venv/bin/activateThen use normally. If Playwright browser binaries aren't installed yet:
python3 scripts/...bash
playwright install chromiumHelper Scripts Available:
- - Manages server lifecycle (supports multiple servers)
scripts/with_server.py
Always run scripts with first to see usage. DO NOT read the source until you try running the script first and find that a customized solution is abslutely necessary. These scripts can be very large and thus pollute your context window. They exist to be called directly as black-box scripts rather than ingested into your context window.
--help运行任何Python脚本之前,请激活opencode虚拟环境:
bash
source ~/.local/opencode-venv/bin/activate之后即可正常使用 命令。如果尚未安装Playwright浏览器二进制文件:
python3 scripts/...bash
playwright install chromium可用的辅助脚本:
- - 管理服务器生命周期(支持多服务器)
scripts/with_server.py
请务必先使用 参数运行脚本查看用法。在尝试运行脚本并确认确实需要自定义解决方案之前,请勿阅读源码。这些脚本可能非常庞大,会占用你的上下文窗口。它们的定位是可直接调用的黑盒脚本,而非需要加载到上下文窗口中的内容。
--helpDecision Tree: Choosing Your Approach
决策树:选择你的实现方式
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 selectorsUser 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 selectorsExample: Using with_server.py
示例:使用 with_server.py
To start a server, run first, then use the helper:
--helpSingle server:
bash
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.pyMultiple servers (e.g., backend + frontend):
bash
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_automation.pyTo create an automation script, include only Playwright logic (servers are managed automatically):
python
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()要启动服务器,请先运行 查看用法,再使用该辅助脚本:
--help单服务器:
bash
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py多服务器(例如:后端 + 前端):
bash
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逻辑即可(服务器会被自动管理):
python
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()Reconnaissance-Then-Action Pattern
先侦察后执行模式
-
Inspect rendered DOM:python
page.screenshot(path='/tmp/inspect.png', full_page=True) content = page.content() page.locator('button').all() -
Identify selectors from inspection results
-
Execute actions using discovered selectors
-
检查渲染后的DOM:python
page.screenshot(path='/tmp/inspect.png', full_page=True) content = page.content() page.locator('button').all() -
根据检查结果识别选择器
-
使用找到的选择器执行操作
Common Pitfall
常见陷阱
❌ Don't inspect the DOM before waiting for on dynamic apps
✅ Do wait for before inspection
networkidlepage.wait_for_load_state('networkidle')❌ 不要在动态应用中等待 之前检查DOM
✅ 请在检查前先执行
networkidlepage.wait_for_load_state('networkidle')Best Practices
最佳实践
- Use bundled scripts as black boxes - To accomplish a task, consider whether one of the scripts available in can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use
scripts/to see usage, then invoke directly.--help - Use for synchronous scripts
sync_playwright() - Always close the browser when done
- Use descriptive selectors: ,
text=, CSS selectors, or IDsrole= - Add appropriate waits: or
page.wait_for_selector()page.wait_for_timeout()
- 将内置脚本视为黑盒 - 完成任务时,请先考虑 目录下的可用脚本是否能提供帮助。这些脚本能够可靠地处理常见的复杂工作流,且不会占用上下文窗口。使用
scripts/查看用法后直接调用即可。--help - 同步脚本请使用
sync_playwright() - 任务完成后务必关闭浏览器
- 使用描述性选择器:、
text=、CSS选择器或IDrole= - 添加合适的等待逻辑:或
page.wait_for_selector()page.wait_for_timeout()
Waiting Correctly
正确的等待方式
networkidlepython
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-retriesNever as a substitute for a condition. A fixed sleep is either too short (flaky) or too long (slow), and it always hides the real race. The one legitimate use is waiting out a known animation duration before a screenshot.
wait_for_timeout()Prefer role-based locators over CSS — they survive restyling and they double as an accessibility assertion:
python
page.get_by_role("button", name="Delete account")
page.get_by_label("Email address")
page.get_by_role("heading", level=1)If cannot find a control, that is usually a genuine accessibility defect, not a test problem. See .
get_by_rolebuilding-accessible-interfacesnetworkidlepython
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定位器——它们不会因为样式调整而失效,同时还能作为可访问性断言:
python
page.get_by_role("button", name="Delete account")
page.get_by_label("Email address")
page.get_by_role("heading", level=1)如果 找不到某个控件,通常这是一个真实的可访问性缺陷,而非测试本身的问题。请参考 。
get_by_rolebuilding-accessible-interfacesAsserting Console Cleanliness
断言控制台无错误
A page that renders correctly while throwing errors is broken. Capture and assert.
python
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一个渲染正常但抛出错误的页面仍然是有问题的。请捕获并断言控制台状态。
python
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, errorsAccessibility Auditing
可访问性审计
Run axe against the live page. This is the automated third of the accessibility check — the keyboard walk and screen-reader pass in are still required.
building-accessible-interfacespython
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, violationsKeyboard traversal — catches missing focus styles and unreachable controls:
python
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}"在运行中的页面上执行axe检测。这只是可访问性检查中自动化的三分之一—— 中提到的键盘遍历和屏幕阅读器检测仍然是必需的。
building-accessible-interfacespython
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键盘遍历——可检测缺失的焦点样式和无法访问的控件:
python
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}"Responsive and Visual Checks
响应式与视觉检查
python
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)Then read the screenshots. A passing assertion says the DOM is not overflowing; it says nothing about whether the page looks right. For a full visual audit use the skill.
reviewing-interface-qualityDeterministic screenshots — disable animation and caret so repeat runs match:
python
page.screenshot(path="x.png", animations="disabled", caret="hide", full_page=True)python
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确定性截图——禁用动画和光标,确保多次运行结果一致:
python
page.screenshot(path="x.png", animations="disabled", caret="hide", full_page=True)Controlling the Network
网络控制
Testing error and empty states requires forcing them. This is how you exercise the state matrix from .
designing-user-experiencepython
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测试错误状态和空状态需要主动触发这些场景。这就是你验证 中提到的状态矩阵的方法。
designing-user-experiencepython
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 stateSlow response — verifies the loading state actually appears
Slow response — verifies the loading state actually appears
page.route("/api/", lambda r: (page.wait_for_timeout(3000), r.continue_())[1])
Emulate constrained conditions:
```python
ctx = browser.new_context(color_scheme="dark", reduced_motion="reduce",
locale="ar-EG", viewport={"width": 390, "height": 844})reduced_motion="reduce"prefers-reduced-motioncolor_scheme="dark"page.route("/api/", lambda r: (page.wait_for_timeout(3000), r.continue_())[1])
模拟受限环境:
```python
ctx = browser.new_context(color_scheme="dark", reduced_motion="reduce",
locale="ar-EG", viewport={"width": 390, "height": 844})reduced_motion="reduce"prefers-reduced-motioncolor_scheme="dark"Debugging a Failing Script
调试失败的脚本
python
browser = p.chromium.launch(headless=False, slow_mo=400) # watch it run
page.pause() # opens Playwright Inspectorbash
PWDEBUG=1 python3 your_script.py # step through
python3 -m playwright codegen localhost:5173 # record interactions into codeCapture a trace when a failure is intermittent:
python
context.tracing.start(screenshots=True, snapshots=True)python
browser = p.chromium.launch(headless=False, slow_mo=400) # watch it run
page.pause() # opens Playwright Inspectorbash
PWDEBUG=1 python3 your_script.py # step through
python3 -m playwright codegen localhost:5173 # record interactions into code当失败是偶发的,请捕获追踪信息:
python
context.tracing.start(screenshots=True, snapshots=True)...
...
context.tracing.stop(path="/tmp/trace.zip") # then: playwright show-trace /tmp/trace.zip
undefinedcontext.tracing.stop(path="/tmp/trace.zip") # then: playwright show-trace /tmp/trace.zip
undefinedCommon Pitfalls
常见陷阱
| Symptom | Cause | Fix |
|---|---|---|
| Element found but click does nothing | Overlay intercepting the event | |
| Passes headed, fails headless | Timing, or a font/layout difference | Wait on conditions, not timeouts; set an explicit viewport |
| Locator matched multiple elements | Narrow with |
| Screenshot is blank | Captured before paint | |
| Works once, fails on rerun | Test mutated state and did not reset | Use a fresh |
| Selector breaks after a restyle | CSS-class-based locator | Use |
| 症状 | 原因 | 解决方案 |
|---|---|---|
| 找到元素但点击无反应 | 遮罩层拦截了事件 | |
| 有头模式通过,无头模式失败 | 时序问题,或字体/布局差异 | 使用条件等待而非超时等待;设置明确的视口 |
| 定位器匹配到了多个元素 | 使用 |
| 截图为空白 | 在渲染完成前就捕获了 | 先执行 |
| 第一次运行成功,重运行失败 | 测试修改了状态且未重置 | 每次运行都使用全新的 |
| 样式调整后选择器失效 | 使用了基于CSS类的定位器 | 使用 |
Reference Files
参考文件
- examples/ - Examples showing common patterns:
- - Discovering buttons, links, and inputs on a page
element_discovery.py - - Using file:// URLs for local HTML
static_html_automation.py - - Capturing console logs during automation
console_logging.py
- examples/ - 包含常见模式的示例:
- - 发现页面上的按钮、链接和输入框
element_discovery.py - - 对本地HTML使用 file:// 协议URL
static_html_automation.py - - 在自动化过程中捕获控制台日志
console_logging.py
Related Skills
相关技能
- reviewing-interface-quality — full visual/UX/a11y audit of a rendered UI
- building-accessible-interfaces — what the axe results mean and how to fix them
- designing-user-experience — the state matrix these network mocks exist to exercise
- systematic-debugging — when a test failure is a real bug, not a test bug
- reviewing-interface-quality — 对渲染后的UI进行完整的视觉/UX/可访问性审计
- building-accessible-interfaces — 讲解axe检测结果的含义及修复方法
- designing-user-experience — 讲解这些网络模拟所要验证的状态矩阵
- systematic-debugging — 当测试失败是真实bug而非测试本身的问题时使用