From a644f0a881962a67b0242624857713f03e17014e Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 1 Jun 2020 18:56:49 -0700 Subject: [PATCH] feat(fill): wait for the element to be enabled/writable/visible (#2435) --- src/dom.ts | 22 +++++--- src/injected/injectedScript.ts | 94 +++++++++++++++++----------------- test/page.spec.js | 49 +++++++++++++----- 3 files changed, 100 insertions(+), 65 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index f49e228a7c..9241b084a0 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -351,13 +351,21 @@ export class ElementHandle extends js.JSHandle { assert(helper.isString(value), 'Value must be string. Found value "' + value + '" of type "' + (typeof value) + '"'); const deadline = this._page._timeoutSettings.computeDeadline(options); await this._page._frameManager.waitForSignalsCreatedBy(async () => { - const injectedResult = await this._evaluateInUtility(({ injected, node }, value) => injected.fill(node, value), value); - const needsInput = handleInjectedResult(injectedResult); - if (needsInput) { - if (value) - await this._page.keyboard.insertText(value); - else - await this._page.keyboard.press('Delete'); + const poll = await this._evaluateHandleInUtility(({ injected, node }, { value }) => { + return injected.waitForEnabledAndFill(node, value); + }, { value }); + try { + const filledPromise = poll.evaluate(poll => poll.result); + const injectedResult = await helper.waitWithDeadline(filledPromise, 'element to be visible and enabled', deadline, 'pw:input'); + const needsInput = handleInjectedResult(injectedResult); + if (needsInput) { + if (value) + await this._page.keyboard.insertText(value); + else + await this._page.keyboard.press('Delete'); + } + } finally { + poll.evaluate(poll => poll.cancel()).catch(e => {}).then(() => poll.dispose()); } }, deadline, options, true); } diff --git a/src/injected/injectedScript.ts b/src/injected/injectedScript.ts index 67a1c3211e..ec2faf3c48 100644 --- a/src/injected/injectedScript.ts +++ b/src/injected/injectedScript.ts @@ -212,53 +212,55 @@ export default class InjectedScript { return { status: 'success', value: options.filter(option => option.selected).map(option => option.value) }; } - fill(node: Node, value: string): types.InjectedScriptResult { - if (node.nodeType !== Node.ELEMENT_NODE) - return { status: 'error', error: 'Node is not of type HTMLElement' }; - const element = node as HTMLElement; - if (!element.isConnected) - return { status: 'notconnected' }; - if (!this.isVisible(element)) - return { status: 'error', error: 'Element is not visible' }; - if (element.nodeName.toLowerCase() === 'input') { - const input = element as HTMLInputElement; - const type = (input.getAttribute('type') || '').toLowerCase(); - const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']); - const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']); - if (!kTextInputTypes.has(type) && !kDateTypes.has(type)) - return { status: 'error', error: 'Cannot fill input of type "' + type + '".' }; - if (type === 'number') { - value = value.trim(); - if (isNaN(Number(value))) - return { status: 'error', error: 'Cannot type text into input[type=number].' }; + waitForEnabledAndFill(node: Node, value: string): types.InjectedScriptPoll> { + return this.poll('raf', () => { + if (node.nodeType !== Node.ELEMENT_NODE) + return { status: 'error', error: 'Node is not of type HTMLElement' }; + const element = node as HTMLElement; + if (!element.isConnected) + return { status: 'notconnected' }; + if (!this.isVisible(element)) + return false; + if (element.nodeName.toLowerCase() === 'input') { + const input = element as HTMLInputElement; + const type = (input.getAttribute('type') || '').toLowerCase(); + const kDateTypes = new Set(['date', 'time', 'datetime', 'datetime-local']); + const kTextInputTypes = new Set(['', 'email', 'number', 'password', 'search', 'tel', 'text', 'url']); + if (!kTextInputTypes.has(type) && !kDateTypes.has(type)) + return { status: 'error', error: 'Cannot fill input of type "' + type + '".' }; + if (type === 'number') { + value = value.trim(); + if (isNaN(Number(value))) + return { status: 'error', error: 'Cannot type text into input[type=number].' }; + } + if (input.disabled) + return false; + if (input.readOnly) + return false; + if (kDateTypes.has(type)) { + value = value.trim(); + input.focus(); + input.value = value; + if (input.value !== value) + return { status: 'error', error: `Malformed ${type} "${value}"` }; + element.dispatchEvent(new Event('input', { 'bubbles': true })); + element.dispatchEvent(new Event('change', { 'bubbles': true })); + return { status: 'success', value: false }; // We have already changed the value, no need to input it. + } + } else if (element.nodeName.toLowerCase() === 'textarea') { + const textarea = element as HTMLTextAreaElement; + if (textarea.disabled) + return false; + if (textarea.readOnly) + return false; + } else if (!element.isContentEditable) { + return { status: 'error', error: 'Element is not an ,