diff --git a/packages/injected/src/domUtils.ts b/packages/injected/src/domUtils.ts
index c88184506a424..4fa51ef387efb 100644
--- a/packages/injected/src/domUtils.ts
+++ b/packages/injected/src/domUtils.ts
@@ -108,9 +108,12 @@ function computeElementStyleVisibilityVisible(element: Element, style?: CSSStyle
return false;
} else {
// Manual workaround for WebKit that does not have checkVisibility.
- const detailsOrSummary = element.closest('details,summary');
- if (detailsOrSummary !== element && detailsOrSummary?.nodeName === 'DETAILS' && !(detailsOrSummary as HTMLDetailsElement).open)
- return false;
+ // Check all ancestors: an open details inside a closed one is still hidden.
+ for (let child: Element | undefined = element; child; child = parentElementOrShadowHost(child)) {
+ const parent = parentElementOrShadowHost(child);
+ if (parent?.nodeName === 'DETAILS' && !(parent as HTMLDetailsElement).open && child.nodeName !== 'SUMMARY')
+ return false;
+ }
}
if (style.visibility !== 'visible')
return false;
diff --git a/tests/page/locator-is-visible.spec.ts b/tests/page/locator-is-visible.spec.ts
index 4349f3b3d8625..1773e63437835 100644
--- a/tests/page/locator-is-visible.spec.ts
+++ b/tests/page/locator-is-visible.spec.ts
@@ -60,6 +60,21 @@ 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 inside a button', async ({ page }) => {
await page.setContent(``);
const span = page.locator('span');