Aalexbit-codemod

testid-a11y-mining

Read-only JSSG miner (Step 1) plus AI validate/fix (Steps 2–3) defined entirely in workflow.yaml. Finds test-ID / a11y smells; emits metrics and persists structured findings (exact spans) to workflow state for downstream AI steps (TkDodo).

testingtesting-libraryaccessibilitya11ydata-testidmetricsminingreacttsx
Public
15 executions
Run locally
npx codemod testid-a11y-mining
Documentation

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

StepWhatDetails
1 — MineDeterministic JSSG scanBroad 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 — ValidateAI step in workflow.yamlFull validation checklist and output contract are inlined in that file’s Step 2 ai block.
3 — FixAI step in workflow.yamlFull 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-id on JSX

Caveats (any repo)

  • Test file detection uses filename patterns (e.g. *.spec.tsx, *.test.tsx, and the fixture path tests/.../input.tsx), not project-specific rules.
  • “Design system path” (locationCategory) is a path heuristic (a components path 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

KindSeverity (default)Summary
clickable_nonsemantic_with_testidhighdiv / span with onClick (or pointer/key handlers) and data-testid
data_testid_on_semantic_tagmediumdata-testid on button, a, input, textarea, select
composite_widget_spec_reviewmediumSpec file whose path suggests composite UI (Select, Menu, …) and uses getByTestId
get_by_testid_role_hintmediumIn spec-like files: getByTestId / queryByTestId / … with a string literal that looks “role-ish” (heuristic tiers A/B)
many_testids_in_ds_filelowUnder 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)
  • severityhigh | medium | low (triage priority, not proof of user impact)
  • confidence — how reliable the matcher is (high | medium | low)
  • replacementOpportunitylikely_getByRole | manual_review (component hits)
  • file — file path of the scanned source
  • line1-based start line of the anchor span (or 0 for file-level-only rows)
  • startLine, startColumn, endLine, endColumn1-based inclusive span of the anchor node (columns match typical editor “Go to Line/Column”)
  • locationRefpath:line:column at the anchor start (grep / terminal friendly)
  • fileUrifile:// 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 in workflow.yaml Step 2 ai.prompt)
  • fixGuide — short Step 3 hint (full playbook is in workflow.yaml Step 3 ai.prompt)
  • locationCategoryspec | design_system_path | app_component
  • interactionType — how the queried id is used in tests when inferable: read (query only), click, type, focus, assertion (inside expect(...)); component-side rules use click for non-semantic click targets and read for static markup signals; file-level findings use read
  • userEditabletrue | false | unknown: semantic tags (input / textarea / selecttrue; button / afalse; contentEditabletrue); test-id queries use a name heuristic; unknown when not classified
  • targetCategoryMaskone cardinality string encoding four target questions (each position is Y = 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 for get_by_testid_role_hint only (role-ish id heuristic)

Additional dimensions when applicable:

  • queriedTestId — literal string for get_by_testid_role_hint
  • testIdValue — static data-testid / data-test-id value (or [dynamic])
  • htmlTag — intrinsic tag for JSX-based findings
  • exampleQueriedTestId — literal for composite_widget_spec_review when available
  • distinctTestIdCount — for many_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.

Ready to contribute?

Build your own codemod and share it with the community.