For engineering and QA teams

One standard for test ids.Playwright that survives refactors.

AI proposes the ids. People stay in control.

tidwright has AI add data-testid by a single convention across the whole project, with checks that keep the code building. QA then writes E2E on those ids through getByTestId — no reading source, no guessing CSS selectors.

/plugin install tidwright@tidwright-marketplace
  • Zero runtime dependencies
  • Nothing is written without approval
  • Files restored if the build breaks

React · Next · Vue · Nuxt · Svelte · Angular · HTML

TL;DR

tidwright adds data-testid to your source by one standard: a tool makes the safe edits and AI decides the rest through a checked plan. A parser places every edit, your build must stay green or all files are restored, and nothing is written without approval. QA then writes Playwright tests on those ids.

01With vs without

With and without tidwright

How a team works before a standard exists, and after.

Topic
Without tidwright
With tidwright
Naming ids
Without tidwrightEveryone invents their own (btn1, submitBtn); tests break on every refactor.
With tidwrightOne grammar, <area>-<element>-<type>, e.g. login-submit-button, enforced by lint.
How QA finds a selector
Without tidwrightReads application source, or falls back to CSS, XPath and text.
With tidwrightReads the generated e2e/TESTIDS.md catalog and uses getByTestId only.
Adding ids to a feature
Without tidwrightAdded by hand, element by element — never finished.
With tidwrightThe tool adds the safe set in one pass; AI decides the rest through a validated plan, never by freehand edits.
Fear of breaking the app
Without tidwrightRead every diff line by eye.
With tidwrightChecked by the project's own parser; typecheck/build must pass before and after, otherwise every file is restored.
Duplicate ids in the DOM
Without tidwrightFound when a test turns flaky — or never.
With tidwrightA guard in unit tests and E2E fails the test and reports the id and how many times it repeats.
Spec uses an id the source dropped
Without tidwrightFound when the run fails.
With tidwright/tidwright:audit catches it before the run, along with sleeps, .only and credentials in specs.
Who owns the selector
Without tidwrightDev and QA argue.
With tidwrightA written contract: dev owns the id, QA owns the test, the auditor catches drift.

Before and after

- <button onClick={save}>Save</button>
+ <button data-testid="order-form-save-button" onClick={save}>Save</button>
- <input name="customerName" />
+ <input data-testid="order-form-customer-name-input" name="customerName" />

Real output of scan --write on a file named OrderForm.tsx. The Save button, whose name comes only from its text, is listed for review unless you pass --accept-text.

02How it works

How it works

Five steps. Nothing is written without approval.

  1. ScanThe project's parser locates every element a test would touch (buttons, links, inputs, forms) and proposes a standard name. Only a narrow subset is marked safe; the rest is a review queue with the reason beside each line.
  2. Add idsThe tool writes the safe subset. AI decides the rest (elements in loops get a stable ${key}) through a validated plan. A person approves before anything is written.
  3. Verify in one transactionEdited files are re-parsed, typecheck/build must stay green and, with --smoke, a browser check confirms ids are unique and present. If a step that ran fails, all files are restored.
  4. Cataloge2e/TESTIDS.md is generated for QA on every write run, stating how each id is defined.
  5. QA writes testsSpecs use getByTestId; the audit catches ids a spec uses that the source no longer defines.
Scan, add ids with AI, compile check, catalog, QA tests

03What you gain

What you gain

What the team has afterwards that it did not have before.

Standard and lint

A closed list of types (button input select row modal toast page…); no vague words such as btn, div, red; no bare numbers — use a business id.

Per-component recipes

recipe modal orders-delete returns the fixed id set for modal, table, form, tabs and toast, so nobody redesigns it on every page.

Locator helpers

modal(page,"orders-delete").confirm and table(page,"orders").row(id) match the recipes.

Duplicate-id guard

Templates for vitest/jest and a Playwright fixture that fail when the same data-testid appears twice in the DOM.

Auditor

Flags sleeps, .only, credential literals, CSS/XPath/text selectors and ids the source does not define.

Host safety

The playwright.config.ts template refuses non-local hosts unless allow-listed, so a run cannot hit production by accident.

Reported by the author (cannot be re-checked by others): on a large internal React/TSX app, the guard found 49 repeated ids that typecheck, lint and about 9,000 unit tests had all passed.

04The standard

The id standard

<area>-<element>-<type> · kebab-case ASCII · the last segment is a type from a closed list · each id defined once (dynamic ids too).

Component
Ids (<n> = the name you give it)
modal / drawer
<n>-modal · -title-text · -close-button · -confirm-button · -cancel-button · -error-banner
form + field
<n>-form · -submit-button · <form>-<field>-input · …-error
table
<n>-table · -row-${id} · -cell-${id}-${col} · -sort-button-${col} · -empty · -loading · -error
tabs · menu · pagination · toast
<n>-tab-${key} / -panel-${key} · -trigger-button / -item-${key} · -prev-button / -next-button · <action>-success-toast

05Safety

Safety by design

Nothing is written unless asked

scan is read-only. Writing needs --write, and the AI workflow shows a dry run and asks first. Files with uncommitted changes are skipped.

A deliberately narrow auto-set

Only native elements with a trustworthy name that are not repeated, spread-over or rendering nothing. Everything else is listed with its reason.

Positions come from a parser

The project's own parser (TypeScript, Vue, Svelte, Angular, parse5), so a string, regex or comment is never mistaken for an element. No parser means list only, never write.

Build stays green or files are restored

--write requires --verify, which must pass before and after. If the browser check is not run, the report says NOT run.

A cloned repo cannot run commands

A verify command that came only from a file in the repository is not run until a person approves it.

Adoptable in legacy code

lint --write-baseline accepts today's problems, then CI fails only on new ones.

06Evidence

Evidence

Re-runnable with node tools/corpus-check.mjs on nine open-source apps pinned to a commit (RealWorld Conduit in Vue / SvelteKit / Angular / React, elk, ngx-admin, shadcn-svelte, excalidraw, vercel/commerce).

9real apps
4,691source files scanned
431ids added across 199 files
0edits that were not a pure attribute insertion
0added ids that break the standard
0edited files with new syntax errors
Proven
  • The tool runs on real code without crashing.
  • What it writes is a pure attribute insertion (removing the added attributes returns the original bytes).
  • Added ids follow the standard and do not collide.
Not proven
  • That the edited apps still compile (use --verify in your project).
  • That the proposed names are the best ones.
  • That tests written on these ids are less flaky.

07Limits

Limits, stated plainly

  • Names come from attributes, i18n keys, routes and handlers — not from business knowledge. Anything without a trustworthy name goes to AI or a person. Dry-run one feature folder and read the diff before --write.
  • The project's own parser is required. Without it the tool lists candidates but does not write.
  • What static analysis cannot know (portals, conditional branches, dynamic components) is left to the browser check and to a person reading the diff.
  • No benchmark yet shows it beats the three source skills, or that tests written on these ids are less flaky.
  • Works with Codex and Cursor through AGENTS.md, but has not yet been exercised in Codex or Cursor themselves.

08Get started

Get started

Full steps for each agent are in the repository README (ask the team for the repository URL).

Claude Code

Install the plugin, then use the three commands below.

Codex

Reads AGENTS.md at the project root, which carries the workflow and the exact commands.

Cursor

Copy .cursor/rules/tidwright.mdc; it points the agent at AGENTS.md.

# install
/plugin install tidwright@tidwright-marketplace

# discover the repo → write its profile → scaffold (with approval)
/tidwright:init

# add ids to one feature, then publish the catalog
/tidwright:testid src/features/orders

# audit specs
/tidwright:audit