tamash-playwright

How healing works

When a wrapped action fails (a fill times out, a click can't find its target), tamash-playwright:

  1. Captures an AI-optimized accessibility snapshot of the page — every element, including nameless ones, with its real parent/child/sibling structure. Not a flat list of named things.
  2. Sends the failed action, your .describe() label, and that snapshot to the configured provider.
  3. Gets back a single element the description points at — anywhere in the tree.
  4. A separate, deterministic step (no extra AI call) works out the most durable way to describe that element going forward.
  5. Retries the original action — with every original argument preserved — against the durable locator.

Deriving a durable locator

Once the model has pointed at an element, the fix is built from, in order of preference:

  1. Its own identity — a real id, test id, accessible role + name, label, or placeholder. Covers the large majority of cases.
  2. A nearby label — if it has none of the above but a label sits right next to it in the page's structure (the actual DOM relationship, not "somewhere on screen"), the fix anchors on that text.
  3. Positional fallback — whatever Playwright's own locator-generation can produce, even one that depends on element order.

Every candidate at every step is verified against the live page — confirmed to be the exact same element by real DOM identity, not "something matched" — before it's trusted.

The 9 structured strategies the derivation can emit: role, text, testId, label, placeholder, css, near ("the field next to this text"), scoped ("this control inside that dialog"), containing ("the row containing this text"). Never a brittle nth-child guess.

Never guess

The healer declines rather than pick wrong. A paraphrased description, a dead CSS selector with no human-readable text, or two equally-plausible elements near the same anchor all get an honest "couldn't resolve this" — reported as stage=ai_declined — not a guessed heal into a similar-looking element.

When a fix needs a second look — needsReview

  • Not flagged — the selector came from the element's own identity (step 1). As solid as anything you'd write by hand.
  • needsReview — it came from a nearby label (step 2) or a positional fallback (step 3). Correct right now; the page could change in a way that breaks it later, so review before relying on it long term.

Shown as needsReview=yes in the console line, a self-heal-needs-review annotation in the HTML report, and a [NEEDS REVIEW] tag in apply-heals output. It's informational, never a gate.

A nearby-label selector (step 2) is verified in the exact form apply-heals writes to source. When the label text appears on more than one element, that form does not resolve to a single element and the next step is used instead (TypeScript).

Identical elements

TypeScript. When several elements share the same role and accessible name and nothing on the page distinguishes them (for example two "Yes" buttons with no label, container or nearby text), the only selector that can be written is positional (.first(), .nth(n)). The heal is flagged needsReview, and the report records identicalElements: { count, index, pickedBy }: how many elements matched, which position was picked (1-based), and whether the text model (ai) or the vision fallback (vision) picked it. The review note states the same, for example: 2 identical elements match (button "Yes") and nothing on the page tells them apart. Picked #2 of 2 (by the AI). ... Verify this is the one you meant. Hidden duplicates are not counted. apply-heals shows an Identical line for that fix. Positional heals are not replayed from the heal cache; each run heals them again.

getDurable() on demand

The same derivation is a method:

const durable = await locator.getDurable('fill');
await durable.click();

Most useful on a locator built from an aria-ref= — resolves it to a stable getByRole / getByLabel / css. Throws if nothing durable can be derived.

What is never healed

  • dragTo / drop — a two-sided operation; guessing a replacement for one side isn't safe.
  • waitFor — a state check, not an action. A timeout can mean a broken selector or "this correctly never appeared" (verifying absence, a real app bug). Those can't be told apart from the error alone, so healing stays out of it — the failure is still reported cleanly.
  • expect(locator).toBeVisible() and similar assertions — same reasoning, and they never reach this mechanism anyway (Playwright's own matcher machinery). A genuinely-failed assertion still gets useful information, just not a fix: see Understanding why a test failed — a separate, TypeScript-only capability that classifies why it failed instead of silently retrying it.
  • A no-argument page.frameLocator() (Playwright 1.63+) on a page with more than one frame — "any frame" can't be narrowed to the one you meant when the target selector is broken, so healing steps aside rather than risk fixing against the wrong document. A single-frame page heals fine; for multi-frame pages, name the frame: page.frameLocator('#id').

Beyond selectors

  • Vision fallback — a screenshot-based search when text matching can't distinguish the element.
  • Action recovery — optional: the element was found but the action still failed (covered, off-screen, mid-animation).
  • Heal caching — a confirmed heal is retried before any AI call.
Questions or bugs? Open an issue  ·  Built from tamash-playwright-support/docs.