feat(iframe): make iframe selectors work w/ element handles (#10063)

This commit is contained in:
Pavel Feldman 2021-11-05 10:06:04 -08:00 committed by GitHub
parent 729ebe49c7
commit f19864890f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 266 additions and 118 deletions

View file

@ -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)) };
}

View file

@ -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;

View file

@ -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));
}

View file

@ -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 };

View file

@ -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.
}
}

View file

@ -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();

View file

@ -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 };

View file

@ -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;
});