feat(frame-selector): intial implementation (#10018)
This commit is contained in:
parent
9b4d9460eb
commit
78e99249a3
|
|
@ -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(params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
return { value: serializeResult(await this._frame.evalOnSelectorAndWaitForSignals(metadata, 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(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
|
||||
return { value: serializeResult(await this._frame.evalOnSelectorAllAndWaitForSignals(metadata, 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(params.selector, params)) };
|
||||
return { element: ElementHandleDispatcher.fromNullable(this._scope, await this._frame.querySelector(metadata, params.selector, params)) };
|
||||
}
|
||||
|
||||
async querySelectorAll(params: channels.FrameQuerySelectorAllParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorAllResult> {
|
||||
const elements = await this._frame.querySelectorAll(params.selector);
|
||||
const elements = await this._frame.querySelectorAll(metadata, params.selector);
|
||||
return { elements: elements.map(e => ElementHandleDispatcher.from(this._scope, e)) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,37 @@ export function parseSelector(selector: string): ParsedSelector {
|
|||
};
|
||||
}
|
||||
|
||||
export function stringifySelector(selector: ParsedSelector): string {
|
||||
export function splitSelectorByFrame(selectorText: string): ParsedSelector[] {
|
||||
const selector = parseSelector(selectorText);
|
||||
const result: ParsedSelector[] = [];
|
||||
let chunk: ParsedSelector = {
|
||||
parts: [],
|
||||
};
|
||||
let chunkStartIndex = 0;
|
||||
for (let i = 0; i < selector.parts.length; ++i) {
|
||||
const part = selector.parts[i];
|
||||
if (part.name === 'content-frame') {
|
||||
result.push(chunk);
|
||||
chunk = { parts: [] };
|
||||
chunkStartIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (selector.capture === i)
|
||||
chunk.capture = i - chunkStartIndex;
|
||||
chunk.parts.push(part);
|
||||
}
|
||||
if (!chunk.parts.length)
|
||||
throw new Error(`Selector cannot end with "content-frame", while parsing selector ${selectorText}`);
|
||||
result.push(chunk);
|
||||
if (typeof selector.capture === 'number' && typeof result[result.length - 1].capture !== 'number')
|
||||
throw new Error(`Can not capture the selector before diving into the frame. Only use * after the last "content-frame"`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export function stringifySelector(selector: string | ParsedSelector): string {
|
||||
if (typeof selector === 'string')
|
||||
return selector;
|
||||
return selector.parts.map((p, i) => `${i === selector.capture ? '*' : ''}${p.name}=${p.source}`).join(' >> ');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation
|
|||
import type InjectedScript from './injected/injectedScript';
|
||||
import type { ElementStateWithoutStable, FrameExpectParams, InjectedScriptPoll, InjectedScriptProgress } from './injected/injectedScript';
|
||||
import { isSessionClosedError } from './common/protocolError';
|
||||
import { splitSelectorByFrame, stringifySelector } from './common/selectorParser';
|
||||
import { SelectorInfo } from './selectors';
|
||||
|
||||
type ContextData = {
|
||||
contextPromise: ManualPromise<dom.FrameExecutionContext | Error>;
|
||||
|
|
@ -704,9 +706,15 @@ export class Frame extends SdkObject {
|
|||
return value;
|
||||
}
|
||||
|
||||
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
async querySelector(metadata: CallMetadata, selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
|
||||
debugLogger.log('api', ` finding element using the selector "${selector}"`);
|
||||
return this._page.selectors.query(this, selector, options);
|
||||
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);
|
||||
}
|
||||
|
||||
async waitForSelector(metadata: CallMetadata, selector: string, options: types.WaitForElementOptions & { omitReturnValue?: boolean } = {}): Promise<dom.ElementHandle<Element> | null> {
|
||||
|
|
@ -718,12 +726,12 @@ export class Frame extends SdkObject {
|
|||
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 = dom.waitForSelectorTask(info, state, options.omitReturnValue);
|
||||
return controller.run(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"${state === 'attached' ? '' : ' to be ' + state}`);
|
||||
while (progress.isRunning()) {
|
||||
const result = await this._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, options);
|
||||
const task = dom.waitForSelectorTask(info, state, options.omitReturnValue);
|
||||
const result = await frame._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
if (!result.asElement()) {
|
||||
result.dispose();
|
||||
return null;
|
||||
|
|
@ -752,24 +760,35 @@ export class Frame extends SdkObject {
|
|||
await this._page._doSlowMo();
|
||||
}
|
||||
|
||||
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const handle = await this.querySelector(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(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 evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
|
||||
const arrayHandle = await this._page.selectors._queryArray(this, selector);
|
||||
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
|
||||
arrayHandle.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 querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
|
||||
return this._page.selectors._queryAll(this, selector, undefined, true /* adoptToMain */);
|
||||
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 content(): Promise<string> {
|
||||
|
|
@ -954,11 +973,11 @@ export class Frame extends SdkObject {
|
|||
selector: string,
|
||||
strict: boolean | undefined,
|
||||
action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
|
||||
const info = this._page.parseSelector(selector, { strict });
|
||||
while (progress.isRunning()) {
|
||||
progress.log(`waiting for selector "${selector}"`);
|
||||
const { frame, info } = await this._resolveFrame(progress, selector, { strict });
|
||||
const task = dom.waitForSelectorTask(info, 'attached');
|
||||
const handle = await this._scheduleRerunnableHandleTask(progress, info.world, task);
|
||||
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
|
||||
|
|
@ -1078,7 +1097,7 @@ 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.querySelector(selector, options);
|
||||
const element = await this._innerQuerySelector(progress, selector, options);
|
||||
return element ? await element.isVisible() : false;
|
||||
}, this._page._timeoutSettings.timeout({}));
|
||||
}
|
||||
|
|
@ -1276,12 +1295,35 @@ export class Frame extends SdkObject {
|
|||
taskData: T,
|
||||
options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean, querySelectorAll?: boolean, logScale?: boolean, omitAttached?: boolean } = {}): Promise<R> {
|
||||
|
||||
const info = this._page.parseSelector(selector, options);
|
||||
const callbackText = body.toString();
|
||||
const data = this._contextData.get(options.mainWorld ? 'main' : info.world)!;
|
||||
|
||||
return controller.run(async progress => {
|
||||
progress.log(`waiting for selector "${selector}"`);
|
||||
while (progress.isRunning()) {
|
||||
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;
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
private async _scheduleRerunnableTaskInFrame<T, R>(
|
||||
progress: Progress,
|
||||
info: SelectorInfo,
|
||||
callbackText: string,
|
||||
taskData: T,
|
||||
options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean, querySelectorAll?: boolean, logScale?: boolean, omitAttached?: boolean }): Promise<R> {
|
||||
if (!progress.isRunning())
|
||||
progress.throwIfAborted();
|
||||
const data = this._contextData.get(options.mainWorld ? 'main' : info!.world)!;
|
||||
|
||||
{
|
||||
const rerunnableTask = new RerunnableTask<R>(data, progress, injectedScript => {
|
||||
return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached, snapshotName }) => {
|
||||
const callback = injected.eval(callbackText) as DomTaskBody<T, R, Element | undefined>;
|
||||
|
|
@ -1321,13 +1363,12 @@ export class Frame extends SdkObject {
|
|||
});
|
||||
}, { info, taskData, callbackText, querySelectorAll: options.querySelectorAll, logScale: options.logScale, omitAttached: options.omitAttached, snapshotName: progress.metadata.afterSnapshot });
|
||||
}, true);
|
||||
|
||||
if (this._detached)
|
||||
rerunnableTask.terminate(new Error('Frame got detached.'));
|
||||
if (data.context)
|
||||
rerunnableTask.rerun(data.context);
|
||||
return await rerunnableTask.promise!;
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
|
||||
|
|
@ -1399,6 +1440,33 @@ export class Frame extends SdkObject {
|
|||
return injectedScript.extend(source, arg);
|
||||
}, { source, arg });
|
||||
}
|
||||
|
||||
private async _resolveFrame(progress: Progress, selector: string, options: types.StrictOptions & types.TimeoutOptions): Promise<{ frame: Frame, info: SelectorInfo }> {
|
||||
const elementPath: 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.
|
||||
for (const element of elementPath)
|
||||
element.dispose();
|
||||
});
|
||||
|
||||
let frame: Frame = 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 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`);
|
||||
}
|
||||
}
|
||||
return { frame, info: this._page.parseSelector(frameChunks[frameChunks.length - 1], options) };
|
||||
}
|
||||
}
|
||||
|
||||
class RerunnableTask<T> {
|
||||
|
|
@ -1509,3 +1577,4 @@ function verifyLifecycle(name: string, waitUntil: types.LifecycleEvent): types.L
|
|||
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
|
||||
return waitUntil;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { debugLogger } from '../utils/debugLogger';
|
|||
import { SelectorInfo, Selectors } from './selectors';
|
||||
import { CallMetadata, SdkObject } from './instrumentation';
|
||||
import { Artifact } from './artifact';
|
||||
import { ParsedSelector } from './common/selectorParser';
|
||||
|
||||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
|
|
@ -515,7 +516,7 @@ export class Page extends SdkObject {
|
|||
this.emit(Page.Events.PageError, error);
|
||||
}
|
||||
|
||||
parseSelector(selector: string, options?: types.StrictOptions): SelectorInfo {
|
||||
parseSelector(selector: string | ParsedSelector, options?: types.StrictOptions): SelectorInfo {
|
||||
const strict = typeof options?.strict === 'boolean' ? options.strict : !!this.context()._options.strictSelectors;
|
||||
return this.selectors.parseSelector(selector, strict);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import * as dom from './dom';
|
|||
import * as frames from './frames';
|
||||
import * as js from './javascript';
|
||||
import * as types from './types';
|
||||
import { ParsedSelector, parseSelector } from './common/selectorParser';
|
||||
import { ParsedSelector, parseSelector, stringifySelector } from './common/selectorParser';
|
||||
import { createGuid } from '../utils/utils';
|
||||
|
||||
export type SelectorInfo = {
|
||||
|
|
@ -44,7 +44,7 @@ export class Selectors {
|
|||
'data-testid', 'data-testid:light',
|
||||
'data-test-id', 'data-test-id:light',
|
||||
'data-test', 'data-test:light',
|
||||
'nth', 'visible'
|
||||
'nth', 'visible', 'content-frame'
|
||||
]);
|
||||
this._builtinEnginesInMainWorld = new Set([
|
||||
'_react', '_vue',
|
||||
|
|
@ -67,8 +67,8 @@ export class Selectors {
|
|||
this._engines.clear();
|
||||
}
|
||||
|
||||
async query(frame: frames.Frame, selector: string, options: { strict?: boolean }, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
|
||||
const info = frame._page.parseSelector(selector, options);
|
||||
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;
|
||||
const context = await frame._context(info.world);
|
||||
const injectedScript = await context.injectedScript();
|
||||
const handle = await injectedScript.evaluateHandle((injected, { parsed, scope, strict }) => {
|
||||
|
|
@ -83,8 +83,8 @@ export class Selectors {
|
|||
return this._adoptIfNeeded(elementHandle, mainContext);
|
||||
}
|
||||
|
||||
async _queryArray(frame: frames.Frame, selector: string, scope?: dom.ElementHandle): Promise<js.JSHandle<Element[]>> {
|
||||
const info = this.parseSelector(selector, false);
|
||||
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;
|
||||
const context = await frame._mainContext();
|
||||
const injectedScript = await context.injectedScript();
|
||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
||||
|
|
@ -93,8 +93,8 @@ export class Selectors {
|
|||
return arrayHandle;
|
||||
}
|
||||
|
||||
async _queryAll(frame: frames.Frame, selector: string, scope?: dom.ElementHandle, adoptToMain?: boolean): Promise<dom.ElementHandle<Element>[]> {
|
||||
const info = this.parseSelector(selector, false);
|
||||
async _queryAll(frame: frames.Frame, selector: string | 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();
|
||||
const arrayHandle = await injectedScript.evaluateHandle((injected, { parsed, scope }) => {
|
||||
|
|
@ -126,13 +126,13 @@ export class Selectors {
|
|||
return adopted;
|
||||
}
|
||||
|
||||
parseSelector(selector: string, strict: boolean): SelectorInfo {
|
||||
const parsed = parseSelector(selector);
|
||||
parseSelector(selector: string | ParsedSelector, strict: boolean): SelectorInfo {
|
||||
const parsed = typeof selector === 'string' ? parseSelector(selector) : selector;
|
||||
let needsMainWorld = false;
|
||||
for (const part of parsed.parts) {
|
||||
const custom = this._engines.get(part.name);
|
||||
if (!custom && !this._builtinEngines.has(part.name))
|
||||
throw new Error(`Unknown engine "${part.name}" while parsing selector ${selector}`);
|
||||
throw new Error(`Unknown engine "${part.name}" while parsing selector ${stringifySelector(parsed)}`);
|
||||
if (custom && !custom.contentScript)
|
||||
needsMainWorld = true;
|
||||
if (this._builtinEnginesInMainWorld.has(part.name))
|
||||
|
|
|
|||
147
tests/page/selectors-frame.spec.ts
Normal file
147
tests/page/selectors-frame.spec.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Page } from 'playwright-core';
|
||||
import { test as it, expect } from './pageTest';
|
||||
|
||||
async function routeIframe(page: Page) {
|
||||
await page.route('**/empty.html', route => {
|
||||
route.fulfill({
|
||||
body: '<iframe src="iframe.html"></iframe>',
|
||||
contentType: 'text/html'
|
||||
}).catch(() => {});
|
||||
});
|
||||
await page.route('**/iframe.html', route => {
|
||||
route.fulfill({
|
||||
body: `
|
||||
<html>
|
||||
<div>
|
||||
<button>Hello iframe</button>
|
||||
<iframe src="iframe-2.html"></iframe>
|
||||
</div>
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
</html>`,
|
||||
contentType: 'text/html'
|
||||
}).catch(() => {});
|
||||
});
|
||||
await page.route('**/iframe-2.html', route => {
|
||||
route.fulfill({
|
||||
body: '<html><button>Hello nested iframe</button></html>',
|
||||
contentType: 'text/html'
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
it('should work in iframe', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('iframe >> content-frame=true >> button');
|
||||
await button.waitFor();
|
||||
expect(await button.innerText()).toBe('Hello iframe');
|
||||
await expect(button).toHaveText('Hello iframe');
|
||||
await button.click();
|
||||
});
|
||||
|
||||
it('should work in 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');
|
||||
await button.waitFor();
|
||||
expect(await button.innerText()).toBe('Hello nested iframe');
|
||||
await expect(button).toHaveText('Hello nested iframe');
|
||||
await button.click();
|
||||
});
|
||||
|
||||
it('should work for $ and $$', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const element = await page.$('iframe >> content-frame=true >> button');
|
||||
expect(await element.textContent()).toBe('Hello iframe');
|
||||
const elements = await page.$$('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);
|
||||
const value = await page.$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);
|
||||
const value = await page.$$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);
|
||||
const button = page.locator('iframe >> content-frame=true');
|
||||
const error = await button.click().catch(e => e);
|
||||
expect(error.message).toContain('Selector cannot end with');
|
||||
expect(error.message).toContain('iframe >> content-frame=true');
|
||||
});
|
||||
|
||||
it('should not allow capturing before content-frame', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const button = page.locator('*css=iframe >> content-frame=true >> div');
|
||||
const error = await await button.click().catch(e => e);
|
||||
expect(error.message).toContain('Can not capture the selector before diving into the frame');
|
||||
});
|
||||
|
||||
it('should capture after the content-frame', async ({ page, server }) => {
|
||||
await routeIframe(page);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const div = page.locator('iframe >> content-frame=true >> *css=div >> button');
|
||||
expect(await div.innerHTML()).toContain('<button>');
|
||||
});
|
||||
|
||||
it('should click in lazy iframe', async ({ page, server }) => {
|
||||
await page.route('**/iframe.html', route => {
|
||||
route.fulfill({
|
||||
body: '<html><button>Hello iframe</button></html>',
|
||||
contentType: 'text/html'
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
// empty pge
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
||||
// add blank iframe
|
||||
setTimeout(() => {
|
||||
page.evaluate(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
// navigate iframe
|
||||
setTimeout(() => {
|
||||
page.evaluate(() => document.querySelector('iframe').src = 'iframe.html');
|
||||
}, 500);
|
||||
}, 500);
|
||||
|
||||
// Click in iframe
|
||||
const button = page.locator('iframe >> content-frame=true >> button');
|
||||
const [, text] = await Promise.all([
|
||||
button.click(),
|
||||
button.innerText(),
|
||||
expect(button).toHaveText('Hello iframe')
|
||||
]);
|
||||
expect(text).toBe('Hello iframe');
|
||||
});
|
||||
Loading…
Reference in a new issue