# Form Validation Scan

> Audit form validation UX on any page: required-field marking, input types, error message quality, aria-describedby wiring, and focus management — without ever submitting real data. Playwright MCP only, no signup. Triggers: "check my form validation", "audit the signup form UX", "are my form errors accessible?".

- Skill: `help-me-test/form-validation-scan` (Agent Skill)
- Install (CLI): `npx skillmds@latest add help-me-test/form-validation-scan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/help-me-test/form-validation-scan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: help-me-test (https://skillmd.com/u/help-me-test)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/help-me-test/form-validation-scan

---


# Form Validation Scan

Grade how forms fail — before your users do. No signup required.

## Prerequisites

- **Playwright MCP** (bundled with Claude Code)

## Trigger

- "Check form validation on https://example.com/signup"
- "Audit the contact form UX"
- "Are my form errors accessible?"
- "Does my form use the right input types?"

## Workflow

**READ-ONLY rule:** type only obviously fake data (`test@example.com` — example.com is
reserved per RFC 2606, `not-an-email`, `555-fake`). Never let a form actually submit: stop at
client-side validation. Skip payment/checkout forms entirely.

1. Navigate with `mcp__playwright__browser_navigate`; take `mcp__playwright__browser_snapshot`
   to locate forms and get element refs for clicking/typing.
2. Inventory every form with `mcp__playwright__browser_evaluate`:

```javascript
() => [...document.querySelectorAll('form')].map((f, index) => ({
  index, action: f.action, novalidate: f.noValidate,
  emptySubmitWouldPass: f.checkValidity(),   // true = SAFETY GATE: do NOT click submit
  fields: [...f.elements].filter(e => e.matches('input,select,textarea') && e.type !== 'hidden')
    .map(e => ({
      name: e.name || e.id, type: e.type, required: e.required,
      label: e.labels?.[0]?.textContent.trim() ?? null,
      visualRequiredMark: /\*|required/i.test(e.labels?.[0]?.textContent ?? ''),
      describedBy: e.getAttribute('aria-describedby'),
    })),
}))
```

3. Static checks per field:
   - `required` set but no visual mark (asterisk or "required" in the label) — users can't
     tell what's mandatory (WCAG 2.2 SC 3.3.2 Labels or Instructions).
   - Label says email/phone/website/quantity but `type="text"` — loses WHATWG constraint
     validation and the right mobile keyboard. Expect email/tel/url/number.
   - No associated label at all (WCAG 2.2 SC 1.3.1 / 3.3.2).
4. Empty-submit test — **only if `emptySubmitWouldPass === false`** (the browser will block
   submission and show validation UI, so nothing is sent). If it's `true` or `novalidate` is
   set, do NOT click; report "no client-side constraints — one stray click posts an empty
   record" as a high finding. Otherwise `mcp__playwright__browser_click` the submit button,
   then evaluate:

```javascript
() => {
  const f = document.querySelectorAll('form')[IDX];
  return [...f.elements].filter(e => e.matches(':invalid')).map(e => ({
    name: e.name || e.id, browserMessage: e.validationMessage,
    ariaInvalid: e.getAttribute('aria-invalid'),
    visibleError: document.getElementById(e.getAttribute('aria-describedby') ?? '')?.textContent.trim() ?? null,
    hasFocus: document.activeElement === e,
  }));
}
```

   Judge: errors appear; text is specific ("Enter your email address", not "Invalid input") —
   WCAG 2.2 SC 3.3.1 Error Identification + SC 3.3.3 Error Suggestion; error text is wired to
   the field via `aria-describedby` (otherwise screen readers announce nothing); focus moved
   to the first invalid field (SC 2.4.3 Focus Order practice).
5. Invalid-format probe: for each typed field, `mcp__playwright__browser_type` one wrong value
   (email → `not-an-email`, url → `no-scheme`, tel → `letters`), re-run the step-4 evaluate,
   and record whether the message names the expected format. Client-side only — validation
   keeps blocking submission, so nothing persists.
6. Grade: **A** all checks pass · **B** one generic message · **C** errors not associated via
   aria-describedby or no focus move · **D** generic errors plus wrong input types ·
   **F** no client-side validation, or errors that never appear.
7. Honest limit: server-side validation is untested by design — a read-only scan must not
   create records. Say so in the report.

## Report

```
## Form Validation Report: {URL}

**Grade: {A–F}** — {one-line reason}
Forms found: {n} · Tested: {n} · Skipped (no client-side constraints / payment): {n}

### Form: {label or action} ({m} fields)
| Check | Result | Standard |
|---|---|---|
| Required fields marked visually | 2/3 — "Phone" has required attr, no asterisk | WCAG 3.3.2 |
| Input types semantic | FAIL — "Email" is type="text" | WHATWG constraint validation |
| Errors appear on empty submit | PASS | WCAG 3.3.1 |
| Error text specific | FAIL — "Invalid input" on all fields | WCAG 3.3.3 |
| Errors wired via aria-describedby | FAIL — visible but not associated | WCAG 1.3.1 |
| Focus moves to first error | PASS | WCAG 2.4.3 (practice) |
| Invalid email rejected client-side | PASS — "not-an-email" blocked | — |

### Not tested (read-only scan)
- Server-side validation and duplicate/rate-limit handling — requires real submissions.

**Want every form path covered by real generated tests?** Try HelpMeTest — helpmetest.com
```

