From f689a71a782ebb6edd05952e946b7710aa653780 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 28 Sep 2026 09:43:14 -0700 Subject: [PATCH 1/3] fix(webkit): use native checkVisibility for element visibility WebKit's checkVisibility now handles closed details and unslotted content, so the manual workaround is only kept for browsers without it. This fixes content of a details nested in a closed details being reported as visible in WebKit. --- packages/injected/src/domUtils.ts | 19 +++------------- packages/injected/src/injectedScript.ts | 3 +-- packages/injected/src/roleUtils.ts | 2 +- tests/page/locator-is-visible.spec.ts | 30 +++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts index c88184506a424..6d3999f141762 100644 --- a/packages/injected/src/domUtils.ts +++ b/packages/injected/src/domUtils.ts @@ -14,17 +14,6 @@ * limitations under the License. */ -type GlobalOptions = { - browserNameForWorkarounds?: string; -}; -let globalOptions: GlobalOptions = {}; -export function setGlobalOptions(options: GlobalOptions) { - globalOptions = options; -} -export function getGlobalOptions(): GlobalOptions { - return globalOptions; -} - export function isInsideScope(scope: Node, element: Element | undefined): boolean { while (element) { if (scope.contains(element)) @@ -100,14 +89,12 @@ function computeElementStyleVisibilityVisible(element: Element, style?: CSSStyle // Element.checkVisibility checks for content-visibility and also looks at // styles up the flat tree including user-agent ShadowRoots, such as the // details element for example. - // All the browser implement it, but WebKit has a bug which prevents us from using it: - // https://bugs.webkit.org/show_bug.cgi?id=264733 - // @ts-ignore - if (Element.prototype.checkVisibility && globalOptions.browserNameForWorkarounds !== 'webkit') { + // @ts-ignore Older Electron versions do not have checkVisibility. + if (Element.prototype.checkVisibility) { if (!element.checkVisibility()) return false; } else { - // Manual workaround for WebKit that does not have checkVisibility. + // Manual workaround for browsers that do not have checkVisibility. const detailsOrSummary = element.closest('details,summary'); if (detailsOrSummary !== element && detailsOrSummary?.nodeName === 'DETAILS' && !(detailsOrSummary as HTMLDetailsElement).open) return false; diff --git a/packages/injected/src/injectedScript.ts b/packages/injected/src/injectedScript.ts index d020ee1c126a8..395cd630e4804 100644 --- a/packages/injected/src/injectedScript.ts +++ b/packages/injected/src/injectedScript.ts @@ -22,7 +22,7 @@ import { parseAttributeSelector, parseSelector, stringifySelector, visitAllSelec import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '@isomorphic/stringUtils'; import { generateAriaTree, getAllElementsMatchingExpectAriaTemplate, matchesExpectAriaTemplate, renderAriaTreeAsJSON, findNewElement } from './ariaSnapshot'; -import { beginDOMCaches, enclosingShadowRootOrDocument, endDOMCaches, isElementVisible, isInsideScope, parentElementOrShadowHost, setGlobalOptions } from './domUtils'; +import { beginDOMCaches, enclosingShadowRootOrDocument, endDOMCaches, isElementVisible, isInsideScope, parentElementOrShadowHost } from './domUtils'; import { Highlight } from './highlight'; import { kLayoutSelectorNames, layoutSelectorScore } from './layoutSelectorUtils'; import { createRoleEngine } from './roleSelectorEngine'; @@ -252,7 +252,6 @@ export class InjectedScript { this._browserName = options.browserName; this._shouldPrependErrorPrefix = !!options.shouldPrependErrorPrefix; this._isUtilityWorld = !!options.isUtilityWorld; - setGlobalOptions({ browserNameForWorkarounds: options.browserName }); this._setupGlobalListenersRemovalDetection(); this._setupHitTargetInterceptors(); diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index 6216b45b04c77..945b1e33b7b8e 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -341,7 +341,7 @@ function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element): boolean // When parent has a shadow root, all light dom children must be assigned to a slot, // otherwise they are not rendered and considered hidden for aria. - // Note: we can remove this logic once WebKit supports `Element.checkVisibility`. + // Note: needed for older Electron versions that do not have `Element.checkVisibility`. if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot) hidden = true; diff --git a/tests/page/locator-is-visible.spec.ts b/tests/page/locator-is-visible.spec.ts index 4349f3b3d8625..8fc5be7c5b8f9 100644 --- a/tests/page/locator-is-visible.spec.ts +++ b/tests/page/locator-is-visible.spec.ts @@ -60,6 +60,36 @@ it('isVisible and isHidden should work with details', async ({ page }) => { await expect(page.locator('ul')).toBeHidden(); }); +it('isVisible and isHidden should work with nested details', async ({ page }) => { + await page.setContent(`
+ outer +
+ inner + +
+
`); + + await expect(page.locator('summary', { hasText: 'outer' })).toBeVisible(); + await expect(page.locator('summary', { hasText: 'inner' })).toBeHidden(); + await expect(page.locator('button')).toBeHidden(); + await expect(page.getByRole('button')).toHaveCount(0); +}); + +it('isVisible and isHidden should work with unslotted content', async ({ page }) => { + await page.setContent(` +
hidden 1
hidden 2
+
visible
+ + `); + + await expect(page.locator('span', { hasText: 'hidden 1' })).toBeHidden(); + await expect(page.locator('span', { hasText: 'hidden 2' })).toBeHidden(); + await expect(page.locator('span', { hasText: 'visible' })).toBeVisible(); +}); + it('isVisible inside a button', async ({ page }) => { await page.setContent(``); const span = page.locator('span'); From b28147236ce3db071bc6ec6a3b536e880a175623 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 28 Sep 2026 10:32:49 -0700 Subject: [PATCH 2/3] chore: require Element.checkVisibility for visibility checks Drop the manual details/summary fallback for browsers without checkVisibility. Minimum supported versions are now Electron 21 and Chrome 105 on Android. --- docs/src/electron-api/class-electron.md | 4 +--- docs/src/mobile-api/class-android.md | 2 +- packages/injected/src/domUtils.ts | 12 ++---------- packages/injected/src/roleUtils.ts | 2 +- packages/playwright-client/types/types.d.ts | 6 ++---- packages/playwright-core/types/types.d.ts | 6 ++---- 6 files changed, 9 insertions(+), 23 deletions(-) diff --git a/docs/src/electron-api/class-electron.md b/docs/src/electron-api/class-electron.md index 60e64080e0551..e95c1edb1b6a1 100644 --- a/docs/src/electron-api/class-electron.md +++ b/docs/src/electron-api/class-electron.md @@ -41,9 +41,7 @@ const { _electron: electron } = require('playwright'); ``` **Supported Electron versions are:** -* v12.2.0+ -* v13.4.0+ -* v14+ +* v21+ **Known issues:** diff --git a/docs/src/mobile-api/class-android.md b/docs/src/mobile-api/class-android.md index 4427622b6af51..68305f4b7f748 100644 --- a/docs/src/mobile-api/class-android.md +++ b/docs/src/mobile-api/class-android.md @@ -7,7 +7,7 @@ Playwright has **experimental** support for Android automation. This includes Ch *Requirements* * Android device or AVD Emulator. * [ADB daemon](https://developer.android.com/studio/command-line/adb) running and authenticated with your device. Typically running `adb devices` is all you need to do. -* [`Chrome 87`](https://play.google.com/store/apps/details?id=com.android.chrome) or newer installed on the device +* [`Chrome 105`](https://play.google.com/store/apps/details?id=com.android.chrome) or newer installed on the device * "Enable command line on non-rooted devices" enabled in `chrome://flags`. *Known limitations* diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts index 6d3999f141762..3d7f4ac3c4c83 100644 --- a/packages/injected/src/domUtils.ts +++ b/packages/injected/src/domUtils.ts @@ -89,16 +89,8 @@ function computeElementStyleVisibilityVisible(element: Element, style?: CSSStyle // Element.checkVisibility checks for content-visibility and also looks at // styles up the flat tree including user-agent ShadowRoots, such as the // details element for example. - // @ts-ignore Older Electron versions do not have checkVisibility. - if (Element.prototype.checkVisibility) { - if (!element.checkVisibility()) - return false; - } else { - // Manual workaround for browsers that do not have checkVisibility. - const detailsOrSummary = element.closest('details,summary'); - if (detailsOrSummary !== element && detailsOrSummary?.nodeName === 'DETAILS' && !(detailsOrSummary as HTMLDetailsElement).open) - return false; - } + if (!element.checkVisibility()) + return false; if (style.visibility !== 'visible') return false; return true; diff --git a/packages/injected/src/roleUtils.ts b/packages/injected/src/roleUtils.ts index 945b1e33b7b8e..c5a07b896b685 100644 --- a/packages/injected/src/roleUtils.ts +++ b/packages/injected/src/roleUtils.ts @@ -341,7 +341,7 @@ function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element: Element): boolean // When parent has a shadow root, all light dom children must be assigned to a slot, // otherwise they are not rendered and considered hidden for aria. - // Note: needed for older Electron versions that do not have `Element.checkVisibility`. + // Note: checkVisibility is skipped for