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 -->
|
||||
|Firefox|Chromium|WebKit|all|
|
||||
|---|---|---|---|
|
||||
|508/632|683/690|325/635|309/632|
|
||||
|506/634|685/692|351/637|322/634|
|
||||
<!-- GEN:stop -->
|
||||
|
||||
# Contributing
|
||||
|
|
|
|||
18
docs/api.md
18
docs/api.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<textarea></textarea>
|
||||
<input></input>
|
||||
<div contenteditable="true"></div>
|
||||
<span>text</span>
|
||||
<script src='mouse-helper.js'></script>
|
||||
<script>
|
||||
window.result = '';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue