osmontero/opencode-skills

testing-webapps

Use when testing local web applications using Playwright.

View source

How to use

  1. Copy the installation command and review the source package before installing it in your own agent environment.
  2. Check the source document for required tools, credentials and supported hosts.
  3. Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/osmontero/opencode-skills --skill testing-webapps

Before you start

  • Installation does not include a model subscription, API allowance or permission to access your files.
  • Repository stars and installation counts describe activity, not an independent quality or security test.
Original skill document

Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.


Testing Web Applications

To test local web applications, write native Python Playwright scripts.

Prerequisites

Before running any Python scripts, activate the opencode virtual environment:

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

Then use python3 scripts/... normally. If Playwright browser binaries aren't installed yet:

bash
playwright install chromium

Helper Scripts Available:

  • scripts/with_server.py - Manages server lifecycle (supports multiple servers)

Always run scripts with `--help` 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.

Decision 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 selectors

Example: Using with_server.py

To start a server, run --help first, then use the helper:

Single server:

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

Multiple 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.py

To 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()

Reconnaissance-Then-Action Pattern

  1. Inspect rendered DOM:
python
   page.screenshot(path='/tmp/inspect.png', full_page=True)
   content = page.content()
   page.locator('button').all()
  1. Identify selectors from inspection results
  1. Execute actions using discovered selectors

Common Pitfall

❌ Don't inspect the DOM before waiting for networkidle on dynamic apps ✅ Do wait for page.wait_for_load_state('networkidle') before inspection

Best Practices

  • Use bundled scripts as black boxes - To accomplish a task, consider whether one of the scripts available in scripts/ can help. These scripts handle common, complex workflows reliably without cluttering the context window. Use --help to see usage, then invoke directly.
  • Use sync_playwright() for synchronous scripts
  • Always close the browser when done
  • Use descriptive selectors: text=, role=, CSS selectors, or IDs
  • Add appropriate waits: page.wait_for_selector() or page.wait_for_timeout()

Waiting Correctly

networkidle is the right default for first load, but it is the wrong tool for asserting that a specific thing happened. Prefer waiting on the condition you actually care about.

python
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

Never `wait_for_timeout()` 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.

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 get_by_role cannot find a control, that is usually a genuine accessibility defect, not a test problem. See building-accessible-interfaces.

Asserting 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

Accessibility Auditing

Run axe against the live page. This is the automated third of the accessibility check — the keyboard walk and screen-reader pass in building-accessible-interfaces are still required.

python
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

Keyboard 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}"

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 reviewing-interface-quality skill.

Deterministic screenshots — disable animation and caret so repeat runs match:

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-experience.

python
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])

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" verifies the prefers-reduced-motion block exists. color_scheme="dark" catches contrast that only fails in dark mode.

Debugging a Failing Script

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

Capture a trace when a failure is intermittent:

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

Common Pitfalls

SymptomCauseFix
Element found but click does nothingOverlay intercepting the eventpage.get_by_role(...).click() reports the interceptor — dismiss it first
Passes headed, fails headlessTiming, or a font/layout differenceWait on conditions, not timeouts; set an explicit viewport
strict mode violationLocator matched multiple elementsNarrow with .first, a role, or a more specific name
Screenshot is blankCaptured before paintwait_for_load_state("networkidle") plus a settle timeout
Works once, fails on rerunTest mutated state and did not resetUse a fresh browser.new_context() per run
Selector breaks after a restyleCSS-class-based locatorUse get_by_role / get_by_label / data-testid

Reference Files

  • examples/ - Examples showing common patterns:
  • element_discovery.py - Discovering buttons, links, and inputs on a page
  • static_html_automation.py - Using file:// URLs for local HTML
  • console_logging.py - Capturing console logs during automation

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

Related by task

from this repository

More skills

All skills
osmontero
Community

building-accessible-interfaces

Use when building or auditing any web UI for accessibility — keyboard navigation, screen readers, focus management, ARIA, color contrast, form labels and error announcements, reduced motion, or touch target size. Triggers include "is this accessible", "WCAG", "a11y", "screen reader", "keyboard navigation", "focus trap", "contrast ratio", "aria-label", or building modals, menus, tabs, comboboxes, toasts, and custom controls that must work without a mouse.

installs
1
GitHub stars
14
Updated
Oct 7
osmontero
Community

designing-frontend-interfaces

Use when building or restyling any web interface — landing pages, dashboards, marketing sites, web apps, React/Vue components, HTML/CSS layouts, artifacts, posters — or when asked to make a UI look better, more polished, more distinctive, less generic, or less "AI-generated". Also use when a design brief, art direction, visual identity, type scale, or color system is needed before implementation.

installs
1
GitHub stars
14
Updated
Oct 7
osmontero
Community

systematic-debugging

Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes

installs
1
GitHub stars
13
Updated
Oct 5
osmontero
Community

designing-user-experience

Use when designing how an interface behaves rather than how it looks — user flows, navigation and information architecture, forms and validation, loading/empty/error states, confirmation and undo for destructive actions, button and error message wording, feedback for slow operations, or mobile and touch behavior. Also use when a UI looks finished but breaks on bad input, slow networks, no data, or first use.

installs
1
GitHub stars
8
Updated
Aug 27