diff --git a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts index a410f90e778a6..3268ee07ded29 100644 --- a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts @@ -192,7 +192,7 @@ export class ElementHandleDispatcher extends JSHandleDispatcher } async querySelector(params: channels.ElementHandleQuerySelectorParams, progress: Progress): Promise { - const handle = await this._elementHandle.querySelector(progress, params.selector, params); + const handle = await this._elementHandle.querySelector(progress, params); return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), handle) }; } @@ -202,7 +202,7 @@ export class ElementHandleDispatcher extends JSHandleDispatcher } async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams, progress: Progress): Promise { - return { value: serializeResult(await this._elementHandle.evalOnSelector(progress, params.selector, !!params.strict, params.expression, { isFunction: params.isFunction, world: params.world }, parseArgument(params.arg))) }; + return { value: serializeResult(await this._elementHandle.evalOnSelector(progress, params, params.expression, { isFunction: params.isFunction, world: params.world }, parseArgument(params.arg))) }; } async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams, progress: Progress): Promise { @@ -214,7 +214,7 @@ export class ElementHandleDispatcher extends JSHandleDispatcher } async waitForSelector(params: channels.ElementHandleWaitForSelectorParams, progress: Progress): Promise { - return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), await this._elementHandle.waitForSelector(progress, params.selector, params)) }; + return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), await this._elementHandle.waitForSelector(progress, params, params)) }; } private _browserContextDispatcher(): BrowserContextDispatcher { diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index b624c4e44ae25..b6b9f7f0dbeab 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -90,15 +90,15 @@ export class FrameDispatcher extends Dispatcher { - return { element: ElementHandleDispatcher.fromNullable(this, await this._frame.waitForSelector(progress, params.selector, true, params)) }; + return { element: ElementHandleDispatcher.fromNullable(this, await this._frame.waitForSelector(progress, params, true, params)) }; } async dispatchEvent(params: channels.FrameDispatchEventParams, progress: Progress): Promise { - return this._frame.dispatchEvent(progress, params.selector, params.type, parseArgument(params.eventInit), params); + return this._frame.dispatchEvent(progress, params, params.type, parseArgument(params.eventInit)); } async evalOnSelector(params: channels.FrameEvalOnSelectorParams, progress: Progress): Promise { - return { value: serializeResult(await this._frame.evalOnSelector(progress, params.selector, !!params.strict, params.expression, { isFunction: params.isFunction, world: params.world }, parseArgument(params.arg))) }; + return { value: serializeResult(await this._frame.evalOnSelector(progress, params, params.expression, { isFunction: params.isFunction, world: params.world }, parseArgument(params.arg))) }; } async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, progress: Progress): Promise { @@ -106,7 +106,7 @@ export class FrameDispatcher extends Dispatcher { - return { element: ElementHandleDispatcher.fromNullable(this, await this._frame.querySelector(progress, params.selector, params)) }; + return { element: ElementHandleDispatcher.fromNullable(this, await this._frame.querySelector(progress, params)) }; } async querySelectorAll(params: channels.FrameQuerySelectorAllParams, progress: Progress): Promise { @@ -143,48 +143,48 @@ export class FrameDispatcher extends Dispatcher { - return await this._frame.click(progress, params.selector, params); + return await this._frame.click(progress, params, params); } async dblclick(params: channels.FrameDblclickParams, progress: Progress): Promise { - return await this._frame.dblclick(progress, params.selector, params); + return await this._frame.dblclick(progress, params, params); } async dragAndDrop(params: channels.FrameDragAndDropParams, progress: Progress): Promise { - return await this._frame.dragAndDrop(progress, params.source, params.target, params); + return await this._frame.dragAndDrop(progress, { selector: params.source, strict: params.strict }, { selector: params.target, strict: params.strict }, params); } async drop(params: channels.FrameDropParams, progress: Progress): Promise { - return await this._frame.drop(progress, params.selector, params, params); + return await this._frame.drop(progress, params, params); } async tap(params: channels.FrameTapParams, progress: Progress): Promise { - return await this._frame.tap(progress, params.selector, params); + return await this._frame.tap(progress, params, params); } async fill(params: channels.FrameFillParams, progress: Progress): Promise { - return await this._frame.fill(progress, params.selector, params.value, params); + return await this._frame.fill(progress, params, params.value, params); } async focus(params: channels.FrameFocusParams, progress: Progress): Promise { - await this._frame.focus(progress, params.selector, params); + await this._frame.focus(progress, params); } async blur(params: channels.FrameBlurParams, progress: Progress): Promise { - await this._frame.blur(progress, params.selector, params); + await this._frame.blur(progress, params); } async textContent(params: channels.FrameTextContentParams, progress: Progress): Promise { - const value = await this._frame.textContent(progress, params.selector, params); + const value = await this._frame.textContent(progress, params); return { value: value === null ? undefined : value }; } async innerText(params: channels.FrameInnerTextParams, progress: Progress): Promise { - return { value: await this._frame.innerText(progress, params.selector, params) }; + return { value: await this._frame.innerText(progress, params) }; } async innerHTML(params: channels.FrameInnerHTMLParams, progress: Progress): Promise { - return { value: await this._frame.innerHTML(progress, params.selector, params) }; + return { value: await this._frame.innerHTML(progress, params) }; } async resolveSelector(params: channels.FrameResolveSelectorParams, progress: Progress): Promise { @@ -192,66 +192,66 @@ export class FrameDispatcher extends Dispatcher { - const value = await this._frame.getAttribute(progress, params.selector, params.name, params); + const value = await this._frame.getAttribute(progress, params, params.name); return { value: value === null ? undefined : value }; } async inputValue(params: channels.FrameInputValueParams, progress: Progress): Promise { - const value = await this._frame.inputValue(progress, params.selector, params); + const value = await this._frame.inputValue(progress, params); return { value }; } async isChecked(params: channels.FrameIsCheckedParams, progress: Progress): Promise { - return { value: await this._frame.isChecked(progress, params.selector, params) }; + return { value: await this._frame.isChecked(progress, params) }; } async isDisabled(params: channels.FrameIsDisabledParams, progress: Progress): Promise { - return { value: await this._frame.isDisabled(progress, params.selector, params) }; + return { value: await this._frame.isDisabled(progress, params) }; } async isEditable(params: channels.FrameIsEditableParams, progress: Progress): Promise { - return { value: await this._frame.isEditable(progress, params.selector, params) }; + return { value: await this._frame.isEditable(progress, params) }; } async isEnabled(params: channels.FrameIsEnabledParams, progress: Progress): Promise { - return { value: await this._frame.isEnabled(progress, params.selector, params) }; + return { value: await this._frame.isEnabled(progress, params) }; } async isHidden(params: channels.FrameIsHiddenParams, progress: Progress): Promise { - return { value: await this._frame.isHidden(progress, params.selector, params) }; + return { value: await this._frame.isHidden(progress, params) }; } async isVisible(params: channels.FrameIsVisibleParams, progress: Progress): Promise { - return { value: await this._frame.isVisible(progress, params.selector, params) }; + return { value: await this._frame.isVisible(progress, params) }; } async hover(params: channels.FrameHoverParams, progress: Progress): Promise { - return await this._frame.hover(progress, params.selector, params); + return await this._frame.hover(progress, params, params); } async selectOption(params: channels.FrameSelectOptionParams, progress: Progress): Promise { const elements = (params.elements || []).map(e => (e as ElementHandleDispatcher)._elementHandle); - return { values: await this._frame.selectOption(progress, params.selector, elements, params.options || [], params) }; + return { values: await this._frame.selectOption(progress, params, elements, params.options || [], params) }; } async setInputFiles(params: channels.FrameSetInputFilesParams, progress: Progress): Promise { - return await this._frame.setInputFiles(progress, params.selector, params); + return await this._frame.setInputFiles(progress, params, params); } async type(params: channels.FrameTypeParams, progress: Progress): Promise { - return await this._frame.type(progress, params.selector, params.text, params); + return await this._frame.type(progress, params, params.text, params); } async press(params: channels.FramePressParams, progress: Progress): Promise { - return await this._frame.press(progress, params.selector, params.key, params); + return await this._frame.press(progress, params, params.key, params); } async check(params: channels.FrameCheckParams, progress: Progress): Promise { - return await this._frame.check(progress, params.selector, params); + return await this._frame.check(progress, params, params); } async uncheck(params: channels.FrameUncheckParams, progress: Progress): Promise { - return await this._frame.uncheck(progress, params.selector, params); + return await this._frame.uncheck(progress, params, params); } async waitForTimeout(params: channels.FrameWaitForTimeoutParams, progress: Progress): Promise { @@ -260,7 +260,7 @@ export class FrameDispatcher extends Dispatcher { if (params.selector !== undefined) { - await this._frame.waitForFunctionExpressionOnElement(progress, params.selector, params.expression, params.isFunction, parseArgument(params.arg), { strict: params.strict }); + await this._frame.waitForFunctionExpressionOnElement(progress, { selector: params.selector, strict: params.strict }, params.expression, params.isFunction, parseArgument(params.arg)); return {}; } const handle = await this._frame.waitForFunctionExpression(progress, params.expression, params.isFunction, parseArgument(params.arg), params); diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 674b8ab81f59c..b0f370d99aa78 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -41,6 +41,17 @@ export type InputFilesItems = { type ActionName = 'click' | 'hover' | 'dblclick' | 'tap' | 'move and up' | 'move and down' | 'drop'; type PerformActionResult = 'error:notvisible' | 'error:notconnected' | 'error:notinviewport' | 'error:optionsnotfound' | 'error:optionnotenabled' | { missingState: ElementState } | { hitTargetDescription: string } | 'done'; +type PointerAction = { + name: ActionName, + waitForEnabled: boolean, + // true: wait for navigations triggered by the action. + // false: noWaitAfter was passed, do not wait and do not stall on hit target cleanup. + // 'disabled': this action never waits for navigations. + waitAfter: boolean | 'disabled', + skipActionPreChecks?: boolean, + perform: (progress: Progress, point: types.Point) => Promise, +}; + export class NonRecoverableDOMError extends Error { } @@ -198,27 +209,27 @@ export class ElementHandle extends js.JSHandle { } async getAttribute(progress: Progress, name: string): Promise { - return this._frame.getAttribute(progress, ':scope', name, {}, this); + return this._frame.getAttribute(progress, { selector: ':scope', scope: this }, name); } async inputValue(progress: Progress): Promise { - return this._frame.inputValue(progress, ':scope', {}, this); + return this._frame.inputValue(progress, { selector: ':scope', scope: this }); } async textContent(progress: Progress): Promise { - return this._frame.textContent(progress, ':scope', {}, this); + return this._frame.textContent(progress, { selector: ':scope', scope: this }); } async innerText(progress: Progress): Promise { - return this._frame.innerText(progress, ':scope', {}, this); + return this._frame.innerText(progress, { selector: ':scope', scope: this }); } async innerHTML(progress: Progress): Promise { - return this._frame.innerHTML(progress, ':scope', {}, this); + return this._frame.innerHTML(progress, { selector: ':scope', scope: this }); } async dispatchEvent(progress: Progress, type: string, eventInit: Object = {}) { - return this._frame.dispatchEvent(progress, ':scope', type, eventInit, {}, this); + return this._frame.dispatchEvent(progress, { selector: ':scope', scope: this }, type, eventInit); } async _scrollRectIntoViewIfNeeded(progress: Progress, rect?: types.Rect): Promise<'error:notvisible' | 'error:notconnected' | 'done'> { @@ -368,11 +379,8 @@ export class ElementHandle extends js.JSHandle { } } - async _retryPointerAction(progress: Progress, actionName: ActionName, waitForEnabled: boolean, action: (progress: Progress, point: types.Point) => Promise, - options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions, frameVisible: boolean): Promise<'error:notconnected' | 'done'> { - // Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation. - const skipActionPreChecks = actionName === 'move and up'; - return await this._retryAction(progress, actionName, async (progress, retry) => { + async _retryPointerAction(progress: Progress, action: PointerAction, options: types.PointerActionOptions & types.PointerActionWaitOptions, frameVisible: boolean): Promise<'error:notconnected' | 'done'> { + return await this._retryAction(progress, action.name, async (progress, retry) => { // By default, we scroll with protocol method to reveal the action point. // However, that might not work to scroll from under position:sticky elements // that overlay the target element. To fight this, we cycle through different @@ -384,17 +392,15 @@ export class ElementHandle extends js.JSHandle { { block: 'start', inline: 'start' }, ]; const forceScrollOptions = scrollOptions[retry % scrollOptions.length]; - return await this._performPointerAction(progress, actionName, waitForEnabled, action, forceScrollOptions, options, frameVisible); - }, { ...options, skipActionPreChecks }); + return await this._performPointerAction(progress, action, forceScrollOptions, options, frameVisible); + }, { trial: options.trial, force: options.force, skipActionPreChecks: action.skipActionPreChecks }); } async _performPointerAction( progress: Progress, - actionName: ActionName, - waitForEnabled: boolean, - action: (progress: Progress, point: types.Point) => Promise, + action: PointerAction, forceScrollOptions: ScrollIntoViewOptions | undefined, - options: { waitAfter: boolean | 'disabled' } & types.PointerActionOptions & types.PointerActionWaitOptions, + options: types.PointerActionOptions & types.PointerActionWaitOptions, frameVisible: boolean, ): Promise { const { force = false, position, scroll } = options; @@ -423,14 +429,14 @@ export class ElementHandle extends js.JSHandle { await progress.race((options as any).__testHookBeforeStable()); if (!force) { - const elementStates: ElementState[] = waitForEnabled ? ['visible', 'enabled', 'stable'] : ['visible', 'stable']; - progress.log(` waiting for element to be ${waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`); + const elementStates: ElementState[] = action.waitForEnabled ? ['visible', 'enabled', 'stable'] : ['visible', 'stable']; + progress.log(` waiting for element to be ${action.waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`); const result = await progress.race(this.evaluateInUtility(async ([injected, node, { elementStates, frameVisible }]) => { return await injected.checkElementStates(node, elementStates, frameVisible); }, { elementStates, frameVisible })); if (result) return result; - progress.log(` element is ${waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`); + progress.log(` element is ${action.waitForEnabled ? 'visible, enabled and stable' : 'visible and stable'}`); } if ((options as any).__testHookAfterStable) @@ -459,7 +465,7 @@ export class ElementHandle extends js.JSHandle { if (frameCheckResult === 'error:notconnected' || ('hitTargetDescription' in frameCheckResult)) return frameCheckResult; const hitPoint = frameCheckResult.framePoint; - const actionType = actionName === 'move and up' ? 'drag' : ((actionName === 'hover' || actionName === 'tap') ? actionName : 'mouse'); + const actionType = action.name === 'move and up' ? 'drag' : ((action.name === 'hover' || action.name === 'tap') ? action.name : 'mouse'); const handle = await progress.race(this._evaluateHandleInUtility(([injected, node, { actionType, hitPoint, trial }]) => injected.setupHitTargetInterceptor(node, actionType, hitPoint, trial), { actionType, hitPoint, trial: !!options.trial } as const)); if (handle === 'error:notconnected') return handle; @@ -472,14 +478,14 @@ export class ElementHandle extends js.JSHandle { hitTargetInterceptionHandle = handle as any; } - const actionResult = await this._page.frameManager.waitForSignalsCreatedBy(progress, options.waitAfter === true, async progress => { + const actionResult = await this._page.frameManager.waitForSignalsCreatedBy(progress, action.waitAfter === true, async progress => { if ((options as any).__testHookBeforePointerAction) await progress.race((options as any).__testHookBeforePointerAction()); let restoreModifiers: types.KeyboardModifier[] | undefined; if (options && options.modifiers) restoreModifiers = await this._page.keyboard.ensureModifiers(progress, options.modifiers); - progress.log(` performing ${actionName} action`); - await action(progress, point); + progress.log(` performing ${action.name} action`); + await action.perform(progress, point); if (restoreModifiers) await this._page.keyboard.ensureModifiers(progress, restoreModifiers); if (hitTargetInterceptionHandle) { @@ -488,7 +494,7 @@ export class ElementHandle extends js.JSHandle { }).catch(e => 'done' as const).finally(() => { hitTargetInterceptionHandle?.dispose(); }); - if (options.waitAfter !== false) { + if (action.waitAfter !== false) { // When noWaitAfter is passed, we do not want to accidentally stall on // non-committed navigation blocking the evaluate. const hitTargetResult = await progress.race(stopHitTargetInterception); @@ -496,7 +502,7 @@ export class ElementHandle extends js.JSHandle { return hitTargetResult; } } - progress.log(` ${options.trial ? 'trial ' : ''}${actionName} action done`); + progress.log(` ${options.trial ? 'trial ' : ''}${action.name} action done`); progress.log(' waiting for scheduled navigations to finish'); if ((options as any).__testHookAfterPointerAction) await progress.race((options as any).__testHookAfterPointerAction()); @@ -527,17 +533,27 @@ export class ElementHandle extends js.JSHandle { } _hover(progress: Progress, options: types.PointerActionOptions & types.PointerActionWaitOptions, frameVisible: boolean): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'hover', false /* waitForEnabled */, (progress, point) => this._page.mouse.move(progress, point.x, point.y), { ...options, waitAfter: 'disabled' }, frameVisible); + return this._retryPointerAction(progress, { + name: 'hover', + waitForEnabled: false, + waitAfter: 'disabled', + perform: (progress, point) => this._page.mouse.move(progress, point.x, point.y), + }, options, frameVisible); } async click(progress: Progress, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions): Promise { await this._markAsTargetElement(progress); - const result = await this._click(progress, { ...options, waitAfter: !options.noWaitAfter }, true /* frameVisible */); + const result = await this._click(progress, options, !options.noWaitAfter, true /* frameVisible */); return assertDone(throwRetargetableDOMError(result)); } - _click(progress: Progress, options: { waitAfter: boolean | 'disabled' } & types.MouseClickOptions & types.PointerActionWaitOptions, frameVisible: boolean): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'click', true /* waitForEnabled */, (progress, point) => this._page.mouse.click(progress, point.x, point.y, options), options, frameVisible); + _click(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions, waitAfter: boolean | 'disabled', frameVisible: boolean): Promise<'error:notconnected' | 'done'> { + return this._retryPointerAction(progress, { + name: 'click', + waitForEnabled: true, + waitAfter, + perform: (progress, point) => this._page.mouse.click(progress, point.x, point.y, options), + }, options, frameVisible); } async dblclick(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions): Promise { @@ -547,7 +563,12 @@ export class ElementHandle extends js.JSHandle { } _dblclick(progress: Progress, options: types.MouseClickOptions & types.PointerActionWaitOptions, frameVisible: boolean): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'dblclick', true /* waitForEnabled */, (progress, point) => this._page.mouse.click(progress, point.x, point.y, { ...options, clickCount: 2 }), { ...options, waitAfter: 'disabled' }, frameVisible); + return this._retryPointerAction(progress, { + name: 'dblclick', + waitForEnabled: true, + waitAfter: 'disabled', + perform: (progress, point) => this._page.mouse.click(progress, point.x, point.y, { ...options, clickCount: 2 }), + }, options, frameVisible); } async tap(progress: Progress, options: types.PointerActionWaitOptions): Promise { @@ -557,7 +578,12 @@ export class ElementHandle extends js.JSHandle { } _tap(progress: Progress, options: types.PointerActionWaitOptions, frameVisible: boolean): Promise<'error:notconnected' | 'done'> { - return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, (progress, point) => this._page.touchscreen.tap(progress, point.x, point.y), { ...options, waitAfter: 'disabled' }, frameVisible); + return this._retryPointerAction(progress, { + name: 'tap', + waitForEnabled: true, + waitAfter: 'disabled', + perform: (progress, point) => this._page.touchscreen.tap(progress, point.x, point.y), + }, options, frameVisible); } async selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { @@ -668,7 +694,7 @@ export class ElementHandle extends js.JSHandle { lastModifiedMs: p.lastModifiedMs, })); } - return this._retryPointerAction(progress, 'drop', false /* waitForEnabled */, async (progress, point) => { + const perform = async (progress: Progress, point: types.Point) => { // Firefox strips files from DataTransfer objects that cross the isolated-world // boundary into the page's main world. Adopt the element to main context and // construct the DataTransfer + dispatch events there. @@ -712,7 +738,8 @@ export class ElementHandle extends js.JSHandle { if (disposeHandle) handle.dispose(); } - }, { ...options, waitAfter: 'disabled' }, frameVisible); + }; + return this._retryPointerAction(progress, { name: 'drop', waitForEnabled: false, waitAfter: 'disabled', perform }, options, frameVisible); } async _setInputFiles(progress: Progress, items: InputFilesItems, box?: types.Rect): Promise<'error:notconnected' | 'done'> { @@ -769,13 +796,13 @@ export class ElementHandle extends js.JSHandle { return await progress.race(this.evaluateInUtility(([injected, node]) => injected.blurNode(node), {})); } - async type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions): Promise { + async type(progress: Progress, text: string, options: { delay?: number }): Promise { await this._markAsTargetElement(progress); const result = await this._type(progress, text, options); return assertDone(throwRetargetableDOMError(result)); } - async _type(progress: Progress, text: string, options: { delay?: number } & types.StrictOptions, box?: types.Rect): Promise<'error:notconnected' | 'done'> { + async _type(progress: Progress, text: string, options: { delay?: number }, box?: types.Rect): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.type("${text}")`); await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); const result = await this._focus(progress, true /* resetSelectionIfNotFocused */); @@ -785,13 +812,13 @@ export class ElementHandle extends js.JSHandle { return 'done'; } - async press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions): Promise { + async press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean }): Promise { await this._markAsTargetElement(progress); const result = await this._press(progress, key, options); return assertDone(throwRetargetableDOMError(result)); } - async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean } & types.StrictOptions, box?: types.Rect): Promise<'error:notconnected' | 'done'> { + async _press(progress: Progress, key: string, options: { delay?: number, noWaitAfter?: boolean }, box?: types.Rect): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.press("${key}")`); await this.instrumentation.onBeforeInputAction(progress, this, undefined, box); return this._page.frameManager.waitForSignalsCreatedBy(progress, !options.noWaitAfter, async progress => { @@ -826,7 +853,7 @@ export class ElementHandle extends js.JSHandle { return 'done'; if (!state && checkedState.isRadio) throw new NonRecoverableDOMError('Cannot uncheck radio button. Radio buttons can only be unchecked by selecting another radio button in the same group.'); - const result = await this._click(progress, { ...options, waitAfter: 'disabled' }, frameVisible); + const result = await this._click(progress, options, 'disabled', frameVisible); if (result !== 'done') return result; if (options.trial) @@ -845,12 +872,8 @@ export class ElementHandle extends js.JSHandle { return await this._page.screenshotter.screenshotElement(progress, this, options, true /* frameVisible */); } - async querySelector(progress: Progress, selector: string, options: types.StrictOptions): Promise { - return progress.race(this._querySelector(selector, options)); - } - - private async _querySelector(selector: string, options: types.StrictOptions): Promise { - return this._frame.selectors.query(selector, options, this); + async querySelector(progress: Progress, target: types.SelectorTarget): Promise { + return progress.race(this._frame.selectors.query({ ...target, scope: this })); } async querySelectorAll(progress: Progress, selector: string): Promise[]> { @@ -870,8 +893,8 @@ export class ElementHandle extends js.JSHandle { return await js.evaluateExpression(context, expression, { isFunction: options.isFunction, serialize: options.serialize, returnByValue: true }, this, arg); } - async evalOnSelector(progress: Progress, selector: string, strict: boolean, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { - return this._frame.evalOnSelector(progress, selector, strict, expression, options, arg, this); + async evalOnSelector(progress: Progress, target: types.SelectorTarget, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { + return this._frame.evalOnSelector(progress, { ...target, scope: this }, expression, options, arg); } async evalOnSelectorAll(progress: Progress, selector: string, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { @@ -879,27 +902,27 @@ export class ElementHandle extends js.JSHandle { } async isVisible(progress: Progress): Promise { - return this._frame.isVisible(progress, ':scope', {}, this); + return this._frame.isVisible(progress, { selector: ':scope', scope: this }); } async isHidden(progress: Progress): Promise { - return this._frame.isHidden(progress, ':scope', {}, this); + return this._frame.isHidden(progress, { selector: ':scope', scope: this }); } async isEnabled(progress: Progress): Promise { - return this._frame.isEnabled(progress, ':scope', {}, this); + return this._frame.isEnabled(progress, { selector: ':scope', scope: this }); } async isDisabled(progress: Progress): Promise { - return this._frame.isDisabled(progress, ':scope', {}, this); + return this._frame.isDisabled(progress, { selector: ':scope', scope: this }); } async isEditable(progress: Progress): Promise { - return this._frame.isEditable(progress, ':scope', {}, this); + return this._frame.isEditable(progress, { selector: ':scope', scope: this }); } async isChecked(progress: Progress): Promise { - return this._frame.isChecked(progress, ':scope', {}, this); + return this._frame.isChecked(progress, { selector: ':scope', scope: this }); } async waitForElementState(progress: Progress, state: 'visible' | 'hidden' | 'stable' | 'enabled' | 'disabled' | 'editable'): Promise { @@ -912,8 +935,8 @@ export class ElementHandle extends js.JSHandle { assertDone(throwRetargetableDOMError(result)); } - async waitForSelector(progress: Progress, selector: string, options: types.WaitForElementOptions): Promise | null> { - return await this._frame.waitForSelector(progress, selector, true, options, this); + async waitForSelector(progress: Progress, target: types.SelectorTarget, options: types.WaitForElementOptions): Promise | null> { + return await this._frame.waitForSelector(progress, { ...target, scope: this }, true, options); } async _adoptTo(context: FrameExecutionContext): Promise> { diff --git a/packages/playwright-core/src/server/frameSelectors.ts b/packages/playwright-core/src/server/frameSelectors.ts index 64f69770b35f2..e227b1799f86b 100644 --- a/packages/playwright-core/src/server/frameSelectors.ts +++ b/packages/playwright-core/src/server/frameSelectors.ts @@ -50,13 +50,12 @@ export class FrameSelectors { this.frame = frame; } - private _parseSelector(selector: string | ParsedSelector, options?: types.StrictOptions): SelectorInfo { - const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.frame._page.browserContext._options.strictSelectors; - return this.frame._page.browserContext.selectors().parseSelector(selector, strict); + private _parseSelector(selector: string | ParsedSelector, strict: boolean | undefined): SelectorInfo { + return this.frame._page.browserContext.selectors().parseSelector(selector, strict ?? !!this.frame._page.browserContext._options.strictSelectors); } - async query(selector: string, options?: types.StrictOptions & { mainWorld?: boolean }, scope?: ElementHandle): Promise | null> { - const resolved = await this.callOnSelectorHandle(selector, { ...options, scope }, ({ elements }) => elements[0], {}); + async query(target: types.SelectorTarget & { mainWorld?: boolean }): Promise | null> { + const resolved = await this.callOnSelectorHandle(target, ({ elements }) => elements[0], {}); if (!resolved) return null; const handle = resolved.result; @@ -69,7 +68,7 @@ export class FrameSelectors { } async queryArrayInWorld(selector: string, world: types.World, scope?: ElementHandle): Promise> { - const resolved = await this.callOnSelectorHandle(selector, { mainWorld: world === 'main', strict: false, scope }, ({ elements }) => elements, {}); + const resolved = await this.callOnSelectorHandle({ selector, mainWorld: world === 'main', strict: false, scope }, ({ elements }) => elements, {}); if (!resolved) { const context = await this.frame.context(world); return await context.evaluateHandle(() => []); @@ -90,12 +89,12 @@ export class FrameSelectors { } async queryCount(selector: string): Promise { - const resolved = await this.callOnSelector(selector, { strict: false }, ({ elements }) => elements.length, {}); + const resolved = await this.callOnSelector({ selector, strict: false }, ({ elements }) => elements.length, {}); return resolved ? resolved.result : 0; } async queryAll(selector: string, scope?: ElementHandle): Promise[]> { - const resolved = await this.callOnSelectorHandle(selector, { strict: false, scope }, ({ elements }) => elements, {}); + const resolved = await this.callOnSelectorHandle({ selector, strict: false, scope }, ({ elements }) => elements, {}); if (!resolved) return []; @@ -133,7 +132,8 @@ export class FrameSelectors { return jumptToFrame; } - async resolveFramesForSelector(selector: string, options: types.StrictOptions & { anyFrame?: boolean } = {}, scope?: ElementHandle): Promise { + async resolveFramesForSelector(target: types.SelectorTarget & { anyFrame?: boolean }): Promise { + const { selector, scope } = target; const { anyFrame, chunks } = splitSelectorByFrame(selector); for (const chunk of chunks) { visitAllSelectorParts(chunk, (part, nested) => { @@ -148,15 +148,15 @@ export class FrameSelectors { }); } - if (!anyFrame && !options.anyFrame) { - const result = await this._resolveChainedSelector(this.frame, selector, options, chunks, scope, false /* noStall */); + if (!anyFrame && !target.anyFrame) { + const result = await this._resolveChainedSelector(this.frame, target, chunks, false /* noStall */); return result ? [result] : []; } const result: SelectorInFrame[] = []; const seenFrames = new Set(); for (const { frame: startFrame, frameVisible } of await this._anyFrameCandidates(scope)) { - const resolved = await this._resolveChainedSelector(startFrame, selector, options, chunks, startFrame === this.frame ? scope : undefined, startFrame !== this.frame /* noStall */, frameVisible); + const resolved = await this._resolveChainedSelector(startFrame, startFrame === this.frame ? target : { ...target, scope: undefined }, chunks, startFrame !== this.frame /* noStall */, frameVisible); // Different starting frames may resolve into the same frame, e.g. with an aria-ref selector. if (!resolved || seenFrames.has(resolved.frame)) continue; @@ -169,7 +169,7 @@ export class FrameSelectors { // All the frames the selector may start in: the frame subtree, restricted to the scope when given. private async _anyFrameCandidates(scope: ElementHandle | undefined): Promise<{ frame: Frame, frameVisible: boolean }[]> { const result: { frame: Frame, frameVisible: boolean }[] = []; - const info = this._parseSelector('css=frame,iframe', { strict: false }); + const info = this._parseSelector('css=frame,iframe', false); const collectSubtree = async (frame: Frame, frameVisible: boolean, scope: ElementHandle | undefined) => { result.push({ frame, frameVisible }); const childFrames = await this._queryFrames(frame, info, scope, true /* noStall */, frameVisible); @@ -220,11 +220,13 @@ export class FrameSelectors { }); } - private async _resolveChainedSelector(startFrame: Frame, selector: string, options: types.StrictOptions, frameChunks: ParsedSelector[], scope: ElementHandle | undefined, noStall: boolean, startFrameVisible = true): Promise { + private async _resolveChainedSelector(startFrame: Frame, target: types.SelectorTarget, frameChunks: ParsedSelector[], noStall: boolean, startFrameVisible = true): Promise { + const { selector } = target; + let scope = target.scope; let frame = startFrame; let frameVisible = startFrameVisible; for (let i = 0; i < frameChunks.length - 1; ++i) { - const info = this._parseSelector(frameChunks[i], options); + const info = this._parseSelector(frameChunks[i], target.strict); frame = this._jumpToAriaRefFrameIfNeeded(selector, info, frame); const frames = await this._queryFrames(frame, info, i === 0 ? scope : undefined, noStall, frameVisible); if (!frames?.length) @@ -235,23 +237,22 @@ export class FrameSelectors { // If we end up in the different frame, we should start from the frame root, so throw away the scope. if (frame !== startFrame) scope = undefined; - const lastChunk = frame.selectors._parseSelector(frameChunks[frameChunks.length - 1], options); + const lastChunk = frame.selectors._parseSelector(frameChunks[frameChunks.length - 1], target.strict); frame = this._jumpToAriaRefFrameIfNeeded(selector, lastChunk, frame); return { frame, info: lastChunk, scope, frameVisible }; } private async _callOnSelectorInternal( - selector: string, - options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none' }, + target: types.SelectorTarget & { mainWorld?: boolean, callWithoutMatches?: boolean, markTargets?: 'all' | 'first' | 'none' }, pageFunction: MatchedElementsCallback, arg: Arg, returnByValue: boolean, ): Promise<{ frame: Frame, info: SelectorInfo, frameVisible: boolean, result: R | SmartHandle } | null> { - const resolved = await this.resolveFramesForSelector(selector, options, options.scope); + const resolved = await this.resolveFramesForSelector(target); let aggregatedResult: { frame: Frame, info: SelectorInfo, frameVisible: boolean, result: R | SmartHandle } | null = null; const noStall = resolved.length > 1; for (const { frame, info, scope, frameVisible } of resolved) { - const world = options.mainWorld ? 'main' : info.world; + const world = target.mainWorld ? 'main' : info.world; const context = noStall ? frame.existingContext(world) : await frame.context(world); if (!context) continue; @@ -273,7 +274,7 @@ export class FrameSelectors { return '--playwright--no--result--value--'; const func = injected.eval('(' + params.functionText + ')') as MatchedElementsCallback; return func({ injected, elements, info: params.info, frameVisible: params.frameVisible }, params.arg); - }, { info, scope, frameVisible, functionText: String(pageFunction), arg, callWithoutMatches: options.callWithoutMatches, markTargets: options.markTargets, returnByValue }); + }, { info, scope, frameVisible, functionText: String(pageFunction), arg, callWithoutMatches: target.callWithoutMatches, markTargets: target.markTargets, returnByValue }); if (returnByValue && evalResult === '--playwright--no--result--value--') return; if (!returnByValue && (evalResult as JSHandle)._value === '--playwright--no--result--value--') { @@ -297,22 +298,20 @@ export class FrameSelectors { } async callOnSelector( - selector: string, - options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none' }, + target: types.SelectorTarget & { mainWorld?: boolean, callWithoutMatches?: boolean, markTargets?: 'all' | 'first' | 'none' }, pageFunction: MatchedElementsCallback, arg: Arg, ): Promise<{ frame: Frame, info: SelectorInfo, frameVisible: boolean, result: R } | null> { - const result = await this._callOnSelectorInternal(selector, options, pageFunction, arg, true /* returnByValue */); + const result = await this._callOnSelectorInternal(target, pageFunction, arg, true /* returnByValue */); return result as { frame: Frame, info: SelectorInfo, frameVisible: boolean, result: R } | null; } async callOnSelectorHandle( - selector: string, - options: types.StrictOptions & { mainWorld?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none' }, + target: types.SelectorTarget & { mainWorld?: boolean, markTargets?: 'all' | 'first' | 'none' }, pageFunction: MatchedElementsCallback, arg: Arg, ): Promise<{ frame: Frame, info: SelectorInfo, frameVisible: boolean, result: SmartHandle } | null> { - const result = await this._callOnSelectorInternal(selector, { ...options, callWithoutMatches: false }, pageFunction, arg, false /* returnByValue */); + const result = await this._callOnSelectorInternal({ ...target, callWithoutMatches: false }, pageFunction, arg, false /* returnByValue */); return result as { frame: Frame, info: SelectorInfo, frameVisible: boolean, result: SmartHandle } | null; } } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 4af02632c26a4..ea161bf3c0017 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -828,12 +828,13 @@ export class Frame extends SdkObject { return value; } - async querySelector(progress: Progress, selector: string, options: types.StrictOptions): Promise | null> { - this.apiLog(` finding element using the selector "${selector}"`); - return progress.race(this.selectors.query(selector, options)); + async querySelector(progress: Progress, target: types.SelectorTarget): Promise | null> { + this.apiLog(` finding element using the selector "${target.selector}"`); + return progress.race(this.selectors.query(target)); } - async waitForSelector(progress: Progress, selector: string, performActionPreChecksAndLog: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise | null> { + async waitForSelector(progress: Progress, target: types.SelectorTarget, performActionPreChecksAndLog: boolean, options: types.WaitForElementOptions): Promise | null> { + const { selector, scope } = target; if ((options as any).visibility) throw new Error('options.visibility is not supported, did you mean options.state?'); if ((options as any).waitFor && (options as any).waitFor !== 'visible') @@ -850,7 +851,7 @@ export class Frame extends SdkObject { if (scope && await progress.race(scope.evaluateInUtility(([injected, node]) => node.isConnected, {})) !== true) throw new dom.NonRecoverableDOMError('Element is not attached to the DOM'); - const resolved = await progress.race(this.selectors.callOnSelectorHandle(selector, { ...options, scope }, ({ injected, elements, frameVisible }) => { + const resolved = await progress.race(this.selectors.callOnSelectorHandle(target, ({ injected, elements, frameVisible }) => { const element: Element | undefined = elements[0]; const visible = element ? injected.elementState(element, 'visible', frameVisible).matches : false; let log = ''; @@ -894,21 +895,21 @@ export class Frame extends SdkObject { return scope ? scope._context.raceAgainstContextDestroyed(promise) : promise; } - async dispatchEvent(progress: Progress, selector: string, type: string, eventInit: Object = {}, options: types.StrictOptions, scope?: dom.ElementHandle): Promise { - await this._waitForFunctionOnSelector(progress, selector, (injectedScript, element, data) => { + async dispatchEvent(progress: Progress, target: types.SelectorTarget, type: string, eventInit: Object = {}): Promise { + await this._waitForFunctionOnSelector(progress, target, (injectedScript, element, data) => { injectedScript.dispatchEvent(element, data.type, data.eventInit); return { result: undefined }; - }, { type, eventInit }, { mainWorld: true, ...options }, scope); + }, { type, eventInit }, { mainWorld: true }); } - async evalOnSelector(progress: Progress, selector: string, strict: boolean, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any, scope?: dom.ElementHandle): Promise { - return progress.race(this._evalOnSelector(selector, strict, expression, options, arg, scope)); + async evalOnSelector(progress: Progress, target: types.SelectorTarget, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { + return progress.race(this._evalOnSelector(target, expression, options, arg)); } - private async _evalOnSelector(selector: string, strict: boolean, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any, scope?: dom.ElementHandle): Promise { - const handle = await this.selectors.query(selector, { strict }, scope); + private async _evalOnSelector(target: types.SelectorTarget, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { + const handle = await this.selectors.query(target); if (!handle) - throw new Error(`Failed to find element matching selector "${selector}"`); + throw new Error(`Failed to find element matching selector "${target.selector}"`); const result = await handle.internalEvaluateExpression(expression, options, arg); handle.dispose(); return result; @@ -1192,17 +1193,17 @@ export class Frame extends SdkObject { private async _retryWithProgressIfNotConnected( progress: Progress, - selector: string, - options: { strict?: boolean, force?: boolean, performActionPreChecks?: boolean, waitForFrameVisible?: boolean }, + target: types.SelectorTarget, + options: { force?: boolean, waitForFrameVisible: boolean, skipActionPreChecks?: boolean }, action: (progress: Progress, handle: dom.ElementHandle, box: types.Rect | undefined, frameVisible: boolean) => Promise): Promise { - progress.log(`waiting for ${this._asLocator(selector)}`); - const performActionPreChecks = options.performActionPreChecks ?? !options.force; + progress.log(`waiting for ${this._asLocator(target.selector)}`); + const performActionPreChecks = !options.skipActionPreChecks && !options.force; const waitForFrameVisible = options.waitForFrameVisible && !options.force; return this.retryWithProgressAndBackoff(progress, async (progress, continuePolling) => { if (performActionPreChecks) await this._page.performActionPreChecks(progress); - const resolved = await progress.race(this.selectors.callOnSelectorHandle(selector, { strict: options.strict, markTargets: 'all' }, ({ injected, elements }) => { + const resolved = await progress.race(this.selectors.callOnSelectorHandle({ ...target, markTargets: 'all' }, ({ injected, elements }) => { const element = elements[0] as Element | undefined; let log = ''; if (elements.length > 1) @@ -1243,64 +1244,67 @@ export class Frame extends SdkObject { } async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise { - return await this._retryWithProgressIfNotConnected(progress, selector, { strict: true, performActionPreChecks: true, waitForFrameVisible: true }, async (progress, handle, box, frameVisible) => { + return await this._retryWithProgressIfNotConnected(progress, { selector, strict: true }, { waitForFrameVisible: true }, async (progress, handle, box, frameVisible) => { await handle._frame.rafrafTimeout(progress, timeout); return await this._page.screenshotter.screenshotElement(progress, handle, options, frameVisible); }); } - async click(progress: Progress, selector: string, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._click(progress, { ...options, waitAfter: !options.noWaitAfter }, frameVisible))); + async click(progress: Progress, target: types.SelectorTarget, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions) { + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { force: options.force, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._click(progress, options, !options.noWaitAfter, frameVisible))); } - async dblclick(progress: Progress, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._dblclick(progress, options, frameVisible))); + async dblclick(progress: Progress, target: types.SelectorTarget, options: types.MouseClickOptions & types.PointerActionWaitOptions) { + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { force: options.force, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._dblclick(progress, options, frameVisible))); } - async dragAndDrop(progress: Progress, source: string, target: string, options: types.DragActionOptions & types.PointerActionWaitOptions) { - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, { ...options, waitForFrameVisible: true }, async (progress, handle, box, frameVisible) => { - return handle._retryPointerAction(progress, 'move and down', false, async (progress, point) => { - await this._page.mouse.move(progress, point.x, point.y); - await this._page.mouse.down(progress); - }, { - ...options, + async dragAndDrop(progress: Progress, source: types.SelectorTarget, target: types.SelectorTarget, options: types.DragActionOptions & types.PointerActionWaitOptions) { + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, { force: options.force, waitForFrameVisible: true }, async (progress, handle, box, frameVisible) => { + return handle._retryPointerAction(progress, { + name: 'move and down', + waitForEnabled: false, waitAfter: 'disabled', - position: options.sourcePosition, - }, frameVisible); + perform: async (progress, point) => { + await this._page.mouse.move(progress, point.x, point.y); + await this._page.mouse.down(progress); + }, + }, { ...options, position: options.sourcePosition }, frameVisible); })); // Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation. - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { ...options, performActionPreChecks: false, waitForFrameVisible: true }, async (progress, handle, box, frameVisible) => { - return handle._retryPointerAction(progress, 'move and up', false, async (progress, point) => { - await this._page.mouse.move(progress, point.x, point.y, { steps: options.steps }); - await this._page.mouse.up(progress); - }, { - ...options, + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { force: options.force, waitForFrameVisible: true, skipActionPreChecks: true }, async (progress, handle, box, frameVisible) => { + return handle._retryPointerAction(progress, { + name: 'move and up', + waitForEnabled: false, waitAfter: 'disabled', - position: options.targetPosition, - }, frameVisible); + skipActionPreChecks: true, + perform: async (progress, point) => { + await this._page.mouse.move(progress, point.x, point.y, { steps: options.steps }); + await this._page.mouse.up(progress); + }, + }, { ...options, position: options.targetPosition }, frameVisible); })); } - async tap(progress: Progress, selector: string, options: types.PointerActionWaitOptions) { + async tap(progress: Progress, target: types.SelectorTarget, options: types.PointerActionWaitOptions) { if (!this._page.browserContext._options.hasTouch) throw new Error('The page does not support tap. Use hasTouch context option to enable touch support.'); - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._tap(progress, options, frameVisible))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { force: options.force, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._tap(progress, options, frameVisible))); } - async fill(progress: Progress, selector: string, value: string, options: types.CommonActionOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, { ...options, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._fill(progress, value, options, frameVisible, box))); + async fill(progress: Progress, target: types.SelectorTarget, value: string, options: types.CommonActionOptions) { + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { force: options.force, waitForFrameVisible: true }, (progress, handle, box, frameVisible) => handle._fill(progress, value, options, frameVisible, box))); } - async focus(progress: Progress, selector: string, options: types.StrictOptions) { - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._focus(progress))); + async focus(progress: Progress, target: types.SelectorTarget) { + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { waitForFrameVisible: false }, (progress, handle) => handle._focus(progress))); } - async blur(progress: Progress, selector: string, options: types.StrictOptions) { - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._blur(progress))); + async blur(progress: Progress, target: types.SelectorTarget) { + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { waitForFrameVisible: false }, (progress, handle) => handle._blur(progress))); } async resolveSelector(progress: Progress, selector: string, options: { mainWorld?: boolean } = {}): Promise<{ resolvedSelector: string }> { - const element = await progress.race(this.selectors.query(selector, options)); + const element = await progress.race(this.selectors.query({ selector, ...options })); if (!element) throw new Error(`No element matching ${selector}`); @@ -1329,57 +1333,57 @@ export class Frame extends SdkObject { return { resolvedSelector }; } - async textContent(progress: Progress, selector: string, options: types.StrictOptions, scope?: dom.ElementHandle): Promise { - const { result } = await this._waitForFunctionOnSelector(progress, selector, (injected, element) => ({ result: element.textContent }), undefined, options, scope); + async textContent(progress: Progress, target: types.SelectorTarget): Promise { + const { result } = await this._waitForFunctionOnSelector(progress, target, (injected, element) => ({ result: element.textContent }), undefined); return result; } - async innerText(progress: Progress, selector: string, options: types.StrictOptions, scope?: dom.ElementHandle): Promise { - const { result } = await this._waitForFunctionOnSelector(progress, selector, (injectedScript, element) => { + async innerText(progress: Progress, target: types.SelectorTarget): Promise { + const { result } = await this._waitForFunctionOnSelector(progress, target, (injectedScript, element) => { if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml') throw injectedScript.createStacklessError('Node is not an HTMLElement'); return { result: (element as HTMLElement).innerText }; - }, undefined, options, scope); + }, undefined); return result; } - async innerHTML(progress: Progress, selector: string, options: types.StrictOptions, scope?: dom.ElementHandle): Promise { - const { result } = await this._waitForFunctionOnSelector(progress, selector, (injected, element) => ({ result: element.innerHTML }), undefined, options, scope); + async innerHTML(progress: Progress, target: types.SelectorTarget): Promise { + const { result } = await this._waitForFunctionOnSelector(progress, target, (injected, element) => ({ result: element.innerHTML }), undefined); return result; } - async getAttribute(progress: Progress, selector: string, name: string, options: types.StrictOptions, scope?: dom.ElementHandle): Promise { - const { result } = await this._waitForFunctionOnSelector(progress, selector, (injected, element, data) => ({ result: element.getAttribute(data.name) }), { name }, options, scope); + async getAttribute(progress: Progress, target: types.SelectorTarget, name: string): Promise { + const { result } = await this._waitForFunctionOnSelector(progress, target, (injected, element, data) => ({ result: element.getAttribute(data.name) }), { name }); return result; } - async inputValue(progress: Progress, selector: string, options: types.StrictOptions, scope?: dom.ElementHandle): Promise { - const { result } = await this._waitForFunctionOnSelector(progress, selector, (injectedScript, node) => { + async inputValue(progress: Progress, target: types.SelectorTarget): Promise { + const { result } = await this._waitForFunctionOnSelector(progress, target, (injectedScript, node) => { const element = injectedScript.retarget(node, 'follow-label'); if (!element || (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')) throw injectedScript.createStacklessError('Node is not an ,