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.
This commit is contained in:
Dmitry Gozman 2020-07-24 09:30:31 -07:00 committed by GitHub
parent 678d16454a
commit 0f0e2acfaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 5 deletions

View file

@ -519,9 +519,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, 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<T extends Node = Node> extends js.JSHandle<T> {
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<T extends Node = Node> extends js.JSHandle<T> {
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.

View file

@ -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';
}

View file

@ -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(`<input type='text' />`);
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(`<input type='text' value='hello' />`);
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(`<input type='text' value='hello' /><div tabIndex=2>text</div>`);
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(`<input type='text' value='hello' />`);
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(`<input type='number' value=2 />`);
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(`<input type='text' />`);
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(`<input type='text' value='hello' />`);
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(`<input type='text' value='hello' /><div tabIndex=2>text</div>`);
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(`<input type='text' value='hello' />`);
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(`<input type='number' value=2 />`);
await page.press('input', '1');
expect(await page.$eval('input', input => input.value)).toBe('12');
});
});