Skip to content

feat: explain element not found errors in all web helpers - #5798

Open
DavertMik wants to merge 1 commit into
4.xfrom
feat/playwright-explain-not-found
Open

DavertMik wants to merge 1 commit into
4.xfrom
feat/playwright-explain-not-found

Conversation

@DavertMik

@DavertMik DavertMik commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

What / why

When a step fails with ElementNotFound, the error now says what the page has instead. This works in every web helper: Playwright, Puppeteer, WebDriver and CDPBrowser. Lightpanda, Obscura, Kitesurf and Moli inherit it from CDPBrowser.

Hints are collected only after a lookup has already failed, so passing steps cost nothing.

Hints, in order (lines with nothing to say are left out):

  1. A match inside a same-origin iframe, when the step ran on the top-level page. Suggests within({ frame }).
  2. Matches outside the current element within() context.
  3. Hidden-only matches when visibleLocator is in effect. Only Playwright has that option.
  4. Up to 3 similar elements:
    • fuzzy text and role locators, plus XPath with a quoted string: element names (role plus aria-label, label, text, value or placeholder)
    • CSS #id / .class locators: page ids or class names

The first line of the message doesn't change. Hints are appended as indented lines.

Design

  • lib/helper/extras/NotFoundExplainer.js is the one shared module:

    • NotFoundExplainer.collect is a plain in-page function. It returns the match count, hidden count, same-origin iframe counts, ids, classes and element names.
    • explain() races the work against a 300 ms timer that is cleared in finally. Any error yields [], so the original error is never masked.
    • format() ranks the candidates with fuse.js (searchWithFusejs) and builds the hint lines.
  • Each helper has a small _explainNotFound(locator) that runs the collector through its own raw evaluate in the current document and passes its own within() and visibleLocator state:

    helper runs the collector with
    Playwright frame.evaluate
    Puppeteer frame.evaluate
    WebDriver browser.execute
    CDPBrowser Runtime.evaluate

    None of them goes through I.* or the recorder.

  • In Playwright, Puppeteer and WebDriver, the module-level assertElementExists became the async helper method _assertElementExists, and every call site awaits it. Direct throw new ElementNotFound(...) sites pass hints too.

  • ElementNotFound takes an optional 4th hints argument (string[]).

  • The option is explainNotFound (boolean, default true), with the same JSDoc @prop in each helper's config.

  • It's documented once, in docs/locators.md ("When an element is not found").

What each helper supports

iframe hint outside within() hidden (visibleLocator) similar ids/classes/names
Playwright yes yes yes yes
Puppeteer yes yes n/a (no visibleLocator) yes
WebDriver yes yes n/a yes
CDPBrowser (+ Lightpanda/Obscura/Kitesurf/Moli) no: CDPBrowser has no switchTo/frame support, so within({ frame }) can't be suggested yes n/a yes

Appium extends WebDriver, but its own _validateConfig doesn't merge WebDriver defaults. explainNotFound is therefore off for Appium unless it is set explicitly, and native apps are unaffected.

Example output

Clickable element "#first-link" was not found by text|CSS|XPath
  found 1 match inside iframe "content" — use within({ frame: 'iframe[name="content"]' })
Field "#emial" was not found by text|CSS|XPath
  similar ids: #email, #email-label, #email-wrapper
Clickable element "Sign im" was not found by text|CSS|XPath
  similar on page: link "Sign in!"

Config

helpers: {
  Playwright: { explainNotFound: false }, // default true; same option in Puppeteer, WebDriver, CDPBrowser
}

Measured cost

Measured with local headless chromium. Each figure is the median of 9 calls to _explainNotFound on a warm page, in ms:

page Playwright text / #id / plain css Puppeteer text / #id / plain css
/form/example4 2.0 / 1.3 / 0.9 1.0 / 0.8 / 0.5
/form/field 1.1 / 1.0 / 0.9 0.5 / 0.4 / 0.4
/login 1.2 / 1.2 / 0.9 0.7 / 0.8 / 0.6

A whole failing fillField('#emial') step in Playwright takes about 1 ms with hints off and about 2 ms with hints on. That is far below the 300 ms cap, so the plan's "skip ranking under autoRetries" fallback isn't needed.

Tests

  • Shared #explainNotFound cases in test/helper/webapi.js:

    • iframe match
    • similar ids
    • similar names
    • outside within()
    • disabled leaves the message unchanged

    The iframe case has a documented skip for CDPBrowser (no iframe support).

  • Playwright-only case (hidden matches with visibleLocator) in test/helper/Playwright_test.js.

  • hints case in test/unit/helper/element_not_found_test.js.

  • Ran locally, each with --grep explainNotFound:

    spec result
    Playwright 6 passing
    Puppeteer 5 passing
    CDPBrowser chrome 4 passing, 1 pending
    Lightpanda 4 passing, 1 pending
    Obscura 4 passing, 1 pending
  • Regression runs:

    • Playwright --grep "explainNotFound|#fillField|#click|within|iframe|visibleLocator|switchTo|Frame": 148 passing.
    • Puppeteer --grep "switchTo|within|#focus": 41 passing.
  • npm run def, npm run dtslint and npx eslint on the changed files.

  • WebDriver runs in CI only (it needs Selenium).

🤖 Generated with Claude Code

@DavertMik DavertMik changed the title feat(playwright): explain element not found errors feat: explain element not found errors in all web helpers Nov 13, 2026
ElementNotFound becomes an Error subclass that explains itself. Right
after it is created it runs one script in the page through the active
web helper's executeScript and appends hints to its message and stack:
a match inside a same-origin iframe (with the within({ frame }) call),
matches excluded by within() or hidden, and up to 3 similar ids, classes
or element names. explain() returns the pending work for callers that
need to wait, and MCP run_code awaits it.

No helper, hook or core change: works for Playwright, Puppeteer,
WebDriver, CDPBrowser and their subclasses as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DavertMik
DavertMik force-pushed the feat/playwright-explain-not-found branch from 06ec8c7 to 4197668 Compare November 13, 2026 10:15

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant