Skip to main content
mendwright

Playwright and Selenium selectors that repair themselves when a site changes.

On a 47-case benchmark of hand-built redesigns: 30 of 35 broken selectors repaired, and 1 wrong element in 47 cases (every case, misses included).

PyPI npm Python versions CI License: MIT

pip install mendwright
mendwright demo   # a redesign breaks two selectors: one is repaired, the pay button is refused. Offline: no browser, no API key

https://github.com/user-attachments/assets/35eb4f22-28ee-46f9-a221-a10ad261ba8c

A redeploy breaks three selectors. mendwright repairs them, and the next run reuses the fixes. Real runs, waits sped up.

A front-end deploy renames #terms, and your nightly run dies on a step that is still on the page. mendwright fixes that step while the script runs.

When a selector works, mendwright saves what the element looked like: its tag, id, name, label and nearby text. When the selector breaks, it looks for that element on the new page, and uses a match only if exactly one visible element with the same role fits. The saved details are often enough, with no API key. When they aren't, or the selector never matched, it asks a model, which can be a 1 GB one on your own machine. Repairs are saved in .mendwright/overrides.json, plain JSON, so the next run reuses them without asking anything, and mendwright fix writes them into your code.

Existing suite? Change one line, page = mendwright.open(page).page, and the calls listed here repair themselves with no other edits.

Quickstart

from playwright.sync_api import sync_playwright
import mendwright

with sync_playwright() as p:
    page = p.chromium.launch().new_page()
    page.goto("https://example.com/signup")

    heal = mendwright.open(page)
    heal.locator("email", "#email").fill("ada@example.com")
    heal.locator("accept_terms", "//input[@id='terms']", role="checkbox").check()
    heal.locator("create_account", "#create-account").click()

heal.locator() takes the selector you would pass to page.locator(), plus a name, like "email", that the repair is saved under. role= is optional: mendwright learns it the first time the selector matches, so pass it only when you add mendwright to a suite whose selectors may already be broken.

The one-line version for a script you don't want to touch:

page = mendwright.open(page).page          # Playwright
page.fill("#email", "ada@example.com")
page.check("#terms")

driver = mendwright.open(driver).driver    # Selenium (By from selenium.webdriver.common.by)
driver.find_element(By.ID, "terms").click()

Here the selector string is the name.

For a model, run the local one (below), or set a hosted provider's key and model:

export ANTHROPIC_API_KEY="sk-ant-..."
export ANTHROPIC_MODEL="..."      # a current model id from Anthropic's model list

OPENAI_API_KEY with OPENAI_MODEL, GEMINI_API_KEY with GEMINI_MODEL, and OLLAMA_MODEL work the same way, and mendwright doctor shows what it found. With no model, repairs from the saved details still happen, and anything else raises an error that says what to set.

Why you can trust a repair, and where you shouldn't

A tool that clicks the wrong button is worse than a failing test. mendwright refuses rather than guesses:

  • Checked on the live page. A repair is used only if it matches exactly one visible element with the step's role (checkbox, button, link and so on; a hidden checkbox is allowed), and never a bare position like /html[1]/body[1]/div[3].
  • Steps you mark are never repaired. Pass destructive=True for pay, submit or delete, and mendwright stops and tells you instead. It can't know which other steps are dangerous: the checks stop ambiguous, hidden and wrong-kind targets, not a plausible button of the same kind, so mark them.
  • Not on a page it wasn't found on. A key is only repaired on URL paths it was seen on, so a missing Continue button doesn't become the Help button on the next page. A step that moved while the path stayed the same isn't covered.
  • Failed repairs are quarantined. If the action on a repaired element raises, or an expect= or expect_value= check fails, the repair is never used again. A wrong click that succeeds isn't caught this way, so mendwright report lists repairs no passing test has confirmed.
  • Reads are checked if you ask. heal.locator("price", "#price", expect_value="money") refuses a repair that reads 4.5 where a price should be.
  • Repairs don't hide drift unless you let them. By default a repaired test passes with one log line. pytest --mendwright-strict fails it instead, with the old and the new selector (give strict jobs a fresh store), and mendwright notify tells your team.

When mendwright won't repair a step, it says why and what to do:

