feat(iframe): make iframe selectors work w/ element handles (#10063)
This commit is contained in:
parent
729ebe49c7
commit
f19864890f
|
|
@ -83,19 +83,19 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
|
|||
}
|
||||
|
||||
async evalOnSelector(params: channels.FrameEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorResult> {
|
||||
return { value: serializeResult(await this._frame.evalOnSelectorAndWaitForSignals(metadata, params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
return { value: serializeResult(await this._frame.evalOnSelectorAndWaitForSignals(params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
}
|
||||
|
||||
async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorAllResult> {
|
||||
return { value: serializeResult(await this._frame.evalOnSelectorAllAndWaitForSignals(metadata, params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
return { value: serializeResult(await this._frame.evalOnSelectorAllAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
}
|
||||
|
||||
async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorResult> {
|
||||
return { element: ElementHandleDispatcher.fromNullable(this._scope, await this._frame.querySelector(metadata, params.selector, params)) };
|
||||
return { element: ElementHandleDispatcher.fromNullable(this._scope, await this._frame.querySelector(params.selector, params)) };
|
||||
}
|
||||
|
||||
async querySelectorAll(params: channels.FrameQuerySelectorAllParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorAllResult> {
|
||||
const elements = await this._frame.querySelectorAll(metadata, params.selector);
|
||||
const elements = await this._frame.querySelectorAll(params.selector);
|
||||
return { elements: elements.map(e => ElementHandleDispatcher.from(this._scope, e)) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,8 @@ export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
|
|||
for (let i = 0; i < selector.parts.length; ++i) {
|
||||
const part = selector.parts[i];
|
||||
if (part.name === 'content-frame') {
|
||||
if (!chunk.parts.length)
|
||||
throw new Error('Selector cannot start with "content-frame", select the iframe first');
|
||||
result.push(chunk);
|
||||
chunk = { parts: [] };
|
||||
chunkStartIndex = i + 1;
|
||||
|
|
|
|||
|
|
@ -109,10 +109,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
declare readonly _context: FrameExecutionContext;
|
||||
readonly _page: Page;
|
||||
declare readonly _objectId: string;
|
||||
private _frame: frames.Frame;
|
||||
|
||||
constructor(context: FrameExecutionContext, objectId: string) {
|
||||
super(context, 'node', undefined, objectId);
|
||||
this._page = context.frame._page;
|
||||
this._frame = context.frame;
|
||||
this._initializePreview().catch(e => {});
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +129,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
|
||||
async evaluateInUtility<R, Arg>(pageFunction: js.Func1<[js.JSHandle<InjectedScript>, ElementHandle<T>, Arg], R>, arg: Arg): Promise<R | 'error:notconnected'> {
|
||||
try {
|
||||
const utility = await this._context.frame._utilityContext();
|
||||
const utility = await this._frame._utilityContext();
|
||||
return await utility.evaluate(pageFunction, [await utility.injectedScript(), this, arg]);
|
||||
} catch (e) {
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
|
||||
|
|
@ -138,7 +140,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
|
||||
async evaluateHandleInUtility<R, Arg>(pageFunction: js.Func1<[js.JSHandle<InjectedScript>, ElementHandle<T>, Arg], R>, arg: Arg): Promise<js.JSHandle<R> | 'error:notconnected'> {
|
||||
try {
|
||||
const utility = await this._context.frame._utilityContext();
|
||||
const utility = await this._frame._utilityContext();
|
||||
return await utility.evaluateHandle(pageFunction, [await utility.injectedScript(), this, arg]);
|
||||
} catch (e) {
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
|
||||
|
|
@ -149,7 +151,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
|
||||
async evaluatePoll<R, Arg>(progress: Progress, pageFunction: js.Func1<[js.JSHandle<InjectedScript>, ElementHandle<T>, Arg], InjectedScriptPoll<R>>, arg: Arg): Promise<R | 'error:notconnected'> {
|
||||
try {
|
||||
const utility = await this._context.frame._utilityContext();
|
||||
const utility = await this._frame._utilityContext();
|
||||
const poll = await utility.evaluateHandle(pageFunction, [await utility.injectedScript(), this, arg]);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
return await pollHandler.finish();
|
||||
|
|
@ -227,7 +229,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async dispatchEvent(type: string, eventInit: Object = {}) {
|
||||
const main = await this._context.frame._mainContext();
|
||||
const main = await this._frame._mainContext();
|
||||
await this._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
|
||||
return main.evaluate(([injected, node, { type, eventInit }]) => injected.dispatchEvent(node, type, eventInit), [await main.injectedScript(), this, { type, eventInit }] as const);
|
||||
});
|
||||
|
|
@ -690,15 +692,21 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async querySelector(selector: string, options: types.StrictOptions): Promise<ElementHandle | null> {
|
||||
return this._page.selectors.query(this._context.frame, selector, options, this);
|
||||
const { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, options, this);
|
||||
// If we end up in the same frame => use the scope again, line above was noop.
|
||||
return this._page.selectors.query(frame, info, this._frame === frame ? this : undefined);
|
||||
}
|
||||
|
||||
async querySelectorAll(selector: string): Promise<ElementHandle<Element>[]> {
|
||||
return this._page.selectors._queryAll(this._context.frame, selector, this, true /* adoptToMain */);
|
||||
const { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
|
||||
// If we end up in the same frame => use the scope again, line above was noop.
|
||||
return this._page.selectors._queryAll(frame, info, this._frame === frame ? this : undefined, true /* adoptToMain */);
|
||||
}
|
||||
|
||||
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const handle = await this._page.selectors.query(this._context.frame, selector, { strict }, this);
|
||||
const { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, { strict }, this);
|
||||
// If we end up in the same frame => use the scope again, line above was noop.
|
||||
const handle = await this._page.selectors.query(frame, info, this._frame === frame ? this : undefined);
|
||||
if (!handle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
|
|
@ -707,7 +715,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
}
|
||||
|
||||
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const arrayHandle = await this._page.selectors._queryArray(this._context.frame, selector, this);
|
||||
const { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
|
||||
// If we end up in the same frame => use the scope again, line above was noop.
|
||||
const arrayHandle = await this._page.selectors._queryArray(frame, info, this._frame === frame ? this : undefined);
|
||||
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
arrayHandle.dispose();
|
||||
return result;
|
||||
|
|
@ -760,21 +770,27 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
const { state = 'visible' } = options;
|
||||
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
|
||||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const task = waitForSelectorTask(info, state, false, this);
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
const context = await this._context.frame._context(info.world);
|
||||
const injected = await context.injectedScript();
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, await task(injected));
|
||||
const result = await pollHandler.finishHandle();
|
||||
if (!result.asElement()) {
|
||||
result.dispose();
|
||||
return null;
|
||||
}
|
||||
const handle = result.asElement() as ElementHandle<Element>;
|
||||
return handle._adoptTo(await this._context.frame._mainContext());
|
||||
return this._frame.retryWithProgress(progress, selector, options, async (frame, info, continuePolling) => {
|
||||
// If we end up in the same frame => use the scope again, line above was noop.
|
||||
const task = waitForSelectorTask(info, state, false, frame === this._frame ? this : undefined);
|
||||
const context = await frame._context(info.world);
|
||||
const injected = await context.injectedScript();
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, await task(injected));
|
||||
const result = await pollHandler.finishHandle();
|
||||
if (!result.asElement()) {
|
||||
result.dispose();
|
||||
return null;
|
||||
}
|
||||
const handle = result.asElement() as ElementHandle<Element>;
|
||||
try {
|
||||
return await handle._adoptTo(await frame._mainContext());
|
||||
} catch (e) {
|
||||
return continuePolling;
|
||||
}
|
||||
}, this);
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -555,7 +555,8 @@ export class FFPage implements PageDelegate {
|
|||
const parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
throw new Error('Frame has been detached.');
|
||||
const handles = await this._page.selectors._queryAll(parent, 'frame,iframe', undefined);
|
||||
const info = this._page.parseSelector('frame,iframe');
|
||||
const handles = await this._page.selectors._queryAll(parent, info);
|
||||
const items = await Promise.all(handles.map(async handle => {
|
||||
const frame = await handle.contentFrame().catch(e => null);
|
||||
return { handle, frame };
|
||||
|
|
|
|||
|
|
@ -458,6 +458,10 @@ export class Frame extends SdkObject {
|
|||
this._subtreeLifecycleEvents.add('commit');
|
||||
}
|
||||
|
||||
isDetached(): boolean {
|
||||
return this._detached;
|
||||
}
|
||||
|
||||
_onLifecycleEvent(event: types.LifecycleEvent) {
|
||||
if (this._firedLifecycleEvents.has(event))
|
||||
return;
|
||||
|
|
@ -706,15 +710,10 @@ export class Frame extends SdkObject {
|
|||
return value;
|
||||
}
|
||||
|
||||
async querySelector(metadata: CallMetadata, selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
debugLogger.log('api', ` finding element using the selector "${selector}"`);
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(progress => this._innerQuerySelector(progress, selector, options));
|
||||
}
|
||||
|
||||
private async _innerQuerySelector(progress: Progress, selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, options);
|
||||
return this._page.selectors.query(frame, info, options);
|
||||
const { frame, info } = await this.resolveFrameForSelectorNoWait(selector, options);
|
||||
return this._page.selectors.query(frame, info);
|
||||
}
|
||||
|
||||
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions & { omitReturnValue?: boolean } = {}): Promise<dom.ElementHandle<Element> | null> {
|
||||
|
|
@ -728,8 +727,7 @@ export class Frame extends SdkObject {
|
|||
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
|
||||
return controller.run(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
while (progress.isRunning()) {
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, options);
|
||||
return this.retryWithProgress(progress, selector, options, async (frame, info, continuePolling) => {
|
||||
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue);
|
||||
const result = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
if (!result.asElement()) {
|
||||
|
|
@ -738,18 +736,13 @@ export class Frame extends SdkObject {
|
|||
}
|
||||
if ((options as any).__testHookBeforeAdoptNode)
|
||||
await (options as any).__testHookBeforeAdoptNode();
|
||||
const handle = result.asElement() as dom.ElementHandle<Element>;
|
||||
try {
|
||||
const handle = result.asElement() as dom.ElementHandle<Element>;
|
||||
const adopted = await handle._adoptTo(await this._mainContext());
|
||||
return adopted;
|
||||
return await handle._adoptTo(await frame._mainContext());
|
||||
} catch (e) {
|
||||
// Navigated while trying to adopt the node.
|
||||
if (js.isJavaScriptErrorInEvaluate(e))
|
||||
throw e;
|
||||
result.dispose();
|
||||
return continuePolling;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
|
|
@ -760,35 +753,27 @@ export class Frame extends SdkObject {
|
|||
await this._page._doSlowMo();
|
||||
}
|
||||
|
||||
async evalOnSelectorAndWaitForSignals(metadata: CallMetadata, selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
const handle = await this._innerQuerySelector(progress, selector, { strict });
|
||||
if (!handle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
handle.dispose();
|
||||
return result;
|
||||
});
|
||||
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const { frame, info } = await this.resolveFrameForSelectorNoWait(selector, { strict });
|
||||
const handle = await this._page.selectors.query(frame, info);
|
||||
if (!handle)
|
||||
throw new Error(`Error: failed to find element matching selector "${selector}"`);
|
||||
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
handle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async evalOnSelectorAllAndWaitForSignals(metadata: CallMetadata, selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, {});
|
||||
const arrayHandle = await this._page.selectors._queryArray(frame, info);
|
||||
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
arrayHandle.dispose();
|
||||
return result;
|
||||
});
|
||||
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const { frame, info } = await this.resolveFrameForSelectorNoWait(selector, {});
|
||||
const arrayHandle = await this._page.selectors._queryArray(frame, info);
|
||||
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
arrayHandle.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
async querySelectorAll(metadata: CallMetadata, selector: string): Promise<dom.ElementHandle<Element>[]> {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, {});
|
||||
return this._page.selectors._queryAll(frame, info, undefined, true /* adoptToMain */);
|
||||
});
|
||||
async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
|
||||
const { frame, info } = await this.resolveFrameForSelectorNoWait(selector, {});
|
||||
return this._page.selectors._queryAll(frame, info, undefined, true /* adoptToMain */);
|
||||
}
|
||||
|
||||
async content(): Promise<string> {
|
||||
|
|
@ -968,31 +953,55 @@ export class Frame extends SdkObject {
|
|||
return result!;
|
||||
}
|
||||
|
||||
async retryWithProgress<R>(
|
||||
progress: Progress,
|
||||
selector: string,
|
||||
options: types.StrictOptions & types.TimeoutOptions,
|
||||
action: (frame: Frame, info: SelectorInfo, continuePolling: symbol) => Promise<R | symbol>,
|
||||
scope?: dom.ElementHandle): Promise<R> {
|
||||
const continuePolling = Symbol('continuePolling');
|
||||
while (progress.isRunning()) {
|
||||
const { frame, info } = await this.resolveFrameForSelector(progress, selector, options, scope);
|
||||
try {
|
||||
const result = await action(frame, info, continuePolling);
|
||||
if (result === continuePolling)
|
||||
continue;
|
||||
return result as R;
|
||||
} catch (e) {
|
||||
// Always fail on JavaScript errors or when the main connection is closed.
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
|
||||
throw e;
|
||||
// If error has happened in the detached inner frame, ignore it, keep polling.
|
||||
if (frame !== this && frame.isDetached())
|
||||
continue;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
progress.throwIfAborted();
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
private async _retryWithProgressIfNotConnected<R>(
|
||||
progress: Progress,
|
||||
selector: string,
|
||||
strict: boolean | undefined,
|
||||
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
||||
while (progress.isRunning()) {
|
||||
return this.retryWithProgress(progress, selector, { strict }, async (frame, info, continuePolling) => {
|
||||
progress.log(`waiting for selector "${selector}"`);
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, { strict });
|
||||
const task = dom.waitForSelectorTask(info, 'attached');
|
||||
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||
progress.cleanupWhenAborted(() => {
|
||||
// Do not await here to avoid being blocked, either by stalled
|
||||
// page (e.g. alert) or unresolved navigation in Chromium.
|
||||
element.dispose();
|
||||
});
|
||||
const result = await action(element);
|
||||
element.dispose();
|
||||
if (result === 'error:notconnected') {
|
||||
progress.log('element was detached from the DOM, retrying');
|
||||
continue;
|
||||
try {
|
||||
const result = await action(element);
|
||||
if (result === 'error:notconnected') {
|
||||
progress.log('element was detached from the DOM, retrying');
|
||||
return continuePolling;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
element?.dispose();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return undefined as any;
|
||||
});
|
||||
}
|
||||
|
||||
async click(metadata: CallMetadata, selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) {
|
||||
|
|
@ -1097,7 +1106,8 @@ export class Frame extends SdkObject {
|
|||
const controller = new ProgressController(metadata, this);
|
||||
return controller.run(async progress => {
|
||||
progress.log(` checking visibility of "${selector}"`);
|
||||
const element = await this._innerQuerySelector(progress, selector, options);
|
||||
const { frame, info } = await this.resolveFrameForSelector(progress, selector, options);
|
||||
const element = await this._page.selectors.query(frame, info);
|
||||
return element ? await element.isVisible() : false;
|
||||
}, this._page._timeoutSettings.timeout({}));
|
||||
}
|
||||
|
|
@ -1214,10 +1224,10 @@ export class Frame extends SdkObject {
|
|||
// Reached the expected state!
|
||||
return result;
|
||||
}, { ...options, isArray }, { strict: true, querySelectorAll: isArray, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
|
||||
if (js.isJavaScriptErrorInEvaluate(e))
|
||||
throw e;
|
||||
// Q: Why not throw upon isSessionClosedError(e) as in other places?
|
||||
// A: We want user to receive a friendly message containing the last intermediate result.
|
||||
if (js.isJavaScriptErrorInEvaluate(e))
|
||||
throw e;
|
||||
return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log };
|
||||
});
|
||||
}
|
||||
|
|
@ -1298,18 +1308,10 @@ export class Frame extends SdkObject {
|
|||
const callbackText = body.toString();
|
||||
|
||||
return controller.run(async progress => {
|
||||
while (progress.isRunning()) {
|
||||
return this.retryWithProgress(progress, selector, options, async (frame, info) => {
|
||||
progress.log(`waiting for selector "${selector}"`);
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, options);
|
||||
try {
|
||||
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
|
||||
} catch (e) {
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isSessionClosedError(e))
|
||||
throw e;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return undefined as any;
|
||||
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
|
||||
});
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
|
|
@ -1319,10 +1321,9 @@ export class Frame extends SdkObject {
|
|||
callbackText: string,
|
||||
taskData: T,
|
||||
options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean, querySelectorAll?: boolean, logScale?: boolean, omitAttached?: boolean }): Promise<R> {
|
||||
if (!progress.isRunning())
|
||||
progress.throwIfAborted();
|
||||
progress.throwIfAborted();
|
||||
const data = this._contextData.get(options.mainWorld ? 'main' : info!.world)!;
|
||||
|
||||
// This potentially runs in a sub-frame.
|
||||
{
|
||||
const rerunnableTask = new RerunnableTask<R>(data, progress, injectedScript => {
|
||||
return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached, snapshotName }) => {
|
||||
|
|
@ -1441,7 +1442,7 @@ export class Frame extends SdkObject {
|
|||
}, { source, arg });
|
||||
}
|
||||
|
||||
private async _resolveFrame(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions): Promise<{ frame: Frame, info: SelectorInfo }> {
|
||||
async resolveFrameForSelector(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<{ frame: Frame, info: SelectorInfo }> {
|
||||
const elementPath: dom.ElementHandle<Element>[] = [];
|
||||
progress.cleanupWhenAborted(() => {
|
||||
// Do not await here to avoid being blocked, either by stalled
|
||||
|
|
@ -1450,20 +1451,35 @@ export class Frame extends SdkObject {
|
|||
element.dispose();
|
||||
});
|
||||
|
||||
let frame: Frame = this;
|
||||
let frame: Frame | null = this;
|
||||
const frameChunks = splitSelectorByFrame(selector);
|
||||
|
||||
for (let i = 0; i < frameChunks.length - 1 && progress.isRunning(); ++i) {
|
||||
const info = this._page.parseSelector(frameChunks[i], options);
|
||||
const task = dom.waitForSelectorTask(info, 'attached');
|
||||
const task = dom.waitForSelectorTask(info, 'attached', false, i === 0 ? scope : undefined);
|
||||
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
const element = handle.asElement() as dom.ElementHandle<Element>;
|
||||
if (i < frameChunks.length - 1) {
|
||||
frame = (await element.contentFrame())!;
|
||||
element.dispose();
|
||||
if (!frame)
|
||||
throw new Error(`Selector "${stringifySelector(info.parsed)}" resolved to ${element.preview()}, <iframe> was expected`);
|
||||
}
|
||||
frame = await element.contentFrame();
|
||||
element.dispose();
|
||||
if (!frame)
|
||||
throw new Error(`Selector "${stringifySelector(info.parsed)}" resolved to ${element.preview()}, <iframe> was expected`);
|
||||
}
|
||||
return { frame, info: this._page.parseSelector(frameChunks[frameChunks.length - 1], options) };
|
||||
}
|
||||
|
||||
async resolveFrameForSelectorNoWait(selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<{ frame: Frame, info: SelectorInfo }> {
|
||||
let frame: Frame | null = this;
|
||||
const frameChunks = splitSelectorByFrame(selector);
|
||||
|
||||
for (let i = 0; i < frameChunks.length - 1; ++i) {
|
||||
const info = this._page.parseSelector(frameChunks[i], options);
|
||||
const element: dom.ElementHandle<Element> | null = await this._page.selectors.query(frame, info, i === 0 ? scope : undefined);
|
||||
if (!element)
|
||||
throw new Error(`Could not find frame while resolving "${stringifySelector(info.parsed)}"`);
|
||||
frame = await element.contentFrame();
|
||||
element.dispose();
|
||||
if (!frame)
|
||||
throw new Error(`Selector "${stringifySelector(info.parsed)}" resolved to ${element.preview()}, <iframe> was expected`);
|
||||
}
|
||||
return { frame, info: this._page.parseSelector(frameChunks[frameChunks.length - 1], options) };
|
||||
}
|
||||
|
|
@ -1519,6 +1535,10 @@ class RerunnableTask<T> {
|
|||
this._reject(e);
|
||||
}
|
||||
|
||||
// Unlike other places, we don't check frame for being detached since the whole scope of this
|
||||
// evaluation is within the frame's execution context. So we only let JavaScript errors and
|
||||
// session termination errors go through.
|
||||
|
||||
// We will try again in the new execution context.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ export class Selectors {
|
|||
this._engines.clear();
|
||||
}
|
||||
|
||||
async query(frame: frames.Frame, selector: string | SelectorInfo, options: { strict?: boolean }, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
const info = typeof selector === 'string' ? frame._page.parseSelector(selector, options) : selector;
|
||||
async query(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
const context = await frame._context(info.world);
|
||||
const injectedScript = await context.injectedScript();
|
||||
const handle = await injectedScript.evaluateHandle((injected, { parsed, scope, strict }) => {
|
||||
|
|
@ -83,8 +82,7 @@ export class Selectors {
|
|||
return this._adoptIfNeeded(elementHandle, mainContext);
|
||||
}
|
||||
|
||||
async _queryArray(frame: frames.Frame, selector: string | SelectorInfo, scope?: dom.ElementHandle): Promise<js.JSHandle<Element[]>> {
|
||||
const info = typeof selector === 'string' ? this.parseSelector(selector, false) : selector;
|
||||
async _queryArray(frame: frames.Frame, info: SelectorInfo, scope?: dom.ElementHandle): Promise<js.JSHandle<Element[]>> {
|
||||
const context = await frame._mainContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
||||
|
|
@ -93,7 +91,7 @@ export class Selectors {
|
|||
return arrayHandle;
|
||||
}
|
||||
|
||||
async _queryAll(frame: frames.Frame, selector: string | SelectorInfo, scope?: dom.ElementHandle, adoptToMain?: boolean): Promise<dom.ElementHandle<Element>[]> {
|
||||
async _queryAll(frame: frames.Frame, selector: SelectorInfo, scope?: dom.ElementHandle, adoptToMain?: boolean): Promise<dom.ElementHandle<Element>[]> {
|
||||
const info = typeof selector === 'string' ? frame._page.parseSelector(selector) : selector;
|
||||
const context = await frame._context(info.world);
|
||||
const injectedScript = await context.injectedScript();
|
||||
|
|
|
|||
|
|
@ -920,7 +920,8 @@ export class WKPage implements PageDelegate {
|
|||
const parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
throw new Error('Frame has been detached.');
|
||||
const handles = await this._page.selectors._queryAll(parent, 'frame,iframe', undefined);
|
||||
const info = this._page.parseSelector('frame,iframe');
|
||||
const handles = await this._page.selectors._queryAll(parent, info);
|
||||
const items = await Promise.all(handles.map(async handle => {
|
||||
const frame = await handle.contentFrame().catch(e => null);
|
||||
return { handle, frame };
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ async function routeIframe(page: Page) {
|
|||
});
|
||||
}
|
||||
|
||||
it('should work in iframe', async ({ page, server }) => {
|
||||
it('should work for iframe', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('iframe >> content-frame=true >> button');
|
||||
|
|
@ -56,7 +56,17 @@ it('should work in iframe', async ({ page, server }) => {
|
|||
await button.click();
|
||||
});
|
||||
|
||||
it('should work in nested iframe', async ({ page, server }) => {
|
||||
it('should work for iframe (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const body = await page.$('body');
|
||||
const button = await body.waitForSelector('iframe >> content-frame=true >> button');
|
||||
expect(await button.innerText()).toBe('Hello iframe');
|
||||
expect(await button.textContent()).toBe('Hello iframe');
|
||||
await button.click();
|
||||
});
|
||||
|
||||
it('should work for nested iframe', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('iframe >> content-frame=true >> iframe >> content-frame=true >> button');
|
||||
|
|
@ -66,6 +76,16 @@ it('should work in nested iframe', async ({ page, server }) => {
|
|||
await button.click();
|
||||
});
|
||||
|
||||
it('should work for nested iframe (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const body = await page.$('body');
|
||||
const button = await body.waitForSelector('iframe >> content-frame=true >> iframe >> content-frame=true >> button');
|
||||
expect(await button.innerText()).toBe('Hello nested iframe');
|
||||
expect(await button.textContent()).toBe('Hello nested iframe');
|
||||
await button.click();
|
||||
});
|
||||
|
||||
it('should work for $ and $$', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -75,6 +95,16 @@ it('should work for $ and $$', async ({ page, server }) => {
|
|||
expect(elements).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should work for $ and $$ (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const body = await page.$('body');
|
||||
const element = await body.$('iframe >> content-frame=true >> button');
|
||||
expect(await element.textContent()).toBe('Hello iframe');
|
||||
const elements = await body.$$('iframe >> content-frame=true >> span');
|
||||
expect(elements).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should work for $eval', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -82,6 +112,14 @@ it('should work for $eval', async ({ page, server }) => {
|
|||
expect(value).toBe('BUTTON');
|
||||
});
|
||||
|
||||
it('should work for $eval (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const body = await page.$('body');
|
||||
const value = await body.$eval('iframe >> content-frame=true >> button', b => b.nodeName);
|
||||
expect(value).toBe('BUTTON');
|
||||
});
|
||||
|
||||
it('should work for $$eval', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -89,6 +127,14 @@ it('should work for $$eval', async ({ page, server }) => {
|
|||
expect(value).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('should work for $$eval (handle)', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const body = await page.$('body');
|
||||
const value = await body.$$eval('iframe >> content-frame=true >> span', ss => ss.map(s => s.textContent));
|
||||
expect(value).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('should not allow dangling content-frame', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -98,6 +144,13 @@ it('should not allow dangling content-frame', async ({ page, server }) => {
|
|||
expect(error.message).toContain('iframe >> content-frame=true');
|
||||
});
|
||||
|
||||
it('should not allow leading content-frame', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const error = await page.waitForSelector('content-frame=true >> button').catch(e => e);
|
||||
expect(error.message).toContain('Selector cannot start with');
|
||||
});
|
||||
|
||||
it('should not allow capturing before content-frame', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -145,3 +198,60 @@ it('should click in lazy iframe', async ({ page, server }) => {
|
|||
]);
|
||||
expect(text).toBe('Hello iframe');
|
||||
});
|
||||
|
||||
it('waitFor should survive frame reattach', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
|
||||
const promise = button.waitFor();
|
||||
await page.locator('iframe').evaluate(e => e.remove());
|
||||
await page.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = 'iframe-2.html';
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('click should survive frame reattach', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
|
||||
const promise = button.click();
|
||||
await page.locator('iframe').evaluate(e => e.remove());
|
||||
await page.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = 'iframe-2.html';
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('click should survive iframe navigation', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
|
||||
const promise = button.click();
|
||||
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('click should survive navigation', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.PREFIX + '/iframe.html');
|
||||
const promise = page.click('button:has-text("Hello nested iframe")');
|
||||
await page.waitForTimeout(100);
|
||||
await page.goto(server.PREFIX + '/iframe-2.html');
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should fail if element removed while waiting on element handle', async ({ page, server }) => {
|
||||
it.fixme();
|
||||
await routeIframe(page);
|
||||
await page.goto(server.PREFIX + '/iframe.html');
|
||||
const button = await page.$('button');
|
||||
const promise = button.waitForSelector('something');
|
||||
await page.waitForTimeout(100);
|
||||
await page.evaluate(() => document.body.innerText = '');
|
||||
await promise;
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue