From 8d2b979d5e52dbbbfe9f5a338c341335588a53c6 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Wed, 13 May 2026 00:10:24 -0700 Subject: [PATCH] fix: traverse shadow roots in toHaveFocus --- src/__tests__/to-have-focus.js | 33 +++++++++++++++++++++++++++++++++ src/to-have-focus.js | 16 ++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/__tests__/to-have-focus.js b/src/__tests__/to-have-focus.js index 8acb8127..8e87fb40 100644 --- a/src/__tests__/to-have-focus.js +++ b/src/__tests__/to-have-focus.js @@ -21,3 +21,36 @@ test('.toHaveFocus', () => { expect(() => expect(focused).not.toHaveFocus()).toThrowError() expect(() => expect(notFocused).toHaveFocus()).toThrowError() }) + +test('.toHaveFocus with element in shadow root', () => { + const host = document.createElement('div') + const shadowRoot = host.attachShadow({mode: 'open'}) + const input = document.createElement('input') + shadowRoot.appendChild(input) + document.body.innerHTML = '' + document.body.appendChild(host) + + input.focus() + + expect(input).toHaveFocus() + expect(host).not.toHaveFocus() + expect(() => expect(input).not.toHaveFocus()).toThrowError() +}) + +test('.toHaveFocus with element in nested shadow root', () => { + const outerHost = document.createElement('div') + const outerShadow = outerHost.attachShadow({mode: 'open'}) + const innerHost = document.createElement('div') + outerShadow.appendChild(innerHost) + const innerShadow = innerHost.attachShadow({mode: 'open'}) + const input = document.createElement('input') + innerShadow.appendChild(input) + document.body.innerHTML = '' + document.body.appendChild(outerHost) + + input.focus() + + expect(input).toHaveFocus() + expect(outerHost).not.toHaveFocus() + expect(innerHost).not.toHaveFocus() +}) diff --git a/src/to-have-focus.js b/src/to-have-focus.js index a792645f..a7f1768b 100644 --- a/src/to-have-focus.js +++ b/src/to-have-focus.js @@ -1,10 +1,20 @@ import {checkHtmlElement} from './utils' +function getDeepActiveElement(doc) { + let active = doc.activeElement + while (active && active.shadowRoot && active.shadowRoot.activeElement) { + active = active.shadowRoot.activeElement + } + return active +} + export function toHaveFocus(element) { checkHtmlElement(element, toHaveFocus, this) + const activeElement = getDeepActiveElement(element.ownerDocument) + return { - pass: element.ownerDocument.activeElement === element, + pass: activeElement === element, message: () => { return [ this.utils.matcherHint( @@ -22,9 +32,7 @@ export function toHaveFocus(element) { 'Expected element with focus:', ` ${this.utils.printExpected(element)}`, 'Received element with focus:', - ` ${this.utils.printReceived( - element.ownerDocument.activeElement, - )}`, + ` ${this.utils.printReceived(activeElement)}`, ]), ].join('\n') },