feat(fill): option to wait for the element to become enabled before filling
This commit is contained in:
parent
2b40361b0a
commit
7b415ef698
|
|
@ -2,7 +2,7 @@
|
||||||
<!-- GEN:test-stats -->
|
<!-- GEN:test-stats -->
|
||||||
|Firefox|Chromium|WebKit|all|
|
|Firefox|Chromium|WebKit|all|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|508/632|683/690|325/635|309/632|
|
|506/634|685/692|351/637|322/634|
|
||||||
<!-- GEN:stop -->
|
<!-- GEN:stop -->
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
|
||||||
18
docs/api.md
18
docs/api.md
|
|
@ -108,7 +108,7 @@
|
||||||
* [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args)
|
* [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args)
|
||||||
* [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args)
|
* [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args)
|
||||||
* [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction)
|
* [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.focus(selector)](#pagefocusselector)
|
||||||
* [page.frames()](#pageframes)
|
* [page.frames()](#pageframes)
|
||||||
* [page.geolocation](#pagegeolocation)
|
* [page.geolocation](#pagegeolocation)
|
||||||
|
|
@ -211,7 +211,7 @@
|
||||||
* [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args)
|
* [frame.evaluate(pageFunction[, ...args])](#frameevaluatepagefunction-args)
|
||||||
* [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args)
|
* [frame.evaluateHandle(pageFunction[, ...args])](#frameevaluatehandlepagefunction-args)
|
||||||
* [frame.executionContext()](#frameexecutioncontext)
|
* [frame.executionContext()](#frameexecutioncontext)
|
||||||
* [frame.fill(selector, value)](#framefillselector-value)
|
* [frame.fill(selector, value, [options])](#framefillselector-value-options)
|
||||||
* [frame.focus(selector)](#framefocusselector)
|
* [frame.focus(selector)](#framefocusselector)
|
||||||
* [frame.goto(url[, options])](#framegotourl-options)
|
* [frame.goto(url[, options])](#framegotourl-options)
|
||||||
* [frame.hover(selector[, options])](#framehoverselector-options)
|
* [frame.hover(selector[, options])](#framehoverselector-options)
|
||||||
|
|
@ -257,7 +257,7 @@
|
||||||
* [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args)
|
* [elementHandle.evaluate(pageFunction[, ...args])](#elementhandleevaluatepagefunction-args)
|
||||||
* [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args)
|
* [elementHandle.evaluateHandle(pageFunction[, ...args])](#elementhandleevaluatehandlepagefunction-args)
|
||||||
* [elementHandle.executionContext()](#elementhandleexecutioncontext)
|
* [elementHandle.executionContext()](#elementhandleexecutioncontext)
|
||||||
* [elementHandle.fill(value)](#elementhandlefillvalue)
|
* [elementHandle.fill(value[, options])](#elementhandlefillvalue-options)
|
||||||
* [elementHandle.focus()](#elementhandlefocus)
|
* [elementHandle.focus()](#elementhandlefocus)
|
||||||
* [elementHandle.getProperties()](#elementhandlegetproperties)
|
* [elementHandle.getProperties()](#elementhandlegetproperties)
|
||||||
* [elementHandle.getProperty(propertyName)](#elementhandlegetpropertypropertyname)
|
* [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.
|
- `selector` <[string]> A [selector] to query page for.
|
||||||
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
|
- `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`.
|
- 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.
|
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.
|
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.
|
- `selector` <[string]> A [selector] to query page for.
|
||||||
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
|
- `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`.
|
- 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.
|
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()
|
#### elementHandle.executionContext()
|
||||||
- returns: <[ExecutionContext]>
|
- returns: <[ExecutionContext]>
|
||||||
|
|
||||||
#### elementHandle.fill(value)
|
#### elementHandle.fill(value[, options])
|
||||||
- `value` <[string]> Value to set for the `<input>`, `<textarea>` or `[contenteditable]` element.
|
- `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.
|
- returns: <[Promise]> Promise which resolves when the element is successfully filled.
|
||||||
|
|
||||||
This method focuses the element and triggers an `input` event after filling.
|
This method focuses the element and triggers an `input` event after filling.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { ExecutionContext } from './ExecutionContext';
|
||||||
import { Frame } from './Frame';
|
import { Frame } from './Frame';
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager } from './FrameManager';
|
||||||
import { assert, helper } from '../helper';
|
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 { LifecycleWatcher } from './LifecycleWatcher';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
const readFileAsync = helper.promisify(fs.readFile);
|
const readFileAsync = helper.promisify(fs.readFile);
|
||||||
|
|
@ -301,10 +301,10 @@ export class DOMWorld {
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fill(selector: string, value: string) {
|
async fill(selector: string, value: string, options?: FillOptions) {
|
||||||
const handle = await this.$(selector);
|
const handle = await this.$(selector);
|
||||||
assert(handle, 'No node found for selector: ' + selector);
|
assert(handle, 'No node found for selector: ' + selector);
|
||||||
await handle.fill(value);
|
await handle.fill(value, options);
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import { CDPSession } from './Connection';
|
||||||
import { DOMWorld } from './DOMWorld';
|
import { DOMWorld } from './DOMWorld';
|
||||||
import { ExecutionContext } from './ExecutionContext';
|
import { ExecutionContext } from './ExecutionContext';
|
||||||
import { FrameManager } from './FrameManager';
|
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 { Response } from './NetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
|
||||||
|
|
@ -152,8 +152,8 @@ export class Frame {
|
||||||
return this._secondaryWorld.tripleclick(selector, options);
|
return this._secondaryWorld.tripleclick(selector, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fill(selector: string, value: string) {
|
async fill(selector: string, value: string, options?: FillOptions) {
|
||||||
return this._secondaryWorld.fill(selector, value);
|
return this._secondaryWorld.fill(selector, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async focus(selector: string) {
|
async focus(selector: string) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { valueFromRemoteObject, releaseObject } from './protocolHelper';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { Modifier, Button } from './Input';
|
import { Modifier, Button } from './Input';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import { TimeoutError } from '../Errors';
|
||||||
|
|
||||||
type Point = {
|
type Point = {
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -47,6 +48,10 @@ export type MultiClickOptions = PointerActionOptions & {
|
||||||
button?: Button;
|
button?: Button;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FillOptions = {
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
|
export function createJSHandle(context: ExecutionContext, remoteObject: Protocol.Runtime.RemoteObject) {
|
||||||
const frame = context.frame();
|
const frame = context.frame();
|
||||||
if (remoteObject.subtype === 'node' && frame) {
|
if (remoteObject.subtype === 'node' && frame) {
|
||||||
|
|
@ -337,8 +342,22 @@ export class ElementHandle extends JSHandle {
|
||||||
}, values);
|
}, 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) + '"');
|
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) => {
|
const error = await this.evaluate((element: HTMLElement) => {
|
||||||
if (element.nodeType !== Node.ELEMENT_NODE)
|
if (element.nodeType !== Node.ELEMENT_NODE)
|
||||||
return 'Node is not of type HTMLElement';
|
return 'Node is not of type HTMLElement';
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import { PDF } from './features/pdf';
|
||||||
import { Frame } from './Frame';
|
import { Frame } from './Frame';
|
||||||
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
import { FrameManager, FrameManagerEvents } from './FrameManager';
|
||||||
import { Keyboard, Mouse } from './Input';
|
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 { NetworkManagerEvents, Response } from './NetworkManager';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
|
import { getExceptionMessage, releaseObject, valueFromRemoteObject } from './protocolHelper';
|
||||||
|
|
@ -707,8 +707,8 @@ export class Page extends EventEmitter {
|
||||||
return this.mainFrame().tripleclick(selector, options);
|
return this.mainFrame().tripleclick(selector, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
fill(selector: string, value: string) {
|
fill(selector: string, value: string, options?: FillOptions) {
|
||||||
return this.mainFrame().fill(selector, value);
|
return this.mainFrame().fill(selector, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
focus(selector: string) {
|
focus(selector: string) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<textarea></textarea>
|
<textarea></textarea>
|
||||||
<input></input>
|
<input></input>
|
||||||
<div contenteditable="true"></div>
|
<div contenteditable="true"></div>
|
||||||
|
<span>text</span>
|
||||||
<script src='mouse-helper.js'></script>
|
<script src='mouse-helper.js'></script>
|
||||||
<script>
|
<script>
|
||||||
window.result = '';
|
window.result = '';
|
||||||
|
|
|
||||||
|
|
@ -1048,6 +1048,43 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
|
||||||
await page.fill('textarea', 123).catch(e => error = e);
|
await page.fill('textarea', 123).catch(e => error = e);
|
||||||
expect(error.message).toContain('Value must be string.');
|
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
|
// FIXME: WebKit shouldn't send targetDestroyed on PSON so that we could
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue