feat(fill): option to wait for the element to become enabled before filling

This commit is contained in:
Dmitry Gozman 2019-11-21 09:56:56 -08:00
parent 2b40361b0a
commit 7b415ef698
8 changed files with 80 additions and 17 deletions

View file

@ -2,7 +2,7 @@
<!-- GEN:test-stats -->
|Firefox|Chromium|WebKit|all|
|---|---|---|---|
|508/632|683/690|325/635|309/632|
|506/634|685/692|351/637|322/634|
<!-- GEN:stop -->
# Contributing

View file

@ -108,7 +108,7 @@
* [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args)
* [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args)
* [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction)
* [page.fill(selector, value)](#pagefillselector-value)
* [page.fill(selector, value, [options])](#pagefillselector-value-options)
* [page.focus(selector)](#pagefocusselector)
* [page.frames()](#pageframes)
* [page.geolocation](#pagegeolocation)
@ -211,7 +211,7 @@
* [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args)
* [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args)
* [frame.executionContext()](#frameexecutioncontext)
* [frame.fill(selector, value)](#framefillselector-value)
* [frame.fill(selector, value, [options])](#framefillselector-value-options)
* [frame.focus(selector)](#framefocusselector)
* [frame.goto(url[, options])](#framegotourl-options)
* [frame.hover(selector[, options])](#framehoverselector-options)
@ -257,7 +257,7 @@
* [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args)
* [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args)
* [elementHandle.executionContext()](#elementhandleexecutioncontext)
* [elementHandle.fill(value)](#elementhandlefillvalue)
* [elementHandle.fill(value[, options])](#elementhandlefillvalue-options)
* [elementHandle.focus()](#elementhandlefocus)
* [elementHandle.getProperties()](#elementhandlegetproperties)
* [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname)
@ -1512,9 +1512,11 @@ const fs = require('fs');
})();
```
#### page.fill(selector, value)
#### page.fill(selector, value, [options])
- `selector` <[string]> A [selector] to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]>
- `enabled` <[boolean]> Whether to wait for the element to become enabled before filling the value. Defaults to `false`.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully filled. The promise will be rejected if there is no element matching `selector`.
This method focuses the element and triggers an `input` event after filling.
@ -2796,9 +2798,11 @@ await resultHandle.dispose();
Returns promise that resolves to the frame's default execution context.
#### frame.fill(selector, value)
#### frame.fill(selector, value, [options])
- `selector` <[string]> A [selector] to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]>
- `enabled` <[boolean]> Whether to wait for the element to become enabled before filling the value. Defaults to `false`.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully filled. The promise will be rejected if there is no element matching `selector`.
This method focuses the element and triggers an `input` event after filling.
@ -3395,8 +3399,10 @@ See [Page.evaluateHandle](#pageevaluatehandlepagefunction-args) for more details
#### elementHandle.executionContext()
- returns: <[ExecutionContext]>
#### elementHandle.fill(value)
#### elementHandle.fill(value[, options])
- `value` <[string]> Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]>
- `enabled` <[boolean]> Whether to wait for the element to become enabled before filling the value. Defaults to `false`.
- returns: <[Promise]> Promise which resolves when the element is successfully filled.
This method focuses the element and triggers an `input` event after filling.

View file

@ -21,7 +21,7 @@ import { ExecutionContext } from './ExecutionContext';
import { Frame } from './Frame';
import { FrameManager } from './FrameManager';
import { assert, helper } from '../helper';
import { ElementHandle, JSHandle, ClickOptions, PointerActionOptions, MultiClickOptions } from './JSHandle';
import { ElementHandle, JSHandle, ClickOptions, PointerActionOptions, MultiClickOptions, FillOptions } from './JSHandle';
import { LifecycleWatcher } from './LifecycleWatcher';
import { TimeoutSettings } from '../TimeoutSettings';
const readFileAsync = helper.promisify(fs.readFile);
@ -301,10 +301,10 @@ export class DOMWorld {
await handle.dispose();
}
async fill(selector: string, value: string) {
async fill(selector: string, value: string, options?: FillOptions) {
const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector);
await handle.fill(value);
await handle.fill(value, options);
await handle.dispose();
}

View file

@ -20,7 +20,7 @@ import { CDPSession } from './Connection';
import { DOMWorld } from './DOMWorld';
import { ExecutionContext } from './ExecutionContext';
import { FrameManager } from './FrameManager';
import { ClickOptions, ElementHandle, JSHandle, MultiClickOptions, PointerActionOptions } from './JSHandle';
import { ClickOptions, ElementHandle, JSHandle, MultiClickOptions, PointerActionOptions, FillOptions } from './JSHandle';
import { Response } from './NetworkManager';
import { Protocol } from './protocol';
@ -152,8 +152,8 @@ export class Frame {
return this._secondaryWorld.tripleclick(selector, options);
}
async fill(selector: string, value: string) {
return this._secondaryWorld.fill(selector, value);
async fill(selector: string, value: string, options?: FillOptions) {
return this._secondaryWorld.fill(selector, value, options);
}
async focus(selector: string) {

View file

@ -25,6 +25,7 @@ import { valueFromRemoteObject, releaseObject } from './protocolHelper';
import { Page } from './Page';
import { Modifier, Button } from './Input';
import { Protocol } from './protocol';
import { TimeoutError } from '../Errors';
type Point = {
x: number;
@ -47,6 +48,10 @@ export type MultiClickOptions = PointerActionOptions & {
button?: Button;
};
export type FillOptions = {
enabled?: boolean;
};
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
@ -337,8 +342,22 @@ export class ElementHandle extends JSHandle {
}, values);
}
async fill(value: string): Promise<void> {
async fill(value: string, options?: FillOptions): Promise<void> {
const { enabled = false } = options || {};
assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"');
if (enabled) {
try {
await this._context._world.waitForFunction((node: Node) => {
return node.nodeType !== Node.ELEMENT_NODE ? true : !(node as Element).hasAttribute('disabled');
}, { polling: 'mutation' }, this);
} catch (e) {
if (e instanceof TimeoutError)
e.message = 'Timed out while waiting for the element to be enabled';
throw e;
}
}
const error = await this.evaluate((element: HTMLElement) => {
if (element.nodeType !== Node.ELEMENT_NODE)
return 'Node is not of type HTMLElement';

View file

@ -33,7 +33,7 @@ import { PDF } from './features/pdf';
import { Frame } from './Frame';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { Keyboard, Mouse } from './Input';
import { ClickOptions, createJSHandle, ElementHandle, JSHandle, MultiClickOptions, PointerActionOptions } from './JSHandle';
import { ClickOptions, createJSHandle, ElementHandle, JSHandle, MultiClickOptions, PointerActionOptions, FillOptions } from './JSHandle';
import { NetworkManagerEvents, Response } from './NetworkManager';
import { Protocol } from './protocol';
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
@ -707,8 +707,8 @@ export class Page extends EventEmitter {
return this.mainFrame().tripleclick(selector, options);
}
fill(selector: string, value: string) {
return this.mainFrame().fill(selector, value);
fill(selector: string, value: string, options?: FillOptions) {
return this.mainFrame().fill(selector, value, options);
}
focus(selector: string) {

View file

@ -7,6 +7,7 @@
<textarea></textarea>
<input></input>
<div contenteditable="true"></div>
<span>text</span>
<script src='mouse-helper.js'></script>
<script>
window.result = '';

View file

@ -1048,6 +1048,43 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
await page.fill('textarea', 123).catch(e => error = e);
expect(error.message).toContain('Value must be string.');
});
it('should not wait for input to be enabled by default', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.evaluate(() => document.querySelector('input').setAttribute('disabled', 'true'));
await page.fill('input', 'some value');
expect(await page.evaluate(() => result)).toBe('');
});
it('should wait for input to be enabled', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.evaluate(() => document.querySelector('input').setAttribute('disabled', 'true'));
let filled = false;
const fillPromise = page.fill('input', 'some value', { enabled: true }).then(() => filled = true);
for (let i = 0; i < 10; i++) {
await page.evaluate(value => document.querySelector('input').setAttribute('foo', value), String(i));
expect(await page.evaluate(() => result)).toBe('');
expect(filled).toBe(false);
}
await Promise.all([
fillPromise,
page.evaluate(() => document.querySelector('input').removeAttribute('disabled')),
]);
expect(await page.evaluate(() => result)).toBe('some value');
});
it('should timeout for disabled input', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.evaluate(() => document.querySelector('input').setAttribute('disabled', 'true'));
let error = null;
page.setDefaultTimeout(1);
await page.fill('input', 'some value', { enabled: true }).catch(e => error = e);
expect(error.message).toBe('Timed out while waiting for the element to be enabled');
});
it('should throw for non-input when waiting for enabled', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
const handle = await page.evaluateHandle(() => document.querySelector('span').firstChild);
let error = null;
await handle.fill('', { enabled: true }).catch(e => error = e);
expect(error.message).toContain('Node is not of type HTMLElement');
});
});
// FIXME: WebKit shouldn't send targetDestroyed on PSON so that we could