diff --git a/packages/playwright-core/src/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/dispatchers/frameDispatcher.ts
index 914cc7e9c1..9c1a73c9a1 100644
--- a/packages/playwright-core/src/dispatchers/frameDispatcher.ts
+++ b/packages/playwright-core/src/dispatchers/frameDispatcher.ts
@@ -83,19 +83,19 @@ export class FrameDispatcher extends Dispatcher {
- 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 {
- 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 {
- 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 {
- 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)) };
}
diff --git a/packages/playwright-core/src/server/common/selectorParser.ts b/packages/playwright-core/src/server/common/selectorParser.ts
index e0c46dd3c2..7d14293203 100644
--- a/packages/playwright-core/src/server/common/selectorParser.ts
+++ b/packages/playwright-core/src/server/common/selectorParser.ts
@@ -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(' >> ');
}
diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts
index 361f84ad81..208aa65023 100644
--- a/packages/playwright-core/src/server/frames.ts
+++ b/packages/playwright-core/src/server/frames.ts
@@ -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;
@@ -704,9 +706,15 @@ export class Frame extends SdkObject {
return value;
}
- async querySelector(selector: string, options: types.StrictOptions): Promise | null> {
+ async querySelector(metadata: CallMetadata, selector: string, options: types.StrictOptions): Promise | 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 | 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 | 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 {
- 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 {
+ 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 {
- 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 {
+ 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[]> {
- return this._page.selectors._queryAll(this, selector, undefined, true /* adoptToMain */);
+ async querySelectorAll(metadata: CallMetadata, selector: string): Promise[]> {
+ 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 {
@@ -954,11 +973,11 @@ export class Frame extends SdkObject {
selector: string,
strict: boolean | undefined,
action: (handle: dom.ElementHandle) => Promise): Promise {
- 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;
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 {
- 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(
+ progress: Progress,
+ info: SelectorInfo,
+ callbackText: string,
+ taskData: T,
+ options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean, querySelectorAll?: boolean, logScale?: boolean, omitAttached?: boolean }): Promise {
+ if (!progress.isRunning())
+ progress.throwIfAborted();
+ const data = this._contextData.get(options.mainWorld ? 'main' : info!.world)!;
+
+ {
const rerunnableTask = new RerunnableTask(data, progress, injectedScript => {
return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached, snapshotName }) => {
const callback = injected.eval(callbackText) as DomTaskBody;
@@ -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(progress: Progress, world: types.World, task: dom.SchedulableTask): Promise> {
@@ -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[] = [];
+ 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;
+ if (i < frameChunks.length - 1) {
+ frame = (await element.contentFrame())!;
+ element.dispose();
+ if (!frame)
+ throw new Error(`Selector "${stringifySelector(info.parsed)}" resolved to ${element.preview()},