chore(frame-selector): add more tests, use frame logic in element handle (#10097)

This commit is contained in:
Pavel Feldman 2021-11-05 15:36:01 -08:00 committed by GitHub
parent 975a00ab31
commit f3fd3ebc37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 184 additions and 63 deletions

View File

@ -177,8 +177,12 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return null;
}
async isIframeElement(): Promise<boolean | 'error:notconnected'> {
return this.evaluateInUtility(([injected, node]) => node && (node.nodeName === 'IFRAME' || node.nodeName === 'FRAME'), {});
}
async contentFrame(): Promise<frames.Frame | null> {
const isFrameElement = throwRetargetableDOMError(await this.evaluateInUtility(([injected, node]) => node && (node.nodeName === 'IFRAME' || node.nodeName === 'FRAME'), {}));
const isFrameElement = throwRetargetableDOMError(await this.isIframeElement());
if (!isFrameElement)
return null;
return this._page._delegate.getContentFrame(this);
@ -692,21 +696,27 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async querySelector(selector: string, options: types.StrictOptions): Promise<ElementHandle | null> {
const { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, options, this);
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, options, this);
if (!pair)
return null;
const { frame, info } = pair;
// 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>[]> {
const { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
if (!pair)
return [];
const { frame, info } = pair;
// 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 { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, { strict }, this);
const pair = 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);
const handle = pair ? await this._page.selectors.query(pair.frame, pair.info, this._frame === pair.frame ? this : undefined) : null;
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
@ -715,7 +725,10 @@ 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 { frame, info } = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
const pair = await this._frame.resolveFrameForSelectorNoWait(selector, {}, this);
if (!pair)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
const { frame, info } = pair;
// 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);
@ -767,31 +780,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions = {}): Promise<ElementHandle<Element> | null> {
const { state = 'visible' } = options;
if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
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));
return this._frame.waitForSelector(metadata, selector, options, this);
}
async _adoptTo(context: FrameExecutionContext): Promise<ElementHandle<T>> {

View File

@ -712,11 +712,13 @@ export class Frame extends SdkObject {
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
debugLogger.log('api', ` finding element using the selector "${selector}"`);
const { frame, info } = await this.resolveFrameForSelectorNoWait(selector, options);
return this._page.selectors.query(frame, info);
const result = await this.resolveFrameForSelectorNoWait(selector, options);
if (!result)
return null;
return this._page.selectors.query(result.frame, result.info);
}
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions & { omitReturnValue?: boolean } = {}): Promise<dom.ElementHandle<Element> | null> {
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions & { omitReturnValue?: boolean }, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
const controller = new ProgressController(metadata, this);
if ((options as any).visibility)
throw new Error('options.visibility is not supported, did you mean options.state?');
@ -728,8 +730,11 @@ export class Frame extends SdkObject {
return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
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);
// Be careful, |this| can be different from |frame|.
const actualScope = this === frame ? scope : undefined;
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue, actualScope);
const result = actualScope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
: await frame._scheduleRerunnableHandleTask(progress, info.world, task);
if (!result.asElement()) {
result.dispose();
return null;
@ -742,7 +747,7 @@ export class Frame extends SdkObject {
} catch (e) {
return continuePolling;
}
});
}, scope);
}, this._page._timeoutSettings.timeout(options));
}
@ -754,8 +759,8 @@ export class Frame extends SdkObject {
}
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);
const pair = await this.resolveFrameForSelectorNoWait(selector, { strict });
const handle = pair ? await this._page.selectors.query(pair.frame, pair.info) : null;
if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
@ -764,16 +769,20 @@ export class Frame extends SdkObject {
}
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 pair = await this.resolveFrameForSelectorNoWait(selector, {});
if (!pair)
throw new Error(`Error: failed to find frame for selector "${selector}"`);
const arrayHandle = await this._page.selectors._queryArray(pair.frame, pair.info);
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
arrayHandle.dispose();
return result;
}
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 */);
const pair = await this.resolveFrameForSelectorNoWait(selector, {});
if (!pair)
return [];
return this._page.selectors._queryAll(pair.frame, pair.info, undefined, true /* adoptToMain */);
}
async content(): Promise<string> {
@ -961,7 +970,13 @@ export class Frame extends SdkObject {
scope?: dom.ElementHandle): Promise<R> {
const continuePolling = Symbol('continuePolling');
while (progress.isRunning()) {
const { frame, info } = await this.resolveFrameForSelector(progress, selector, options, scope);
const pair = await this._resolveFrameForSelector(progress, selector, options, scope);
if (!pair) {
// Missing content frame.
await new Promise(f => setTimeout(f, 100));
continue;
}
const { frame, info } = pair;
try {
const result = await action(frame, info, continuePolling);
if (result === continuePolling)
@ -987,6 +1002,7 @@ export class Frame extends SdkObject {
strict: boolean | undefined,
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
return this.retryWithProgress(progress, selector, { strict }, async (frame, info, continuePolling) => {
// Be careful, |this| can be different from |frame|.
progress.log(`waiting for selector "${selector}"`);
const task = dom.waitForSelectorTask(info, 'attached');
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
@ -1106,8 +1122,10 @@ export class Frame extends SdkObject {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
progress.log(` checking visibility of "${selector}"`);
const { frame, info } = await this.resolveFrameForSelector(progress, selector, options);
const element = await this._page.selectors.query(frame, info);
const pair = await this.resolveFrameForSelectorNoWait(selector, options);
if (!pair)
return false;
const element = await this._page.selectors.query(pair.frame, pair.info);
return element ? await element.isVisible() : false;
}, this._page._timeoutSettings.timeout({}));
}
@ -1309,6 +1327,7 @@ export class Frame extends SdkObject {
return controller.run(async progress => {
return this.retryWithProgress(progress, selector, options, async (frame, info) => {
// Be careful, |this| can be different from |frame|.
progress.log(`waiting for selector "${selector}"`);
return await frame._scheduleRerunnableTaskInFrame(progress, info, callbackText, taskData, options);
});
@ -1442,7 +1461,7 @@ export class Frame extends SdkObject {
}, { source, arg });
}
async resolveFrameForSelector(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<{ frame: Frame, info: SelectorInfo }> {
private async _resolveFrameForSelector(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions, scope?: dom.ElementHandle): Promise<{ frame: Frame, info: SelectorInfo } | null> {
const elementPath: dom.ElementHandle<Element>[] = [];
progress.cleanupWhenAborted(() => {
// Do not await here to avoid being blocked, either by stalled
@ -1457,8 +1476,31 @@ export class Frame extends SdkObject {
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', false, i === 0 ? scope : undefined);
const handle = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
const handle = i === 0 && scope ? await frame._runWaitForSelectorTaskOnce(progress, stringifySelector(info.parsed), info.world, task)
: await frame._scheduleRerunnableHandleTask(progress, info.world, task);
const element = handle.asElement() as dom.ElementHandle<Element>;
const isIframe = await element.isIframeElement();
if (isIframe === 'error:notconnected')
return null; // retry
if (!isIframe)
throw new Error(`Selector "${stringifySelector(info.parsed)}" resolved to ${element.preview()}, <iframe> was expected`);
frame = await element.contentFrame();
element.dispose();
if (!frame)
return null; // retry
}
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 } | null> {
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)
return null;
frame = await element.contentFrame();
element.dispose();
if (!frame)
@ -1467,21 +1509,17 @@ export class Frame extends SdkObject {
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`);
private async _runWaitForSelectorTaskOnce<T>(progress: Progress, selector: string, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
const context = await this._context(world);
const injected = await context.injectedScript();
try {
const pollHandler = new dom.InjectedScriptPollHandler(progress, await task(injected));
const result = await pollHandler.finishHandle();
progress.cleanupWhenAborted(() => result.dispose());
return result;
} catch (e) {
throw new Error(`Error: frame navigated while waiting for selector "${selector}"`);
}
return { frame, info: this._page.parseSelector(frameChunks[frameChunks.length - 1], options) };
}
}
@ -1508,6 +1546,7 @@ class RerunnableTask<T> {
terminate(error: Error) {
this._reject(error);
}
private _resolve(value: T | js.SmartHandle<T>) {
if (this.promise)
this.promise.resolve(value as T);

View File

@ -81,7 +81,8 @@ it('elementHandle.waitForSelector should throw on navigation', async ({ page, se
await page.evaluate(() => 1);
await page.goto(server.EMPTY_PAGE);
const error = await promise;
expect(error.message).toContain('Execution context was destroyed');
expect(error.message).toContain('Error: frame navigated while waiting for selector');
expect(error.message).toContain('span');
});
it('should work with removed MutationObserver', async ({ page, server }) => {

View File

@ -288,3 +288,23 @@ it('should work when navigating before node adoption', async ({ page, mode, serv
// This text is coming from /one-style.html
expect(await div.textContent()).toBe('hello, world!');
});
it('should fail when navigating while on handle', async ({ page, mode, server }) => {
it.skip(mode !== 'default');
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<div>Hello</div>`);
let navigatedOnce = false;
const __testHookBeforeAdoptNode = async () => {
if (!navigatedOnce) {
navigatedOnce = true;
await page.goto(server.PREFIX + '/one-style.html');
}
};
const body = await page.waitForSelector('body');
const error = await body.waitForSelector('div', { __testHookBeforeAdoptNode } as any).catch(e => e);
expect(error.message).toContain('Error: frame navigated while waiting for selector');
expect(error.message).toContain('"div"');
});

View File

@ -95,6 +95,46 @@ it('should work for $ and $$', async ({ page, server }) => {
expect(elements).toHaveLength(2);
});
it('$ should not wait for frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
expect(await page.$('iframe >> content-frame=true >> canvas')).toBeFalsy();
const body = await page.$('body');
expect(await body.$('iframe >> content-frame=true >> canvas')).toBeFalsy();
});
it('$$ should not wait for frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
expect(await page.$$('iframe >> content-frame=true >> canvas')).toHaveLength(0);
const body = await page.$('body');
expect(await body.$$('iframe >> content-frame=true >> canvas')).toHaveLength(0);
});
it('$eval should throw for missing frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
{
const error = await page.$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find element matching selector');
}
{
const body = await page.$('body');
const error = await body.$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find element matching selector');
}
});
it('$$eval should throw for missing frame', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
{
const error = await page.$$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find frame for selector');
}
{
const body = await page.$('body');
const error = await body.$$eval('iframe >> content-frame=true >> canvas', e => 1).catch(e => e);
expect(error.message).toContain('Error: failed to find frame for selector');
}
});
it('should work for $ and $$ (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
@ -213,6 +253,29 @@ it('waitFor should survive frame reattach', async ({ page, server }) => {
await promise;
});
it('waitForSelector should survive frame reattach (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const promise = body.waitForSelector('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
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('waitForSelector should survive iframe navigation (handle)', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
const body = await page.$('body');
const promise = body.waitForSelector('iframe >> content-frame=true >> button:has-text("Hello nested iframe")');
page.locator('iframe').evaluate(e => (e as HTMLIFrameElement).src = 'iframe-2.html');
await promise;
});
it('click should survive frame reattach', async ({ page, server }) => {
await routeIframe(page);
await page.goto(server.EMPTY_PAGE);
@ -255,3 +318,12 @@ it('should fail if element removed while waiting on element handle', async ({ pa
await page.evaluate(() => document.body.innerText = '');
await promise;
});
it('should non work for non-frame', async ({ page, server }) => {
await routeIframe(page);
await page.setContent('<div></div>');
const button = page.locator('div >> content-frame=true >> button');
const error = await button.waitFor().catch(e => e);
expect(error.message).toContain('<div></div>');
expect(error.message).toContain('<iframe> was expected');
});