From abd4258080a2d15cb3e8b3de1202ad147e4159d2 Mon Sep 17 00:00:00 2001 From: Ross Wollman Date: Thu, 19 May 2022 15:19:39 -0700 Subject: [PATCH] =?UTF-8?q?chery-pick(#14267):=20fix:=20page.locator.focus?= =?UTF-8?q?()=20and=20page.locator(=E2=80=A6).type(=E2=80=A6)=20(#14296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherry-pick of fbb364c1cd0dec9e4a75b885d51e67ca397b1d6a: Fixes focus and blur management when `page.locator(…).focus()` and `page.locator(…).type(…)` are used which was regressed by 7a5b070 (#13510). However, some elements are [not focusable](https://html.spec.whatwg.org/multipage/interaction.html#focusable-area), so we were blurring incorrectly, and losing focus that we should have maintained. Two regression tests were added that pass on the commit prior to 7a5b070e9507b622877a2af010373585f2184196 (and match manual testing/expectations): * `page.locator(…).focus()`: _keeps focus on element when attempting to focus a non-focusable element_ * `page.locator(…).type(…)`: _should type repeatedly in input in shadow dom_ Additionally, a third test (_should type repeatedly in input in shadow dom_) was added to check the invariant from #13510 that states: > This affects [contenteditable] elements, but not input elements. and allows us to introduce the targeted fix (contenteditble check before blur) without breaking FF again. And _should type repeatedly in contenteditable in shadow dom with nested elements_ was added to ensure the above fix works with nest contenteditble detection. Fixes #14254. --- .../src/server/injected/injectedScript.ts | 2 +- tests/page/page-focus.spec.ts | 28 ++++++ tests/page/page-keyboard.spec.ts | 92 +++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 6aa6ac1e52d98..749ff91d11f2a 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -674,7 +674,7 @@ export class InjectedScript { const activeElement = (node.getRootNode() as (Document | ShadowRoot)).activeElement; const wasFocused = activeElement === node && node.ownerDocument && node.ownerDocument.hasFocus(); - if (!wasFocused && activeElement && (activeElement as HTMLElement | SVGElement).blur) { + if ((node as HTMLElement).isContentEditable && !wasFocused && activeElement && (activeElement as HTMLElement | SVGElement).blur) { // Workaround the Firefox bug where focusing the element does not switch current // contenteditable to the new element. However, blurring the previous one helps. (activeElement as HTMLElement | SVGElement).blur(); diff --git a/tests/page/page-focus.spec.ts b/tests/page/page-focus.spec.ts index 5fcc7c6ad8ed9..23c49bd8de6aa 100644 --- a/tests/page/page-focus.spec.ts +++ b/tests/page/page-focus.spec.ts @@ -117,3 +117,31 @@ it('clicking checkbox should activate it', async ({ page, browserName, headless, const nodeName = await page.evaluate(() => document.activeElement.nodeName); expect(nodeName).toBe('INPUT'); }); + +it('keeps focus on element when attempting to focus a non-focusable element', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/14254' }); + + await page.setContent(` +
focusable
+
not focusable
+ + `); + await page.locator('#focusable').click(); + expect.soft(await page.evaluate(() => document.activeElement?.id)).toBe('focusable'); + await page.locator('#non-focusable').focus(); + expect.soft(await page.evaluate(() => document.activeElement?.id)).toBe('focusable'); + expect.soft(await page.evaluate(() => window['eventLog'])).toEqual([ + 'focus focusable', + ]); +}); diff --git a/tests/page/page-keyboard.spec.ts b/tests/page/page-keyboard.spec.ts index 32a4134ca3e7c..1a510da2d23bb 100644 --- a/tests/page/page-keyboard.spec.ts +++ b/tests/page/page-keyboard.spec.ts @@ -532,6 +532,98 @@ it('should type repeatedly in contenteditable in shadow dom', async ({ page }) = expect(await sectionEditor.textContent()).toBe('This is the second box.'); }); +it('should type repeatedly in contenteditable in shadow dom with nested elements', async ({ page }) => { + await page.setContent(` + + + + + + + `); + + const editor = page.locator('shadow-element > .editor').first(); + await editor.type('This is the first box: '); + + const sectionEditor = page.locator('section .editor'); + await sectionEditor.type('This is the second box: '); + + expect(await editor.textContent()).toBe('This is the first box: hello'); + expect(await sectionEditor.textContent()).toBe('This is the second box: world'); +}); + +it('should type repeatedly in input in shadow dom', async ({ page }) => { + await page.setContent(` + + + + + + + `); + + const editor = page.locator('shadow-element > .editor').first(); + await editor.type('This is the first box.'); + + const sectionEditor = page.locator('section .editor'); + await sectionEditor.type('This is the second box.'); + + expect(await editor.inputValue()).toBe('This is the first box.'); + expect(await sectionEditor.inputValue()).toBe('This is the second box.'); +}); + +it('type to non-focusable element should maintain old focus', async ({ page }) => { + await page.setContent(` +
focusable div
+
non-editable, non-focusable
+ `); + + await page.locator('#focusable').focus(); + expect(await page.evaluate(() => document.activeElement?.id)).toBe('focusable'); + await page.locator('#non-focusable-and-non-editable').type('foo'); + expect(await page.evaluate(() => document.activeElement?.id)).toBe('focusable'); +}); + async function captureLastKeydown(page) { const lastEvent = await page.evaluateHandle(() => { const lastEvent = {