mendwright.exceptions.AttentionRequired: No matching element for: pay (tried 1 candidate(s)).
  -> 'pay' is marked as a destructive control, and mendwright never auto-heals those, because a validated but wrong selector here could act on a live account. Fix the selector in your code or seed file by hand.

Other questions people ask:

  • Is it deterministic? The saved-details match is. A model is asked only when that match isn't confident, and once a repair is saved it is reused, so later runs ask nothing.
  • What does it cost? Nothing when the saved details are enough, which is also the case on every later run. A model prompt is about 2,500 tokens, at most 2 calls per attempt, and attempts are capped at 3 per key and 60 in all, per hour.
  • Is it slow? A broken selector waits MENDWRIGHT_TIMEOUT_MS (10 s) before a repair, as a normal wait would. On the next run the saved repair is found on the first try.
  • Why not just use data-testid? Use it where you can; mendwright reads test ids too. It is for markup you don't control, and for suites already written.
  • What if I stop using it? mendwright fix --apply writes the repaired selectors into your code, and removing mendwright is one line.

How well it works

On a 47-case benchmark of hand-built pages that mimic common redesigns (ids renamed, hashed classes, new wrappers, reworded labels), plus 12 cases where the right answer is to not repair:

Repaired (of 35) Wrong element (of 47 cases)
Saved details alone, no model 20 0
Plus mendwright's own local model, headless Chromium 30 1

Both rows are in-sample: the thresholds were tuned on these cases, and the local model was revised after its misses here, so expect a wrong repair now and then on your pages. The model runs used mendwright 0.4.0; the saved-details row is re-checked by the test suite on every commit. No hosted-model numbers are published yet. The results list every case and every miss.

More than broken selectors

Pages also change in ways that aren't a broken selector. Each of these is off until you ask for it:

  • Cookie banners from the common consent managers: mendwright.open(page, cookies="reject").
  • Interruptions you know about, such as a newsletter modal, an "are you still there?" dialog or a session-expired sign-in: register them once with heal.interrupt() and they are handled wherever they open. run= takes a function for ones that need a form.
  • Popups you didn't know about, with dismiss=True. When one covers the element of an action, mendwright presses Escape, then a close button, then a decline button such as No thanks, chosen by its label. It never clicks Subscribe, Accept, a priced button, or Continue (except "Continue without..."), though it can't see what a button's own script does.
  • Steps in a different order, such as a checkout that now asks for the card before the address: heal.step() and heal.run_steps().
  • Iframes one level down, and open shadow roots at any depth.
  • Playwright's get_by_role() and the other get_by_* calls, with heal_get_by=True, repaired from the saved details only.
heal = mendwright.open(page, cookies="reject")      # close common consent banners
heal.interrupt("newsletter", "#newsletter-close")   # your own modal, wherever it opens

heal.locator("email", "#email").fill("ada@example.com")
heal.locator("create_account", "#create-account").click()

Each has its own section, and most have a short clip, in the guide.

In CI

  • mendwright check --base-url https://your.site loads the pages your selectors were last found on and reports which ones broke, before the nightly run. It doesn't save anything or call a model, and it clicks nothing but a consent banner when you pass --cookies.
  • mendwright fix --apply writes confirmed repairs into your code, as CSS where your code used CSS.
  • The GitHub Action does that after your tests and opens a pull request for you to review (guide).
  • mendwright notify posts a summary to Slack or any webhook: what was repaired and what needs a human. It sends counts and key names, never page text or form values, and --detail counts leaves the names out. The URL comes from MENDWRIGHT_WEBHOOK_URL.

Selenium, async and pytest

from selenium import webdriver
import mendwright

driver = webdriver.Chrome()
driver.get("https://example.com/signup")

heal = mendwright.open(driver)
heal.locator("email", "#email").fill("ada@example.com")
heal.locator("accept_terms", "//input[@id='terms']", role="checkbox").check()
driver.quit()

Playwright verbs like fill() and check() work on Selenium too, so step code reads the same on both.

import asyncio
from playwright.async_api import async_playwright
import mendwright.aio

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.com/signup")

        heal = await mendwright.aio.open(page)
        await heal.locator("accept_terms", "//input[@id='terms']", role="checkbox").check()

