diff --git a/src/server/common/domErrors.ts b/src/server/common/domErrors.ts index 0b1c1ad26ca77..693713ac632b4 100644 --- a/src/server/common/domErrors.ts +++ b/src/server/common/domErrors.ts @@ -22,6 +22,7 @@ export type FatalDOMError = 'error:notfillablenumberinput' | 'error:notvaliddate' | 'error:notinput' | - 'error:notselect'; + 'error:notselect' | + 'error:notcheckbox'; export type RetargetableDOMError = 'error:notconnected'; diff --git a/src/server/dom.ts b/src/server/dom.ts index 9717542881d03..93418397c502f 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -16,7 +16,7 @@ import * as frames from './frames'; import { assert } from '../utils/utils'; -import type { InjectedScript, InjectedScriptPoll } from './injected/injectedScript'; +import type { ElementStateWithoutStable, InjectedScript, InjectedScriptPoll } from './injected/injectedScript'; import * as injectedScriptSource from '../generated/injectedScriptSource'; import * as js from './javascript'; import { Page } from './page'; @@ -85,9 +85,11 @@ export class FrameExecutionContext extends js.ExecutionContext { const source = ` (() => { ${injectedScriptSource.source} - return new pwExport([ - ${custom.join(',\n')} - ]); + return new pwExport( + ${this.frame._page._delegate.rafCountForStablePosition()}, + ${!!process.env.PW_USE_TIMEOUT_FOR_RAF}, + [${custom.join(',\n')}] + ); })(); `; this._injectedScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId)); @@ -451,12 +453,14 @@ export class ElementHandle extends js.JSHandle { } async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.NavigatingActionWaitOptions): Promise { - const selectOptions = [...elements, ...values]; + const optionsToSelect = [...elements, ...values]; return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { progress.throwIfAborted(); // Avoid action that has side-effects. progress.log(' selecting specified option(s)'); await progress.checkpoint('before'); - const poll = await this._evaluateHandleInUtility(([injected, node, selectOptions]) => injected.waitForOptionsAndSelect(node, selectOptions), selectOptions); + const poll = await this._evaluateHandleInUtility(([injected, node, optionsToSelect]) => { + return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], injected.selectOptions.bind(injected, optionsToSelect)); + }, optionsToSelect); const pollHandler = new InjectedScriptPollHandler(progress, poll); const result = throwFatalDOMError(await pollHandler.finish()); await this._page._doSlowMo(); @@ -477,7 +481,7 @@ export class ElementHandle extends js.JSHandle { return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { progress.log(' waiting for element to be visible, enabled and editable'); const poll = await this._evaluateHandleInUtility(([injected, node, value]) => { - return injected.waitForEnabledAndFill(node, value); + return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled', 'editable'], injected.fill.bind(injected, value)); }, value); const pollHandler = new InjectedScriptPollHandler(progress, poll); const filled = throwFatalDOMError(await pollHandler.finish()); @@ -504,7 +508,7 @@ export class ElementHandle extends js.JSHandle { return controller.run(async progress => { progress.throwIfAborted(); // Avoid action that has side-effects. const poll = await this._evaluateHandleInUtility(([injected, node]) => { - return injected.waitForVisibleAndSelectText(node); + return injected.waitForElementStatesAndPerformAction(node, ['visible'], injected.selectText.bind(injected)); }, {}); const pollHandler = new InjectedScriptPollHandler(progress, poll); const result = throwFatalDOMError(await pollHandler.finish()); @@ -615,13 +619,17 @@ export class ElementHandle extends js.JSHandle { } async _setChecked(progress: Progress, state: boolean, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { - if (await this._evaluateInUtility(([injected, node]) => injected.isCheckboxChecked(node), {}) === state) + const isChecked = async () => { + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); + }; + if (await isChecked() === state) return 'done'; const result = await this._click(progress, options); if (result !== 'done') return result; - if (await this._evaluateInUtility(([injected, node]) => injected.isCheckboxChecked(node), {}) !== state) - throw new Error('Unable to click checkbox'); + if (await isChecked() !== state) + throw new Error('Clicking the checkbox did not change its state'); return 'done'; } @@ -661,94 +669,44 @@ export class ElementHandle extends js.JSHandle { } async isVisible(): Promise { - return this._evaluateInUtility(([injected, node]) => { - const element = node.nodeType === Node.ELEMENT_NODE ? node as Node as Element : node.parentElement; - return element ? injected.isVisible(element) : false; - }, {}); + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'visible'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); } async isHidden(): Promise { - return !(await this.isVisible()); + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'hidden'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); } async isEnabled(): Promise { - return !(await this.isDisabled()); + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'enabled'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); } async isDisabled(): Promise { - return this._evaluateInUtility(([injected, node]) => { - const element = node.nodeType === Node.ELEMENT_NODE ? node as Node as Element : node.parentElement; - return element ? injected.isElementDisabled(element) : false; - }, {}); + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'disabled'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); } async isEditable(): Promise { - return this._evaluateInUtility(([injected, node]) => { - const element = node.nodeType === Node.ELEMENT_NODE ? node as Node as Element : node.parentElement; - return element ? !injected.isElementDisabled(element) && !injected.isElementReadOnly(element) : false; - }, {}); + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'editable'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); } async isChecked(): Promise { - return this._evaluateInUtility(([injected, node]) => { - return injected.isCheckboxChecked(node); - }, {}); + const result = await this._evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {}); + return throwRetargetableDOMError(throwFatalDOMError(result)); } async waitForElementState(metadata: CallMetadata, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable', options: types.TimeoutOptions = {}): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { progress.log(` waiting for element to be ${state}`); - if (state === 'visible') { - const poll = await this._evaluateHandleInUtility(([injected, node]) => { - return injected.waitForNodeVisible(node); - }, {}); - const pollHandler = new InjectedScriptPollHandler(progress, poll); - assertDone(throwRetargetableDOMError(await pollHandler.finish())); - return; - } - if (state === 'hidden') { - const poll = await this._evaluateHandleInUtility(([injected, node]) => { - return injected.waitForNodeHidden(node); - }, {}); - const pollHandler = new InjectedScriptPollHandler(progress, poll); - assertDone(await pollHandler.finish()); - return; - } - if (state === 'enabled') { - const poll = await this._evaluateHandleInUtility(([injected, node]) => { - return injected.waitForNodeEnabled(node); - }, {}); - const pollHandler = new InjectedScriptPollHandler(progress, poll); - assertDone(throwRetargetableDOMError(await pollHandler.finish())); - return; - } - if (state === 'disabled') { - const poll = await this._evaluateHandleInUtility(([injected, node]) => { - return injected.waitForNodeDisabled(node); - }, {}); - const pollHandler = new InjectedScriptPollHandler(progress, poll); - assertDone(throwRetargetableDOMError(await pollHandler.finish())); - return; - } - if (state === 'editable') { - const poll = await this._evaluateHandleInUtility(([injected, node]) => { - return injected.waitForNodeEnabled(node, true /* waitForEnabled */); - }, {}); - const pollHandler = new InjectedScriptPollHandler(progress, poll); - assertDone(throwRetargetableDOMError(await pollHandler.finish())); - return; - } - if (state === 'stable') { - const rafCount = this._page._delegate.rafCountForStablePosition(); - const poll = await this._evaluateHandleInUtility(([injected, node, rafOptions]) => { - return injected.waitForDisplayedAtStablePosition(node, rafOptions, false /* waitForEnabled */); - }, { rafCount, useTimeout: !!process.env.PW_USE_TIMEOUT_FOR_RAF }); - const pollHandler = new InjectedScriptPollHandler(progress, poll); - assertDone(throwRetargetableDOMError(await pollHandler.finish())); - return; - } - throw new Error(`state: expected one of (visible|hidden|stable|enabled|disabled|editable)`); + const poll = await this._evaluateHandleInUtility(([injected, node, state]) => { + return injected.waitForElementStatesAndPerformAction(node, [state], () => 'done' as const); + }, state); + const pollHandler = new InjectedScriptPollHandler(progress, poll); + assertDone(throwRetargetableDOMError(throwFatalDOMError(await pollHandler.finish()))); }, this._page._timeoutSettings.timeout(options)); } @@ -785,20 +743,20 @@ export class ElementHandle extends js.JSHandle { async _waitForDisplayedAtStablePosition(progress: Progress, waitForEnabled: boolean): Promise<'error:notconnected' | 'done'> { if (waitForEnabled) - progress.log(` waiting for element to be visible, enabled and not moving`); + progress.log(` waiting for element to be visible, enabled and stable`); else - progress.log(` waiting for element to be visible and not moving`); - const rafCount = this._page._delegate.rafCountForStablePosition(); - const poll = this._evaluateHandleInUtility(([injected, node, { rafOptions, waitForEnabled }]) => { - return injected.waitForDisplayedAtStablePosition(node, rafOptions, waitForEnabled); - }, { rafOptions: { rafCount, useTimeout: !!process.env.PW_USE_TIMEOUT_FOR_RAF }, waitForEnabled }); + progress.log(` waiting for element to be visible and stable`); + const poll = this._evaluateHandleInUtility(([injected, node, waitForEnabled]) => { + return injected.waitForElementStatesAndPerformAction(node, + waitForEnabled ? ['visible', 'stable', 'enabled'] : ['visible', 'stable'], () => 'done' as const); + }, waitForEnabled); const pollHandler = new InjectedScriptPollHandler(progress, await poll); const result = await pollHandler.finish(); if (waitForEnabled) - progress.log(' element is visible, enabled and does not move'); + progress.log(' element is visible, enabled and stable'); else - progress.log(' element is visible and does not move'); - return result; + progress.log(' element is visible and stable'); + return throwFatalDOMError(result); } async _checkHitTargetAt(point: types.Point): Promise<'error:notconnected' | { hitTargetDescription: string } | 'done'> { @@ -898,10 +856,12 @@ export function throwFatalDOMError(result: T | FatalDOMError): T { throw new Error('Node is not an HTMLInputElement'); if (result === 'error:notselect') throw new Error('Element is not a