Skip to content

Pick the right element by its label when a click matches several - #162

Open
DavertMik wants to merge 1 commit into
mainfrom
fix/click-disambiguation-labels
Open

Pick the right element by its label when a click matches several#162
DavertMik wants to merge 1 commit into
mainfrom
fix/click-disambiguation-labels

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

When I.click("New test") matches more than one element, CodeceptJS raises MultipleElementsFound and we ask the model which candidate to click. Each candidate was described by its markup alone — and that markup came from toSimplifiedHTML(), which strips non-interactive descendants. A button whose label lives in child spans arrives as an empty shell:

Element 1:
XPath: //html/body/div[3]/div[3]/div/ul/li[3]/button
HTML: <button type="button"><span class="content inline-flex items-center w-full"></span></button>

Element 2:
XPath: //html/body/div[3]/div[3]/div/ul/li[4]/div/button
HTML: <button class="  " type="button"><span class="content flex items-center w-full"></span></button>

Those are a New test menu item and a New tests from requirement menu item. They are indistinguishable, so the choice is a guess — and when it guesses wrong the click still succeeds, the wrong screen opens, and the step is reported as passed. Observed twice in one run, picking the same wrong element both times, even after xpathCheck had already proven a unique locator for the right one.

The utility classes surviving while the label is destroyed is exactly backwards for this purpose.

What changed

  • Each candidate now carries its visible text (getText()), whitespace-collapsed via normalizeInlineText and capped at 80 characters.
  • Markup now comes from the element's own toOuterHTML() cleaned through cleanHtmlSnippet() instead of the upstream simplifier, so nothing nested is discarded.
  • cleanHtmlSnippet() is a new export in src/utils/html.ts: parseFragment → the existing cleanAllElementsserialize. It reuses the project's single class-filter policy (TAILWIND_CLASS_PATTERNS, digit classes, attribute whitelist) rather than adding a fourth copy of that chain.
  • Cleaning runs before truncation, so the 300-character budget buys signal instead of Tailwind.
  • One line in cleanElement: .filter(Boolean) after the class split, so a whitespace-only class=" " drops out instead of surviving as class=" ".
  • formatElementList() replaces the two near-identical map expressions that were drifting between the model-facing prompt and the failure message shown to the Tester.

Result

Element 1:
Text: "New test"
XPath: //html/body/div[3]/div[3]/div/ul/li[3]/button
HTML: <button type="button"><span class="content"><span class="badge badge-type hidden-type manual"><svg class="md-icon md-icon-file-document-outline"></svg></span><span>New test</span></span></button>

Element 2:
Text: "New tests from requirement"
XPath: //html/body/div[3]/div[3]/div/ul/li[4]/div/button
HTML: <button type="button"><span class="content"><svg class="md-icon md-icon-briefcase-check-outline"></svg><span>New tests from requirement</span></span></button>

badge-type manual survives because it says something about which button this is; inline-flex items-center gap-3 w-full does not.

Tests

tests/unit/matched-elements.test.ts — 5 tests: label present when the markup would otherwise be stripped, whitespace collapse, the 80-character cap, utility classes dropped while meaningful ones survive, and the fallback when the error carries no elements.

Full unit suite: 1125 pass, 0 fail — cleanElement is shared, so that was the check that mattered.

An upstream fix for simplifyHtmlElement erasing an element's accessible name is being raised separately against CodeceptJS; this change does not depend on it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JbhzmY1M51pyRysUx32MdP

A click that matched more than one element asked the model to choose
between them, but described each candidate with markup that had been
stripped of everything nested inside it. A button whose label lives in
child spans arrived as an empty shell, so two menu items reading
"New test" and "New tests from requirement" were indistinguishable and
the pick was a guess that opened the wrong screen and reported success.

Each candidate now carries its own visible text, whitespace collapsed
and capped, and its markup comes from the element's own outer HTML
cleaned through the project's class filter rather than from an upstream
simplifier that removes non-interactive descendants. Labels and
meaningful class names survive; layout and generated styling classes do
not, so the 300-character budget is spent on signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbhzmY1M51pyRysUx32MdP
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