testid-a11y-mining
Read-only codemod that mines patterns around data-testid and Testing Library queries—it does not rewrite files in Step 1. Findings go to the testid_mining metric via useMetricAtom, and the same rows (with exact source spans) are appended to workflow state testid_a11y_findings via setState from codemod:workflow so Steps 2–3 can consume them as ${{ state.testid_a11y_findings }} in workflow.yaml.
Background: Test IDs are an a11y smell (Dominik Dorfmeister). The tool encodes that role-based queries align with how users experience the app; getByTestId does not exercise accessible names/roles, and test IDs on clickable non-semantic elements can mask regressions (e.g. <div onClick … data-testid>).
Three-step workflow
| Step | What | Details |
|---|---|---|
| 1 — Mine | Deterministic JSSG scan | Broad detection, no false negatives on those rules; false positives OK. Each emission includes line, snippet, validationGuide, fixGuide, start/end line & column, locationRef, fileUri, and a state array for agents. |
| 2 — Validate | AI step in workflow.yaml | Full validation checklist and output contract are inlined in that file’s Step 2 ai block. |
| 3 — Fix | AI step in workflow.yaml | Full fix playbook is inlined in that file’s Step 3 ai block. |
Operational detail for Steps 2–3 lives only in workflow.yaml (no separate docs folder).
Applicability
Broadly applicable to frontends that:
- Use React (TSX) and
@testing-library/react-style APIs (screen.getByTestId, etc.) - Use
data-testid/data-test-idon JSX
Caveats (any repo)
- Test file detection uses filename patterns (e.g.
*.spec.tsx,*.test.tsx, and the fixture pathtests/.../input.tsx), not project-specific rules. - “Design system path” (
locationCategory) is a path heuristic (acomponentspath segment). It may or may not match how your repo organizes design-system or shared UI code. - Static analysis only — there is no runtime a11y (no axe, no browser). Step 2 is where you add that judgment.
What it detects
| Kind | Severity (default) | Summary |
|---|---|---|
clickable_nonsemantic_with_testid | high | div / span with onClick (or pointer/key handlers) and data-testid |
data_testid_on_semantic_tag | medium | data-testid on button, a, input, textarea, select |
composite_widget_spec_review | medium | Spec file whose path suggests composite UI (Select, Menu, …) and uses getByTestId |
get_by_testid_role_hint | medium | In spec-like files: getByTestId / queryByTestId / … with a string literal that looks “role-ish” (heuristic tiers A/B) |
many_testids_in_ds_file | low | Under a path with a components segment: ≥ 5 distinct static data-testid values in one file |
Replacement opportunity (component-side): likely_getByRole vs manual_review (icon-only, dynamic/i18n text, form controls, etc.).
Workflow state
After Step 1, state.testid_a11y_findings is a JSON array of objects: kind, file, line, snippet, validationGuide, fixGuide, dims (string dimensions from the miner), and location (startLine, startColumn, endLine, endColumn, fileUri, locationRef). Spans use 1-based lines and columns (editor-style). fileUri follows file:// (relative paths are preserved as scanned). Steps 2–3 prompts in workflow.yaml embed this state for coding agents.
Metric dimensions
Each emission includes string dimensions where applicable:
kind— finding type (see table above)severity—high|medium|low(triage priority, not proof of user impact)confidence— how reliable the matcher is (high|medium|low)replacementOpportunity—likely_getByRole|manual_review(component hits)file— file path of the scanned sourceline— 1-based start line of the anchor span (or0for file-level-only rows)startLine,startColumn,endLine,endColumn— 1-based inclusive span of the anchor node (columns match typical editor “Go to Line/Column”)locationRef—path:line:columnat the anchor start (grep / terminal friendly)fileUri—file://URL for the file (fragment-less; pair with line/column when opening)snippet— truncated one-line source at the anchor (or(file-level))validationGuide— short Step 2 hint (full checklist is inworkflow.yamlStep 2ai.prompt)fixGuide— short Step 3 hint (full playbook is inworkflow.yamlStep 3ai.prompt)locationCategory—spec|design_system_path|app_componentinteractionType— how the queried id is used in tests when inferable:read(query only),click,type,focus,assertion(insideexpect(...)); component-side rules useclickfor non-semantic click targets andreadfor static markup signals; file-level findings usereaduserEditable—true|false|unknown: semantic tags (input/textarea/select→true;button/a→false;contentEditable→true); test-id queries use a name heuristic;unknownwhen not classifiedtargetCategoryMask— one cardinality string encoding four target questions (each position isY= yes,N= no,?= unknown): **C**ontainer · user-Editable field · Interactive control · implementation Hook (e.g.YNYN,????for file-level findings). Heuristics use tag/attributes,role, handlers, and test-id naming patterns.tier— tier A/B forget_by_testid_role_hintonly (role-ish id heuristic)
Additional dimensions when applicable:
queriedTestId— literal string forget_by_testid_role_hinttestIdValue— staticdata-testid/data-test-idvalue (or[dynamic])htmlTag— intrinsic tag for JSX-based findingsexampleQueriedTestId— literal forcomposite_widget_spec_reviewwhen availabledistinctTestIdCount— formany_testids_in_ds_file
Heuristic test-id names are hints, not proof that getByRole will work. data-testid on semantic elements is often intentional (i18n, flaky names). Density is a weak signal. See Applicability → Caveats above.