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'); + }); +});