asyncio.run(main())
def test_signup(page, heal):
    page.goto("https://example.com/signup")
    heal.locator("email", "#email").fill("ada@example.com")
    heal.locator("accept_terms", "//input[@id='terms']", role="checkbox").check()

The heal fixture comes from mendwright's pytest plugin, which loads on its own and wraps pytest-playwright's page. Selenium suites point it at their own fixture with mendwright_page_fixture. A repair is marked confirmed when its test passes.

TypeScript

A port for Playwright Test is on npm. It has this package's fingerprint scoring, checks and store format, so Python and TypeScript suites can share one store:

import { test, expect } from 'mendwright/test';   // instead of '@playwright/test'

It does less than the Python package for now (no dismiss, expect_value, iframe or shadow-root repairs yet). See js/README.md.

A model that runs on your machine

mendwright-selector is a 1 GB fine-tune of Qwen2.5-1.5B that answers one question: which element on this page did the broken step mean, if any. It picks from a numbered list, mendwright builds the selector itself, and the usual checks still apply, so the model never writes a selector.

mendwright model install                      # needs Ollama; checks the file's SHA-256
export MENDWRIGHT_LOCAL_MODEL=mendwright-selector

It answers in about a quarter of a second on a laptop GPU, or about 1.5 s on a CPU, and the page never leaves your machine. On 3,000 held-out synthetic redesigns from its own generator, it picked the right element for 97.1% of steps (94.6% of those the saved details couldn't place) and a wrong one for 0.14% (0.36%). When the element was gone, it still picked one 2.9% of the time. training/README.md has the generator and the recipe.

Compared with other tools

Feature comparison of mendwright, Healenium, robotframework-heal, Skyvern, Stagehand and auto-playwright across thirteen features, including two where mendwright does not lead. The same data is in docs/comparison.md.

Healenium is the better fit for Java, and for any Selenium client through its proxy. Robot Framework suites have robotframework-heal. Agent tools such as Stagehand and Skyvern suit steps written in plain English. docs/comparison.md has a source for every cell.

Limits

  • Repairs are saved as XPath, with a CSS form when your code used CSS and CSS can name the element; repairs inside a shadow root are CSS. Closed shadow roots are out of reach.
  • With a hosted model, a cleaned copy of the page is sent whenever the saved details aren't enough. Redaction is best-effort: it removes field values, absolute URLs and long digit runs, but visible text and most attributes stay. The local model keeps the page on your machine. See SECURITY.md.
  • A list that matches nothing is never repaired while the script runs, because an emptied cart looks the same as a broken selector. heal.locator_all(..., nonempty=True) proposes a fix for you to accept instead.
  • It repairs a selector on the page the script is on. A step that moved to another page, or a new required step, still needs you.
  • Positional selectors like (//form//input)[1] can quietly point at the wrong field after a redesign, because nothing broke. Anchor on ids, names, labels or text.

MIT license. If mendwright saved a run for you, a star helps other people find it.

Metadata

Release files for mendwright 0.10.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mendwright 0.10.1
File Size Uploaded Provenance Actions
mendwright-0.10.1.tar.gz 1.1 MB With provenance Details for mendwright-0.10.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for mendwright 0.10.1
File Interpreter ABI Platform Actions
mendwright-0.10.1-py3-none-any.whl Python 3 none any Details for mendwright-0.10.1-py3-none-any.whl

Total release size: 1.6 MB

Release files / mendwright-0.10.1.tar.gz

Download URL mendwright-0.10.1.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
b56ba6e8311aea775db07388d546d368bf4cc1fe2290dde14bf6110e508904c2
BLAKE2b-256 checksum
How to use checksums
723350eb2c5937ee02727788a04c2e715650c33458bb695cd71ebd271a9aa37e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Release files / mendwright-0.10.1-py3-none-any.whl

Download URL mendwright-0.10.1-py3-none-any.whl
Size 488.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
67e3610128f31135586802624ef45e5b62c97e4a2626747b16101b7a157ac815
BLAKE2b-256 checksum
How to use checksums
56674a0b26fd76961a9e1a46203986cbe7d2b09bd7d6d481a9d1f91f14aa85d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

Security for mendwright 0.10.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for mendwright-0.10.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page