You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(locator): prevent click(text, container) from matching the container itself (#5532)
`I.click("Description", 'ul[role="tablist"]')` was clicking the <ul> itself
instead of the intended <li role="tab">. The fallback xpath
`./self::*[contains(normalize-space(string(.)), literal)]` matched the scope
element whenever its concatenated string-value contained the literal — and a
container with text-bearing children (tab labels, menu items) always will.
Playwright then clicked the container's geometric center, landing on whatever
child sat there.
Fix: narrow `Locator.clickable.self` to prefer the deepest descendant whose
string-value contains the literal, and only match self when self *is* that
deepest element (or its @value matches, preserving the input case).
Also extend `Locator.clickable.wide` with ARIA widget roles so clicks hit
the semantic element directly rather than relying on bubble-up:
tab, link, menuitem (+ checkbox/radio variants), option, treeitem.
Tests:
- test/data/app/view/form/tablist.php — ember-like 5-tab fixture with
a #selected-tab marker driven by click handlers.
- test/helper/Playwright_test.js — regression scenario clicks History →
Description → Runs → Code template against ul[role="tablist"] and
asserts the correct tab fired. Plus an explicit <a>+span case scoped
by tag 'a' to document that behavior.
- test/unit/locator_test.js — three xpath-level tests for clickable.self
(tablist narrowest-match, <div>Submit</div> self-match, <input value>
self-match) and two for clickable.wide (role="tab", role="menuitem").
Co-authored-by: DavertMik <davert@testomat.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`.//*[@role='tab' or @role='link' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='treeitem'][contains(normalize-space(string(.)), ${literal})]`,
594
595
]),
595
596
596
597
/**
597
598
* @param {string} literal
598
599
* @returns {string}
599
600
*/
600
-
self: literal=>`./self::*[contains(normalize-space(string(.)), ${literal}) or contains(normalize-space(@value), ${literal})]`,
601
+
self: literal=>{
602
+
// Narrowest-match: prefer the deepest descendant whose string-value contains the literal.
603
+
// Falling back to `self` without the `not(descendant...)` guard would match a container
604
+
// whose concatenated text happens to include the literal (e.g. a <ul role="tablist"> whose
605
+
// tab labels all sit in its string-value) and click the container itself.
606
+
constnarrowest=`contains(normalize-space(string(.)), ${literal}) and not(.//*[contains(normalize-space(string(.)), ${literal})])`
607
+
returnxpathLocator.combine([
608
+
`.//*[${narrowest}]`,
609
+
`./self::*[${narrowest} or contains(normalize-space(@value), ${literal})]`,
0 commit comments