# QA Testing

> Perform QA testing on running web applications using cmux_browser. Covers functional testing, accessibility auditing, edge case testing, and structured reporting with evidence. **Triggers — use this skill when:** - User says "test this", "QA this", "QA this PR", "check the app" - User says "run QA", "verify the build", "test the UI" - User asks to "check if it works", "validate the feature" - User says "acceptance testing", "smoke test", "regression test" - User asks to "find bugs", "test for bugs", "break it" - User wants to "check accessibility", "run axe", "a11y audit" **Covers:** Any web application accessible via HTTP. Uses cmux_browser (Playwright under the hood) for all browser interactions.

- Skill: `espennilsen/qa-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add espennilsen/qa-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/espennilsen/qa-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: espennilsen (https://skillmd.com/u/espennilsen)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/espennilsen/qa-testing

---


# QA Testing — Web Application Testing with cmux_browser

Test running web applications by interacting as a real user via cmux_browser, then report findings with structured evidence.

## Prerequisites

- The application must be running and accessible via HTTP
- cmux_browser tool must be available
- If the app isn't running, start it first (see Setup below)

## Workflow

### Step 1: Setup

**If the app is already running**, skip to Step 2.

**If the app needs to be started:**

```
# Start in a background cmux pane
cmux_split({ direction: "down", command: "cd /path/to/project && npm run dev\n" })

# Wait for it to be ready (check the pane output)
cmux_read({ surface: "surface:N" })

# Or poll the URL
cmux_browser({ action: "open", url: "http://localhost:3000" })
cmux_browser({ action: "wait", waitCondition: "load-state", loadState: "networkidle" })
```

**If Docker is needed:**
```bash
cd /path/to/project && docker compose up -d
# Wait for healthy
timeout 60 bash -c 'until curl -sf http://localhost:3000 > /dev/null; do sleep 2; done'
```

### Step 2: Smoke Test

Verify the app loads at all before deeper testing.

```
# Open the app
cmux_browser({ action: "open", url: "http://localhost:3000" })

# Wait for it to load
cmux_browser({ action: "wait", waitCondition: "load-state", loadState: "networkidle" })

# Screenshot the initial state
cmux_browser({ action: "screenshot" })

# Check for JavaScript errors
cmux_browser({ action: "errors" })

# Check console output
cmux_browser({ action: "console" })

# Get a DOM snapshot (accessibility tree)
cmux_browser({ action: "snapshot" })
```

If the smoke test fails (page doesn't load, crash errors), stop and report immediately.

### Step 3: Functional Testing

For each acceptance criterion, follow this pattern:

```
# 1. Navigate to the relevant state
cmux_browser({ action: "navigate", url: "/some-page" })
cmux_browser({ action: "wait", waitCondition: "load-state", loadState: "networkidle" })

# 2. Interact as a real user
cmux_browser({ action: "click", selector: "button.submit" })
cmux_browser({ action: "fill", selector: "input[name='email']", value: "test@example.com" })
cmux_browser({ action: "press", key: "Enter" })

# 3. Verify outcomes
cmux_browser({ action: "wait", waitCondition: "text", text: "Success" })
cmux_browser({ action: "snapshot", interactive: true })  # check element states
cmux_browser({ action: "get", selector: ".result", subaction: "textContent" })
cmux_browser({ action: "is", selector: ".modal", subaction: "visible" })

# 4. Screenshot as evidence
cmux_browser({ action: "screenshot" })

# 5. Check for errors after the interaction
cmux_browser({ action: "errors" })
```

**Tips for reliable element selection:**
- Use `snapshot` to see the accessibility tree and find the right selectors
- Use `find` with `role` for semantic targeting: `cmux_browser({ action: "find", subaction: "role", name: "Submit" })`
- Use `identify` to see interactive elements on the page
- Prefer `data-testid`, `aria-label`, or semantic selectors over fragile CSS paths

### Step 4: Edge Case Testing

**Be skeptical.** Agents often ship code that works for the happy path but breaks on edge cases. Actively try to break things:

| Test | How | What to look for |
|------|-----|-----------------|
| **Empty state** | Clear all data, visit pages with no content | Crashes, blank screens, missing "no data" messages |
| **Empty inputs** | Submit forms with empty required fields | Missing validation, silent failures, crashes |
| **Long text** | Paste 500+ character strings into inputs | Overflow, layout breaking, truncation without indication |
| **Special characters** | Input `<script>alert(1)</script>`, emoji 🎉, Unicode ñ | XSS, encoding errors, display issues |
| **Rapid clicks** | Double-click submit buttons, rapidly toggle switches | Duplicate submissions, race conditions, broken state |
| **Back button** | Navigate forward through a flow, then press back | Lost state, stale data, errors |
| **Refresh** | F5 / reload mid-flow | Lost state, errors, unexpected redirects |
| **Network errors** | Disconnect WiFi / block API calls (if possible) | Missing error handling, infinite spinners, blank screens |

```
# Example: test empty form submission
cmux_browser({ action: "click", selector: "button[type='submit']" })
cmux_browser({ action: "screenshot" })
cmux_browser({ action: "errors" })

# Example: test long text
cmux_browser({ action: "fill", selector: "input[name='title']", value: "A".repeat(500) })
cmux_browser({ action: "screenshot" })

