ClickMonkey harvests classes of defects that repeat across pages so people stop re-walking chrome, locators, and crash paths. It does not replace someone who knows the product.
A report counts sites (pages that show the class), not “10 unique bugs.” One illegal div in a button in the shell can be 50 page-instances. That is the point: fix once. Chapters in a report are Quality → Visual → Accessibility → Testability.
clickmonkey nasty) is the junk + missed-validation pass. Engineers fix classes.git log, “can Test Mode start a Salesforce → Filevine flow”). CLI clickmonkey explore is exploratory testing without MCP. MCP is the same walk plus freeze/replay of a spec (mcp.md). A person still aims the monkey.spec_save / spec_run, or CLI clickmonkey spec.data-testid is an upgrade (unique, stable). It is not required. Role + accessible name, field name, and <label> are enough to walk a random site. Unnamed icon buttons, duplicate names, and disabled/covered controls are the real limits.
These merge into per-run ledgers (quality.json, testability.json, broken.json) and finding folders. clickmonkey report collapses repeats.
| Class | What it catches | How |
|---|---|---|
| Runtime | Uncaught JS, HTTP 5xx, 401/403, GET 4xx, 404 (including soft SPA 404); console error/warning | Walk oracles. Catalog Q-14…Q-18. |
| HTML validity | Illegal nesting, duplicate ids, … | html-validate after inspect |
| A11y scanners | WCAG 2.0/2.1 A/AA (contrast, names, ARIA, labels, keyboard 2.1.1, …) plus a small extra allowlist — not all axe best-practice |
axe-core after inspect; clickableNonWidget is DOM 2.1.1 |
| SEO hygiene | Missing title/description/OG on public paths; the same title on every route | seo on the leash. Catalog Q-04…Q-13. |
| Testability | Unlabeled fields, unnamed controls, no main, occluded widgets, duplicate names, missing stable ids |
Inspect audit. Findings explain why. Stable ids T-01…T-08: catalog. |
| Keyboard 2.1.1 | clickableNonWidget: click on a div/span/svg (onclick, React onClick, addEventListener) or a role=button that is not tabbable |
Inspect audit. Spec: WCAG 2.1.1. List rows and #root are not mapped as actions. |
| Keyboard 2.1.2 / 2.4.3 | Form Tab walk: keyboardTrap (Tab cannot leave a field), focusOrder (next stop sits a row above) | DOM on inspect when a form has two+ tabbables — not a full-page Tab session. Spec: 2.1.2 / 2.4.3. |
| Layout / DOM extras | Overflow (1280, 375, 320), clip, overlap, z-index, scanline, sparse, broken images, hit targets, focus-obscured, focus rings, text occlusion, tiny type, text-spacing, dead hashes, implicit submit, noopener, scroll-padding, pointer-events:none | DOM on every inspect (no model). High confidence → finding folder; medium → quality ledger |
| Silent Save | Submit/Save: no navigation, no write request, and no accessible invalid (aria-invalid, {id}-error, HTML5 constraint). WCAG 3.3.1. Not Existing/Create-new mode toggles (*_mode_*) |
Unleash after submit |
| Invalid accepted | Required blank, or typed junk a control should not accept (email/date/number/pattern): after Save the field is not invalid, and the form sent that value or left the page. XSS/SQLi in a free-text notes field is not this |
Unleash / empty-required. Catalog Q-02. |
| Server refused submit | Write (POST/PUT/PATCH/DELETE) returned 400/409/422: the UI let Save go and the API refused the value | HTTP oracle. Catalog Q-01. |
| Threw instead of invalid | Fill then uncaught pageerror (e.g. Invalid time value) instead of a field error |
Runtime oracle. Catalog Q-03. |
| Map / harness | Unknown or drifted ids, ambiguous locators | Inspect + replay. Catalog T-09…T-12. |
| Shallow payload | XSS/SQLi/overlong in fields on a site you own | unleash --nasty / nasty_* |
| Explore oracles | Claim vs behavior, purpose, consistency, interruption, affordance | MCP / explore when the host files them |
| Frozen contracts | Path, text, value, visible/hidden, invalid | MCP spec_save / spec_run, CLI clickmonkey spec fences |
| Regression | Same tape still fails, or a later walk files the same ik_ after replay said FIXED |
clickmonkey replay (STILL / FIXED / LOOK) |
On top of wcag2a / wcag2aa / wcag21a / wcag21aa, inspect enables these rules only (landmarks / region / frame-tested stay off):
| Rule | What it catches |
|---|---|
| tabindex | tabindex greater than 0 |
| heading-order | Skipped heading level (h1 then h3) |
| skip-link | Skip link whose target is missing or not focusable |
| empty-heading | Heading with no discernible text |
| label-title-only | Field named only by title / hidden label |
| aria-dialog-name | Dialog / alertdialog with no accessible name |
| label-content-name-mismatch | Visible label not in the accessible name (WCAG 2.5.3) |
WCAG 2.2 target-size is not taken from axe — the DOM targetSize rule owns 2.5.8. 2.1.1 Keyboard is the DOM clickableNonWidget check (click on a non-control, or role=button that is not tabbable), not an axe rule. 2.1.2 / 2.4.3 are the form Tab walk (keyboardTrap, focusOrder).
Reports tag issues by spec name: AXE {rule} (Deque), WCAG {sc} {name} (W3C), HTML authoring (html-validate or the HTML spec), or ClickMonkey catalog titles. The catalog lists each set separately and links the official page when there is one. ClickMonkey-owned permalinks stay T-01 / V-03 / Q-01.
Leftover A/AA (media, sequence of prose, shortcuts, pointer/drag, consistent nav, error suggestion, authentication, live regions, AAA) is a human checklist, not a report chapter: What a person still tests.
Inspect measures boxes, overflow, hit-testing, and a few markup defects on the live page. It does not need vision. DOM hits stamp via: dom and win if the model later files the same rule. High confidence → finding folder; medium → quality ledger only.
| Rule | What it catches | Not this |
|---|---|---|
| overflow | Page or container leaking past its edge (document wider than the viewport past the scrollbar gutter; ~40px+ is high). Also at 375px (phone) and 320px (WCAG 1.4.10 reflow). Findings tagged @ 320px / @ 375px attach a still at that width, not the 1280 step shot |
Intended overflow: auto / scroll, closed off-canvas drawers, sticky 100vh chrome, overflow: hidden clip (that is clip), data tables that need two-axis scroll |
| clip | Text cut mid-word with no … — table cells (including overflow on an inner span, not only the td), column headers that collide or overflow into the next header, fields, tabs, chips, headings. Also a value or tab title whose glyphs collide with a trailing icon / $ / % inside a box that still “fits”. Finding still is a crop around the control |
Clean ellipsis, scrollable panes, roomy inputs whose letters are readable; native date/color UA picker icons; a table cell whose only overflow is the editor input (that is field clip); a header that wraps onto two lines inside its own column; collapsed icon-rail labels (icon-sized item ≤72px with an icon in nav/aside, data-sidebar, or a flush viewport-edge strip — the word is not meant to show) |
| overlap | Two actable controls sharing ≥8×8 px | Parent/child, labels on labels, an open menu covering the page, header/nav chrome crowding (Sign Out vs sidebar — quality ledger, not a finding) |
| zIndex | elementFromPoint at a control’s center is something else |
Sticky header/nav, an open dialog/menu covering the page behind it |
| scanline | Repeating list/table row edges; card/list trailing values shoved by variable-width titles; tab titles in a strip that do not share a left inset; form field chrome (outlined/combobox/custom select, including a hidden native <select> whose labeled button is the painted control) on a row that does not share a top; side-label first-line ink vs the value; a column of fields/labels that does not share a left; a column header that does not line up with its cells (box or first-line text, ≥16px) |
Nested nav indent, masonry / multi-column cards, items inside a menu, a floating/stacked label vs its own outline, right-locked amounts (space-between / grid), left-aligned values that only grow to the right, a Search-row ⌘K / Ctrl+K (<kbd> or ml-auto shortcut chip), wrapped filter toolbars, fields in different settings cards, fields on the page behind an open dialog, header text vs padded inputs in an editable grid, repeating two-column field grids (code | description rows), two-column details grids (each field already has a row partner) |
| sparse | Main pane is left-locked and the content column uses ≤50% of the width (~30% is high) | Centered cards/login, a second column on the right, small dialogs |
| broken | Visible <img> that finished loading with naturalWidth === 0 |
data: placeholders, hidden/empty src, still loading |
| targetSize | Actable control smaller than 24×24 CSS px on both axes, unless a 24px circle around it misses every other target (WCAG 2.5.8 spacing). Finding still is a crop around the control | Inline text links, native checkbox/radio/file, isolated icons with enough gap, disabled, a wrapping label ≥24×24, 1×1 / sr-only native <select> (and a 1×1 Close) under a custom combobox — the painted trigger is the target, Close × nested in a ≥24px tab chip (Close {title} / 18×18 tab-strip × stays on the quality ledger, not a finding) |
| focusObscured | Focused control is entirely hidden (sticky header, cookie/chat) — WCAG 2.4.11 | Partial cover; an open dialog, menu, or popover covering the page behind it |
| focusVisible | Focused control has no outline, box-shadow, border, or fill change — WCAG 2.4.7 | Native checkbox/radio/range; author :focus-visible ring |
| textOcclusion | Heading/body text fully covered by a painted sibling (badge, overlay). Finding still is a crop around the cell | Partial corner overlap; clip inside the same box; actable overlap; chips restacked over the same cell (same labels, or an empty Tailwind min-w-0 / font-medium / after:absolute wrapper) |
| fontSize | Body copy in main under 12px (<10px is high) | code/pre, nav chrome |
| textSpacing | Clip or overflow after WCAG 1.4.12 spacing (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em) | Wrapping body copy that still fits; axe avoid-inline-spacing (inline styles that block spacing) |
| deadHash | Visible a[href="#id"] whose id/name is not on the page |
href="#", #top, hidden links, hash-router state (#/route, #!/app, #tab=x) |
| implicitSubmit | <button> with no type that owns a form (HTML default is submit) |
Explicit type="button|submit|reset"; buttons outside a form; toolbar chrome |
| noopener | target="_blank" without rel containing noopener or noreferrer |
Same-tab links; hidden; either token is a pass |
| scrollPadding | Sticky/fixed top chrome taller than scroll-padding-top |
Static headers; padding ≥ header height |
| pointerEvents | Shown, enabled actable with computed pointer-events: none |
Disabled/aria-hidden; child with auto under a none parent |
vision adds sitemap blurbs, Sight, and pixel-only defects. It does not re-file DOM-owned rules. Parse drops geometry restated as other.
| Hunt | What it catches | Not this |
|---|---|---|
| contrast | Type unreadable in this screenshot | WCAG ratio (axe color-contrast) |
| align | One control in a row obviously stepped vs siblings. Catalog V-17. | 1px taste; stacked label above its field |
| other | Empty-vs-broken, toast chrome, missing mapped widgets, icon collision, canvas/icon-font holes, abnormal ellipsis, mojibake/tofu, chart labels cut, leftover lorem/TODO | Overflow/clip/overlap (DOM); hash-router state; --nasty catalog strings |
Fence bounce (/logout, off-app URL) is leash control, not a product finding. After a self-logout the walker gotos the leash url and runs intro again.
--nasty leftover text in a field is content, not a visual bug. The vision prompt lists catalog samples and tells the model to ignore them; parse still drops a finding that quotes a catalog string (we typed it). A table column that shears a product name mid-word (no …) is clip even when a nearby cell is leftover junk.
Native <select> only accepts its <option> list. Unleash (and --nasty) pick one of those values. A spec fill that is not in the <option> list fails immediately and names the options. ARIA comboboxes / typeaheads (role="combobox", aria-autocomplete, <datalist>) are driven from the live list, not from Faker. The executor opens the widget (click / ArrowDown) without typing — a virtual list can throw if it is filtered by a keystroke. If rows appear, it clicks a match or any listed row. Unmatched Faker/catalog clicks the painted row first (name matching after a chip is selected can sit for minutes because input.value is empty). A token chip plus Clear still counts as a filled value. Form mode Escape-closes the list after a pick so the next field is not covered. It does not type a filter string into a list that already has rows. Form mode does the same when mapped option_* actions are live: click one row from the open list, then Escape so the menu does not cover the next field. Another list (industry, then attorney) can still get a click; walking every option_* in the same dropdown is a marathon. HTML5 Save often focuses the empty combobox and opens the list. Inner <input>s of a combobox map as combobox, not text. If opening paints no rows, it probes a / e / s, a digit (1), then two-character tokens (an, in, st, 11) for a debounced list — never the planned Faker/catalog string, and not at all when that string was --nasty catalog junk. A listed row is clicked so the form can submit. Empty list after that is a miss unless the planned string was catalog junk (the list correctly has no XSS/SQL option). A painted list that could not be clicked is still a miss. The list is found via aria-controls / aria-owns on the input or its closest combobox — including a listbox portaled to document.body. Catalog junk still goes into text, textarea, and type-in comboboxes under --nasty.
After inspect, clickmonkey playbook empty-required walks every mapped surface that has a submit and required fields (HTML required). For each required field:
open <page>
click <opener> # if the field lives in a dialog
fill <surface>.<field> ""
click <submit>
expect <surface>.<field> invalid
Invalid means aria-invalid="true", a visible {id}-error node, or HTML5 constraint validation when the form is not novalidate. Native validity.valid === false on a novalidate form is not a visible error. Silent accept of a blank required field is a finding. A pass is not.
A pageerror after a fill is Q-03, not Q-02: validation did not mark the field invalid, and the page threw an uncaught JavaScript error. Invalid time value is that crash, not “you typed a bad date.” The report names the field and value and says the throw means validation is missing or does not wrap parsing.
Unleash and explore prefer empty then invalid then a plausible value. After --nasty junk and a submit, if the form sent those values or left and the field is still not invalid, that is a finding. A click on submit that does not send and paints aria-invalid / a visible {id}-error / HTML5 constraint validation (form not novalidate) is not a miss: client-side validation blocked the write. A click on submit that does not send, does not navigate, and paints no accessible invalid is a finding (WCAG 3.3.1 Error Identification): users get no error and think Save worked. That is not implicitSubmit (a <button> with no type that can submit by accident). Existing / Create-new (wizard_source_mode_create and other *_mode_* ids) are mode toggles, not Save — staying put with no write is the control working. Disabled Save is page.button_save is disabled. A pageerror on that fill is reported as a crash because validation is missing.
Unleash fills every empty field on the form (no field-count cap) then Save. Repeating child rows (lineitems_0__…, items[0].…, row_0_…) stay at one row — extra rows are not filled, and Add Line/Row is not clicked once a row exists. Once any body field has a value and empties remain, the walker stays in form — it does not hop Active tabs / list chrome. After fills it clicks Save (including a mapped Save that is still disabled). If Save does not enable, the executor requestSubmits the nearest <form> (or clicks type=submit) so constraint validation still runs. Native type=date stays ISO; letter-token placeholders (MM/DD/YYYY, dd.mm.yyyy) get that layout. --nasty uses a Faker date on those controls (not catalog XSS); typeaheads still pick a listed row. A masked or native date that refuses a non-date string (SQL, XSS, <img src=x onerror=alert(1)>) and stays on the placeholder is the control working, not a fill miss. Catalog junk that does not stick in a type-in field is the same: not a fill miss. A real date (2026-01-31 / 01/31/2026) that does not stick is. Faker is scored from field id/label, HTML type / autocomplete / inputmode, and live min / max / minlength / maxlength / step / pattern. Native <select> and ARIA typeaheads still use the option list. --nasty still uses catalog junk on type-in fields. A click on Save that does not send is not 3.3.1 if any visible field is already HTML-invalid (validity.valid === false on a validating form, or aria-invalid), including fields the walker has not filled yet.
Unleash after submit also flags typed junk the control should not accept: HTML type (email/url/number/date), pattern, min/max/minlength/maxlength. If the form sent those values or left without marking the fields invalid, that is the same class as empty-required. --nasty XSS/SQLi in a text or textarea field is not Q-02 when the product stores it as text (no crash, no injection). A crash on that fill is Q-03. The empty-required playbook still expects visible invalid marks even when the form stays put.
Not automatic:
actions: until a fill enables it)writePolicy: validationOnly is a leash, not a validator. It blocks submit/save/delete when required fields are already filled so a random walk does not commit. allow will submit.
Wizard Next below the Playwright viewport is still a live action; click scrolls it into view. Disabled is still skipped.
ClickMonkey has no domain brain unless you put it in a charter, skills (clickmonkey/explore-context.md), or a spec fence.
| Stays with QA | Why |
|---|---|
| Business correctness | Wrong total, wrong tenant, wrong binding. Specs pin a path you already know; they do not invent the rule. |
| Authz policy | HTTP 403 + “You do not have access” is often the product working. Whether that nav item should exist for this role is a human call. Do not mute 403s with an allowlist — the interesting bug is 200 where this role must not go. Use skip / fence if this leash should not walk it. |
| Triage | Axe/html noise vs a user-visible break; 1px align / brand tokens; --nasty false friends; expected empty states. |
| Journeys the walker will not finish alone | Multi-user, email/PDF, payments, long wizards that need real org data, races, load. |
| Security beyond junk-in-fields | Not a pentest. No CSRF, IDOR campaigns, session fixation. --nasty is only for a site you own. |
| Taste and intent | Copy, information architecture, “is this the right wizard.” |
| Coverage strategy | Which role, leash, charter, and which fences to freeze after a good walk. |
| Command | Harvests |
|---|---|
map |
Pages/surfaces, testability, quality + DOM layout on navigate-only |
unleash |
Hunt mapped forms; wizard/form/list/nav on the tile; same layout pass |
nasty |
Same hunt on the nasty clock; junk fills; site you own (unleash --nasty is the same) |
playbook empty-required |
Blank required + submit → invalid |
explore |
Unattended charter walk (needs brain) |
mcp |
Host LLM walk, then spec freeze/replay |
spec |
Fences as a real walk; findings still harvest unless you only care about PASS |
replay |
Comparison vs a report, not a new survey |
report |
Shareable markdown in the same chapters as the sitemap page sheet: Findings, Testability, Accessibility, Visual, Quality (HTML/SEO/Runtime). Default caps unique-to-a-route pages with issues at 8; --quality-full lists every such page. By page is a report-only index (default: those pages; full: every labeled ledger page) |
ClickMonkey shrinks the recurring survey. It does not own “is this the right system for this customer.”