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:
parent
678d16454a
commit
0f0e2acfaf
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue