diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fce039..87fa621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to qmax-code. Versions follow [Semantic Versioning](https://semver.org/). +## [1.18.0] - 2026-06-04 + +### Added +- Imported the 15 browser/runtime QA skills from [Quality-Max/free-qa-skills](https://github.com/Quality-Max/free-qa-skills): `accessibility-check`, `broken-link-scan`, `cold-load-waterfall`, `console-error-scan`, `cookie-privacy-scan`, `core-web-vitals`, `form-validation-scan`, `i18n-rtl-audit`, `mixed-content-scan`, `page-weight-budget`, `responsive-screenshots`, `security-headers-check`, `seo-check`, `third-party-bloat`, `ui-ux-scan`. Unlike the static-analysis skills, these drive a live page load and declare a Playwright MCP dependency (surfaced to Codex via `agents/openai.yaml`). The catalog now ships **27 skills** and fully mirrors free-qa-skills plus the qmax-specific ones. + ## [1.17.1] - 2026-06-04 ### Added diff --git a/internal/skills/catalog.go b/internal/skills/catalog.go index 732f450..71080f9 100644 --- a/internal/skills/catalog.go +++ b/internal/skills/catalog.go @@ -76,6 +76,10 @@ func (s Skill) Body() string { // that orch installs into both CLIs. var qmaxDep = MCPDep{Value: "qmax", Description: "qmax QA tools (list, run, generate, review)"} +// playwrightDep is the dependency for the browser/runtime QA skills imported +// from free-qa-skills: they drive a live page load via the Playwright MCP. +var playwrightDep = MCPDep{Value: "playwright", Description: "Playwright MCP for live browser automation"} + // Catalog is the full set of qmax QA skills shipped with qmax-code. Adding a // skill is: drop a catalog/.md body and append an entry here. var Catalog = []Skill{ @@ -159,6 +163,115 @@ var Catalog = []Skill{ ShortDescription: "Find brittle UI locators, suggest stable ones", bodyFile: "flaky-selector-scan.md", }, + + // Browser / runtime QA skills imported from Quality-Max/free-qa-skills. + // Unlike the static-analysis skills above, these drive a live page load and + // declare a Playwright MCP dependency (surfaced to Codex via openai.yaml). + { + Name: "accessibility-check", + Description: "Quick WCAG accessibility scan of any URL — color contrast, missing alt text, keyboard navigation, ARIA labels, heading hierarchy, and focus indicators — producing a graded report. Use when the user wants to check a page's accessibility or WCAG compliance.", + ShortDescription: "WCAG accessibility scan of a URL", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "accessibility-check.md", + }, + { + Name: "broken-link-scan", + Description: "Find broken links on any website: crawl the page and check every link for 404s, redirects, and timeouts, reporting each dead link with its location. Use when the user wants to find broken or dead links on a site.", + ShortDescription: "Find broken links (404s, redirects) on a site", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "broken-link-scan.md", + }, + { + Name: "cold-load-waterfall", + Description: "Profile a cold, cache-empty page load and build a text request waterfall — longest-pole requests, time to first byte, and time to interactive — flagging critical-path bottlenecks. Use when the user wants to know why a page loads slowly on first visit.", + ShortDescription: "Cold-load request waterfall + bottlenecks", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "cold-load-waterfall.md", + }, + { + Name: "console-error-scan", + Description: "Detect JavaScript errors, warnings, and failed network requests on any page by navigating the URL and collecting console output and network failures. Use when the user wants to find runtime JS errors or failing requests on a page.", + ShortDescription: "Catch JS errors + failed requests on a page", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "console-error-scan.md", + }, + { + Name: "cookie-privacy-scan", + Description: "Audit a site's cookies and trackers — inventory every cookie, flag missing Secure/HttpOnly/SameSite, list third-party trackers, and detect tracking that fires before consent. Use when the user wants a cookie, tracker, or consent privacy audit.", + ShortDescription: "Audit cookies, trackers, consent timing", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "cookie-privacy-scan.md", + }, + { + Name: "core-web-vitals", + Description: "Measure Core Web Vitals on any URL — LCP, CLS, INP, TTFB, FCP — using the browser's own performance APIs, grading each against Google's thresholds into an A–F report. Use when the user wants to measure page performance or Core Web Vitals.", + ShortDescription: "Measure Core Web Vitals (LCP/CLS/INP) A-F", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "core-web-vitals.md", + }, + { + Name: "form-validation-scan", + Description: "Probe the forms on a page for validation gaps — missing required-field enforcement, no client-side validation, malformed input accepted, and absent error messaging — reporting per-field findings. Use when the user wants to test form validation.", + ShortDescription: "Probe forms for validation gaps", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "form-validation-scan.md", + }, + { + Name: "i18n-rtl-audit", + Description: "Audit a page for internationalization readiness — layout breaks under long translations, RTL rendering issues, hardcoded UI strings, and missing lang/dir attributes. Use when the user wants an i18n, localization, or RTL readiness review.", + ShortDescription: "i18n/RTL readiness audit of a page", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "i18n-rtl-audit.md", + }, + { + Name: "mixed-content-scan", + Description: "Scan an HTTPS page for mixed content — HTTP scripts, styles, images, iframes, and insecure form actions — separating browser-blocked active mixed content from passive, with the offending URLs. Use when the user wants to find insecure mixed content on an HTTPS page.", + ShortDescription: "Find HTTP mixed content on an HTTPS page", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "mixed-content-scan.md", + }, + { + Name: "page-weight-budget", + Description: "Audit a page's weight against a performance budget — total transfer bytes, request count, render-blocking JS/CSS, and oversized or uncompressed images — producing a pass/fail report. Use when the user wants to check page weight or enforce a performance budget.", + ShortDescription: "Check page weight vs a perf budget", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "page-weight-budget.md", + }, + { + Name: "responsive-screenshots", + Description: "Screenshot any URL at 5 viewport sizes — mobile, tablet, laptop, desktop, and ultrawide — saving PNGs and reporting layout issues. Use when the user wants responsive screenshots or to check how a page renders across screen sizes.", + ShortDescription: "Screenshot a URL at 5 viewport sizes", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "responsive-screenshots.md", + }, + { + Name: "security-headers-check", + Description: "Check HTTP security headers on any URL — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy — producing an A–F report with specific missing or misconfigured headers. Use when the user wants to audit a site's security headers.", + ShortDescription: "Grade HTTP security headers A-F", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "security-headers-check.md", + }, + { + Name: "seo-check", + Description: "Quick SEO health check of any URL — meta tags, headings, image alts, structured data, open graph, and common SEO issues. Use when the user wants an SEO audit or to check a page's search-engine readiness.", + ShortDescription: "SEO health check of a URL", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "seo-check.md", + }, + { + Name: "third-party-bloat", + Description: "Inventory third-party scripts on a page — analytics, tag managers, chat widgets, ads, A/B tools — and rank them by transfer size and main-thread cost, flagging the heaviest. Use when the user wants to find what third-party scripts are slowing a page down.", + ShortDescription: "Rank third-party script bloat by cost", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "third-party-bloat.md", + }, + { + Name: "ui-ux-scan", + Description: "Quick UI/UX deficiency scan of any page — touch targets, contrast, font consistency, spacing, empty states, loading indicators, and common UX anti-patterns. Use when the user wants a UI/UX review or to find usability problems on a page.", + ShortDescription: "Scan a page for UI/UX deficiencies", + MCPDeps: []MCPDep{playwrightDep}, + bodyFile: "ui-ux-scan.md", + }, } // SortedCatalog returns the catalog ordered by name for deterministic output. diff --git a/internal/skills/catalog/accessibility-check.md b/internal/skills/catalog/accessibility-check.md new file mode 100644 index 0000000..3e97a59 --- /dev/null +++ b/internal/skills/catalog/accessibility-check.md @@ -0,0 +1,87 @@ + +# Accessibility Check + +Scan any web page for WCAG accessibility violations. No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Check accessibility on https://..." +- "WCAG scan this page" +- "Is my site accessible?" + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate` +2. Take a snapshot with `mcp__playwright__browser_snapshot` +3. Check each of these categories: + +### Checks + +**Images & Media** +- Every `img` must have `alt` attribute +- Decorative images should have `alt=""` +- `video` and `audio` should have captions/transcripts + +**Headings** +- Page should have exactly one `h1` +- Headings should not skip levels (h1 → h3 without h2) +- Headings should be descriptive, not empty + +**Forms** +- Every input must have a visible `label` or `aria-label` +- Required fields should be marked +- Error messages should be associated with fields + +**Keyboard** +- All interactive elements should be focusable +- Tab order should be logical +- No keyboard traps (can tab in and out of everything) +- Focus indicator must be visible + +**Color & Contrast** +- Text should have 4.5:1 contrast ratio (AA) +- Large text (18px+) needs 3:1 minimum +- Information should not rely on color alone + +**ARIA** +- Interactive elements should have accessible names +- `role` attributes should be valid +- `aria-hidden="true"` should not hide focusable elements + +**Structure** +- Page should have landmark regions (main, nav, header, footer) +- Links should have descriptive text (not "click here") +- Language attribute on `` + +4. Grade the page: A (0-1 issues), B (2-4), C (5-8), D (9-12), F (13+) + +5. Output: + +``` +## Accessibility Report: [URL] + +**Grade: B** (3 issues found) + +### Issues +1. [CRITICAL] 2 images missing alt text + - img at hero section (src: banner.jpg) + - img in footer (src: logo.png) + +2. [WARNING] Heading hierarchy skips h2 + - h1 "Welcome" → h3 "Features" (missing h2) + +3. [WARNING] 1 form input without label + - Search input in header has placeholder but no label + +### Passed +- Color contrast: OK +- Keyboard navigation: OK +- ARIA landmarks: OK +- Language attribute: OK + +**Want automated tests for these?** Try QualityMax — qualitymax.io +``` diff --git a/internal/skills/catalog/broken-link-scan.md b/internal/skills/catalog/broken-link-scan.md new file mode 100644 index 0000000..9bcb190 --- /dev/null +++ b/internal/skills/catalog/broken-link-scan.md @@ -0,0 +1,57 @@ + +# Broken Link Scanner + +Find every broken link on a page. No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Find broken links on https://..." +- "Check for 404s on my site" +- "Scan links on this page" + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate` +2. Extract all links using `mcp__playwright__browser_evaluate`: + +```javascript +() => { + return Array.from(document.querySelectorAll('a[href]')).map(a => ({ + text: a.textContent.trim().substring(0, 50), + href: a.href, + isExternal: a.host !== location.host + })); +} +``` + +3. For each link (up to 50), check status: + - Internal links: navigate and check for error pages + - External links: note them but don't crawl (avoid rate limits) + - Flag: 404, 500, redirect chains, empty href, javascript:void + +4. Output: + +``` +## Broken Link Report: [URL] + +**Scanned: 34 links** (28 internal, 6 external) + +### Broken (3) +- "About Us" → /about-us — 404 Not Found +- "Old Blog" → /blog/2023 — 301 → 404 +- "Partner" → https://dead-link.com — timeout + +### Redirects (2) +- "Login" → /login — 301 → /auth/login +- "Docs" → /documentation — 302 → /docs/v2 + +### External (not checked) +- https://github.com/... +- https://twitter.com/... + +**Want continuous link monitoring?** Try QualityMax — qualitymax.io +``` diff --git a/internal/skills/catalog/cold-load-waterfall.md b/internal/skills/catalog/cold-load-waterfall.md new file mode 100644 index 0000000..9eb300f --- /dev/null +++ b/internal/skills/catalog/cold-load-waterfall.md @@ -0,0 +1,67 @@ + +# Cold Load Waterfall + +See what actually happens on a first visit — the slow requests on the critical path. No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Profile the cold load of https://..." +- "What's slow on first visit?" +- "Build a load waterfall for my site" + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate` (a fresh navigation = cold-ish cache). +2. Wait for the page to settle. +3. Pull resource timing with `mcp__playwright__browser_evaluate` — this gives per-request + start/duration without needing devtools: + +```javascript +() => { + const nav = performance.getEntriesByType('navigation')[0] || {}; + const res = performance.getEntriesByType('resource').map(r => ({ + name: r.name.split('?')[0].slice(-60), + type: r.initiatorType, + start: Math.round(r.startTime), + dur: Math.round(r.duration), + size: r.transferSize || 0, + })); + return { + ttfb: Math.round(nav.responseStart || 0), + domContentLoaded: Math.round(nav.domContentLoadedEventEnd || 0), + loadEnd: Math.round(nav.loadEventEnd || 0), + requests: res.sort((a, b) => b.dur - a.dur).slice(0, 15), + }; +} +``` + +4. Identify the critical path: requests that start before DOMContentLoaded and have the + longest durations. Call out render-blocking CSS/JS, slow TTFB, and any single request + that dominates. + +5. Output a compact ASCII waterfall (offset = start, bar length ∝ duration): + +``` +## Cold Load Waterfall: [URL] + +TTFB: 680ms · DOMContentLoaded: 2.1s · Load: 3.4s + + 0ms 1s 2s 3s + |----------|----------|----------| + ███ document (680ms TTFB) + █████████ app.css (blocking, 900ms) + ████████████ vendor.js (blocking, 1.2s) + ██████ hero.jpg (640ms) + ████ analytics.js (420ms) + +### Bottlenecks on the critical path +1. vendor.js (1.2s, render-blocking) is the long pole — code-split or defer. +2. TTFB 680ms — server/CDN warmup; consider edge caching. +3. app.css blocks first paint for 900ms — inline critical CSS. + +**Want load profiling on every deploy?** Try QualityMax — qualitymax.io +``` diff --git a/internal/skills/catalog/console-error-scan.md b/internal/skills/catalog/console-error-scan.md new file mode 100644 index 0000000..2b5a4b0 --- /dev/null +++ b/internal/skills/catalog/console-error-scan.md @@ -0,0 +1,59 @@ + +# Console Error Scanner + +Find JS errors and failed requests hiding in the browser console. No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Check for console errors on https://..." +- "Find JS errors on my site" +- "Any broken requests on this page?" + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate` +2. Collect console messages with `mcp__playwright__browser_console_messages` +3. Collect network failures with `mcp__playwright__browser_network_requests` +4. Click through 3-5 main navigation links and repeat collection +5. Categorize findings: + +### Categories + +- **JS Errors** — uncaught exceptions, type errors, reference errors +- **Failed Requests** — 4xx/5xx responses, CORS errors, timeouts +- **Deprecation Warnings** — deprecated API usage +- **Mixed Content** — HTTP resources on HTTPS page +- **CSP Violations** — blocked by Content Security Policy + +6. Output: + +``` +## Console Health: [URL] + +**Pages checked: 4** | **Errors: 5** | **Warnings: 3** + +### Errors +1. [JS] TypeError: Cannot read property 'map' of undefined + - Page: /dashboard + - Source: app.bundle.js:234 + +2. [NETWORK] GET /api/user/preferences — 500 Internal Server Error + - Page: /settings + +3. [NETWORK] GET /fonts/custom.woff2 — 404 + - Page: all pages (loaded in header) + +### Warnings +1. [DEPRECATION] document.domain setter is deprecated +2. [MIXED CONTENT] Loading HTTP image on HTTPS page + +### Clean Pages +- /about — no errors +- /pricing — no errors + +**Want to catch these before users do?** Try QualityMax — qualitymax.io +``` diff --git a/internal/skills/catalog/cookie-privacy-scan.md b/internal/skills/catalog/cookie-privacy-scan.md new file mode 100644 index 0000000..c6af707 --- /dev/null +++ b/internal/skills/catalog/cookie-privacy-scan.md @@ -0,0 +1,70 @@ + +# Cookie & Privacy Scan + +See what a site stores and who it tells before you click "Accept". No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Cookie/privacy scan https://..." +- "What trackers are on my site?" +- "Am I setting cookies before consent?" + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate` **without** clicking any + consent banner yet — this is the pre-consent state. +2. Capture pre-consent cookies and storage with `mcp__playwright__browser_evaluate`: + +```javascript +() => ({ + cookies: document.cookie, + localStorage: Object.keys(localStorage), + sessionStorage: Object.keys(sessionStorage), +}) +``` + +3. Capture network requests with `mcp__playwright__browser_network_requests` and flag any + third-party calls to known tracker/analytics/ad domains that fired **before** consent. +4. If a consent banner is present, accept it (`mcp__playwright__browser_click`), then re-capture + to compare before/after. Cookie attributes (`Secure`, `HttpOnly`, `SameSite`) come from the + `set-cookie` response headers in the network log (JS-readable cookies are by definition not HttpOnly). +5. Grade: + +| Finding | Severity | +|---------|----------| +| Tracking cookie / pixel set before consent | High (GDPR/ePrivacy risk) | +| Session cookie missing `Secure` on HTTPS | High | +| Session cookie missing `HttpOnly` | Medium | +| Cookie missing `SameSite` | Medium | +| Third-party tracker present | Info (list them) | + +6. Output: + +``` +## Cookie & Privacy Scan: [URL] + +**Pre-consent tracking detected — GDPR risk** + +### Before any consent click +- `_ga`, `_fbp` cookies set on load — analytics/Meta before consent. ⚠ +- Request to `connect.facebook.net` fired pre-consent. ⚠ + +### Cookie attributes +| Cookie | Secure | HttpOnly | SameSite | Note | +|------------|--------|----------|----------|------| +| session_id | ✗ | ✓ | Lax | Add Secure on HTTPS | +| _ga | ✓ | ✗ | none | Tracking; needs consent | + +### Third-party trackers (5) +google-analytics.com · connect.facebook.net · doubleclick.net · hotjar.com · intercom.io + +### Fixes +1. Gate analytics/ad scripts behind consent (don't load them on first paint). +2. Add `Secure` + `SameSite=Lax` to `session_id`. + +**Want privacy regressions caught automatically?** Try QualityMax — qualitymax.io +``` diff --git a/internal/skills/catalog/core-web-vitals.md b/internal/skills/catalog/core-web-vitals.md new file mode 100644 index 0000000..609cb8c --- /dev/null +++ b/internal/skills/catalog/core-web-vitals.md @@ -0,0 +1,92 @@ + +# Core Web Vitals + +Measure the Core Web Vitals of any page using real browser performance APIs. No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Check Core Web Vitals for https://..." +- "How's the LCP/CLS on my site?" +- "Web vitals audit mysite.com" + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate` +2. Let the page settle (`mcp__playwright__browser_wait_for` ~3s, or wait for network idle) +3. Collect metrics with `mcp__playwright__browser_evaluate`: + +```javascript +() => new Promise((resolve) => { + const out = {}; + // Navigation timing → TTFB, FCP + const nav = performance.getEntriesByType('navigation')[0]; + if (nav) out.ttfb = Math.round(nav.responseStart); + const fcp = performance.getEntriesByName('first-contentful-paint')[0]; + if (fcp) out.fcp = Math.round(fcp.startTime); + + // LCP + try { + new PerformanceObserver((list) => { + const e = list.getEntries(); + out.lcp = Math.round(e[e.length - 1].startTime); + }).observe({ type: 'largest-contentful-paint', buffered: true }); + } catch (e) {} + + // CLS (cumulative) + let cls = 0; + try { + new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + if (!entry.hadRecentInput) cls += entry.value; + } + out.cls = Math.round(cls * 1000) / 1000; + }).observe({ type: 'layout-shift', buffered: true }); + } catch (e) {} + + // Give observers a tick to flush + setTimeout(() => resolve(out), 1500); +}) +``` + +4. INP can't be measured passively — note it requires interaction. If you can click a primary + button via `mcp__playwright__browser_click`, measure the `event` timing entry; otherwise + report INP as "not measured (needs interaction)". + +5. Grade each metric (Google thresholds): + +| Metric | Good | Needs work | Poor | +|--------|-----------|-------------------|-----------| +| LCP | ≤ 2500ms | 2500–4000ms | > 4000ms | +| CLS | ≤ 0.1 | 0.1–0.25 | > 0.25 | +| INP | ≤ 200ms | 200–500ms | > 500ms | +| TTFB | ≤ 800ms | 800–1800ms | > 1800ms | +| FCP | ≤ 1800ms | 1800–3000ms | > 3000ms | + +6. Overall grade: A = all Good · B = one "Needs work" · C = two/three · D = any Poor · F = multiple Poor. + +7. Output: + +``` +## Core Web Vitals Report: [URL] + +**Grade: B** — one metric needs work + +| Metric | Value | Rating | +|--------|---------|--------------| +| LCP | 2.9s | Needs work | +| CLS | 0.04 | Good | +| INP | (not measured — needs interaction) | +| TTFB | 410ms | Good | +| FCP | 1.6s | Good | + +### What's hurting LCP +The largest element rendered at 2.9s — likely a hero image without +priority loading. Add `fetchpriority="high"` and preload it; serve in +AVIF/WebP; ensure it isn't behind render-blocking JS. + +**Want CWV tracked on every deploy?** Try QualityMax — qualitymax.io +``` diff --git a/internal/skills/catalog/form-validation-scan.md b/internal/skills/catalog/form-validation-scan.md new file mode 100644 index 0000000..0f91151 --- /dev/null +++ b/internal/skills/catalog/form-validation-scan.md @@ -0,0 +1,71 @@ + +# Form Validation Scan + +Find out if your forms accept garbage. No signup required. + +## Prerequisites + +- **Playwright MCP** (comes with Claude Code) + +## Trigger + +- "Scan my forms for validation gaps" +- "Do my forms validate input?" +- "Form validation audit https://..." + +## Workflow + +1. Navigate to the URL using `mcp__playwright__browser_navigate`. +2. Inventory the forms and fields with `mcp__playwright__browser_evaluate`: + +```javascript +() => [...document.forms].map(f => ({ + id: f.id || f.name || '(unnamed)', + action: f.action, + fields: [...f.elements].filter(e => e.name).map(e => ({ + name: e.name, type: e.type, required: e.required, + pattern: e.pattern || null, maxLength: e.maxLength, + })), +})) +``` + +3. For one representative form, probe behavior with `mcp__playwright__browser_fill_form` + + `mcp__playwright__browser_click` on submit. Test these cases and observe whether the form + blocks submission and shows a message: + +| Case | Input | Expected | +|------|-------|----------| +| Empty required | leave required field blank | Blocked + message | +| Bad email | `notanemail` in an email field | Blocked + message | +| Out-of-range | negative qty, huge number | Blocked or clamped | +| Oversized | very long string in a short field | Truncated / blocked | +| Whitespace-only | `" "` in a required field | Treated as empty | +| Script payload | `` | Accepted but escaped on render (note for XSS follow-up) | + + Read the page state after each via `mcp__playwright__browser_snapshot` — look for an error + message, an `aria-invalid`, or a blocked navigation. + +4. Output: + +``` +## Form Validation Scan: [URL] + +**Form: "signup" — 3 gaps** + +| Field | Required enforced | Format checked | Error shown | Verdict | +|----------|-------------------|----------------|-------------|---------| +| email | yes | NO | no | Accepts "abc" as email | +| password | yes | yes (min 8) | yes | ok | +| age | no | NO | no | Accepts -5 and 9999 | + +### Findings +1. `email` — no format validation; `notanemail` submits. Add `type=email` + server check. +2. `age` — accepts negative and absurd values; add `min`/`max` + server validation. +3. Submitting empty `email` shows no inline error — fails silently. + +### Note +Script payload `