From 0f0e2acfafbc393c7a3042795522c5fc95a01bd8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 24 Jul 2020 09:30:31 -0700 Subject: [PATCH] fix(type): unify selection behavior when typing (#3141) Before typing/pressing, we focus the target element. WebKit sometimes selects the value in this case. To unify the behavior between the browsers we behave similar to human: - when the input is already focused, we just type; - when the input is not focused, we focus it, move caret to the start (like if user clicked at the start to focus the input) and then type. Note this only affects inputs with non-empty value. --- src/dom.ts | 8 ++-- src/injected/injectedScript.ts | 12 +++++- test/elementhandle.jest.js | 76 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index 51096ef74c..e79261ec84 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -519,9 +519,9 @@ export class ElementHandle extends js.JSHandle { }, 0, 'elementHandle.focus'); } - async _focus(progress: Progress): Promise<'error:notconnected' | 'done'> { + async _focus(progress: Progress, resetSelectionIfNotFocused?: boolean): Promise<'error:notconnected' | 'done'> { progress.throwIfAborted(); // Avoid action that has side-effects. - const result = await this._evaluateInUtility(([injected, node]) => injected.focusNode(node), {}); + const result = await this._evaluateInUtility(([injected, node, resetSelectionIfNotFocused]) => injected.focusNode(node, resetSelectionIfNotFocused), resetSelectionIfNotFocused); return throwFatalDOMError(result); } @@ -535,7 +535,7 @@ export class ElementHandle extends js.JSHandle { async _type(progress: Progress, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { progress.logger.info(`elementHandle.type("${text}")`); return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { - const result = await this._focus(progress); + const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') return result; progress.throwIfAborted(); // Avoid action that has side-effects. @@ -554,7 +554,7 @@ export class ElementHandle extends js.JSHandle { async _press(progress: Progress, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { progress.logger.info(`elementHandle.press("${key}")`); return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { - const result = await this._focus(progress); + const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); if (result !== 'done') return result; progress.throwIfAborted(); // Avoid action that has side-effects. diff --git a/src/injected/injectedScript.ts b/src/injected/injectedScript.ts index 591169514d..e6756eef68 100644 --- a/src/injected/injectedScript.ts +++ b/src/injected/injectedScript.ts @@ -357,12 +357,22 @@ export default class InjectedScript { }); } - focusNode(node: Node): FatalDOMError | 'error:notconnected' | 'done' { + focusNode(node: Node, resetSelectionIfNotFocused?: boolean): FatalDOMError | 'error:notconnected' | 'done' { if (!node.isConnected) return 'error:notconnected'; if (node.nodeType !== Node.ELEMENT_NODE) return 'error:notelement'; + const wasFocused = (node.getRootNode() as (Document | ShadowRoot)).activeElement === node && node.ownerDocument && node.ownerDocument.hasFocus(); (node as HTMLElement | SVGElement).focus(); + + if (resetSelectionIfNotFocused && !wasFocused && node.nodeName.toLowerCase() === 'input') { + try { + const input = node as HTMLInputElement; + input.setSelectionRange(0, 0); + } catch (e) { + // Some inputs do not allow selection. + } + } return 'done'; } diff --git a/test/elementhandle.jest.js b/test/elementhandle.jest.js index 57137195ee..f58482df54 100644 --- a/test/elementhandle.jest.js +++ b/test/elementhandle.jest.js @@ -610,3 +610,79 @@ describe('ElementHandle.focus', function() { expect(await button.evaluate(button => document.activeElement === button)).toBe(true); }); }); + +describe('ElementHandle.type', function() { + it('should work', async ({page}) => { + await page.setContent(``); + await page.type('input', 'hello'); + expect(await page.$eval('input', input => input.value)).toBe('hello'); + }); + it('should not select existing value', async ({page}) => { + await page.setContent(``); + await page.type('input', 'world'); + expect(await page.$eval('input', input => input.value)).toBe('worldhello'); + }); + it('should reset selection when not focused', async ({page}) => { + await page.setContent(`
text
`); + await page.$eval('input', input => { + input.selectionStart = 2; + input.selectionEnd = 4; + document.querySelector('div').focus(); + }); + await page.type('input', 'world'); + expect(await page.$eval('input', input => input.value)).toBe('worldhello'); + }); + it('should not modify selection when focused', async ({page}) => { + await page.setContent(``); + await page.$eval('input', input => { + input.focus(); + input.selectionStart = 2; + input.selectionEnd = 4; + }); + await page.type('input', 'world'); + expect(await page.$eval('input', input => input.value)).toBe('heworldo'); + }); + it('should work with number input', async ({page}) => { + await page.setContent(``); + await page.type('input', '13'); + expect(await page.$eval('input', input => input.value)).toBe('132'); + }); +}); + +describe('ElementHandle.press', function() { + it('should work', async ({page}) => { + await page.setContent(``); + await page.press('input', 'h'); + expect(await page.$eval('input', input => input.value)).toBe('h'); + }); + it('should not select existing value', async ({page}) => { + await page.setContent(``); + await page.press('input', 'w'); + expect(await page.$eval('input', input => input.value)).toBe('whello'); + }); + it('should reset selection when not focused', async ({page}) => { + await page.setContent(`
text
`); + await page.$eval('input', input => { + input.selectionStart = 2; + input.selectionEnd = 4; + document.querySelector('div').focus(); + }); + await page.press('input', 'w'); + expect(await page.$eval('input', input => input.value)).toBe('whello'); + }); + it('should not modify selection when focused', async ({page}) => { + await page.setContent(``); + await page.$eval('input', input => { + input.focus(); + input.selectionStart = 2; + input.selectionEnd = 4; + }); + await page.press('input', 'w'); + expect(await page.$eval('input', input => input.value)).toBe('hewo'); + }); + it('should work with number input', async ({page}) => { + await page.setContent(``); + await page.press('input', '1'); + expect(await page.$eval('input', input => input.value)).toBe('12'); + }); +});