fix: page.locator.focus() and page.locator(…).type(…) (#14267)

Fixes focus and blur management when `page.locator(…).focus()`  and  `page.locator(…).type(…)` are used which was regressed by 7a5b070 (#13510).

#13510 relied on an implicit assumption that this (conditional) [`blur`](7a5b070e95/packages/playwright-core/src/server/injected/injectedScript.ts (L672)) call would always be followed by a call that resulted in a newly focused element via this [`focus`](7a5b070e95/packages/playwright-core/src/server/injected/injectedScript.ts (L674)) call.

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 7a5b070e95 (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.
This commit is contained in:
Ross Wollman 2022-05-19 14:31:56 -07:00 committed by GitHub
parent 9a73dfe773
commit fbb364c1cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 121 additions and 1 deletions

View File

@ -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();

View File

@ -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(`
<div id="focusable" tabindex="0">focusable</div>
<div id="non-focusable">not focusable</div>
<script>
window.eventLog = [];
const focusable = document.getElementById("focusable");
focusable.addEventListener('blur', () => window.eventLog.push('blur focusable'));
focusable.addEventListener('focus', () => window.eventLog.push('focus focusable'));
const nonFocusable = document.getElementById("non-focusable");
nonFocusable.addEventListener('blur', () => window.eventLog.push('blur non-focusable'));
nonFocusable.addEventListener('focus', () => window.eventLog.push('focus non-focusable'));
</script>
`);
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',
]);
});

View File

@ -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(`
<html>
<body>
<shadow-element></shadow-element>
<script>
customElements.define('shadow-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = \`
<style>
.editor { padding: 1rem; margin: 1rem; border: 1px solid #ccc; }
</style>
<div class=editor contenteditable id=foo><p>hello</p></div>
<hr>
<section>
<div class=editor contenteditable id=bar><p>world</p></div>
</section>
\`;
}
});
</script>
</body>
</html>
`);
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(`
<html>
<body>
<shadow-element></shadow-element>
<script>
customElements.define('shadow-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = \`
<style>
.editor { padding: 1rem; margin: 1rem; border: 1px solid #ccc; }
</style>
<input class=editor id=foo>
<hr>
<section>
<input class=editor id=bar>
</section>
\`;
}
});
</script>
</body>
</html>
`);
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(`
<div id="focusable" tabindex="0">focusable div</div>
<div id="non-focusable-and-non-editable">non-editable, non-focusable</div>
`);
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 = {