# Example: test special characters
cmux_browser({ action: "fill", selector: "input[name='name']", value: "<script>alert('xss')</script>" })
cmux_browser({ action: "click", selector: "button[type='submit']" })
cmux_browser({ action: "screenshot" })
cmux_browser({ action: "errors" })
```

### Step 5: Accessibility Audit

Inject axe-core and run a full accessibility audit:

```
# Inject axe-core library
cmux_browser({ action: "eval", value: `
  await new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
  const results = await axe.run();
  return JSON.stringify({
    violations: results.violations.map(v => ({
      id: v.id,
      impact: v.impact,
      description: v.description,
      helpUrl: v.helpUrl,
      nodes: v.nodes.length
    })),
    passes: results.passes.length,
    incomplete: results.incomplete.length,
    inapplicable: results.inapplicable.length
  }, null, 2);
` })
```

**CDN fallback:** If the CDN is unreachable (air-gapped CI, network restriction, outage), the script injection will fail. In that case, fall back to:
- `npx axe-cli http://localhost:3000 --save axe-report.json` (CLI-based audit)
- or skip the accessibility audit and note "A11y audit skipped — axe-core CDN unavailable" in the report

**Security note:** For production use, add Subresource Integrity (SRI) verification to the script tag: `script.integrity = "sha384-..."; script.crossOrigin = "anonymous";` (hash available on cdnjs.com).

**Interpreting axe-core results:**
- **critical** impact — Must fix. Screen readers can't use the page.
- **serious** impact — Should fix. Major barrier for some users.
- **moderate** impact — Nice to fix. Some users affected.
- **minor** impact — Optional. Best practice improvements.

**Scoring accessibility:**
| Violations | Score |
|-----------|-------|
| 0 violations | 10 |
| 1-3 minor | 8-9 |
| 1-3 serious | 6-7 |
| 4-10 mixed | 4-5 |
| 10+ or any critical | 2-3 |

### Step 6: Performance (Optional)

Run Lighthouse for performance auditing when requested:

```bash
npx lighthouse http://localhost:3000 \
  --output=json \
  --output-path=./lighthouse-report.json \
  --chrome-flags="--headless --no-sandbox" \
  --only-categories=performance,accessibility,best-practices \
  --quiet
```

Then read and summarize the results:
```
read lighthouse-report.json
```

### Step 7: Report

Produce a structured QA report. Always include:

1. **Verdict** — PASS (all dimensions ≥ 6, no critical failures) or FAIL
2. **Scores** — 1-10 for each dimension with brief justification
3. **Acceptance criteria checklist** — each item explicitly PASS or FAIL
4. **Bugs** — with severity, reproduction steps, expected vs actual, screenshot
5. **Edge cases tested** — what you tried and what happened
6. **Accessibility results** — axe-core violation count and details
7. **Screenshots** — numbered, referenced in bug descriptions

### Step 8: Cleanup

If you started a dev server or Docker container in Step 1, clean up:

```
# Stop a dev server running in a cmux pane
cmux_close({ surface: "surface:N" })

# Or stop Docker containers
cd /path/to/project && docker compose down
```

This prevents orphaned processes and keeps the environment clean for the next test run.

## Report Template

```markdown
# QA Report: [Feature/PR Name]

**Date:** YYYY-MM-DD
**App URL:** http://localhost:XXXX
**Tested by:** QA Agent

## Verdict: PASS ✅ / FAIL ❌

## Scores

| Dimension | Score | Notes |
|-----------|-------|-------|
| Functionality | X/10 | Brief justification |
| Completeness | X/10 | Brief justification |
| UX | X/10 | Brief justification |
| Robustness | X/10 | Brief justification |
| Accessibility | X/10 | N violations (N critical, N serious) |

**Average:** X.X/10

## Acceptance Criteria

- [x] Criterion 1 — PASS
- [x] Criterion 2 — PASS
- [ ] Criterion 3 — FAIL: [specific issue]

## Bugs Found

### 🔴 Bug 1: [Title] (Critical)
- **Steps:** 1. Navigate to /page 2. Click button 3. ...
- **Expected:** Form submits and shows success
- **Actual:** Page crashes with TypeError in console
- **Screenshot:** #3

### 🟡 Bug 2: [Title] (Major)
...

### 🔵 Bug 3: [Title] (Minor)
...

## Edge Cases Tested

| Test | Result | Notes |
|------|--------|-------|
| Empty form submission | ✅ PASS | Shows validation errors |
| Long text (500 chars) | ⚠️ WARN | Text overflows container |
| Special characters | ✅ PASS | Properly escaped |
| Back button | ❌ FAIL | State lost, shows blank page |
| Rapid double-click | ✅ PASS | Button disabled after first click |

## Accessibility (axe-core)

- **Violations:** N
- **Passes:** N
- **Details:**
  - [serious] button-name: 2 buttons missing accessible names
  - [moderate] color-contrast: 3 elements with insufficient contrast

## Screenshots

1. Initial load — [description]
2. After form submission — [description]
3. Bug #1 evidence — [description]
```

## Grading Rubric Reference

| Dimension | 10 (Exceptional) | 8-9 (Very Good) | 6-7 (Good) | 4-5 (Acceptable) | 2-3 (Poor) |
|-----------|-------------------|-----------------|------------|------------------|------------|
| **Functionality** | All criteria pass, flows smooth | — | Most pass, minor issues | Some criteria fail | Core flows broken |
| **Completeness** | Everything built and working | — | Minor features missing | Significant gaps | Mostly stubs |
| **UX** | Polished, delightful | — | Good, minor rough edges | Functional but clunky | Confusing/broken |
| **Robustness** | Handles everything gracefully | — | Handles common cases | Some edge cases crash | Fragile |
| **Accessibility** | 0 violations | 1-3 minor violations | 1-3 serious violations | 4-10 mixed violations | 10+ or any critical |

## Verdict Rules

- **PASS** = All dimensions ≥ 6 AND no critical functionality failures
- **FAIL** = Any dimension < 6 OR acceptance criteria not met

When reporting FAIL, always provide specific, actionable feedback the builder can use to fix the issues. Reference exact elements, URLs, and steps.

