diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 939988c9a..674b708c0 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -50,6 +50,7 @@ let defaultSelectorEnginesInitialized = false const popupStore = new Popup() const consoleLogStore = new Console() const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron'] +const checkableRoles = ['checkbox', 'radio', 'switch'] import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js' import { createValueEngine, createDisabledEngine } from './extras/PlaywrightPropEngine.js' @@ -4385,6 +4386,17 @@ async function findCheckable(locator, context) { return findElements.call(this, contextEl, matchedLocator) } + for (const exact of [true, false]) { + for (const role of checkableRoles) { + try { + const roleEls = await contextEl.getByRole(role, { name: matchedLocator.value, exact }).all() + if (roleEls.length) return roleEls + } catch (err) { + // getByRole not supported or failed + } + } + } + const literal = xpathLocator.literal(matchedLocator.value) let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal)) if (els.length) { diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index ff00f6dd8..4d9c709ba 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -64,6 +64,7 @@ function wrapError(e) { let perfTiming const popupStore = new Popup() const consoleLogStore = new Console() +const checkableRoles = ['checkbox', 'radio', 'switch'] /** * ## Configuration @@ -3192,8 +3193,19 @@ async function findCheckable(locator, context) { return findElements.call(this, contextEl, matchedLocator) } + // Try ARIA selector for accessible name + let els + for (const role of checkableRoles) { + try { + els = await contextEl.$$(`::-p-aria([name="${matchedLocator.value}"][role="${role}"])`) + if (els.length) return els + } catch (err) { + // ARIA selector not supported or failed + } + } + const literal = xpathLocator.literal(matchedLocator.value) - let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal)) + els = await findElements.call(this, contextEl, Locator.checkable.byText(literal)) if (els.length) { return els } @@ -3202,14 +3214,6 @@ async function findCheckable(locator, context) { return els } - // Try ARIA selector for accessible name - try { - els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`) - if (els.length) return els - } catch (err) { - // ARIA selector not supported or failed - } - return findElements.call(this, contextEl, matchedLocator.value) } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 62c0b4dc2..32f6a0d8d 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -3245,24 +3245,39 @@ async function findCheckable(locator, locateFn) { if (locator.isRole()) return locateFn(locator, true) if (!locator.isFuzzy()) return locateFn(locator, true) - const literal = xpathLocator.literal(locator.value) - els = await locateFn(Locator.checkable.byText(literal)) - if (els.length) return els - // Try ARIA selector for accessible name try { - els = await locateFn(`aria/${locator.value}`) + els = await keepCheckable.call(this, await locateFn(`aria/${locator.value}`)) if (els.length) return els } catch (e) { // ARIA selector not supported or failed } + const literal = xpathLocator.literal(locator.value) + els = await locateFn(Locator.checkable.byText(literal)) + if (els.length) return els + els = await locateFn(Locator.checkable.byName(literal)) if (els.length) return els return await locateFn(locator.value) // by css or xpath } +async function keepCheckable(els) { + if (!els || !els.length) return [] + + const checkable = await this.browser.execute(function () { + return Array.prototype.slice.call(arguments).map(function (el) { + if (!el) return false + const role = el.getAttribute('role') + if (role) return ['checkbox', 'radio', 'switch'].indexOf(role) > -1 + return el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio') + }) + }, ...els) + + return els.filter((el, index) => checkable[index]) +} + function withStrictLocator(locator) { locator = new Locator(locator) return locator.simplify() diff --git a/test/data/app/view/form/checkable/baseui.php b/test/data/app/view/form/checkable/baseui.php new file mode 100644 index 000000000..c06ad5258 --- /dev/null +++ b/test/data/app/view/form/checkable/baseui.php @@ -0,0 +1,58 @@ + + + + + Base UI Checkables + + + + +

Base UI Checkables

+
+ + + diff --git a/test/data/app/view/form/checkable/collision.php b/test/data/app/view/form/checkable/collision.php new file mode 100644 index 000000000..eaeffbb45 --- /dev/null +++ b/test/data/app/view/form/checkable/collision.php @@ -0,0 +1,15 @@ + + + + + Checkable name collision + + +

Accept terms

+
+ + + +
+ + diff --git a/test/data/app/view/form/checkable/radix.php b/test/data/app/view/form/checkable/radix.php new file mode 100644 index 000000000..357948e12 --- /dev/null +++ b/test/data/app/view/form/checkable/radix.php @@ -0,0 +1,55 @@ + + + + + Radix Checkables + + + + +

Radix Checkables

+
+ + + diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 40b51c3c3..a0577f33e 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -514,6 +514,85 @@ export function tests() { }) }) + describe('#checkOption - ARIA roles', function () { + this.timeout(60000) + + async function open(page) { + await I.amOnPage(`/form/checkable/${page}`) + await I.waitForFunction(() => window.__ready === true, [], 30) + } + + async function ariaChecked(css) { + return I.grabAttributeFrom(css, 'aria-checked') + } + + for (const page of ['radix', 'baseui']) { + describe(page, () => { + beforeEach(function () { + // webdriverio resolves `