mendwright 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mendwright-0.4.0/CHANGELOG.md +61 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/CONTRIBUTING.md +7 -3
- {mendwright-0.3.0 → mendwright-0.4.0}/MANIFEST.in +1 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/PKG-INFO +81 -36
- {mendwright-0.3.0 → mendwright-0.4.0}/README.md +80 -35
- {mendwright-0.3.0 → mendwright-0.4.0}/SECURITY.md +3 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/examples/demo/README.md +13 -11
- {mendwright-0.3.0 → mendwright-0.4.0}/examples/demo/demo.py +3 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/examples/heal_gateway.mjs +45 -12
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/__init__.py +2 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/aio.py +277 -43
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/capture.py +8 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/cli.py +169 -17
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/coordinator.py +180 -39
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/demo/_page.py +58 -10
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/demo/_story.py +42 -19
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/demo/v2.html +14 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/drivers/__init__.py +3 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/drivers/base.py +19 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/drivers/playwright_async.py +9 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/drivers/playwright_sync.py +10 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/drivers/registry.py +2 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/drivers/selenium.py +17 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/facade.py +540 -50
- mendwright-0.4.0/mendwright/fingerprint.py +738 -0
- mendwright-0.4.0/mendwright/fixback.py +976 -0
- mendwright-0.4.0/mendwright/flow.py +187 -0
- mendwright-0.4.0/mendwright/interruptions.py +311 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/journal.py +15 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/prompt.py +38 -6
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/_errors.py +12 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/gateway.py +3 -1
- mendwright-0.4.0/mendwright/proxies.py +322 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/pytest_plugin.py +13 -1
- mendwright-0.4.0/mendwright/report_html.py +428 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/store.py +90 -13
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/types.py +2 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright.egg-info/PKG-INFO +81 -36
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright.egg-info/SOURCES.txt +19 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/pyproject.toml +3 -1
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/browser/mw_engines.py +38 -4
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/browser/test_aio_browser.py +198 -0
- mendwright-0.4.0/tests/browser/test_browser_fingerprint.py +90 -0
- mendwright-0.4.0/tests/browser/test_browser_flow.py +254 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/browser/test_browser_healing.py +4 -2
- mendwright-0.4.0/tests/browser/test_browser_intent.py +36 -0
- mendwright-0.4.0/tests/browser/test_browser_proxies.py +122 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/conftest.py +16 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_aio.py +47 -0
- mendwright-0.4.0/tests/test_benchmark.py +368 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_cli.py +78 -13
- mendwright-0.4.0/tests/test_driver_fingerprint.py +199 -0
- mendwright-0.4.0/tests/test_fingerprint.py +527 -0
- mendwright-0.4.0/tests/test_fix.py +992 -0
- mendwright-0.4.0/tests/test_flow.py +1012 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_gateway_worker.py +36 -2
- mendwright-0.4.0/tests/test_intent.py +188 -0
- mendwright-0.4.0/tests/test_interruptions.py +318 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_logging.py +4 -2
- mendwright-0.4.0/tests/test_prompt.py +77 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_last_error.py +48 -0
- mendwright-0.4.0/tests/test_proxies.py +480 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_pytest_plugin.py +10 -2
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_readme.py +113 -5
- mendwright-0.4.0/tests/test_report_html.py +493 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_store_meta.py +70 -0
- mendwright-0.3.0/CHANGELOG.md +0 -28
- mendwright-0.3.0/CODE_OF_CONDUCT.md +0 -68
- mendwright-0.3.0/tests/test_prompt.py +0 -37
- {mendwright-0.3.0 → mendwright-0.4.0}/LICENSE +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/examples/demo/v1.html +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/examples/demo/v2.html +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/examples/wrangler.toml +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/__main__.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/client.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/demo/__init__.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/demo/v1.html +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/exceptions.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/gate.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/limits.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/__init__.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/anthropic.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/base.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/bedrock.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/detect.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/gemini.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/litellm.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/providers/openai_compat.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/publish.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/py.typed +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/resolver.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/roles.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/selectors.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright/validation.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright.egg-info/dependency_links.txt +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright.egg-info/entry_points.txt +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright.egg-info/requires.txt +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/mendwright.egg-info/top_level.txt +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/setup.cfg +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/browser/conftest.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/browser/test_browser_drivers.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/fixtures/terms_moved.html +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_action_proxy.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_apply_retired.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_attention_messages.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_auto_provider.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_capture_clean.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_core_correctness.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_demo.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_destructive_flags.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_act_on.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_describe_inventory.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_detection.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_protocol.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_selector_kinds.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_selenium.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_driver_selenium_waits_frames.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_drivers.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_facade.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_facade_code_wins.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_facade_roles.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_failure_capture.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_frame_hint.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_heal_client.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_heal_coordinator.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_healer_hooks.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_http_useragent.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_journal.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_limits.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_live_gate.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_open_config.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_anthropic.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_base_url.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_bedrock.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_errors.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_gemini.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_litellm.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_provider_openai_compat.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_providers.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_public_api.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_publish_selector.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_selector_kinds.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_selector_resolver.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_selector_validation.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_selectors_store.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_store_robustness.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_success_heal.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_units.py +0 -0
- {mendwright-0.3.0 → mendwright-0.4.0}/tests/test_version.py +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and version numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
While the version starts with 0, a minor release can change the public API.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
- Heals without a model (see the [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#heals-without-a-model)). Each time a key's selector matches, mendwright saves a fingerprint of the element: its tag, the attributes that survive redesigns, its text or label, and its ancestors, never an input's value. When the selector breaks, mendwright scores the page's elements against it first, and heals with no model call when one element scores at least 0.40, no other comes within 0.12, it shares an id, test id, name, link, label or text with the recorded element (the same numbers and words, allowing a typo, a plural or a filler word), and it sits inside the recorded containers that are still on the page. The pick goes through the same checks as a model's. With no provider configured, these heals still happen. It is on by default; `mendwright.open(fingerprint=False)`, `MENDWRIGHT_FINGERPRINT=0` or `MENDWRIGHT_DISABLE=1` turns it off. On the benchmark it healed 20 of 35 broken selectors with no wrong heal, with thresholds tuned on those same cases.
|
|
13
|
+
- No changes per step (see the [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#no-changes-per-step)): `page = mendwright.open(page).page` on Playwright, sync or async, and `driver = mendwright.open(driver).driver` on Selenium. `page.locator(selector)`, the page shortcuts that take a selector, and `find_element()` with `By.XPATH`, `By.CSS_SELECTOR`, `By.ID` or `By.NAME` go through `heal.locator()`, with the selector as the key. Reads, `set_input_files`, `focus` and `find_element()` keep Playwright's and Selenium's own answer for a hidden element or several matches, and heal only a selector that matches nothing. Everything else goes to the real object, and `.native` is that object. A `find_element()` that can't find or heal the element raises an `AttentionRequired` that is also Selenium's `NoSuchElementException`.
|
|
14
|
+
- `heal.locator(..., intent="the terms checkbox in the signup form")` describes an element in words (see the [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#describing-an-element-with-intent)). It is saved with the key, redacted, and given to the model on one line of the prompt (the reference gateway builds the same line). With `role=` and no selector, a model finds the element on first use, and the saved XPath is used from then on.
|
|
15
|
+
- `mendwright fix` writes confirmed heals back into your code (see the [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#mendwright-fix)). It prints a diff of the string literals it would change, and `--apply` writes them, checking that each Python file still parses and changed only in the planned strings. It leaves alone keys marked destructive, heals inside an iframe, unconfirmed heals (unless `--include-unconfirmed`), keys whose old selector is a bare word, a key in `heal.locator(key, ...)` and `role=` or `text=` values, and a selector passed where only CSS runs, such as `By.CSS_SELECTOR`, and lists each with the reason.
|
|
16
|
+
- `mendwright report --format html` writes one self-contained page (no scripts, nothing loaded from the network) with the keys, recent heals, flow events and quarantines, and `--out FILE` writes any format to a file.
|
|
17
|
+
- `mendwright doctor` says whether the fingerprint tier is on, and `--json` has a `fingerprint` entry.
|
|
18
|
+
- `HealEvent` has a `source`: `"fingerprint"`, `"model"` or `"intent"`. The journal's `heal.proposed`, `heal.rejected` and `heal.applied` carry the same `source`, and a fingerprint heal has `"provider": "fingerprint"`.
|
|
19
|
+
- Drivers can have an optional `fingerprint(selector, frame_chain=())`, which the built-in Playwright and Selenium drivers have.
|
|
20
|
+
- The benchmark runs the fingerprint tier first, as `mendwright.open()` does. `--no-fingerprint` measures the provider alone, and the results show which tier made each heal.
|
|
21
|
+
- Flow changes (see the [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#flow-changes)):
|
|
22
|
+
- `heal.interrupt(key, selector, action="click", args=(), role=None)` registers a step that can show up at any point, such as a cookie banner, a newsletter modal or an "are you still there?" dialog. mendwright handles it when it is in the way of a step (the step's element is missing, the page shows it before an action, or an action fails because something covered the element), then goes on with the step. A failed action is retried once, and only after an interruption was handled, never when Playwright's call log says the action was already done. At most 3 are handled per step. An interruption is never healed, and a later `heal.locator()` for its key is skipped when the element isn't there. When an action fails because its element was covered or couldn't be reached and no interruption matched, one WARNING names the ones mendwright looked for, since one whose selector no longer matches is the likeliest cause.
|
|
23
|
+
- `mendwright.open(page, cookies="reject")` or `cookies="accept"` registers an interruption for the consent banners of OneTrust, Cookiebot, Didomi, TrustArc, Usercentrics, Quantcast Choice, Osano, cookieconsent, CookieYes and Complianz, with a fallback for other banners. The fallback acts only inside a short element that mentions cookies and holds no text field or navigation, so a consent screen that isn't about cookies (an OAuth Allow, a terms step) or a button such as Close account is never clicked. It is off by default.
|
|
24
|
+
- `heal.locator(..., optional=True)` skips a step whose element isn't there instead of healing it. Proxies have `.present`, which is False for a skipped step, and its actions do nothing.
|
|
25
|
+
- The prompt tells the model it may answer `{"xpath": null, "reason": "..."}` when no element on the page fits the step, and not to pick an element that is only similar. Providers report that as "the model says no element on this page fits: <reason>", and the reference gateway passes it on. The prompt also names the page path and the path where the key was last found.
|
|
26
|
+
- All of this works the same in `mendwright.aio`, where actions awaited together look for interruptions one at a time.
|
|
27
|
+
- New journal events: `flow.interrupt`, `flow.skip` and `flow.page_guard`.
|
|
28
|
+
- `mendwright demo` shows a cookie banner that now opens first and covers the form, handled as a registered interruption. `StandInPage` takes a `url=` and knows an overlay that blocks clicks until it is closed.
|
|
29
|
+
- An accuracy benchmark in `benchmarks/`: 47 cases where a redesign breaks a selector (12 of them where the right answer is no heal, 4 of those a step that moved to another page), a harness that runs them through `mendwright.open()` with any provider or a model-free baseline, and the baseline results. It is not part of the package. See [docs/benchmark.md](https://github.com/saadbuilt/mendwright/blob/main/docs/benchmark.md).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- `heal.page` on a Playwright healer is now a stand-in for the page (`HealingPage`, or `AsyncHealingPage` in `mendwright.aio`), and `heal.driver` on a Selenium healer is a stand-in for the WebDriver (`HealingDriver`). They pass `isinstance()` checks, and `.native` is the real page or driver. `heal.driver` on Playwright and `heal.page` on Selenium are as before.
|
|
34
|
+
- With no provider configured, a broken selector heals when its fingerprint finds the element with confidence. The `mendwright.open()` warning, `mendwright doctor` and the pytest header say so.
|
|
35
|
+
- `overrides.json` now stores a `fingerprint` for every key whose selector matched while the tier is on, not only for healed keys, and an `intent` when you pass one. The file is written when a fingerprint changes. [SECURITY.md](https://github.com/saadbuilt/mendwright/blob/main/SECURITY.md) lists what a fingerprint holds.
|
|
36
|
+
- A fingerprint heal doesn't count toward the hourly rate limit, and isn't reported to the provider's `report_outcome()`.
|
|
37
|
+
- A key is no longer healed on a page other than the last few its element was found on. The store saves those pages (`lastPage` and `pages` in `overrides.json`, at most 5; numbers, uuids and other generated ids in a path count as one page), and such a heal is declined before any model call with an error that says the flow looks changed. Pass `heal_across_pages=True` to `mendwright.open()` to heal there anyway, for example after a redesign that only changed the URL. Pages loaded with `page.set_content()` (`about:blank`) are never compared.
|
|
38
|
+
|
|
39
|
+
## [0.3.0] - 2026-10-11
|
|
40
|
+
|
|
41
|
+
First public release. Earlier versions were developed privately as selfheal and never published.
|
|
42
|
+
|
|
43
|
+
- Drivers for Playwright (sync and async, including patchright and camoufox) and Selenium, behind one `mendwright.open(page)`. `heal.locator(key, selector)` takes XPath, CSS or Playwright selectors. A heal is always an XPath.
|
|
44
|
+
- Providers for Anthropic, OpenAI and compatible APIs, Gemini, Bedrock, LiteLLM, Ollama and your own gateway. `auto_provider()` picks one from the environment, and there is no default model. `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` are honoured for proxies and compatible hosts.
|
|
45
|
+
- Local checks before a heal is used: not an index-only path, not quarantined or superseded for the key, one match in the captured HTML, the step's role, and one visible match on the live page that still has the role. A heal whose action fails is quarantined, and the quarantine blocks the same element, not just the same selector string.
|
|
46
|
+
- Keys you mark as destructive are never healed and never sent to a model: `destructive=True` in `heal.locator()`, `mendwright.open(destructive_keys=...)`, or `"destructive": true` in a seed file. mendwright doesn't guess this from the key or the page.
|
|
47
|
+
- Page capture removes scripts, styles, comments and head metadata, and redacts values, URLs and long numbers, including in broken selectors and expected text. Direct providers have an hourly rate limit and an optional spend cap.
|
|
48
|
+
- A pytest plugin: the `heal` fixture, an overridable `mendwright_provider` fixture, and `--mendwright-strict`, which also fails reruns from pytest-rerunfailures. Heals from a passing test are confirmed.
|
|
49
|
+
- The `mendwright` command: `demo` (offline, or `--live` with the model from your environment), `doctor` and `report`.
|
|
50
|
+
- Heals are saved in `.mendwright/overrides.json`, with a journal next to it. A corrupt store raises instead of being overwritten. Each applied heal prints one line to stderr.
|
|
51
|
+
- Python 3.10 to 3.14, with lxml as the only dependency.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
From the private builds. If you never used them under the name selfheal, you can skip this.
|
|
56
|
+
|
|
57
|
+
- On Selenium, the handle for a candidate inside an iframe is a `FramedElement`. `driver.execute_script()` and `ActionChains` need `el.in_frame(lambda e: ...)` to get an element that still works in the frame (see the [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#selenium)).
|
|
58
|
+
|
|
59
|
+
[Unreleased]: https://github.com/saadbuilt/mendwright/compare/v0.4.0...HEAD
|
|
60
|
+
[0.4.0]: https://github.com/saadbuilt/mendwright/compare/v0.3.0...v0.4.0
|
|
61
|
+
[0.3.0]: https://github.com/saadbuilt/mendwright/releases/tag/v0.3.0
|
|
@@ -129,17 +129,20 @@ The modules:
|
|
|
129
129
|
|
|
130
130
|
- `selectors.py`: `classify()` (XPath, CSS or Playwright engine selector), `check_syntax()`, `normalize()` and `InvalidSelector`.
|
|
131
131
|
- `drivers/`: `base.py` holds the `Driver` protocol, the shared describe and inventory scripts, and the fallback helpers. `registry.py` has `wrap()` and `register_driver()`. The built-in drivers are `playwright_sync.py`, `playwright_async.py` (used by `mendwright.aio`) and `selenium.py`, which handles frames, implicit waits and the verb mapping.
|
|
132
|
+
- `fingerprint.py`: the fingerprint tier. `FINGERPRINT_JS` reads an element in one round trip and `from_raw()` normalizes it; `find()` scores the elements of a captured page against a stored fingerprint and returns a unique XPath only for a confident match.
|
|
133
|
+
- `proxies.py`: `heal.page` and `heal.driver`, the page and driver stand-ins that send selector lookups through `heal.locator()` and everything else to the real object.
|
|
132
134
|
- `resolver.py`: `resolve()` and `present()`. Tries the candidates in order and polls until the timeout, then raises `AttentionRequired`, or `InvalidSelector` when no candidate can run.
|
|
133
|
-
- `facade.py`: `mendwright.open()`, `Healer` and `ActionProxy`. Puts the selectors from your code first, reads the role from the page, never acts through a saved heal on a key marked destructive, and runs actions, quarantining a heal whose action fails.
|
|
135
|
+
- `facade.py`: `mendwright.open()`, `Healer` and `ActionProxy`. Puts the selectors from your code first, reads the role from the page, never acts through a saved heal on a key marked destructive, and runs actions, quarantining a heal whose action fails. It also handles interruptions, optional steps and the page guard.
|
|
136
|
+
- `flow.py` and `interruptions.py`: page paths, interception errors and the texts for flow changes; `heal.interrupt()`'s checks and the selectors of the `cookies=` preset.
|
|
134
137
|
- `aio.py`: `mendwright.aio.open()` and `AsyncHealer`. A heal runs the sync coordinator in a worker thread, which reaches the page through `SyncBridge`.
|
|
135
|
-
- `coordinator.py`: `HealCoordinator` and `resolve_with_heal()`. Runs the checks before any
|
|
138
|
+
- `coordinator.py`: `HealCoordinator` and `resolve_with_heal()`. Runs the checks before any heal, tries the fingerprint tier, builds the bundle, asks the provider (with one re-prompt on rejection), applies, journals, and keeps the reason for `explain()`.
|
|
136
139
|
- `validation.py` and `gate.py`: `validate()` checks a proposal against the captured HTML with lxml; `live_match()` checks it against the page through the driver. `client.apply()` runs both.
|
|
137
140
|
- `store.py` and `roles.py`: the seed and override layers, candidate status, per-key metadata, the lock file and the merge; and the role vocabulary.
|
|
138
141
|
- `capture.py`: `build_bundle()`, `redact()` and `clean_html()`.
|
|
139
142
|
- `providers/`: the `HealProvider` protocol (`base.py`), the built-in providers, `auto_provider()` (`detect.py`) and the error text (`_errors.py`). `prompt.py` holds the one prompt every provider uses.
|
|
140
143
|
- `limits.py` and `journal.py`: the rate limit and spend cap for direct providers, and the JSONL journal.
|
|
141
144
|
- `publish.py`: writes a confirmed selector into a seed JSON file.
|
|
142
|
-
- `cli.py` and `demo/`: the `mendwright` command and the demo that ships in the wheel.
|
|
145
|
+
- `cli.py` and `demo/`: the `mendwright` command and the demo that ships in the wheel. `fixback.py` is `mendwright fix`, and `report_html.py` is `mendwright report --format html`.
|
|
143
146
|
- `pytest_plugin.py`: the `heal` fixture, confirmation from test results, strict mode and the summary.
|
|
144
147
|
|
|
145
148
|
## Writing a driver
|
|
@@ -158,6 +161,7 @@ A driver wraps one live page or session.
|
|
|
158
161
|
| `outer_html(scope=None)` | expected | The HTML the checks run against. |
|
|
159
162
|
| `act_on(native, verb, *args, **kwargs)` | optional | Run a Playwright-style verb on a `handle()` result. Without it, `driver_act_on()` calls `getattr(native, verb)(*args, **kwargs)`. |
|
|
160
163
|
| `describe(selector, frame_chain=())` | optional | `{'tag', 'role', 'text', 'outer_html'}` for a single match, else `None`. Without it, `driver_describe()` returns `None` and the role is guessed from the selector text. |
|
|
164
|
+
| `fingerprint(selector, frame_chain=())` | optional | The fingerprint (see `mendwright/fingerprint.py`) of the one element the selector matches, else `None`. Run `FINGERPRINT_JS` on the match and pass the result through `from_raw()`. Without it, `driver_fingerprint()` returns `None` and the fingerprint tier never has anything to look for. |
|
|
161
165
|
| `inventory(limit=200)` | optional | Up to `limit` rows of `{'tag', 'role', 'id', 'name', 'text', 'aria_label'}` for buttons, links, inputs, selects, textareas and `[role]` elements. Without it, `driver_inventory()` returns `[]`. |
|
|
162
166
|
|
|
163
167
|
Rules:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The sdist carries what it needs to run the offline test suite from the tarball.
|
|
2
2
|
# Explicit patterns rather than graft, so __pycache__ and .pyc files never get in.
|
|
3
3
|
# Each pattern must match at least one file, or the build prints a warning.
|
|
4
|
-
include CHANGELOG.md CONTRIBUTING.md SECURITY.md
|
|
4
|
+
include CHANGELOG.md CONTRIBUTING.md SECURITY.md
|
|
5
5
|
recursive-include tests *.py *.html
|
|
6
6
|
recursive-include examples/demo *.py *.html *.md
|
|
7
7
|
# The reference gateway and its config: tests/test_gateway_worker.py checks the worker
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mendwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Self-healing selectors for Playwright and Selenium. When a selector breaks, a model suggests a new one, mendwright checks it against the page, and the step is retried.
|
|
5
5
|
Author: saadbuilt
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,15 +67,15 @@ Dynamic: license-file
|
|
|
67
67
|
Self-healing selectors for Playwright and Selenium.
|
|
68
68
|
|
|
69
69
|
[](https://pypi.org/project/mendwright/)
|
|
70
|
-
[](https://pypi.org/project/mendwright/)
|
|
70
|
+
[](https://pypi.org/project/mendwright/)
|
|
71
71
|
[](https://github.com/saadbuilt/mendwright/actions/workflows/ci.yml)
|
|
72
72
|
[](https://github.com/saadbuilt/mendwright/blob/main/LICENSE)
|
|
73
73
|
|
|
74
74
|
</div>
|
|
75
75
|
|
|
76
|
-
When a site update breaks
|
|
76
|
+
mendwright heals many broken selectors without any API key. Each time a selector matches, it saves a fingerprint of the element: its tag, ids, names, label, text and surroundings. When a site update breaks the selector, mendwright looks for that element first, checks the new XPath against the live page, saves it, and retries the step. It asks a model only when the fingerprint match isn't confident. On the next run the saved selector is used and nothing is asked.
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
For the model it supports Anthropic, OpenAI and compatible APIs, Gemini, Bedrock, LiteLLM, or a local model through Ollama.
|
|
79
79
|
|
|
80
80
|
## Install
|
|
81
81
|
|
|
@@ -86,28 +86,29 @@ mendwright demo
|
|
|
86
86
|
|
|
87
87
|
If the `mendwright` command isn't on your PATH (common on Windows), run `python -m mendwright demo`.
|
|
88
88
|
|
|
89
|
-
The demo runs offline, with no browser and no API key. A fake page built on lxml stands in for the browser and a canned reply stands in for the model. A redesign breaks two selectors. mendwright heals the checkbox, and refuses to heal the pay button because the demo marks it `destructive=True`.
|
|
89
|
+
The demo runs offline, with no browser and no API key. A fake page built on lxml stands in for the browser and a canned reply stands in for the model. A redesign breaks two selectors, and a cookie banner that used to come later now opens first and covers the form. mendwright clicks the banner away because the demo registered it with `heal.interrupt()`, heals the checkbox, and refuses to heal the pay button because the demo marks it `destructive=True`.
|
|
90
90
|
|
|
91
|
-
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/demo.svg" width="100%" alt="Terminal recording of mendwright demo. On v2 of a checkout page the accept_terms selector misses, the model proposes an XPath anchored on the label text, it passes the checks and the box is checked. The pay selector, marked destructive=True, also misses and is refused without a model call.">
|
|
91
|
+
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/demo.svg" width="100%" alt="Terminal recording of mendwright demo. On v2 of a checkout page a cookie banner covers the form and is clicked away as a registered interruption. The accept_terms selector misses, the model proposes an XPath anchored on the label text, it passes the checks and the box is checked. The pay selector, marked destructive=True, also misses and is refused without a model call.">
|
|
92
92
|
|
|
93
93
|
<details>
|
|
94
94
|
<summary>The same output as text</summary>
|
|
95
95
|
|
|
96
96
|
```text
|
|
97
|
-
mendwright demo: a redesign breaks two selectors on a checkout page.
|
|
97
|
+
mendwright demo: a redesign breaks two selectors on a checkout page and adds a cookie banner.
|
|
98
98
|
Offline: a stand-in page (lxml, no browser) and a scripted model reply (no API call).
|
|
99
99
|
|
|
100
|
-
v1 accept_terms #accept-terms
|
|
101
|
-
v1 pay #pay
|
|
102
|
-
-- v2: a redesign ships
|
|
103
|
-
v2
|
|
100
|
+
v1 accept_terms #accept-terms ok matched, box checked
|
|
101
|
+
v1 pay #pay ok matched (not clicked)
|
|
102
|
+
-- v2: a redesign ships, both ids are gone, and a cookie banner opens first --
|
|
103
|
+
v2 cookies #cookie-accept ok banner covered the form, clicked it (heal.interrupt)
|
|
104
|
+
v2 accept_terms #accept-terms MISS no element matches
|
|
104
105
|
model proposed //label[contains(normalize-space(.),'accept the terms')]//input[@type='checkbox'] (scripted reply, no API call)
|
|
105
106
|
checks pass: 1 checkbox in the captured HTML, 1 match on the live page
|
|
106
107
|
retry ok: box checked with the healed selector, heal confirmed
|
|
107
|
-
v2 pay #pay
|
|
108
|
+
v2 pay #pay MISS no element matches
|
|
108
109
|
refused 'pay' is marked destructive=True. Not healed, no model call.
|
|
109
110
|
|
|
110
|
-
Done: 1 heal applied and confirmed, 1 refused, 1 model call in total.
|
|
111
|
+
Done: 1 heal applied and confirmed, 1 refused, 1 interruption handled, 1 model call in total.
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
</details>
|
|
@@ -133,8 +134,9 @@ with sync_playwright() as p:
|
|
|
133
134
|
`heal.locator()` takes the selector you'd pass to `page.locator()`, plus a key: a name for the element that its heals are saved under.
|
|
134
135
|
|
|
135
136
|
`role=` is optional. mendwright needs to know what kind of element it is looking for before it can check a heal, and it learns that the first time the selector matches. Pass `role=` when you add mendwright to a suite whose selectors might already be broken.
|
|
137
|
+
`intent=` describes the element in words, such as `intent="the terms checkbox in the signup form"`. The model gets it with the page, and with `role=` and no selector, a model finds the element on first use.
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
A selector that never matched has no fingerprint, and some changes are beyond one, so to let mendwright ask a model:
|
|
138
140
|
|
|
139
141
|
```bash
|
|
140
142
|
export ANTHROPIC_API_KEY="sk-ant-..."
|
|
@@ -145,11 +147,26 @@ $env:ANTHROPIC_API_KEY = "sk-ant-..."
|
|
|
145
147
|
$env:ANTHROPIC_MODEL = "..."
|
|
146
148
|
```
|
|
147
149
|
|
|
148
|
-
There is no default model. `OPENAI_API_KEY` with `OPENAI_MODEL`, `GEMINI_API_KEY` with `GEMINI_MODEL`, and `OLLAMA_MODEL` work the same way.
|
|
149
|
-
|
|
150
|
-
`mendwright doctor` shows which provider it found. Without one, selectors still resolve, and a broken one raises an error that lists the variables to set.
|
|
150
|
+
There is no default model. `OPENAI_API_KEY` with `OPENAI_MODEL`, `GEMINI_API_KEY` with `GEMINI_MODEL`, and `OLLAMA_MODEL` work the same way. For Bedrock, LiteLLM or another OpenAI-compatible host, pass `provider=` to `mendwright.open()`.
|
|
151
|
+
`mendwright doctor` shows which provider it found. Without one, selectors still resolve and fingerprint heals still happen, and a selector the fingerprint can't place raises an error that lists the variables to set.
|
|
151
152
|
|
|
152
|
-
A selector that stops matching is waited for 10 seconds (`MENDWRIGHT_TIMEOUT_MS`) before mendwright
|
|
153
|
+
A selector that stops matching is waited for 10 seconds (`MENDWRIGHT_TIMEOUT_MS`) before mendwright tries to heal it. Each applied heal prints one line to stderr, and `mendwright report` shows what was saved.
|
|
154
|
+
|
|
155
|
+
## No changes per step
|
|
156
|
+
|
|
157
|
+
To heal an existing script without touching its steps, change the line that makes the page or the driver:
|
|
158
|
+
|
|
159
|
+
<!-- readme-test: wrappers -->
|
|
160
|
+
```python
|
|
161
|
+
page = mendwright.open(page).page # Playwright
|
|
162
|
+
page.fill("#email", "ada@example.com")
|
|
163
|
+
page.check("#terms")
|
|
164
|
+
|
|
165
|
+
driver = mendwright.open(driver).driver # Selenium (By from selenium.webdriver.common.by)
|
|
166
|
+
driver.find_element(By.ID, "terms").click()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Each selector is also the key its heals are saved under. Chained locators, `get_by_*` and the other `By` values go to the real page or driver unhealed, and `.native` is the real object. The [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#no-changes-per-step) lists what heals.
|
|
153
170
|
|
|
154
171
|
## Selenium
|
|
155
172
|
|
|
@@ -166,8 +183,7 @@ heal.locator("accept_terms", "//input[@id='terms']", role="checkbox").check()
|
|
|
166
183
|
driver.quit()
|
|
167
184
|
```
|
|
168
185
|
|
|
169
|
-
Playwright verbs such as `fill()`, `check()` and `select_option()` are mapped to WebElement calls, so step code reads the same on both libraries.
|
|
170
|
-
Selenium runs XPath and CSS, so Playwright selectors such as `text=Save` raise `InvalidSelector` at once.
|
|
186
|
+
Playwright verbs such as `fill()`, `check()` and `select_option()` are mapped to WebElement calls, so step code reads the same on both libraries. Selenium runs XPath and CSS, so Playwright selectors such as `text=Save` raise `InvalidSelector` at once.
|
|
171
187
|
|
|
172
188
|
## Async Playwright
|
|
173
189
|
|
|
@@ -209,13 +225,13 @@ To make CI show selector drift instead of quietly healing it, run `pytest --mend
|
|
|
209
225
|
|
|
210
226
|
<picture>
|
|
211
227
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/how-it-works-dark.svg">
|
|
212
|
-
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/how-it-works-light.svg" width="800" alt="How a heal works: a selector misses; a key marked destructive, an unknown role or a
|
|
228
|
+
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/how-it-works-light.svg" width="800" alt="How a heal works: a selector misses; a key marked destructive, an unknown role or a page the key was never found on stops with AttentionRequired before anything is tried; otherwise the page is captured, and an XPath comes from the saved fingerprint when one element matches it with confidence, else from the model within the hourly limit; local checks run; a failed fingerprint pick goes to the model, a failed model pick gets the reason and one more try, and a second failure raises AttentionRequired with nothing saved; a pass is saved and the step retried; if the action then fails the healed selector is quarantined.">
|
|
213
229
|
</picture>
|
|
214
230
|
|
|
215
|
-
A step is refused before
|
|
216
|
-
mendwright
|
|
217
|
-
A rejected attempt counts, and an attempt makes up to 2 model calls.
|
|
218
|
-
|
|
231
|
+
A step is refused before anything is tried when its key is marked destructive, when mendwright doesn't know what kind of element it is (see `role=` above), or when the page isn't one the key was found on. mendwright doesn't guess which controls are destructive. Mark a pay, submit or delete step with `heal.locator(..., destructive=True)`, or pass its key in `mendwright.open(destructive_keys=...)`.
|
|
232
|
+
Next, mendwright scores the page's elements against the key's fingerprint. It takes the best one, with no model call, only when it shares an id, test id, name, link, label or text with the recorded element and no other element comes close.
|
|
233
|
+
Otherwise it sends a cleaned, redacted copy of the page to the model (at most 60,000 characters of HTML and 60,000 of element inventory per request), within an hourly limit of 3 heal attempts per key and 60 in all. A rejected attempt counts, and an attempt makes up to 2 model calls. A gateway meters on its side instead.
|
|
234
|
+
Either way, the XPath is used only if:
|
|
219
235
|
|
|
220
236
|
- it isn't an index-only path such as `/html[1]/body[1]/div[3]/input[1]`
|
|
221
237
|
- the store doesn't hold it as quarantined or superseded for this key, and it doesn't reach an element the key has quarantined
|
|
@@ -223,13 +239,43 @@ Otherwise mendwright sends a cleaned, redacted copy of the page to the model (at
|
|
|
223
239
|
- that element has the step's role (checkbox, button, link, and so on)
|
|
224
240
|
- it matches exactly one element on the live page, and that element is visible (hidden checkboxes are allowed) and still has the role
|
|
225
241
|
|
|
226
|
-
If the first proposal fails,
|
|
242
|
+
If a fingerprint pick fails, the model is asked. If the model's first proposal fails, it gets the reason and one more try. Heals are saved in `.mendwright/overrides.json`, and each request, proposal, rejection and outcome goes to `journal.jsonl` next to it.
|
|
227
243
|
On the next run the selector in your code is tried first and the saved heal right after it, in the same pass. That resolves without a wait or a model call. If the action on a healed element fails in the browser, the heal is quarantined and not used again.
|
|
228
244
|
|
|
245
|
+
## What it can and can't fix
|
|
246
|
+
|
|
247
|
+
mendwright repairs a selector on the page your script is on. It handles layout changes within that page:
|
|
248
|
+
|
|
249
|
+
- ids and classes renamed, or replaced by build-generated ones
|
|
250
|
+
- labels reworded or translated (with a model)
|
|
251
|
+
- elements wrapped in a new container or component
|
|
252
|
+
- elements moved to another part of the page, or form fields put in a different order
|
|
253
|
+
- lookalike elements where the surrounding text decides which one is meant
|
|
254
|
+
|
|
255
|
+
It also handles some changes in your script's flow:
|
|
256
|
+
|
|
257
|
+
- A step that can show up at any point, such as a cookie banner, a newsletter modal or an "are you still there?" dialog. Register it once with `heal.interrupt()`. When it is in the way of a step (the step's element is missing, or the page shows the interruption), mendwright handles it first (a click, unless you register another action) and goes on with the step. If your script also has its own step for it, that step is skipped once the interruption was handled, so a cookie banner that now appears before signup instead of after it needs no other change.
|
|
258
|
+
- The consent banners of common consent managers, with `cookies="reject"` or `cookies="accept"`. This is off by default.
|
|
259
|
+
- A step that may not be there, with `heal.locator(..., optional=True)`.
|
|
260
|
+
|
|
261
|
+
<!-- readme-test: flow -->
|
|
262
|
+
```python
|
|
263
|
+
heal = mendwright.open(page, cookies="reject") # close common consent banners
|
|
264
|
+
heal.interrupt("newsletter", "#newsletter-close") # your own modal, wherever it opens
|
|
265
|
+
|
|
266
|
+
heal.locator("email", "#email").fill("ada@example.com")
|
|
267
|
+
heal.locator("create_account", "#create-account").click()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
A step that moved to another page, steps in a new order, or a new required step still need a change to your script.
|
|
271
|
+
mendwright remembers the last few pages each key was found on and doesn't heal a key on another page, so you get an error that says the flow changed, instead of a heal onto a similar element such as a Help button where a Continue button used to be. If a redesign only changed the URL, or the same steps run under language paths such as `/en/` and `/de/`, pass `heal_across_pages=True` to `mendwright.open()`.
|
|
272
|
+
The model is also told it may answer that no element on the page fits, but a step that moved within the same address is still the model's call, so mark irreversible steps with `destructive=True`. The [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#flow-changes) has the details.
|
|
273
|
+
|
|
274
|
+
Anchor selectors on ids, names, labels or text rather than position. A positional selector such as `(//form//input)[1]` that still matches after two fields swap now points at the wrong field, and since nothing broke, mendwright doesn't step in.
|
|
275
|
+
|
|
229
276
|
## Errors
|
|
230
277
|
|
|
231
|
-
When mendwright can't or won't heal a step, it raises `mendwright.AttentionRequired` with the reason and a next step.
|
|
232
|
-
This is the error from `heal.locator("pay", "#pay", destructive=True)` when `#pay` stopped matching:
|
|
278
|
+
When mendwright can't or won't heal a step, it raises `mendwright.AttentionRequired` with the reason and a next step. This is the error from `heal.locator("pay", "#pay", destructive=True)` when `#pay` stopped matching:
|
|
233
279
|
|
|
234
280
|
<!-- readme-test: attention-pay -->
|
|
235
281
|
```text
|
|
@@ -237,7 +283,8 @@ mendwright.exceptions.AttentionRequired: No matching element for: pay (tried 1 c
|
|
|
237
283
|
-> '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.
|
|
238
284
|
```
|
|
239
285
|
|
|
240
|
-
`mendwright doctor` checks the install, the provider and the store path. `mendwright report` lists each key with its selectors and recent heals; `--format md` prints a Markdown table you can append to `$GITHUB_STEP_SUMMARY
|
|
286
|
+
`mendwright doctor` checks the install, the provider and the store path. `mendwright report` lists each key with its selectors and recent heals; `--format md` prints a Markdown table you can append to `$GITHUB_STEP_SUMMARY`, and `--format html --out report.html` writes one self-contained page.
|
|
287
|
+
`mendwright fix` puts confirmed heals back into your code: it prints a diff of the string literals it would change, and `--apply` writes them.
|
|
241
288
|
|
|
242
289
|
## Compatibility
|
|
243
290
|
|
|
@@ -249,26 +296,24 @@ mendwright.exceptions.AttentionRequired: No matching element for: pay (tried 1 c
|
|
|
249
296
|
| Model providers | Anthropic, OpenAI, Azure OpenAI, OpenAI-compatible hosts (Groq, OpenRouter, vLLM, LM Studio), Ollama, Gemini, Bedrock, LiteLLM, or your own gateway |
|
|
250
297
|
| Python | 3.10 and newer, with lxml as the only dependency |
|
|
251
298
|
|
|
252
|
-
The real-browser suite (`python -m pytest -m browser`) passed locally on Windows 11 with Python 3.12 on Playwright Chromium, Firefox and WebKit, patchright, camoufox, and Selenium with Chrome and Firefox. The async tests passed on all three Playwright browsers.
|
|
253
|
-
CI runs the offline suite on Linux (Python 3.10 to 3.14), Windows and macOS, and the browser suite on Linux.
|
|
299
|
+
The real-browser suite (`python -m pytest -m browser`) passed locally on Windows 11 with Python 3.12 on Playwright Chromium, Firefox and WebKit, patchright, camoufox, and Selenium with Chrome and Firefox. The async tests passed on all three Playwright browsers. CI runs the offline suite on Linux (Python 3.10 to 3.14), Windows and macOS, and the browser suite on Linux.
|
|
254
300
|
|
|
255
301
|
## Comparison
|
|
256
302
|
|
|
257
303
|
<picture>
|
|
258
304
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/feature-matrix-dark.svg">
|
|
259
|
-
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/feature-matrix-light.svg" width="800" alt="Feature comparison of mendwright, Healenium, robotframework-heal, Skyvern, Stagehand and auto-playwright across thirteen features, including
|
|
305
|
+
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/feature-matrix-light.svg" width="800" alt="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.">
|
|
260
306
|
</picture>
|
|
261
307
|
|
|
262
|
-
Where another tool fits better: Healenium
|
|
263
|
-
[docs/comparison.md](https://github.com/saadbuilt/mendwright/blob/main/docs/comparison.md) has the full comparison, with a source for every cell and the footprint charts.
|
|
308
|
+
Where another tool fits better: Healenium covers Java, and any Selenium client through its proxy, and its open source edition never calls a model in any setup. Robot Framework suites have robotframework-heal. Agent tools such as Stagehand and Skyvern handle steps written in plain English. [docs/comparison.md](https://github.com/saadbuilt/mendwright/blob/main/docs/comparison.md) has the full comparison, with a source for every cell and the footprint charts.
|
|
264
309
|
|
|
265
310
|
## Limitations
|
|
266
311
|
|
|
267
312
|
- Heals are always XPath, even when your code used CSS or a Playwright selector.
|
|
268
313
|
- An element inside an iframe heals only when you open mendwright on the frame: a `Frame` from `page.frames` on Playwright, or the driver after `switch_to.frame()` on Selenium. On Playwright, a selector that matches only inside a child iframe raises `AttentionRequired` and says so, without a model call.
|
|
269
|
-
- With a hosted provider, page HTML is sent to it. Redaction removes quoted `value` attributes, absolute http(s) URLs and runs of 12 to 19 digits, but visible text and other attributes stay. See [SECURITY.md](https://github.com/saadbuilt/mendwright/blob/main/SECURITY.md). A local model through Ollama keeps the page on your machine.
|
|
314
|
+
- With a hosted provider, page HTML is sent to it whenever a fingerprint match isn't confident. Redaction removes quoted `value` attributes, absolute http(s) URLs and runs of 12 to 19 digits, but visible text and other attributes stay. See [SECURITY.md](https://github.com/saadbuilt/mendwright/blob/main/SECURITY.md). A local model through Ollama keeps the page on your machine.
|
|
270
315
|
- If a page's cleaned HTML is longer than 60,000 characters, the rest is cut before it reaches the model. An element near the end is then visible to it only through the element inventory.
|
|
271
|
-
-
|
|
316
|
+
- Accuracy has been measured only without a model. On the [47-case benchmark](https://github.com/saadbuilt/mendwright/blob/main/docs/benchmark.md), the fingerprint tier alone healed 20 of 35 broken selectors and none onto a wrong element, but its thresholds were tuned on those same cases, so expect it to be wrong now and then on yours. With a model-free similarity baseline behind it, 28 of 35 healed, and 3 of the 47 cases went to the wrong element ([results](https://github.com/saadbuilt/mendwright/blob/main/benchmarks/RESULTS.md)). No model numbers are published yet.
|
|
272
317
|
- `--mendwright-strict` only fails the run that made the heal. Later runs pick up the saved heal and pass, which is why strict CI runs need a fresh store.
|
|
273
318
|
|
|
274
319
|
## Links
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
Self-healing selectors for Playwright and Selenium.
|
|
9
9
|
|
|
10
10
|
[](https://pypi.org/project/mendwright/)
|
|
11
|
-
[](https://pypi.org/project/mendwright/)
|
|
11
|
+
[](https://pypi.org/project/mendwright/)
|
|
12
12
|
[](https://github.com/saadbuilt/mendwright/actions/workflows/ci.yml)
|
|
13
13
|
[](https://github.com/saadbuilt/mendwright/blob/main/LICENSE)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
-
When a site update breaks
|
|
17
|
+
mendwright heals many broken selectors without any API key. Each time a selector matches, it saves a fingerprint of the element: its tag, ids, names, label, text and surroundings. When a site update breaks the selector, mendwright looks for that element first, checks the new XPath against the live page, saves it, and retries the step. It asks a model only when the fingerprint match isn't confident. On the next run the saved selector is used and nothing is asked.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
For the model it supports Anthropic, OpenAI and compatible APIs, Gemini, Bedrock, LiteLLM, or a local model through Ollama.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -27,28 +27,29 @@ mendwright demo
|
|
|
27
27
|
|
|
28
28
|
If the `mendwright` command isn't on your PATH (common on Windows), run `python -m mendwright demo`.
|
|
29
29
|
|
|
30
|
-
The demo runs offline, with no browser and no API key. A fake page built on lxml stands in for the browser and a canned reply stands in for the model. A redesign breaks two selectors. mendwright heals the checkbox, and refuses to heal the pay button because the demo marks it `destructive=True`.
|
|
30
|
+
The demo runs offline, with no browser and no API key. A fake page built on lxml stands in for the browser and a canned reply stands in for the model. A redesign breaks two selectors, and a cookie banner that used to come later now opens first and covers the form. mendwright clicks the banner away because the demo registered it with `heal.interrupt()`, heals the checkbox, and refuses to heal the pay button because the demo marks it `destructive=True`.
|
|
31
31
|
|
|
32
|
-
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/demo.svg" width="100%" alt="Terminal recording of mendwright demo. On v2 of a checkout page the accept_terms selector misses, the model proposes an XPath anchored on the label text, it passes the checks and the box is checked. The pay selector, marked destructive=True, also misses and is refused without a model call.">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/demo.svg" width="100%" alt="Terminal recording of mendwright demo. On v2 of a checkout page a cookie banner covers the form and is clicked away as a registered interruption. The accept_terms selector misses, the model proposes an XPath anchored on the label text, it passes the checks and the box is checked. The pay selector, marked destructive=True, also misses and is refused without a model call.">
|
|
33
33
|
|
|
34
34
|
<details>
|
|
35
35
|
<summary>The same output as text</summary>
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
|
-
mendwright demo: a redesign breaks two selectors on a checkout page.
|
|
38
|
+
mendwright demo: a redesign breaks two selectors on a checkout page and adds a cookie banner.
|
|
39
39
|
Offline: a stand-in page (lxml, no browser) and a scripted model reply (no API call).
|
|
40
40
|
|
|
41
|
-
v1 accept_terms #accept-terms
|
|
42
|
-
v1 pay #pay
|
|
43
|
-
-- v2: a redesign ships
|
|
44
|
-
v2
|
|
41
|
+
v1 accept_terms #accept-terms ok matched, box checked
|
|
42
|
+
v1 pay #pay ok matched (not clicked)
|
|
43
|
+
-- v2: a redesign ships, both ids are gone, and a cookie banner opens first --
|
|
44
|
+
v2 cookies #cookie-accept ok banner covered the form, clicked it (heal.interrupt)
|
|
45
|
+
v2 accept_terms #accept-terms MISS no element matches
|
|
45
46
|
model proposed //label[contains(normalize-space(.),'accept the terms')]//input[@type='checkbox'] (scripted reply, no API call)
|
|
46
47
|
checks pass: 1 checkbox in the captured HTML, 1 match on the live page
|
|
47
48
|
retry ok: box checked with the healed selector, heal confirmed
|
|
48
|
-
v2 pay #pay
|
|
49
|
+
v2 pay #pay MISS no element matches
|
|
49
50
|
refused 'pay' is marked destructive=True. Not healed, no model call.
|
|
50
51
|
|
|
51
|
-
Done: 1 heal applied and confirmed, 1 refused, 1 model call in total.
|
|
52
|
+
Done: 1 heal applied and confirmed, 1 refused, 1 interruption handled, 1 model call in total.
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
</details>
|
|
@@ -74,8 +75,9 @@ with sync_playwright() as p:
|
|
|
74
75
|
`heal.locator()` takes the selector you'd pass to `page.locator()`, plus a key: a name for the element that its heals are saved under.
|
|
75
76
|
|
|
76
77
|
`role=` is optional. mendwright needs to know what kind of element it is looking for before it can check a heal, and it learns that the first time the selector matches. Pass `role=` when you add mendwright to a suite whose selectors might already be broken.
|
|
78
|
+
`intent=` describes the element in words, such as `intent="the terms checkbox in the signup form"`. The model gets it with the page, and with `role=` and no selector, a model finds the element on first use.
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
A selector that never matched has no fingerprint, and some changes are beyond one, so to let mendwright ask a model:
|
|
79
81
|
|
|
80
82
|
```bash
|
|
81
83
|
export ANTHROPIC_API_KEY="sk-ant-..."
|
|
@@ -86,11 +88,26 @@ $env:ANTHROPIC_API_KEY = "sk-ant-..."
|
|
|
86
88
|
$env:ANTHROPIC_MODEL = "..."
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
There is no default model. `OPENAI_API_KEY` with `OPENAI_MODEL`, `GEMINI_API_KEY` with `GEMINI_MODEL`, and `OLLAMA_MODEL` work the same way.
|
|
90
|
-
|
|
91
|
-
`mendwright doctor` shows which provider it found. Without one, selectors still resolve, and a broken one raises an error that lists the variables to set.
|
|
91
|
+
There is no default model. `OPENAI_API_KEY` with `OPENAI_MODEL`, `GEMINI_API_KEY` with `GEMINI_MODEL`, and `OLLAMA_MODEL` work the same way. For Bedrock, LiteLLM or another OpenAI-compatible host, pass `provider=` to `mendwright.open()`.
|
|
92
|
+
`mendwright doctor` shows which provider it found. Without one, selectors still resolve and fingerprint heals still happen, and a selector the fingerprint can't place raises an error that lists the variables to set.
|
|
92
93
|
|
|
93
|
-
A selector that stops matching is waited for 10 seconds (`MENDWRIGHT_TIMEOUT_MS`) before mendwright
|
|
94
|
+
A selector that stops matching is waited for 10 seconds (`MENDWRIGHT_TIMEOUT_MS`) before mendwright tries to heal it. Each applied heal prints one line to stderr, and `mendwright report` shows what was saved.
|
|
95
|
+
|
|
96
|
+
## No changes per step
|
|
97
|
+
|
|
98
|
+
To heal an existing script without touching its steps, change the line that makes the page or the driver:
|
|
99
|
+
|
|
100
|
+
<!-- readme-test: wrappers -->
|
|
101
|
+
```python
|
|
102
|
+
page = mendwright.open(page).page # Playwright
|
|
103
|
+
page.fill("#email", "ada@example.com")
|
|
104
|
+
page.check("#terms")
|
|
105
|
+
|
|
106
|
+
driver = mendwright.open(driver).driver # Selenium (By from selenium.webdriver.common.by)
|
|
107
|
+
driver.find_element(By.ID, "terms").click()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each selector is also the key its heals are saved under. Chained locators, `get_by_*` and the other `By` values go to the real page or driver unhealed, and `.native` is the real object. The [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#no-changes-per-step) lists what heals.
|
|
94
111
|
|
|
95
112
|
## Selenium
|
|
96
113
|
|
|
@@ -107,8 +124,7 @@ heal.locator("accept_terms", "//input[@id='terms']", role="checkbox").check()
|
|
|
107
124
|
driver.quit()
|
|
108
125
|
```
|
|
109
126
|
|
|
110
|
-
Playwright verbs such as `fill()`, `check()` and `select_option()` are mapped to WebElement calls, so step code reads the same on both libraries.
|
|
111
|
-
Selenium runs XPath and CSS, so Playwright selectors such as `text=Save` raise `InvalidSelector` at once.
|
|
127
|
+
Playwright verbs such as `fill()`, `check()` and `select_option()` are mapped to WebElement calls, so step code reads the same on both libraries. Selenium runs XPath and CSS, so Playwright selectors such as `text=Save` raise `InvalidSelector` at once.
|
|
112
128
|
|
|
113
129
|
## Async Playwright
|
|
114
130
|
|
|
@@ -150,13 +166,13 @@ To make CI show selector drift instead of quietly healing it, run `pytest --mend
|
|
|
150
166
|
|
|
151
167
|
<picture>
|
|
152
168
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/how-it-works-dark.svg">
|
|
153
|
-
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/how-it-works-light.svg" width="800" alt="How a heal works: a selector misses; a key marked destructive, an unknown role or a
|
|
169
|
+
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/how-it-works-light.svg" width="800" alt="How a heal works: a selector misses; a key marked destructive, an unknown role or a page the key was never found on stops with AttentionRequired before anything is tried; otherwise the page is captured, and an XPath comes from the saved fingerprint when one element matches it with confidence, else from the model within the hourly limit; local checks run; a failed fingerprint pick goes to the model, a failed model pick gets the reason and one more try, and a second failure raises AttentionRequired with nothing saved; a pass is saved and the step retried; if the action then fails the healed selector is quarantined.">
|
|
154
170
|
</picture>
|
|
155
171
|
|
|
156
|
-
A step is refused before
|
|
157
|
-
mendwright
|
|
158
|
-
A rejected attempt counts, and an attempt makes up to 2 model calls.
|
|
159
|
-
|
|
172
|
+
A step is refused before anything is tried when its key is marked destructive, when mendwright doesn't know what kind of element it is (see `role=` above), or when the page isn't one the key was found on. mendwright doesn't guess which controls are destructive. Mark a pay, submit or delete step with `heal.locator(..., destructive=True)`, or pass its key in `mendwright.open(destructive_keys=...)`.
|
|
173
|
+
Next, mendwright scores the page's elements against the key's fingerprint. It takes the best one, with no model call, only when it shares an id, test id, name, link, label or text with the recorded element and no other element comes close.
|
|
174
|
+
Otherwise it sends a cleaned, redacted copy of the page to the model (at most 60,000 characters of HTML and 60,000 of element inventory per request), within an hourly limit of 3 heal attempts per key and 60 in all. A rejected attempt counts, and an attempt makes up to 2 model calls. A gateway meters on its side instead.
|
|
175
|
+
Either way, the XPath is used only if:
|
|
160
176
|
|
|
161
177
|
- it isn't an index-only path such as `/html[1]/body[1]/div[3]/input[1]`
|
|
162
178
|
- the store doesn't hold it as quarantined or superseded for this key, and it doesn't reach an element the key has quarantined
|
|
@@ -164,13 +180,43 @@ Otherwise mendwright sends a cleaned, redacted copy of the page to the model (at
|
|
|
164
180
|
- that element has the step's role (checkbox, button, link, and so on)
|
|
165
181
|
- it matches exactly one element on the live page, and that element is visible (hidden checkboxes are allowed) and still has the role
|
|
166
182
|
|
|
167
|
-
If the first proposal fails,
|
|
183
|
+
If a fingerprint pick fails, the model is asked. If the model's first proposal fails, it gets the reason and one more try. Heals are saved in `.mendwright/overrides.json`, and each request, proposal, rejection and outcome goes to `journal.jsonl` next to it.
|
|
168
184
|
On the next run the selector in your code is tried first and the saved heal right after it, in the same pass. That resolves without a wait or a model call. If the action on a healed element fails in the browser, the heal is quarantined and not used again.
|
|
169
185
|
|
|
186
|
+
## What it can and can't fix
|
|
187
|
+
|
|
188
|
+
mendwright repairs a selector on the page your script is on. It handles layout changes within that page:
|
|
189
|
+
|
|
190
|
+
- ids and classes renamed, or replaced by build-generated ones
|
|
191
|
+
- labels reworded or translated (with a model)
|
|
192
|
+
- elements wrapped in a new container or component
|
|
193
|
+
- elements moved to another part of the page, or form fields put in a different order
|
|
194
|
+
- lookalike elements where the surrounding text decides which one is meant
|
|
195
|
+
|
|
196
|
+
It also handles some changes in your script's flow:
|
|
197
|
+
|
|
198
|
+
- A step that can show up at any point, such as a cookie banner, a newsletter modal or an "are you still there?" dialog. Register it once with `heal.interrupt()`. When it is in the way of a step (the step's element is missing, or the page shows the interruption), mendwright handles it first (a click, unless you register another action) and goes on with the step. If your script also has its own step for it, that step is skipped once the interruption was handled, so a cookie banner that now appears before signup instead of after it needs no other change.
|
|
199
|
+
- The consent banners of common consent managers, with `cookies="reject"` or `cookies="accept"`. This is off by default.
|
|
200
|
+
- A step that may not be there, with `heal.locator(..., optional=True)`.
|
|
201
|
+
|
|
202
|
+
<!-- readme-test: flow -->
|
|
203
|
+
```python
|
|
204
|
+
heal = mendwright.open(page, cookies="reject") # close common consent banners
|
|
205
|
+
heal.interrupt("newsletter", "#newsletter-close") # your own modal, wherever it opens
|
|
206
|
+
|
|
207
|
+
heal.locator("email", "#email").fill("ada@example.com")
|
|
208
|
+
heal.locator("create_account", "#create-account").click()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
A step that moved to another page, steps in a new order, or a new required step still need a change to your script.
|
|
212
|
+
mendwright remembers the last few pages each key was found on and doesn't heal a key on another page, so you get an error that says the flow changed, instead of a heal onto a similar element such as a Help button where a Continue button used to be. If a redesign only changed the URL, or the same steps run under language paths such as `/en/` and `/de/`, pass `heal_across_pages=True` to `mendwright.open()`.
|
|
213
|
+
The model is also told it may answer that no element on the page fits, but a step that moved within the same address is still the model's call, so mark irreversible steps with `destructive=True`. The [guide](https://github.com/saadbuilt/mendwright/blob/main/docs/guide.md#flow-changes) has the details.
|
|
214
|
+
|
|
215
|
+
Anchor selectors on ids, names, labels or text rather than position. A positional selector such as `(//form//input)[1]` that still matches after two fields swap now points at the wrong field, and since nothing broke, mendwright doesn't step in.
|
|
216
|
+
|
|
170
217
|
## Errors
|
|
171
218
|
|
|
172
|
-
When mendwright can't or won't heal a step, it raises `mendwright.AttentionRequired` with the reason and a next step.
|
|
173
|
-
This is the error from `heal.locator("pay", "#pay", destructive=True)` when `#pay` stopped matching:
|
|
219
|
+
When mendwright can't or won't heal a step, it raises `mendwright.AttentionRequired` with the reason and a next step. This is the error from `heal.locator("pay", "#pay", destructive=True)` when `#pay` stopped matching:
|
|
174
220
|
|
|
175
221
|
<!-- readme-test: attention-pay -->
|
|
176
222
|
```text
|
|
@@ -178,7 +224,8 @@ mendwright.exceptions.AttentionRequired: No matching element for: pay (tried 1 c
|
|
|
178
224
|
-> '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.
|
|
179
225
|
```
|
|
180
226
|
|
|
181
|
-
`mendwright doctor` checks the install, the provider and the store path. `mendwright report` lists each key with its selectors and recent heals; `--format md` prints a Markdown table you can append to `$GITHUB_STEP_SUMMARY
|
|
227
|
+
`mendwright doctor` checks the install, the provider and the store path. `mendwright report` lists each key with its selectors and recent heals; `--format md` prints a Markdown table you can append to `$GITHUB_STEP_SUMMARY`, and `--format html --out report.html` writes one self-contained page.
|
|
228
|
+
`mendwright fix` puts confirmed heals back into your code: it prints a diff of the string literals it would change, and `--apply` writes them.
|
|
182
229
|
|
|
183
230
|
## Compatibility
|
|
184
231
|
|
|
@@ -190,26 +237,24 @@ mendwright.exceptions.AttentionRequired: No matching element for: pay (tried 1 c
|
|
|
190
237
|
| Model providers | Anthropic, OpenAI, Azure OpenAI, OpenAI-compatible hosts (Groq, OpenRouter, vLLM, LM Studio), Ollama, Gemini, Bedrock, LiteLLM, or your own gateway |
|
|
191
238
|
| Python | 3.10 and newer, with lxml as the only dependency |
|
|
192
239
|
|
|
193
|
-
The real-browser suite (`python -m pytest -m browser`) passed locally on Windows 11 with Python 3.12 on Playwright Chromium, Firefox and WebKit, patchright, camoufox, and Selenium with Chrome and Firefox. The async tests passed on all three Playwright browsers.
|
|
194
|
-
CI runs the offline suite on Linux (Python 3.10 to 3.14), Windows and macOS, and the browser suite on Linux.
|
|
240
|
+
The real-browser suite (`python -m pytest -m browser`) passed locally on Windows 11 with Python 3.12 on Playwright Chromium, Firefox and WebKit, patchright, camoufox, and Selenium with Chrome and Firefox. The async tests passed on all three Playwright browsers. CI runs the offline suite on Linux (Python 3.10 to 3.14), Windows and macOS, and the browser suite on Linux.
|
|
195
241
|
|
|
196
242
|
## Comparison
|
|
197
243
|
|
|
198
244
|
<picture>
|
|
199
245
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/feature-matrix-dark.svg">
|
|
200
|
-
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/feature-matrix-light.svg" width="800" alt="Feature comparison of mendwright, Healenium, robotframework-heal, Skyvern, Stagehand and auto-playwright across thirteen features, including
|
|
246
|
+
<img src="https://raw.githubusercontent.com/saadbuilt/mendwright/main/docs/assets/feature-matrix-light.svg" width="800" alt="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.">
|
|
201
247
|
</picture>
|
|
202
248
|
|
|
203
|
-
Where another tool fits better: Healenium
|
|
204
|
-
[docs/comparison.md](https://github.com/saadbuilt/mendwright/blob/main/docs/comparison.md) has the full comparison, with a source for every cell and the footprint charts.
|
|
249
|
+
Where another tool fits better: Healenium covers Java, and any Selenium client through its proxy, and its open source edition never calls a model in any setup. Robot Framework suites have robotframework-heal. Agent tools such as Stagehand and Skyvern handle steps written in plain English. [docs/comparison.md](https://github.com/saadbuilt/mendwright/blob/main/docs/comparison.md) has the full comparison, with a source for every cell and the footprint charts.
|
|
205
250
|
|
|
206
251
|
## Limitations
|
|
207
252
|
|
|
208
253
|
- Heals are always XPath, even when your code used CSS or a Playwright selector.
|
|
209
254
|
- An element inside an iframe heals only when you open mendwright on the frame: a `Frame` from `page.frames` on Playwright, or the driver after `switch_to.frame()` on Selenium. On Playwright, a selector that matches only inside a child iframe raises `AttentionRequired` and says so, without a model call.
|
|
210
|
-
- With a hosted provider, page HTML is sent to it. Redaction removes quoted `value` attributes, absolute http(s) URLs and runs of 12 to 19 digits, but visible text and other attributes stay. See [SECURITY.md](https://github.com/saadbuilt/mendwright/blob/main/SECURITY.md). A local model through Ollama keeps the page on your machine.
|
|
255
|
+
- With a hosted provider, page HTML is sent to it whenever a fingerprint match isn't confident. Redaction removes quoted `value` attributes, absolute http(s) URLs and runs of 12 to 19 digits, but visible text and other attributes stay. See [SECURITY.md](https://github.com/saadbuilt/mendwright/blob/main/SECURITY.md). A local model through Ollama keeps the page on your machine.
|
|
211
256
|
- If a page's cleaned HTML is longer than 60,000 characters, the rest is cut before it reaches the model. An element near the end is then visible to it only through the element inventory.
|
|
212
|
-
-
|
|
257
|
+
- Accuracy has been measured only without a model. On the [47-case benchmark](https://github.com/saadbuilt/mendwright/blob/main/docs/benchmark.md), the fingerprint tier alone healed 20 of 35 broken selectors and none onto a wrong element, but its thresholds were tuned on those same cases, so expect it to be wrong now and then on yours. With a model-free similarity baseline behind it, 28 of 35 healed, and 3 of the 47 cases went to the wrong element ([results](https://github.com/saadbuilt/mendwright/blob/main/benchmarks/RESULTS.md)). No model numbers are published yet.
|
|
213
258
|
- `--mendwright-strict` only fails the run that made the heal. Later runs pick up the saved heal and pass, which is why strict CI runs need a fresh store.
|
|
214
259
|
|
|
215
260
|
## Links
|