2026-05-20 21:39:12 +08:00

9407 lines
404 KiB
Diff
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NOTE: This patch file is generated automatically and is not used, it is only for documentation. The driver is actually patched using [patchright_driver_patch](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/blob/main/patchright_driver_patch.ts), see [the workflow](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright/blob/main/.github/workflows/patch_file_updater.yml)
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/cli/program.ts patchright/node_modules/playwright-core/src/cli/program.ts
---
+++
@@ -1,21 +1,3 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * 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.
- */
-
-/* eslint-disable no-console */
-
import '../bootstrap';
import { gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils';
import { addTraceCommands } from '../tools/trace/traceCli';
@@ -69,10 +51,10 @@
program
.command('install [browser...]')
- .description('ensure browsers necessary for this version of Playwright are installed')
+ .description('ensure browsers necessary for this version of Patchright are installed')
.option('--with-deps', 'install system dependencies for browsers')
.option('--dry-run', 'do not execute installation, only print information')
- .option('--list', 'prints list of browsers from all playwright installations')
+ .option('--list', 'prints list of browsers from all patchright installations')
.option('--force', 'force reinstall of already installed browsers')
.option('--only-shell', 'only install headless shell when installing chromium')
.option('--no-shell', 'do not install chromium headless shell')
@@ -95,8 +77,8 @@
program
.command('uninstall')
- .description('Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.')
- .option('--all', 'Removes all browsers used by any Playwright installation from the system.')
+ .description('Removes browsers used by this installation of Patchright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.')
+ .option('--all', 'Removes all browsers used by any Patchright installation from the system.')
.action(async (options: { all?: boolean }) => {
const { uninstallBrowsers } = await import('./installActions');
uninstallBrowsers(options).catch(logErrorAndExit);
@@ -284,7 +266,7 @@
.option('--save-har-glob <glob pattern>', 'filter entries in the HAR by matching url against this glob pattern')
.option('--save-storage <filename>', 'save context storage state at the end, for later use with --load-storage')
.option('--timezone <time zone>', 'time zone to emulate, for example "Europe/Rome"')
- .option('--timeout <timeout>', 'timeout for Playwright actions in milliseconds, no timeout by default')
+ .option('--timeout <timeout>', 'timeout for Patchright actions in milliseconds, no timeout by default')
.option('--user-agent <ua string>', 'specify user agent string')
.option('--user-data-dir <directory>', 'use the specified user data directory instead of a new context')
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"');
@@ -293,14 +275,14 @@
function buildBasePlaywrightCLICommand(cliTargetLang: string | undefined): string {
switch (cliTargetLang) {
case 'python':
- return `playwright`;
+ return `patchright`;
case 'java':
return `mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="...options.."`;
case 'csharp':
return `pwsh bin/Debug/netX/playwright.ps1`;
default: {
const packageManagerCommand = getPackageManagerExecCommand();
- return `${packageManagerCommand} playwright`;
+ return `${packageManagerCommand} patchright`;
}
}
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/cli/programWithTestStub.ts patchright/node_modules/playwright-core/src/cli/programWithTestStub.ts
---
+++
@@ -1,21 +1,3 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * 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.
- */
-
-/* eslint-disable no-console */
-
import { gracefullyProcessExitDoNotHang } from '../server/utils/processLauncher';
import { getPackageManager } from '../utils';
import { program } from './program';
@@ -34,24 +16,24 @@
packages.push('playwright');
const packageManager = getPackageManager();
if (packageManager === 'yarn') {
- console.error(`Please install @playwright/test package before running "yarn playwright ${command}"`);
+ console.error(`Please install @playwright/test package before running "yarn patchright ${command}"`);
console.error(` yarn remove ${packages.join(' ')}`);
console.error(' yarn add -D @playwright/test');
} else if (packageManager === 'pnpm') {
- console.error(`Please install @playwright/test package before running "pnpm exec playwright ${command}"`);
+ console.error(`Please install @playwright/test package before running "pnpm exec patchright ${command}"`);
console.error(` pnpm remove ${packages.join(' ')}`);
console.error(' pnpm add -D @playwright/test');
} else {
- console.error(`Please install @playwright/test package before running "npx playwright ${command}"`);
+ console.error(`Please install @playwright/test package before running "npx patchright ${command}"`);
console.error(` npm uninstall ${packages.join(' ')}`);
console.error(' npm install -D @playwright/test');
}
}
const kExternalPlaywrightTestCommands = [
- ['test', 'Run tests with Playwright Test.'],
- ['show-report', 'Show Playwright Test HTML report.'],
- ['merge-reports', 'Merge Playwright Test Blob reports'],
+ ['test', 'Run tests with Patchright Test.'],
+ ['show-report', 'Show Patchright Test HTML report.'],
+ ['merge-reports', 'Merge Patchright Test Blob reports'],
];
function addExternalPlaywrightTestCommands() {
for (const [command, description] of kExternalPlaywrightTestCommands) {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/browserContext.ts patchright/node_modules/playwright-core/src/client/browserContext.ts
---
+++
@@ -146,9 +146,9 @@
// a) removing "dialog" listener subscription (client->server)
// b) actual "dialog" event (server->client)
if (dialogObject.type() === 'beforeunload')
- dialog.accept({}).catch(() => {});
+ dialogObject._wrapApiCall(() => dialog.accept({}).catch(() => {}), { internal: true });
else
- dialog.dismiss().catch(() => {});
+ dialogObject._wrapApiCall(() => dialog.dismiss().catch(() => {}), { internal: true });
}
});
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
@@ -356,17 +356,20 @@
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
+ await this.installInjectRoute();
const source = await evaluationScript(this._platform, script, arg);
return DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
}
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}): Promise<DisposableObject> {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
this._bindings.set(name, callback);
return DisposableObject.from(result.disposable);
}
async exposeFunction(name: string, callback: Function): Promise<DisposableObject> {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name });
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
this._bindings.set(name, binding);
@@ -560,6 +563,25 @@
await this._channel.exposeConsoleApi();
}
+ routeInjecting: boolean = false;
+
+ async installInjectRoute() {
+
+ if (this.routeInjecting) return;
+ await this.route('**/*', async route => {
+ try {
+ if (route.request().resourceType() === 'document' && route.request().url().startsWith('http')) {
+ await route.fallback({ patchrightInitScript: true } as any);
+ } else {
+ await route.fallback();
+ }
+ } catch (error) {
+ await route.fallback();
+ }
+ });
+ this.routeInjecting = true;
+
+ }
}
async function prepareStorageState(platform: Platform, storageState: string | SetStorageState): Promise<NonNullable<channels.BrowserNewContextParams['storageState']>> {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/clientHelper.ts patchright/node_modules/playwright-core/src/client/clientHelper.ts
---
+++
@@ -50,5 +50,5 @@
}
export function addSourceUrlToScript(source: string, path: string): string {
- return `${source}\n//# sourceURL=${path.replace(/\n/g, '')}`;
+ return source
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/clock.ts patchright/node_modules/playwright-core/src/client/clock.ts
---
+++
@@ -25,6 +25,7 @@
}
async install(options: { time?: number | string | Date } = { }) {
+ await this._browserContext.installInjectRoute()
await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/frame.ts patchright/node_modules/playwright-core/src/client/frame.ts
---
+++
@@ -177,25 +177,34 @@
}
async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise<void> {
- if (urlMatches(this._page?.context()._options.baseURL, this.url(), url))
- return await this.waitForLoadState(options.waitUntil, options);
- await this.waitForNavigation({ url, ...options });
+ if (urlMatches(this._page?.context()._options.baseURL, this.url(), url))
+ return await this.waitForLoadState(options.waitUntil, options);
+ try {
+ await this.waitForNavigation({ url, ...options });
+ } catch (error) {
+ if (urlMatches(this._page?.context()._options.baseURL, this.url(), url)) {
+ await this.waitForLoadState(options.waitUntil, options);
+ return;
+ }
+ throw error;
+ }
+
}
async frameElement(): Promise<ElementHandle> {
return ElementHandle.from((await this._channel.frameElement()).element);
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return parseResult(result.value);
}
@@ -231,9 +240,9 @@
return parseResult(result.value);
}
- async $$eval<R, Arg>(selector: string, pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 3);
- const result = await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async $$eval<R, Arg>(selector: string, pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 4);
+ const result = await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return parseResult(result.value);
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/jsHandle.ts patchright/node_modules/playwright-core/src/client/jsHandle.ts
---
+++
@@ -36,14 +36,28 @@
this._channel.on('previewUpdated', ({ preview }) => this._preview = preview);
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg): Promise<R> {
- const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
- return parseResult(result.value);
+ async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+
+ const result = await this._channel.evaluateExpression({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === 'function',
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ });
+ return parseResult(result.value);
+
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
- return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+
+ const result = await this._channel.evaluateExpressionHandle({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === 'function',
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ });
+ return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
+
}
async getProperty(propertyName: string): Promise<JSHandle> {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/locator.ts patchright/node_modules/playwright-core/src/client/locator.ts
---
+++
@@ -27,7 +27,7 @@
import type * as api from '../../types/types';
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
import type * as channels from '@protocol/channels';
-
+import { JSHandle, parseResult, serializeArgument } from "./jsHandle";
export type LocatorOptions = {
hasText?: string | RegExp;
@@ -124,16 +124,56 @@
});
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<R> {
- return await this._withElement(h => h.evaluate(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout });
+ async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg?: Arg, options?: TimeoutOptions, isolatedContext: boolean = true): Promise<R> {
+
+ if (typeof options === 'boolean') {
+ isolatedContext = options;
+ options = undefined;
+ }
+ return await this._withElement(
+ async (h) =>
+ parseResult(
+ (
+ await h._channel.evaluateExpression({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === "function",
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ })
+ ).value
+ ),
+ { title: "Evaluate", timeout: options?.timeout }
+ );
+
}
- async evaluateAll<R, Arg>(pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg): Promise<R> {
- return await this._frame.$$eval(this._selector, pageFunction, arg);
+ async evaluateAll<R, Arg>(pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+
+ return await this._frame.$$eval(this._selector, pageFunction, arg, isolatedContext);
+
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<any, Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<structs.SmartHandle<R>> {
- return await this._withElement(h => h.evaluateHandle(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout });
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<any, Arg, R>, arg?: Arg, options?: TimeoutOptions, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+
+ if (typeof options === 'boolean') {
+ isolatedContext = options;
+ options = undefined;
+ }
+ return await this._withElement(
+ async (h) =>
+ JSHandle.from(
+ (
+ await h._channel.evaluateExpressionHandle({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === "function",
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ })
+ ).handle
+ ) as any as structs.SmartHandle<R>,
+ { title: "Evaluate", timeout: options?.timeout }
+ );
+
}
async fill(value: string, options: channels.ElementHandleFillOptions & TimeoutOptions = {}): Promise<void> {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/network.ts patchright/node_modules/playwright-core/src/client/network.ts
---
+++
@@ -15,7 +15,7 @@
*/
import { ChannelOwner } from './channelOwner';
-import { isTargetClosedError } from './errors';
+import { isTargetClosedError, TargetClosedError } from './errors';
import { Events } from './events';
import { APIResponse } from './fetch';
import { Frame } from './frame';
@@ -183,7 +183,13 @@
}
async allHeaders(): Promise<Headers> {
- return (await this._actualHeaders()).headers();
+
+ const headers = await this._actualHeaders();
+ const page = this._safePage();
+ if (page?._closeWasCalled)
+ throw new TargetClosedError();
+ return headers.headers();
+
}
async headersArray(): Promise<HeadersArray> {
@@ -272,19 +278,22 @@
}
_applyFallbackOverrides(overrides: FallbackOverrides) {
- if (overrides.url)
- this._fallbackOverrides.url = overrides.url;
- if (overrides.method)
- this._fallbackOverrides.method = overrides.method;
- if (overrides.headers)
- this._fallbackOverrides.headers = overrides.headers;
- if (isString(overrides.postData))
- this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, 'utf-8');
- else if (overrides.postData instanceof Buffer)
- this._fallbackOverrides.postDataBuffer = overrides.postData;
- else if (overrides.postData)
- this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), 'utf-8');
+ if (overrides.url)
+ this._fallbackOverrides.url = overrides.url;
+ if (overrides.method)
+ this._fallbackOverrides.method = overrides.method;
+ if (overrides.headers)
+ this._fallbackOverrides.headers = overrides.headers;
+ if ((overrides as any).patchrightInitScript)
+ (this._fallbackOverrides as any).patchrightInitScript = true;
+ if (isString(overrides.postData))
+ this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, "utf-8");
+ else if (overrides.postData instanceof Buffer)
+ this._fallbackOverrides.postDataBuffer = overrides.postData;
+ else if (overrides.postData)
+ this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), "utf-8");
+
}
_fallbackOverridesForContinue() {
@@ -449,6 +458,7 @@
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
postData: options.postDataBuffer,
isFallback,
+ patchrightInitScript: (options as any).patchrightInitScript
}));
}
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/page.ts patchright/node_modules/playwright-core/src/client/page.ts
---
+++
@@ -313,9 +313,9 @@
return await this._mainFrame.dispatchEvent(selector, type, eventInit, options);
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- assertMaxArguments(arguments.length, 2);
- return await this._mainFrame.evaluateHandle(pageFunction, arg);
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+ assertMaxArguments(arguments.length, 3);
+ return await this._mainFrame.evaluateHandle(pageFunction, arg, isolatedContext);
}
async $eval<R, Arg>(selector: string, pageFunction: structs.PageFunctionOn<Element, Arg, R>, arg?: Arg): Promise<R> {
@@ -341,6 +341,7 @@
}
async exposeFunction(name: string, callback: Function) {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name });
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
this._bindings.set(name, binding);
@@ -348,6 +349,7 @@
}
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}) {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
this._bindings.set(name, callback);
return DisposableObject.from(result.disposable);
@@ -507,12 +509,13 @@
return this._viewportSize || null;
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 2);
- return await this._mainFrame.evaluate(pageFunction, arg);
+ async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 3);
+ return await this._mainFrame.evaluate(pageFunction, arg, isolatedContext);
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
+ await this.installInjectRoute();
const source = await evaluationScript(this._platform, script, arg);
return DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
}
@@ -864,6 +867,26 @@
async _setDockTile(image: Buffer) {
await this._channel.setDockTile({ image });
}
+
+ routeInjecting: boolean = false;
+
+ async installInjectRoute() {
+
+ if (this.routeInjecting || this.context().routeInjecting) return;
+ await this.route('**/*', async route => {
+ try {
+ if (route.request().resourceType() === 'document' && route.request().url().startsWith('http')) {
+ await route.fallback({ patchrightInitScript: true } as any);
+ } else {
+ await route.fallback();
+ }
+ } catch (error) {
+ await route.fallback();
+ }
+ });
+ this.routeInjecting = true;
+
+ }
}
export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/tracing.ts patchright/node_modules/playwright-core/src/client/tracing.ts
---
+++
@@ -38,6 +38,7 @@
}
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, live?: boolean } = {}) {
+ if (typeof this._parent.installInjectRoute === 'function') await this._parent.installInjectRoute();
await this._wrapApiCall(async () => {
this._includeSources = !!options.sources;
this._isLive = !!options.live;
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/client/worker.ts patchright/node_modules/playwright-core/src/client/worker.ts
---
+++
@@ -62,15 +62,15 @@
return this._initializer.url;
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return parseResult(result.value);
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/android/android.ts patchright/node_modules/playwright-core/src/server/android/android.ts
---
+++
@@ -1,19 +1,3 @@
-/**
- * Copyright 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 { EventEmitter } from 'events';
import fs from 'fs';
import os from 'os';
@@ -184,7 +168,7 @@
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
const fullName = path.join(executable.directory!, file);
if (!fs.existsSync(fullName))
- throw new Error(`Please install Android driver apk using '${packageManagerCommand} playwright install android'`);
+ throw new Error(`Please install Android driver apk using '${packageManagerCommand} patchright install android'`);
await this.installApk(progress, await progress.race(fs.promises.readFile(fullName)));
}
} else {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/browserContext.ts patchright/node_modules/playwright-core/src/server/browserContext.ts
---
+++
@@ -167,7 +167,7 @@
await this.exposeConsoleApi();
if (this._options.serviceWorkers === 'block')
- await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
+ await this.addInitScript(`if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { };`);
if (this._options.permissions)
await this.grantPermissions(this._options.permissions);
@@ -352,18 +352,13 @@
if (page.getBinding(name))
throw new Error(`Function "${name}" has been already registered in one of the pages`);
}
- await progress.race(this.exposePlaywrightBindingIfNeeded());
const binding = new PageBinding(this, name, playwrightBinding, needsHandle);
binding.forClient = forClient;
this._pageBindings.set(name, binding);
- try {
- await progress.race(this.doAddInitScript(binding.initScript));
- await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
- return binding;
- } catch (error) {
- this._pageBindings.delete(name);
- throw error;
- }
+
+ await this.doExposeBinding(binding);
+ return binding;
+
}
async removeExposedBinding(binding: PageBinding) {
@@ -881,4 +876,5 @@
strictSelectors: false,
serviceWorkers: 'allow',
locale: 'en-US',
+ focusControl: false
};
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/chromium.ts patchright/node_modules/playwright-core/src/server/chromium/chromium.ts
---
+++
@@ -320,8 +320,6 @@
const chromeArguments = [...chromiumSwitches(options.assistantMode, options.channel)];
// See https://issues.chromium.org/issues/40277080
- chromeArguments.push('--enable-unsafe-swiftshader');
-
if (options.headless) {
chromeArguments.push('--headless');
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/chromiumSwitches.ts patchright/node_modules/playwright-core/src/server/chromium/chromiumSwitches.ts
---
+++
@@ -51,26 +51,16 @@
'--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
'--disable-background-networking',
'--disable-background-timer-throttling',
- '--disable-backgrounding-occluded-windows',
- '--disable-back-forward-cache', // Avoids surprises like main request not being intercepted during page.goBack().
- '--disable-breakpad',
- '--disable-client-side-phishing-detection',
- '--disable-component-extensions-with-background-pages',
- '--disable-component-update', // Avoids unneeded network activity after startup.
+ '--disable-backgrounding-occluded-windows', // Avoids surprises like main request not being intercepted during page.goBack().
+ '--disable-breakpad', // Avoids unneeded network activity after startup.
'--no-default-browser-check',
- '--disable-default-apps',
'--disable-dev-shm-usage',
- '--disable-extensions',
'--disable-features=' + disabledFeatures(assistantMode).join(','),
process.env.PLAYWRIGHT_LEGACY_SCREENSHOT ? '' : '--enable-features=CDPScreenshotNewSurface',
- '--allow-pre-commit-input',
'--disable-hang-monitor',
- '--disable-ipc-flooding-protection',
- '--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--force-color-profile=srgb',
- '--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--use-mock-keychain',
@@ -79,11 +69,8 @@
'--export-tagged-pdf',
// https://chromium-review.googlesource.com/c/chromium/src/+/4853540
'--disable-search-engine-choice-screen',
- // https://issues.chromium.org/41491762
- '--unsafely-disable-devtools-self-xss-warnings',
// Edge can potentially restart on Windows (msRelaunchNoCompatLayer) which looses its file descriptors (stdout/stderr) and CDP (3/4). Disable until fixed upstream.
'--edge-skip-compat-layer-relaunch',
- assistantMode ? '' : '--enable-automation',
// This disables Chrome for Testing infobar that is visible in the persistent context.
// The switch is ignored everywhere else, including Chromium/Chrome/Edge.
'--disable-infobars',
@@ -91,4 +78,5 @@
'--disable-search-engine-choice-screen',
// Prevents the "three dots" menu crash in IdentityManager::HasPrimaryAccount for ephemeral contexts.
android ? '' : '--disable-sync',
+ '--disable-blink-features=AutomationControlled'
].filter(Boolean);
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/crBrowser.ts patchright/node_modules/playwright-core/src/server/chromium/crBrowser.ts
---
+++
@@ -522,8 +522,10 @@
}
async doRemoveInitScripts(initScripts: InitScript[]) {
- for (const page of this.pages())
- await (page.delegate as CRPage).removeInitScripts(initScripts);
+
+ for (const page of this.pages())
+ await (page.delegate as CRPage).removeInitScripts(initScripts);
+
}
async doUpdateRequestInterception(): Promise<void> {
@@ -611,6 +613,20 @@
const rootSession = await this._browser._clientRootSession();
return rootSession.attachToTarget(targetId);
}
+
+ async doExposeBinding(binding: PageBinding) {
+
+ for (const page of this.pages())
+ await (page.delegate as CRPage).exposeBinding(binding);
+
+ }
+
+ async doRemoveExposedBindings() {
+
+ for (const page of this.pages())
+ await (page.delegate as CRPage).removeExposedBindings();
+
+ }
}
export function shouldProxyLoopback(bypass: string | undefined) {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/crCoverage.ts patchright/node_modules/playwright-core/src/server/chromium/crCoverage.ts
---
+++
@@ -82,10 +82,13 @@
this._scriptIds.clear();
this._scriptSources.clear();
this._eventListeners = [
- eventsHelper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
- eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
- eventsHelper.addEventListener(this._client, 'Debugger.paused', this._onDebuggerPaused.bind(this)),
- ];
+ eventsHelper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
+
+ eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
+ eventsHelper.addEventListener(this._client, 'Page.frameNavigated', this._onFrameNavigated.bind(this)),
+
+ eventsHelper.addEventListener(this._client, 'Debugger.paused', this._onDebuggerPaused.bind(this)),
+ ];
await Promise.all([
this._client.send('Profiler.enable'),
this._client.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }),
@@ -142,6 +145,11 @@
}
return coverage;
}
+
+ _onFrameNavigated(event: Protocol.Page.frameNavigatedPayload) {
+ if (event.frame.parentId) return;
+ this._onExecutionContextsCleared();
+ }
}
class CSSCoverage {
@@ -169,9 +177,12 @@
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
this._eventListeners = [
- eventsHelper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
- eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
- ];
+ eventsHelper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
+
+ eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
+ eventsHelper.addEventListener(this._client, 'Page.frameNavigated', this._onFrameNavigated.bind(this)),
+
+ ];
await Promise.all([
this._client.send('DOM.enable'),
this._client.send('CSS.enable'),
@@ -235,6 +246,11 @@
return coverage;
}
+
+ _onFrameNavigated(event: Protocol.Page.frameNavigatedPayload) {
+ if (event.frame.parentId) return;
+ this._onExecutionContextsCleared();
+ }
}
function convertToDisjointRanges(nestedRanges: {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/crDevTools.ts patchright/node_modules/playwright-core/src/server/chromium/crDevTools.ts
---
+++
@@ -64,7 +64,6 @@
}).catch(e => null);
});
Promise.all([
- session.send('Runtime.enable'),
session.send('Runtime.addBinding', { name: kBindingName }),
session.send('Page.enable'),
session.send('Page.addScriptToEvaluateOnNewDocument', { source: `
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/crNetworkManager.ts patchright/node_modules/playwright-core/src/server/chromium/crNetworkManager.ts
---
+++
@@ -30,7 +30,7 @@
import type * as types from '../types';
import type { CRPage } from './crPage';
import type { CRServiceWorker } from './crServiceWorker';
-
+import crypto from "crypto";
type SessionInfo = {
session: CRSession;
@@ -97,6 +97,7 @@
if (info)
eventsHelper.removeEventListeners(info.eventListeners);
this._sessions.delete(session);
+ if (!this._sessions.size) this._alreadyTrackedNetworkIds.clear();
}
private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
@@ -142,6 +143,10 @@
async setRequestInterception(value: boolean) {
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
+
+ if (this._page)
+ await this._forEachSession(info => info.session.send('Network.setCacheDisabled', { cacheDisabled: this._page.needsRequestInterception() }));
+
}
async _updateProtocolRequestInterception() {
@@ -156,7 +161,11 @@
const enabled = this._protocolRequestInterceptionEnabled;
if (initial && !enabled)
return;
- const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
+
+ const hasHarRecorders = !!this._page?.browserContext?._harRecorders?.size;
+ const userInterception = this._page ? this._page.needsRequestInterception() : false;
+ const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: userInterception || hasHarRecorders });
+
let fetchPromise = Promise.resolve<any>(undefined);
if (!info.workerFrame) {
if (enabled)
@@ -238,6 +247,7 @@
}
_onRequestPaused(sessionInfo: SessionInfo, event: Protocol.Fetch.requestPausedPayload) {
+ if (this._alreadyTrackedNetworkIds.has(event.networkId)) return;
if (!event.networkId) {
// Fetch without networkId means that request was not recognized by inspector, and
// it will never receive Network.requestWillBeSent. Continue the request to not affect it.
@@ -276,6 +286,10 @@
}
_onRequest(requestWillBeSentSessionInfo: SessionInfo, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedSessionInfo: SessionInfo | undefined, requestPausedEvent: Protocol.Fetch.requestPausedPayload | undefined) {
+
+ if (this._alreadyTrackedNetworkIds.has(requestWillBeSentEvent.requestId))
+ return;
+
if (requestWillBeSentEvent.request.url.startsWith('data:'))
return;
let redirectedFrom: InterceptableRequest | null = null;
@@ -287,6 +301,13 @@
redirectedFrom = request;
}
}
+ const isInterceptedOptionsPreflight = !!requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && requestWillBeSentEvent.initiator.type === 'preflight';
+
+ if (isInterceptedOptionsPreflight && !(this._page || this._serviceWorker).needsRequestInterception()) {
+ requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent!.requestId });
+ return;
+ }
+
let frame = requestWillBeSentEvent.frameId ? this._page?.frameManager.frame(requestWillBeSentEvent.frameId) : requestWillBeSentSessionInfo.workerFrame;
// Requests from workers lack frameId, because we receive Network.requestWillBeSent
// on the worker target. However, we receive Fetch.requestPaused on the page target,
@@ -306,7 +327,6 @@
// we accept all CORS options, assuming that this was intended when setting route.
//
// Note: it would be better to match the URL against interception patterns.
- const isInterceptedOptionsPreflight = !!requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && requestWillBeSentEvent.initiator.type === 'preflight';
if (isInterceptedOptionsPreflight && (this._page || this._serviceWorker)!.needsRequestInterception()) {
const requestHeaders = requestPausedEvent.request.headers;
const responseHeaders: Protocol.Fetch.HeaderEntry[] = [
@@ -346,7 +366,7 @@
}
requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers: headersOverride });
} else {
- route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId);
+ route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId, this._page, requestPausedEvent.networkId ?? requestPausedEvent.requestId, this);
}
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@@ -558,6 +578,8 @@
if (request.session !== sessionInfo.session && !sessionInfo.isMain && (request._documentId === request._requestId || sessionInfo.workerFrame))
request.session = sessionInfo.session;
}
+
+ _alreadyTrackedNetworkIds: Set<string> = new Set();
}
class InterceptableRequest {
@@ -614,38 +636,149 @@
_alreadyContinuedParams: Protocol.Fetch.continueRequestParameters | undefined;
_fulfilled: boolean = false;
- constructor(session: CRSession, interceptionId: string) {
+ constructor(session: CRSession, interceptionId: string, page: Page | null, networkId: string, sessionManager: CRNetworkManager) {
+ this._page = void 0;
+ this._networkId = void 0;
+ this._sessionManager = void 0;
this._session = session;
this._interceptionId = interceptionId;
+ this._page = page;
+ this._networkId = networkId;
+ this._sessionManager = sessionManager;
+ eventsHelper.addEventListener(this._session, 'Fetch.requestPaused', async e => await this._networkRequestIntercepted(e));
}
async continue(overrides: types.NormalizedContinueOverrides): Promise<void> {
- this._alreadyContinuedParams = {
- requestId: this._interceptionId!,
- url: overrides.url,
- headers: overrides.headers,
- method: overrides.method,
- postData: overrides.postData ? overrides.postData.toString('base64') : undefined
- };
- await catchDisallowedErrors(async () => {
- await this._session.send('Fetch.continueRequest', this._alreadyContinuedParams);
- });
+ ;
+ const patchrightInitScript = !!(overrides as any).patchrightInitScript;
+ this._alreadyContinuedParams = {
+ requestId: this._interceptionId,
+ url: overrides.url,
+ headers: overrides.headers,
+ method: overrides.method,
+ postData: overrides.postData?.toString('base64'),
+ };
+ if (patchrightInitScript) {
+ await catchDisallowedErrors(async () => {
+ this._sessionManager._alreadyTrackedNetworkIds.add(this._networkId);
+ try {
+ await this._session.send('Fetch.continueRequest', { requestId: this._interceptionId, interceptResponse: true });
+ } catch (e) {
+ this._sessionManager._alreadyTrackedNetworkIds.delete(this._networkId);
+ throw e;
+ }
+ });
+ } else {
+ await catchDisallowedErrors(async () => {
+ await this._session.send('Fetch.continueRequest', this._alreadyContinuedParams);
+ });
+ }
+
}
async fulfill(response: types.NormalizedFulfillResponse) {
- this._fulfilled = true;
- const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
- const responseHeaders = splitSetCookieHeader(response.headers);
- await catchDisallowedErrors(async () => {
- await this._session.send('Fetch.fulfillRequest', {
- requestId: this._interceptionId!,
- responseCode: response.status,
- responsePhrase: network.statusText(response.status),
- responseHeaders,
- body,
- });
- });
+ const isTextHtml = response.headers.some((header) => header.name.toLowerCase() === "content-type" && header.value.includes("text/html"));
+ const pageDelegate = this._page?.delegate ?? null;
+ const initScriptTag = pageDelegate?.initScriptTag ?? "";
+ const allInjections = pageDelegate
+ ? [...pageDelegate._mainFrameSession._evaluateOnNewDocumentScripts]
+ : [];
+
+ if (isTextHtml && allInjections.length && initScriptTag) {
+ // Decode body if needed
+ if (response.isBase64) {
+ response.isBase64 = false;
+ response.body = Buffer.from(response.body, "base64").toString("utf-8");
+ }
+
+ // CSP Detection and Fixing
+ const cspHeaderNames = ["content-security-policy", "content-security-policy-report-only"];
+ const extractNonce = (cspValue) => {
+ const match = cspValue.match(/script-src[^;]*'nonce-([^'"s;]+)'/i);
+ return match?.[1] ?? null;
+ };
+ let useNonce = false;
+ let scriptNonce = null;
+
+ // Fix CSP in headers
+ for (const header of response.headers) {
+ if (cspHeaderNames.includes(header.name.toLowerCase())) {
+ const originalCsp = header.value ?? "";
+ // Extract nonce if present
+ const nonce = !useNonce && extractNonce(originalCsp);
+ if (nonce) {
+ scriptNonce = nonce;
+ useNonce = true;
+ }
+
+ header.value = this._fixCSP(originalCsp, scriptNonce);
+ }
+ }
+
+ // Fix CSP in meta tags
+ if (typeof response.body === "string" && response.body.length) {
+ response.body = response.body.replace(
+ /<meta[^>]*http-equiv=(?:"|')?Content-Security-Policy(?:"|')?[^>]*>/gi,
+ (match) => {
+ const contentMatch = match.match(/content=(?:"|')([^"']*)(?:"|')/i);
+ if (!contentMatch)
+ return match;
+
+ let originalCsp = contentMatch[1];
+ // Decode HTML entities
+ originalCsp = originalCsp
+ .replace(/&amp;/g, '&') // Must be first!
+ .replace(/&lt;/g, '<')
+ .replace(/&gt;/g, '>')
+ .replace(/&quot;/g, '"')
+ .replace(/&#x27;/g, "'")
+ .replace(/&#x22;/g, '"')
+ .replace(/&nbsp;/g, ' ')
+ .replace(/&#(d+);/g, (match, dec) => String.fromCharCode(dec))
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
+
+ // Extract nonce if present
+ const nonce = !useNonce && extractNonce(originalCsp);
+ if (nonce) {
+ scriptNonce = nonce;
+ useNonce = true;
+ }
+
+ const fixedCsp = this._fixCSP(originalCsp, scriptNonce);
+ // Re-encode for HTML
+ const encodedCsp = fixedCsp.replace(/'/g, '&#x27;').replace(/"/g, '&#x22;');
+ return match.replace(contentMatch[1], encodedCsp);
+ }
+ );
+ }
+
+ // Build injection HTML - only use nonce if one was found in existing CSP
+ const nonceAttr = useNonce ? `nonce="${scriptNonce}"` : '';
+ let injectionHTML = "";
+ allInjections.forEach((script) => {
+ let scriptId = crypto.randomBytes(22).toString("hex");
+ let scriptSource = script.source ?? script;
+ injectionHTML += `<script class="${initScriptTag}" ${nonceAttr} id="${scriptId}" type="text/javascript">document.getElementById("${scriptId}")?.remove();${scriptSource}</script>`;
+ });
+
+ // Inject at END of <head>
+ response.body = this._injectIntoHead(response.body, injectionHTML);
+ }
+
+ this._fulfilled = true;
+ const body = response.isBase64 ? response.body : Buffer.from(response.body).toString("base64");
+ const responseHeaders = splitSetCookieHeader(response.headers);
+ await catchDisallowedErrors(async () => {
+ await this._session.send("Fetch.fulfillRequest", {
+ requestId: response.interceptionId ? response.interceptionId : this._interceptionId,
+ responseCode: response.status,
+ responsePhrase: network.statusText(response.status),
+ responseHeaders,
+ body
+ });
+ });
+
}
async abort(errorCode: string = 'failed') {
@@ -658,6 +791,211 @@
});
});
}
+
+ _fixCSP(csp: string | null, scriptNonce: string | null) {
+
+ if (!csp || typeof csp !== 'string')
+ return csp;
+
+ // Split by semicolons and clean up
+ const directives = csp.split(';')
+ .map(d => d.trim())
+ .filter(Boolean);
+
+ const fixedDirectives = [];
+ let hasScriptSrc = false;
+
+ const addIfMissing = (values: string[], ...items: string[]) => {
+ for (const item of items)
+ if (!values.includes(item))
+ values.push(item);
+ };
+
+
+ for (let directive of directives) {
+ // Improved directive parsing to handle more edge cases
+ const directiveMatch = directive.match(/^([a-zA-Z-]+)\s+(.*)$/);
+ if (!directiveMatch) {
+ fixedDirectives.push(directive);
+ continue;
+ }
+
+ const directiveName = directiveMatch[1].toLowerCase();
+ const directiveValues = directiveMatch[2].split(/\s+/).filter(Boolean);
+
+ switch (directiveName) {
+ case 'script-src':
+ hasScriptSrc = true;
+
+ // Add nonce if we have one and it's not already present
+ if (scriptNonce && !directiveValues.some(v => v.includes(`nonce-${scriptNonce}`)))
+ directiveValues.push(`'nonce-${scriptNonce}'`);
+
+ // Add 'unsafe-eval' if not present
+ addIfMissing(directiveValues, "'unsafe-eval'");
+
+ // Add unsafe-inline if not present and no nonce is being used
+ if (!scriptNonce)
+ addIfMissing(directiveValues, "'unsafe-inline'");
+
+ // Add wildcard for external scripts if not already present
+ if (!directiveValues.includes("*") && !directiveValues.includes("'self'") && !directiveValues.some(v => v.includes("https:")))
+ directiveValues.push("*");
+
+ fixedDirectives.push(`script-src ${directiveValues.join(' ')}`);
+ break;
+
+ case 'style-src':
+ // Add 'unsafe-inline' for styles if not present
+ addIfMissing(directiveValues, "'unsafe-inline'");
+ fixedDirectives.push(`style-src ${directiveValues.join(' ')}`);
+ break;
+
+ case 'img-src':
+ case 'font-src':
+ // Allow data: URLs for images/fonts if not already allowed
+ if (!directiveValues.includes('*'))
+ addIfMissing(directiveValues, 'data:');
+ fixedDirectives.push(`${directiveName} ${directiveValues.join(' ')}`);
+ break;
+
+ case 'connect-src':
+ // Allow WebSocket connections if not already allowed
+ if (!directiveValues.some(v => v.includes('ws:') || v.includes('wss:') || v === '*'))
+ addIfMissing(directiveValues, 'ws:', 'wss:');
+ fixedDirectives.push(`connect-src ${directiveValues.join(' ')}`);
+ break;
+
+ case 'frame-ancestors':
+ // If completely blocked with 'none', allow 'self' at least
+ let frameAncestorValues = directiveValues.includes("'none'") ? "'self'" : directiveValues.join(' ');
+ fixedDirectives.push(`frame-ancestors ${frameAncestorValues}`);
+ break;
+
+ default:
+ // Keep other directives as-is
+ fixedDirectives.push(directive);
+ }
+ }
+
+ // Add script-src if it doesn't exist (for our injected scripts)
+ if (!hasScriptSrc) {
+ fixedDirectives.push(
+ scriptNonce
+ ? `script-src 'self' 'unsafe-eval' 'nonce-${scriptNonce}' *`
+ : `script-src 'self' 'unsafe-eval' 'unsafe-inline' *`
+ );
+ }
+
+ return fixedDirectives.join('; ');
+
+ }
+
+ _injectIntoHead(body: string, injectionHTML: string) {
+
+ // Inject at END of <head>
+ const lower = body.toLowerCase();
+ const headStartIndex = lower.indexOf("<head");
+
+ if (headStartIndex !== -1) {
+ const headStartTagEndIndex = lower.indexOf(">", headStartIndex) + 1;
+ const headEndTagIndex = lower.indexOf("</head>", headStartIndex);
+
+ if (headEndTagIndex !== -1) {
+ // Find the first <script> tag in <head>, skipping HTML comments
+ const headContent = lower.slice(headStartTagEndIndex, headEndTagIndex);
+
+ // Look for the first <script> tag in the head content but ignore comments
+ let firstScriptIndex = -1;
+ let searchPos = 0;
+
+ while (searchPos < headContent.length) {
+ const commentStart = headContent.indexOf("<!--", searchPos);
+ const scriptStart = headContent.indexOf("<script", searchPos);
+
+ // No more script tags, inject at the end of head content
+ if (scriptStart === -1)
+ break;
+
+ if (commentStart !== -1 && commentStart < scriptStart) {
+ const commentEnd = headContent.indexOf("-->", commentStart);
+ if (commentEnd === -1)
+ break;
+
+ // Skip past the comment and keep searching
+ searchPos = commentEnd + 3;
+ } else {
+ // Found a script tag outside a comment
+ firstScriptIndex = scriptStart;
+ break;
+ }
+ }
+
+ const insertAt =
+ firstScriptIndex !== -1
+ ? headStartTagEndIndex + firstScriptIndex // Before first <script>
+ : headEndTagIndex; // Before </head>
+
+ return body.slice(0, insertAt) + injectionHTML + body.slice(insertAt);
+ }
+
+ // No </head> found — inject right after the opening <head> tag
+ return body.slice(0, headStartTagEndIndex) + injectionHTML + body.slice(headStartTagEndIndex);
+ }
+
+ // No <head> — try after <!DOCTYPE>
+ const doctypeIndex = lower.indexOf("<!doctype");
+ if (doctypeIndex === 0) {
+ const doctypeEnd = body.indexOf(">", doctypeIndex) + 1;
+ return body.slice(0, doctypeEnd) + injectionHTML + body.slice(doctypeEnd);
+ }
+
+ // Try after <html>
+ const htmlTagIndex = lower.indexOf("<html");
+ if (htmlTagIndex !== -1) {
+ const htmlTagEnd = body.indexOf(">", htmlTagIndex) + 1;
+ return body.slice(0, htmlTagEnd) + `<head>${injectionHTML}</head>` + body.slice(htmlTagEnd);
+ }
+
+ // Last resort — prepend to body
+ return injectionHTML + body;
+
+ }
+
+ async _networkRequestIntercepted(event: Protocol.Fetch.requestPausedPayload) {
+
+ if (this._networkId != event.networkId || !this._sessionManager._alreadyTrackedNetworkIds.has(event.networkId))
+ return;
+
+ const trackedNetworkId = event.networkId;
+ try {
+ if (event.resourceType !== 'Document')
+ return;
+
+ if (event.responseStatusCode >= 301 && event.responseStatusCode <= 308 || (event.redirectedRequestId && !event.responseStatusCode)) {
+ await this._session.send('Fetch.continueRequest', { requestId: event.requestId, interceptResponse: true });
+ } else {
+ const responseBody = await this._session.send('Fetch.getResponseBody', { requestId: event.requestId });
+ await this.fulfill({
+ headers: event.responseHeaders,
+ isBase64: true,
+ body: responseBody.body,
+ status: event.responseStatusCode,
+ interceptionId: event.requestId,
+ resourceType: event.resourceType,
+ });
+ }
+ } catch (error) {
+ if (error.message.includes("Can only get response body on HeadersReceived pattern matched requests.")) {
+ await this._session.send("Fetch.continueRequest", { requestId: event.requestId, interceptResponse: true });
+ } else {
+ await this._session._sendMayFail("Fetch.continueRequest", { requestId: event.requestId });
+ }
+ } finally {
+ this._sessionManager._alreadyTrackedNetworkIds.delete(trackedNetworkId);
+ }
+
+ }
}
// In certain cases, protocol will return error if the request was already canceled
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/crPage.ts patchright/node_modules/playwright-core/src/server/chromium/crPage.ts
---
+++
@@ -45,7 +45,7 @@
import type { Progress } from '../progress';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
-
+import crypto from "crypto";
export type WindowBounds = { top?: number, left?: number, width?: number, height?: number };
@@ -96,7 +96,10 @@
this.updateOffline();
this.updateExtraHTTPHeaders();
this.updateHttpCredentials();
- this.updateRequestInterception();
+
+ this._networkManager.setRequestInterception(true);
+ this.initScriptTag = crypto.randomBytes(20).toString('hex');
+
this._mainFrameSession = new FrameSession(this, client, targetId, null);
this._sessions.set(targetId, this._mainFrameSession);
if (opener && !browserContext._options.noDefaultViewport) {
@@ -126,15 +129,15 @@
}
_sessionForFrame(frame: frames.Frame): FrameSession {
- // Frame id equals target id.
- while (!this._sessions.has(frame._id)) {
- const parent = frame.parentFrame();
- if (!parent)
- throw new Error(`Frame has been detached.`);
- frame = parent;
+ // Frame id equals target id.
+ while (!this._sessions.has(frame._id)) {
+ const parent = frame.parentFrame();
+ if (!parent)
+ throw new Error(`Frame was detached`);
+ frame = parent;
+ }
+ return this._sessions.get(frame._id)!;
}
- return this._sessions.get(frame._id)!;
- }
private _sessionForHandle(handle: dom.ElementHandle): FrameSession {
const frame = handle._context.frame;
@@ -225,6 +228,7 @@
}
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
+ this._page.initScripts.push(initScript);
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
}
@@ -364,6 +368,19 @@
async setDockTile(image: Buffer): Promise<void> {
await this._mainFrameSession._client.send('Browser.setDockTile', { image: image.toString('base64') });
}
+
+ async exposeBinding(binding: PageBinding) {
+
+ await this._forAllFrameSessions(frame => frame._initBinding(binding));
+ await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source).catch(e => {})));
+
+ }
+
+ async removeExposedBindings() {
+
+ await this._forAllFrameSessions(frame => frame._removeExposedBindings());
+
+ }
}
class FrameSession {
@@ -438,6 +455,7 @@
}
async _initialize(hasUIWindow: boolean) {
+ const pageEnablePromise = this._client.send('Page.enable');
if (!this._page.isStorageStatePage && hasUIWindow &&
!this._crPage._browserContext._browser.isClank() &&
!this._crPage._browserContext._options.noDefaultViewport) {
@@ -451,6 +469,11 @@
let lifecycleEventsEnabled: Promise<any>;
if (!this._isMainFrame())
this._addRendererListeners();
+
+ let bufferedDialogEvents: any[] | undefined = this._isMainFrame() ? [] : undefined;
+ if (bufferedDialogEvents)
+ this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.javascriptDialogOpening', (event: any) => bufferedDialogEvents ? bufferedDialogEvents.push(event) : undefined));
+
this._addBrowserListeners();
// Buffer attachedToTarget events until we receive the frame tree.
@@ -460,11 +483,18 @@
this._bufferedAttachedToTargetEvents = [];
const promises: Promise<any>[] = [
- this._client.send('Page.enable'),
+ pageEnablePromise,
this._client.send('Page.getFrameTree').then(({ frameTree }) => {
if (this._isMainFrame()) {
this._handleFrameTree(frameTree);
this._addRendererListeners();
+
+ // Replay any dialog events that arrived before _addRendererListeners
+ const pendingDialogEvents = bufferedDialogEvents || [];
+ bufferedDialogEvents = undefined;
+ for (const event of pendingDialogEvents)
+ this._onDialog(event);
+
}
// Now that we have the frame tree, it is possible to insert oopif targets at the right place.
@@ -472,17 +502,6 @@
this._bufferedAttachedToTargetEvents = undefined;
for (const event of attachedToTargetEvents)
this._onAttachedToTarget(event);
-
- const localFrames = this._isMainFrame() ? this._page.frames() : [this._page.frameManager.frame(this._targetId)!];
- for (const frame of localFrames) {
- // Note: frames might be removed before we send these.
- this._client._sendMayFail('Page.createIsolatedWorld', {
- frameId: frame._id,
- grantUniveralAccess: true,
- worldName: this._crPage.utilityWorldName,
- });
- }
-
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) {
// Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page
@@ -492,13 +511,24 @@
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
});
} else {
+
+ const localFrames = this._isMainFrame() ? this._page.frames() : [this._page.frameManager.frame(this._targetId)!];
+ for (const frame of localFrames) {
+ this._page.frameManager.frame(frame._id)._context("utility").catch(() => {});
+ for (const binding of this._crPage._browserContext._pageBindings.values())
+ frame.evaluateExpression(binding.source).catch(e => {});
+ for (const source of this._crPage._browserContext.initScripts)
+ frame.evaluateExpression(source.source).catch(e => {});
+ for (const source of this._crPage._page.initScripts)
+ frame.evaluateExpression(source.source).catch(e => {});
+ }
+
this._firstNonInitialNavigationCommittedFulfill();
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
}
}),
this._client.send('Log.enable', {}),
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
- this._client.send('Runtime.enable', {}),
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: '',
worldName: this._crPage.utilityWorldName,
@@ -509,8 +539,10 @@
if (!this._page.isStorageStatePage) {
if (this._crPage._browserContext.needsPlaywrightBinding())
promises.push(this.exposePlaywrightBinding());
- if (this._isMainFrame())
- promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true }));
+
+ if (this._isMainFrame() && !this._crPage._browserContext._options.focusControl)
+ promises.push(this._client.send("Emulation.setFocusEmulationEnabled", { enabled: true }));
+
const options = this._crPage._browserContext._options;
if (options.bypassCSP)
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
@@ -533,12 +565,22 @@
promises.push(this._updateGeolocation(true));
promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true));
- for (const initScript of this._crPage._page.allInitScripts())
- promises.push(this._evaluateOnNewDocument(initScript, 'main', true /* runImmediately */));
+
+ for (const binding of this._crPage._page.allBindings()) promises.push(this._initBinding(binding));
+ for (const initScript of this._crPage._browserContext.initScripts) promises.push(this._evaluateOnNewDocument(initScript, 'main'));
+ for (const initScript of this._crPage._page.initScripts) promises.push(this._evaluateOnNewDocument(initScript, 'main'));
+
}
- promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
+
+ if (!(this._crPage._page._pageBindings.size || this._crPage._browserContext._pageBindings.size))
+ promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
+
promises.push(this._firstNonInitialNavigationCommittedPromise);
await Promise.all(promises);
+
+ if (this._crPage._page._pageBindings.size || this._crPage._browserContext._pageBindings.size)
+ await this._client.send('Runtime.runIfWaitingForDebugger');
+
}
dispose() {
@@ -562,13 +604,33 @@
return { newDocumentId: response.loaderId };
}
- _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
+ async _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
if (this._eventBelongsToStaleFrame(event.frameId))
return;
if (event.name === 'load')
this._page.frameManager.frameLifecycleEvent(event.frameId, 'load');
else if (event.name === 'DOMContentLoaded')
this._page.frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');
+
+ // Only do full init script cleanup on load to reduce CDP round-trip pressure.
+ // Other lifecycle events just get a minimal runIfWaitingForDebugger call.
+ if (event.name !== "load") {
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ return;
+ }
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ var document = await this._client._sendMayFail("DOM.getDocument");
+ if (!document) return
+ var query = await this._client._sendMayFail("DOM.querySelectorAll", {
+ nodeId: document.root.nodeId,
+ selector: "[class=" + this._crPage.initScriptTag + "]"
+ });
+ if (!query) return
+ for (const nodeId of query.nodeIds) await this._client._sendMayFail("DOM.removeNode", { nodeId: nodeId });
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ // ensuring execution context
+ try { await this._page.frameManager.frame(this._targetId)._context("utility") } catch { };
+
}
_handleFrameTree(frameTree: Protocol.Page.FrameTree) {
@@ -615,12 +677,33 @@
this._page.frameManager.frameAttached(frameId, parentFrameId);
}
- _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
+ async _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
if (this._eventBelongsToStaleFrame(framePayload.id))
return;
this._page.frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url + (framePayload.urlFragment || ''), framePayload.name || '', framePayload.loaderId, initial);
if (!initial)
this._firstNonInitialNavigationCommittedFulfill();
+
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ // patchright: For non-initial navigations, skip DOM cleanup since the document just changed
+ // and init script tags haven't been re-added yet. The _onLifecycleEvent("load") handler
+ // will perform cleanup after the page finishes loading.
+ if (!initial) {
+ try { await this._page.frameManager.frame(this._targetId)._context("utility") } catch { };
+ return;
+ }
+ var document = await this._client._sendMayFail("DOM.getDocument");
+ if (!document) return
+ var query = await this._client._sendMayFail("DOM.querySelectorAll", {
+ nodeId: document.root.nodeId,
+ selector: "[class=" + this._crPage.initScriptTag + "]"
+ });
+ if (!query) return
+ for (const nodeId of query.nodeIds) await this._client._sendMayFail("DOM.removeNode", { nodeId: nodeId });
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ // ensuring execution context
+ try { await this._page.frameManager.frame(this._targetId)._context("utility") } catch { };
+
}
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
@@ -657,19 +740,33 @@
}
_onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription) {
+
+ for (const name of this._exposedBindingNames)
+ this._client._sendMayFail('Runtime.addBinding', { name: name, executionContextId: contextPayload.id });
+
const frame = contextPayload.auxData ? this._page.frameManager.frame(contextPayload.auxData.frameId) : null;
+
+ if (contextPayload.auxData?.type === "worker") throw new Error("ExecutionContext is worker");
+
if (!frame || this._eventBelongsToStaleFrame(frame._id))
return;
const delegate = new CRExecutionContext(this._client, contextPayload);
- let worldName: types.World|null = null;
- if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
- worldName = 'main';
- else if (contextPayload.name === this._crPage.utilityWorldName)
- worldName = 'utility';
+ let worldName = contextPayload.name;
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
- if (worldName)
- frame._contextCreated(worldName, context);
+
+ if (worldName && (worldName === 'main' || worldName === 'utility'))
+ frame._contextCreated(worldName, context);
+
this._contextIdToContext.set(contextPayload.id, context);
+
+ for (const source of this._exposedBindingScripts) {
+ this._client._sendMayFail("Runtime.evaluate", {
+ expression: source,
+ contextId: contextPayload.id,
+ awaitPromise: true,
+ })
+ }
+
}
_onExecutionContextDestroyed(executionContextId: number) {
@@ -685,7 +782,7 @@
this._onExecutionContextDestroyed(contextId);
}
- _onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
+ async _onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
if (this._bufferedAttachedToTargetEvents) {
this._bufferedAttachedToTargetEvents.push(event);
return;
@@ -727,12 +824,22 @@
session.once('Runtime.executionContextCreated', async event => {
worker.createExecutionContext(new CRExecutionContext(session, event.context));
});
+
+ var globalThis = await session._sendMayFail('Runtime.evaluate', {
+ expression: "globalThis",
+ serializationOptions: { serialization: "idOnly" }
+ });
+ if (globalThis && globalThis.result) {
+ var globalThisObjId = globalThis.result.objectId;
+ var executionContextId = parseInt(globalThisObjId.split('.')[1], 10);
+ worker.createExecutionContext(new CRExecutionContext(session, { id: executionContextId }));
+ }
+
if (this._crPage._browserContext._browser.majorVersion() >= 143)
session.on('Inspector.workerScriptLoaded', () => worker.workerScriptLoaded());
else
worker.workerScriptLoaded();
// This might fail if the target is closed before we initialize.
- session._sendMayFail('Runtime.enable');
// TODO: attribute workers to the right frame.
this._crPage._networkManager.addSession(session, this._page.frameManager.frame(this._targetId) ?? undefined).catch(() => {});
session._sendMayFail('Runtime.runIfWaitingForDebugger');
@@ -811,8 +918,10 @@
const pageOrError = await this._crPage._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(event.executionContextId);
- if (context)
- await this._page.onBindingCalled(event.payload, context);
+
+ if (context) await this._page.onBindingCalled(event.payload, context);
+ else await this._page._onBindingCalled(event.payload, (await this._page.mainFrame()._mainContext())) // This might be a bit sketchy but it works for now
+
}
}
@@ -1008,20 +1117,14 @@
}
async _evaluateOnNewDocument(initScript: InitScript, world: types.World, runImmediately?: boolean): Promise<void> {
- const worldName = world === 'utility' ? this._crPage.utilityWorldName : undefined;
- const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName, runImmediately });
- this._initScriptIds.set(initScript, identifier);
+ this._evaluateOnNewDocumentScripts.push(initScript);
}
async _removeEvaluatesOnNewDocument(initScripts: InitScript[]): Promise<void> {
- const ids: string[] = [];
- for (const script of initScripts) {
- const id = this._initScriptIds.get(script);
- if (id)
- ids.push(id);
- this._initScriptIds.delete(script);
- }
- await Promise.all(ids.map(identifier => this._client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier }).catch(() => {}))); // target can be closed
+
+ const toRemove = new Set(initScripts);
+ this._evaluateOnNewDocumentScripts = this._evaluateOnNewDocumentScripts.filter(script => !toRemove.has(script));
+
}
async exposePlaywrightBinding() {
@@ -1126,12 +1229,53 @@
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
const result = await this._client._sendMayFail('DOM.resolveNode', {
backendNodeId,
- executionContextId: (to.delegate as CRExecutionContext)._contextId,
+ executionContextId: to.delegate._contextId,
});
if (!result || result.object.subtype === 'null')
throw new Error(dom.kUnableToAdoptErrorMessage);
return createHandle(to, result.object).asElement()!;
}
+
+ _exposedBindingNames: string[] = [];
+ _evaluateOnNewDocumentScripts: InitScript[] = [];
+ _parsedExecutionContextIds: number[] = [];
+ _exposedBindingScripts: string[] = [];
+
+ async _initBinding(binding = PageBinding) {
+
+ // Remember this binding so future execution contexts get it in _onExecutionContextCreated.
+ this._exposedBindingNames.push(binding.name);
+ this._exposedBindingScripts.push(binding.source);
+
+ // Install binding in all existing execution contexts.
+ const contextIds = Array.from(this._contextIdToContext.keys());
+ await Promise.all([
+ this._client._sendMayFail('Runtime.addBinding', { name: binding.name }),
+ ...contextIds.map(executionContextId => this._client._sendMayFail('Runtime.addBinding', { name: binding.name, executionContextId })),
+ ]);
+
+ // Evaluate binding bootstrap in all existing execution contexts.
+ const evaluationPromises = contextIds.map(contextId =>
+ this._client._sendMayFail('Runtime.evaluate', {
+ expression: binding.source,
+ contextId,
+ awaitPromise: true,
+ }).catch(e => { }),
+ );
+ await Promise.all(evaluationPromises);
+
+ }
+
+ async _removeExposedBindings() {
+
+ const toRetain: string[] = [];
+ const toRemove: string[] = [];
+ for (const name of this._exposedBindingNames)
+ (name.startsWith('__pw_') ? toRetain : toRemove).push(name);
+ this._exposedBindingNames = toRetain;
+ await Promise.all(toRemove.map(name => this._client.send('Runtime.removeBinding', { name })));
+
+ }
}
async function emulateLocale(session: CRSession, locale: string) {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/chromium/crServiceWorker.ts patchright/node_modules/playwright-core/src/server/chromium/crServiceWorker.ts
---
+++
@@ -63,13 +63,23 @@
const message = new ConsoleMessage(null, this, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace), event.timestamp);
this.browserContext.emit(BrowserContext.Events.Console, message);
});
-
- session.send('Runtime.enable', {}).catch(e => {});
session.send('Runtime.runIfWaitingForDebugger').catch(e => {});
session.on('Inspector.targetReloadedAfterCrash', () => {
// Resume service worker after restart.
session._sendMayFail('Runtime.runIfWaitingForDebugger', {});
});
+
+ session._sendMayFail("Runtime.evaluate", {
+ expression: "globalThis",
+ serializationOptions: { serialization: "idOnly" }
+ }).then(globalThis => {
+ if (globalThis && globalThis.result) {
+ var globalThisObjId = globalThis.result.objectId;
+ var executionContextId = parseInt(globalThisObjId.split(".")[1], 10);
+ this.createExecutionContext(new CRExecutionContext(session, { id: executionContextId }));
+ }
+ });
+
}
override didClose() {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/clock.ts patchright/node_modules/playwright-core/src/server/clock.ts
---
+++
@@ -92,8 +92,18 @@
}
private async _installIfNeeded() {
- if (this._initScripts.length)
- return;
+
+ if (this._initScripts.length) {
+ const initScriptSources = JSON.stringify(this._initScripts.map((initScript) => initScript.source));
+ await this._evaluateInFrames(`(() => {
+ if (globalThis.__pwClock?.controller)
+ return;
+ for (const source of ${initScriptSources})
+ (0, eval)(source);
+ })();`);
+ return;
+ }
+
const script = `(() => {
const module = {};
${rawClockSource.source}
@@ -106,6 +116,15 @@
}
private async _evaluateInFrames(script: string) {
+
+ // Dont ask me why this works
+ const frames = this._browserContext.pages().flatMap((page) => page.frames());
+ await Promise.all(frames.map(async (frame) => {
+ try {
+ await frame.evaluateExpression("");
+ } catch {}
+ }));
+
await this._browserContext.safeNonStallingEvaluateInAllFrames(script, 'main', { throwOnJSErrors: true });
}
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/dispatchers/browserContextDispatcher.ts patchright/node_modules/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
---
+++
@@ -124,12 +124,12 @@
});
}
});
- this._dialogHandler = dialog => {
- if (!this._shouldDispatchEvent(dialog.page(), 'dialog'))
- return false;
- this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) });
- return true;
- };
+
+ this._dialogHandler = dialog => {
+ this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) });
+ return true;
+ };
+
context.dialogManager.addDialogHandler(this._dialogHandler);
if (context._browser.options.name === 'chromium' && this._object._browser instanceof CRBrowser) {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/dispatchers/frameDispatcher.ts patchright/node_modules/playwright-core/src/server/dispatchers/frameDispatcher.ts
---
+++
@@ -84,11 +84,36 @@
}
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, progress: Progress): Promise<channels.FrameEvaluateExpressionResult> {
- return { value: serializeResult(await progress.race(this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)))) };
+
+ return {
+ value: serializeResult(
+ await progress.race(
+ this._frame.evaluateExpression(
+ params.expression,
+ { isFunction: params.isFunction, world: params.isolatedContext ? 'utility': 'main' },
+ parseArgument(params.arg)
+ )
+ )
+ )
+ };
+
}
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, progress: Progress): Promise<channels.FrameEvaluateExpressionHandleResult> {
- return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await progress.race(this._frame.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)))) };
+
+ return {
+ handle: ElementHandleDispatcher.fromJSOrElementHandle(
+ this,
+ await progress.race(
+ this._frame.evaluateExpressionHandle(
+ params.expression,
+ { isFunction: params.isFunction, world: params.isolatedContext ? 'utility': 'main' },
+ parseArgument(params.arg)
+ )
+ )
+ )
+ };
+
}
async waitForSelector(params: channels.FrameWaitForSelectorParams, progress: Progress): Promise<channels.FrameWaitForSelectorResult> {
@@ -104,7 +129,20 @@
}
async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, progress: Progress): Promise<channels.FrameEvalOnSelectorAllResult> {
- return { value: serializeResult(await progress.race(this._frame.evalOnSelectorAll(params.selector, params.expression, params.isFunction, parseArgument(params.arg)))) };
+
+ return {
+ value: serializeResult(
+ await this._frame.evalOnSelectorAll(
+ params.selector,
+ params.expression,
+ params.isFunction,
+ parseArgument(params.arg),
+ null,
+ params.isolatedContext
+ )
+ )
+ };
+
}
async querySelector(params: channels.FrameQuerySelectorParams, progress: Progress): Promise<channels.FrameQuerySelectorResult> {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts patchright/node_modules/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts
---
+++
@@ -43,12 +43,12 @@
}
async evaluateExpression(params: channels.JSHandleEvaluateExpressionParams, progress: Progress): Promise<channels.JSHandleEvaluateExpressionResult> {
- const jsHandle = await progress.race(this._object.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)));
+ const jsHandle = await progress.race(this._object.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg), params.isolatedContext));
return { value: serializeResult(jsHandle) };
}
async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams, progress: Progress): Promise<channels.JSHandleEvaluateExpressionHandleResult> {
- const jsHandle = await progress.race(this._object.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)));
+ const jsHandle = await progress.race(this._object.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg), params.isolatedContext));
// If "jsHandle" is an ElementHandle, it belongs to the same frame as "this".
return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this.parentScope() as FrameDispatcher, jsHandle) };
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/dispatchers/networkDispatchers.ts patchright/node_modules/playwright-core/src/server/dispatchers/networkDispatchers.ts
---
+++
@@ -153,6 +153,7 @@
headers: params.headers,
postData: params.postData,
isFallback: params.isFallback,
+ patchrightInitScript: (params as any).patchrightInitScript
});
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/frameSelectors.ts patchright/node_modules/playwright-core/src/server/frameSelectors.ts
---
+++
@@ -23,7 +23,10 @@
import type { JSHandle } from './javascript';
import type * as types from './types';
import type { ParsedSelector } from '../utils/isomorphic/selectorParser';
-
+import { ElementHandle } from "./dom";
+import type { CRSession } from "./chromium/crConnection";
+import type { Progress } from "./progress";
+import type { Protocol } from "./chromium/protocol";
export type SelectorInfo = {
parsed: ParsedSelector,
@@ -65,8 +68,8 @@
return adoptIfNeeded(elementHandle, await resolved.frame._mainContext());
}
- async queryArrayInMainWorld(selector: string, scope?: ElementHandle): Promise<JSHandle<Element[]>> {
- const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: true }, scope);
+ async queryArrayInMainWorld(selector: string, scope?: ElementHandle, isolatedContext?: boolean): Promise<JSHandle<Element[]>> {
+ const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: !isolatedContext }, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
throw new Error(`Failed to find frame for selector "${selector}"`);
@@ -156,9 +159,26 @@
throw injected.createStacklessError(`Selector "${selectorString}" resolved to ${injected.previewNode(element)}, <iframe> was expected`);
return element;
}, { info, scope: i === 0 ? scope : undefined, selectorString: stringifySelector(info.parsed) });
- const element = handle.asElement() as ElementHandle<Element> | null;
- if (!element)
- return null;
+ let element = handle.asElement() as ElementHandle<Element> | null;
+
+ if (!element) {
+ try {
+ var client = frame._page.delegate._sessionForFrame(frame)._client;
+ } catch (e) {
+ var client = frame._page.delegate._mainFrameSession._client;
+ }
+ var mainContext = await frame._context("main");
+ const documentNode = await client.send("Runtime.evaluate", {
+ expression: "document",
+ serializationOptions: { serialization: "idOnly" },
+ contextId: mainContext.delegate._contextId
+ });
+ const documentScope = new ElementHandle(mainContext, documentNode.result.objectId);
+ var check = await this._customFindFramesByParsed(injectedScript, client, mainContext, documentScope, undefined, info.parsed);
+ if (check.length === 0) return null;
+ element = check[0];
+ }
+
const maybeFrame = await frame._page.delegate.getContentFrame(element);
element.dispose();
if (!maybeFrame)
@@ -179,9 +199,194 @@
if (!resolved)
return;
const context = await resolved.frame._context(options?.mainWorld ? 'main' : resolved.info.world);
+ if (!context) throw new Error("Frame was detached");
const injected = await context.injectedScript();
return { injected, info: resolved.info, frame: resolved.frame, scope: resolved.scope };
}
+
+ async _customFindFramesByParsed(resolved: JSHandle<InjectedScript>, client: CRSession, context: FrameExecutionContext, documentScope: ElementHandle, progress: Progress | undefined, parsed: ParsedSelector) {
+
+ var parsedEdits = { ...parsed };
+ const callId = progress?.metadata.id;
+ // Note: We start scoping at document level
+ var currentScopingElements = [documentScope];
+
+ for (const part of [...parsed.parts]) {
+ parsedEdits.parts = [part];
+ var elements = [];
+
+ if (part.name === "nth") {
+ const partNth = Number(part.body);
+ // Check if any Elements are currently scoped, else return empty array to continue polling
+ if (currentScopingElements.length == 0)
+ return [];
+
+ if (partNth > currentScopingElements.length-1 || partNth < -(currentScopingElements.length-1)) {
+ if (parsed.capture !== undefined)
+ throw new Error("Can't query n-th element in a request with the capture.");
+ return [];
+ }
+ currentScopingElements = [currentScopingElements.at(partNth)];
+ continue;
+ } else if (part.name === "internal:or") {
+ var orredElements = await this._customFindFramesByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ elements = [...currentScopingElements, ...orredElements];
+ } else if (part.name == "internal:and") {
+ var andedElements = await this._customFindFramesByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ const backendNodeIds = new Set(andedElements.map(elem => elem.backendNodeId));
+ elements = currentScopingElements.filter(elem => backendNodeIds.has(elem.backendNodeId));
+ } else {
+ for (const scope of currentScopingElements) {
+ const describedScope = await client.send("DOM.describeNode", {
+ objectId: scope._objectId,
+ depth: -1,
+ pierce: true
+ });
+
+ let findClosedShadowRoots = function(node, results = []) {
+ if (!node || typeof node !== "object") return results;
+ if (node.shadowRoots && Array.isArray(node.shadowRoots)) {
+ for (const shadowRoot of node.shadowRoots) {
+ if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) {
+ results.push(shadowRoot.backendNodeId);
+ }
+ findClosedShadowRoots(shadowRoot, results);
+ }
+ }
+ if (node.nodeName !== "IFRAME" && node.children && Array.isArray(node.children)) {
+ for (const child of node.children) {
+ findClosedShadowRoots(child, results);
+ }
+ }
+ return results;
+ };
+ var shadowRootBackendIds = findClosedShadowRoots(describedScope.node);
+
+ const shadowRoots = await Promise.all(
+ shadowRootBackendIds.map(async backendNodeId => {
+ const resolved = await client.send("DOM.resolveNode", {
+ backendNodeId,
+ contextId: context.delegate._contextId,
+ });
+ return new ElementHandle(context, resolved.object.objectId);
+ })
+ );
+
+ // Elements Queryed in the "current round"
+ const queryGroups: { handles: any; parentNode: any }[] = [];
+ for (var shadowRoot of shadowRoots) {
+ const shadowHandles = await shadowRoot.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId
+ }
+ );
+ queryGroups.push({ handles: shadowHandles, parentNode: shadowRoot });
+ }
+
+ // Document Root Elements (not in CSR)
+ const rootHandles = await scope.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId
+ }
+ );
+ queryGroups.push({ handles: rootHandles, parentNode: scope });
+
+ // Querying and Sorting the elements by their backendNodeId
+ for (const { handles, parentNode } of queryGroups) {
+ const handlesAmount = await (await handles.getProperty("length")).jsonValue();
+ for (var i = 0; i < handlesAmount; i++) {
+ if (parentNode instanceof ElementHandle) {
+ var element = await parentNode.evaluateHandleInUtility(
+ ([injected, node, { i, handles: elems }]) => elems[i],
+ { i, handles }
+ );
+ } else {
+ var element = await parentNode.evaluateHandle(
+ (injected, { i, handles: elems }) => elems[i],
+ { i, handles }
+ );
+ }
+
+ // For other Functions/Utilities
+ element.parentNode = parentNode;
+ const resolvedElement = await client.send("DOM.describeNode", { objectId: element._objectId, depth: -1 });
+ element.backendNodeId = resolvedElement.node.backendNodeId;
+ element.nodePosition = await this._findElementPositionInDomTree(element, describedScope.node, context, "");
+ elements.push(element);
+ }
+ }
+ }
+ }
+
+ // Sorting elements by their nodePosition, which is a index to the Element in the DOM tree
+ const getParts = (pos) => (pos || '').split('.').filter(Boolean).map(Number);
+ elements.sort((a, b) => {
+ const partsA = getParts(a.nodePosition);
+ const partsB = getParts(b.nodePosition);
+
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
+ const diff = (partsA[i] ?? -1) - (partsB[i] ?? -1);
+ if (diff !== 0) return diff;
+ }
+ return 0;
+ });
+
+ // Remove duplicates by backendNodeId, keeping the first occurrence
+ currentScopingElements = Array.from(
+ new Map(elements.map(e => [e.backendNodeId, e])).values()
+ );
+ }
+
+ return currentScopingElements;
+
+ }
+
+ async _findElementPositionInDomTree(element: { backendNodeId: number }, queryingElement: Protocol.DOM.Node, context: FrameExecutionContext, currentIndex: string) {
+
+ // Get Element Position in DOM Tree by Indexing it via their children indexes, like a search tree index
+ // Check if backendNodeId matches, if so, return currentIndex
+ if (element.backendNodeId === queryingElement.backendNodeId)
+ return currentIndex;
+
+ // Iterating through children of queryingElement
+ for (const [childrenNodeIndex, child] of (queryingElement.children || []).entries()) {
+ // Further querying the child recursively and appending the children index to the currentIndex
+ const childIndex = await this._findElementPositionInDomTree(element, child, context, currentIndex + "." + childrenNodeIndex.toString());
+ if (childIndex !== null) return childIndex;
+ }
+
+ for (const shadowRoot of queryingElement.shadowRoots || []) {
+ // For CSRs, we dont have to append its index because patchright treats CSRs like they dont exist
+ if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) {
+ // Resolve the CDP client for the current context so closed shadow roots can be traversed safely.
+ const client = context.frame._page.delegate._sessionForFrame(context.frame)._client;
+ const describedShadowRoot = await client.send("DOM.describeNode", { backendNodeId: shadowRoot.backendNodeId, depth: -1, pierce: true });
+ if (describedShadowRoot && describedShadowRoot.node) {
+ const childIndex = await this._findElementPositionInDomTree(element, describedShadowRoot.node, context, currentIndex);
+ if (childIndex !== null) return childIndex;
+ }
+ }
+ // Traverse into shadow root children (open and closed) to properly position elements inside shadow DOMs
+ for (const [shadowChildIndex, shadowChild] of (shadowRoot.children || []).entries()) {
+ const childIndex = await this._findElementPositionInDomTree(element, shadowChild, context, currentIndex + "." + shadowChildIndex.toString());
+ if (childIndex !== null) return childIndex;
+ }
+ }
+ return null;
+
+ }
}
async function adoptIfNeeded<T extends Node>(handle: ElementHandle<T>, context: FrameExecutionContext): Promise<ElementHandle<T>> {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/frames.ts patchright/node_modules/playwright-core/src/server/frames.ts
---
+++
@@ -42,6 +42,10 @@
import type { RegisteredListener } from './utils/eventsHelper';
import type { ParsedSelector } from '../utils/isomorphic/selectorParser';
import type * as channels from '@protocol/channels';
+import { CRExecutionContext } from "./chromium/crExecutionContext";
+import { FrameExecutionContext } from "./dom";
+import type { CRSession } from "./chromium/crConnection";
+import crypto from "crypto";
type ContextData = {
contextPromise: ManualPromise<dom.FrameExecutionContext | { destroyedReason: string }>;
@@ -239,6 +243,9 @@
// No pending - just commit a new document.
frame._currentDocument = { documentId, request: undefined };
}
+ frame._iframeWorld = undefined;
+ frame._mainWorld = undefined;
+ frame._isolatedWorld = undefined;
frame._onClearLifecycle();
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument, isPublic: true };
@@ -572,12 +579,15 @@
}
nonStallingEvaluateInExistingContext(expression: string, world: types.World): Promise<any> {
- return this.raceAgainstEvaluationStallingEvents(() => {
- const context = this._contextData.get(world)?.context;
- if (!context)
- throw new Error('Frame does not yet have the execution context');
- return context.evaluateExpression(expression, { isFunction: false });
- });
+
+ return this.raceAgainstEvaluationStallingEvents(async () => {
+ try { await this._context(world); } catch {}
+ const context = this._contextData.get(world)?.context;
+ if (!context)
+ throw new Error('Frame does not yet have the execution context');
+ return context.evaluateExpression(expression, { isFunction: false });
+ });
+
}
_recalculateNetworkIdle(frameThatAllowsRemovingNetworkIdle?: Frame) {
@@ -722,12 +732,69 @@
return this._page.delegate.getFrameElement(this);
}
- _context(world: types.World): Promise<dom.FrameExecutionContext> {
- return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
- if (contextOrDestroyedReason instanceof js.ExecutionContext)
- return contextOrDestroyedReason;
- throw new Error(contextOrDestroyedReason.destroyedReason);
- });
+ async _context(world: types.World): Promise<dom.FrameExecutionContext> {
+
+ if (this.isDetached())
+ throw new Error('Frame was detached');
+
+ let client;
+ try {
+ client = this._page.delegate._sessionForFrame(this)._client;
+ } catch (e) {
+ client = this._page.delegate._mainFrameSession._client;
+ }
+
+ var iframeExecutionContextId = await this._getFrameMainFrameContextId(client);
+ const isMainFrame = this === this._page.mainFrame();
+ const session = this._page.delegate._sessionForFrame(this);
+
+ const registerContext = (executionContextId: number, worldName: string) => {
+ const crContext = new CRExecutionContext(client, { id: executionContextId }, this._id);
+ const frameContext = new FrameExecutionContext(crContext, this, worldName);
+ session._onExecutionContextCreated({
+ id: executionContextId,
+ origin: worldName,
+ name: worldName,
+ auxData: { isDefault: isMainFrame, type: 'isolated', frameId: this._id },
+ });
+ return frameContext;
+ };
+
+ if (world === "main") {
+ // Iframe Only
+ if (!isMainFrame && iframeExecutionContextId && this._iframeWorld === undefined) {
+ this._iframeWorld = registerContext(iframeExecutionContextId, world);
+ } else if (this._mainWorld === undefined) {
+ const globalThis = await client._sendMayFail('Runtime.evaluate', {
+ expression: "globalThis",
+ serializationOptions: { serialization: "idOnly" },
+ });
+ if (!globalThis) {
+ if (this.isDetached()) throw new Error('Frame was detached');
+ return;
+ }
+ const executionContextId = parseInt(globalThis.result.objectId.split('.')[1], 10);
+ this._mainWorld = registerContext(executionContextId, world);
+ }
+ }
+
+ if (world !== "main" && this._isolatedWorld === undefined) {
+ const result = await client._sendMayFail('Page.createIsolatedWorld', {
+ frameId: this._id, grantUniveralAccess: true, worldName: world,
+ });
+ if (!result) {
+ if (this.isDetached()) throw new Error("Frame was detached");
+ return;
+ }
+ this._isolatedWorld = registerContext(result.executionContextId, "utility");
+ }
+
+ if (world !== "main")
+ return this._isolatedWorld;
+ if (!isMainFrame && this._iframeWorld)
+ return this._iframeWorld;
+ return this._mainWorld;
+
}
_mainContext(): Promise<dom.FrameExecutionContext> {
@@ -743,107 +810,168 @@
}
async evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<any> {
- const context = await this._context(options.world ?? 'main');
- const value = await context.evaluateExpression(expression, options, arg);
- return value;
+
+ const context = await this._detachedScope.race(this._context(options.world ?? "main"));
+ return await this._detachedScope.race(context.evaluateExpression(expression, options, arg));
+
}
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<js.JSHandle<any>> {
- const context = await this._context(options.world ?? 'main');
- const value = await context.evaluateExpressionHandle(expression, options, arg);
- return value;
+
+ const context = await this._detachedScope.race(this._context(options.world ?? "utility"));
+ return await this._detachedScope.race(context.evaluateExpressionHandle(expression, options, arg));
+
}
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
- this.apiLog(` finding element using the selector "${selector}"`);
- return this.selectors.query(selector, options);
+
+ return this.querySelectorAll(selector, options).then((handles) => {
+ if (handles.length === 0)
+ return null;
+ if (handles.length > 1 && options?.strict)
+ throw new Error(`Strict mode: expected one element matching selector "${selector}", found ${handles.length}`);
+ return handles[0];
+ });
+
}
async waitForSelector(progress: Progress, selector: string, performActionPreChecksAndLog: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
- if ((options as any).visibility)
- throw new Error('options.visibility is not supported, did you mean options.state?');
- if ((options as any).waitFor && (options as any).waitFor !== 'visible')
- throw new Error('options.waitFor is not supported, did you mean options.state?');
- const { state = 'visible' } = options;
- if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
- throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
- if (performActionPreChecksAndLog)
- progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
- const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
- if (performActionPreChecksAndLog)
- await this._page.performActionPreChecks(progress);
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope));
- if (!resolved) {
- if (state === 'hidden' || state === 'detached')
- return null;
- return continuePolling;
- }
- const result = await progress.race(resolved.injected.evaluateHandle((injected, { info, root }) => {
- if (root && !root.isConnected)
- throw injected.createStacklessError('Element is not attached to the DOM');
- const elements = injected.querySelectorAll(info.parsed, root || document);
- const element: Element | undefined = elements[0];
- const visible = element ? injected.utils.isElementVisible(element) : false;
- let log = '';
- if (elements.length > 1) {
- if (info.strict)
- throw injected.strictModeViolationError(info.parsed, elements);
- log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
- } else if (element) {
- log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`;
- }
- injected.checkDeprecatedSelectorUsage(info.parsed, elements);
- return { log, element, visible, attached: !!element };
- }, { info: resolved.info, root: resolved.frame === this ? scope : undefined }));
- const { log, visible, attached } = await progress.race(result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached })));
- if (log)
- progress.log(log);
- const success = { attached, detached: !attached, visible, hidden: !visible }[state];
- if (!success) {
- result.dispose();
- return continuePolling;
- }
- if (options.omitReturnValue) {
- result.dispose();
- return null;
- }
- const element = state === 'attached' || state === 'visible' ? await progress.race(result.evaluateHandle(r => r.element)) : null;
- result.dispose();
- if (!element)
- return null;
- if ((options as any).__testHookBeforeAdoptNode)
- await progress.race((options as any).__testHookBeforeAdoptNode());
- try {
- const mainContext = await progress.race(resolved.frame._mainContext());
- return await progress.race(element._adoptTo(mainContext));
- } catch (e) {
- return continuePolling;
- }
- });
- return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+ if ((options as any).visibility)
+ throw new Error('options.visibility is not supported, did you mean options.state?');
+ if ((options as any).waitFor && (options as any).waitFor !== 'visible')
+ throw new Error('options.waitFor is not supported, did you mean options.state?');
+
+ const { state = 'visible' } = options;
+ if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
+ throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
+
+ if (performActionPreChecksAndLog)
+ progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
+
+ const promise = this._retryWithProgressIfNotConnected(progress, selector, { ...options, performActionPreChecks: true, __patchrightWaitForSelector: true, __patchrightInitialScope: scope }, async handle => {
+ if (scope) {
+ const scopeIsConnected = await scope.evaluateInUtility(([injected, node]) => node.isConnected, {}).catch(() => false);
+ if (scopeIsConnected !== true) {
+ if (state === 'hidden' || state === 'detached')
+ return null;
+ throw new dom.NonRecoverableDOMError('Element is not attached to the DOM');
+ }
+ }
+
+ const attached = !!handle;
+ var visible = false;
+
+ if (attached) {
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ visible = await handle.parentNode.evaluateInUtility(([injected, node, { handle }]) => {
+ return handle ? injected.utils.isElementVisible(handle) : false;
+ }, { handle });
+ } else {
+ visible = await handle.parentNode.evaluate((injected, { handle }) => {
+ return handle ? injected.utils.isElementVisible(handle) : false;
+ }, { handle });
+ }
+ }
+
+ const success = {
+ attached,
+ detached: !attached,
+ visible,
+ hidden: !visible
+ }[state];
+ if (!success) return "internal:continuepolling";
+ if (options.omitReturnValue) return null;
+
+ const element = state === 'attached' || state === 'visible' ? handle : null;
+ if (!element) return null;
+ if (options.__testHookBeforeAdoptNode) await options.__testHookBeforeAdoptNode();
+ try {
+ return element;
+ } catch (e) {
+ return "internal:continuepolling";
+ }
+ }, "returnOnNotResolved");
+
+ const resultPromise = scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+ return resultPromise.catch(e => {
+ if (this.isDetached() && (e as any)?.message?.includes('Execution context was destroyed'))
+ throw new Error('Frame was detached');
+ throw e;
+ });
+
}
async dispatchEvent(progress: Progress, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions, scope?: dom.ElementHandle): Promise<void> {
- await this._callOnElementOnceMatches(progress, selector, (injectedScript, element, data) => {
- injectedScript.dispatchEvent(element, data.type, data.eventInit);
- }, { type, eventInit }, { mainWorld: true, ...options }, scope);
+
+ const eventInitHandles: js.JSHandle[] = [];
+ const visited = new WeakSet();
+ const collectHandles = (value: any) => {
+ if (!value || typeof value !== "object")
+ return;
+ if (value instanceof js.JSHandle) {
+ eventInitHandles.push(value);
+ return;
+ }
+ if (visited.has(value))
+ return;
+ visited.add(value);
+ if (Array.isArray(value)) {
+ for (const item of value)
+ collectHandles(item);
+ return;
+ }
+ for (const propertyValue of Object.values(value))
+ collectHandles(propertyValue);
+ };
+ collectHandles(eventInit);
+
+ const handlesFrame = eventInitHandles[0]?._context?.frame;
+ const allHandlesFromSameFrame = eventInitHandles.length > 0 && eventInitHandles.every(handle => handle._context?.frame === handlesFrame);
+ const canRetryInSecondaryContext = allHandlesFromSameFrame && (handlesFrame !== this || !selector.includes("internal:control=enter-frame"));
+ const callback = (injectedScript, element, data) => {
+ injectedScript.dispatchEvent(element, data.type, data.eventInit);
+ };
+ try {
+ await this._callOnElementOnceMatches(progress, selector, callback, { type, eventInit }, { mainWorld: true, ...options }, scope);
+ } catch (e) {
+ if ("JSHandles can be evaluated only in the context they were created!" === e.message && canRetryInSecondaryContext) {
+ await this._callOnElementOnceMatches(progress, selector, callback, { type, eventInit }, { ...options }, scope);
+ return;
+ }
+ throw e;
+ }
+
}
async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
- const handle = await this.selectors.query(selector, { strict }, scope);
- if (!handle)
- throw new Error(`Failed to find element matching selector "${selector}"`);
- const result = await handle.evaluateExpression(expression, { isFunction }, arg);
- handle.dispose();
- return result;
+
+ const handle = await this.selectors.query(selector, { strict }, scope);
+ if (!handle)
+ throw new Error('Failed to find element matching selector "' + selector + '"');
+ const result = await handle.evaluateExpression(expression, { isFunction }, arg, true);
+ handle.dispose();
+ return result;
+
}
- async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
- const arrayHandle = await this.selectors.queryArrayInMainWorld(selector, scope);
- const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
- arrayHandle.dispose();
- return result;
+ async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle, isolatedContext?: boolean): Promise<any> {
+
+ const maxAttempts = 3;
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ isolatedContext = this.selectors._parseSelector(selector, { strict: false }).world !== "main" && isolatedContext;
+ const arrayHandle = await this.selectors.queryArrayInMainWorld(selector, scope, isolatedContext);
+ const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg, isolatedContext);
+ arrayHandle.dispose();
+ return result;
+ } catch (e) {
+ // Retry only on specific context mismatch errors, and only a bounded number of times.
+ if ("JSHandles can be evaluated only in the context they were created!" !== e.message || attempt === maxAttempts) throw e;
+ await new Promise(resolve => setTimeout(resolve, 50 * attempt));
+ }
+ }
+
}
async maskSelectors(selectors: ParsedSelector[], color: string): Promise<void> {
@@ -855,17 +983,34 @@
}
async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
- return this.selectors.queryAll(selector);
+
+ const metadata = { internal: false, log: [], method: "querySelectorAll" };
+ const progress = {
+ log: message => metadata.log.push(message),
+ metadata,
+ race: (promise) => Promise.race(Array.isArray(promise) ? promise : [promise])
+ }
+ return await this._retryWithoutProgress(progress, selector, {strict: null, performActionPreChecks: false}, async (result) => {
+ if (!result || !result[0]) return [];
+ return Array.isArray(result[1]) ? result[1] : [];
+ }, 'returnAll', null);
+
}
async queryCount(selector: string, options: any): Promise<number> {
- try {
- return await this.selectors.queryCount(selector, options);
- } catch (e) {
- if (this.isNonRetriableError(e))
- throw e;
- return 0;
- }
+
+ const metadata = { internal: false, log: [], method: "queryCount" };
+ const progress = {
+ log: message => metadata.log.push(message),
+ metadata,
+ race: (promise) => Promise.race(Array.isArray(promise) ? promise : [promise])
+ }
+ return await this._retryWithoutProgress(progress, selector, {strict: null, performActionPreChecks: false }, async (result) => {
+ if (!result || !result[0])
+ return 0;
+ return Array.isArray(result[1]) ? result[1].length : 0;
+ }, 'returnAll', null);
+
}
async content(): Promise<string> {
@@ -887,29 +1032,23 @@
}
async setContent(progress: Progress, html: string, options: types.NavigateOptions): Promise<void> {
- const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
- await this.raceNavigationAction(progress, async () => {
- const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
- progress.log(`setting frame content, waiting until "${waitUntil}"`);
- const context = await progress.race(this._utilityContext());
- const tagPromise = new ManualPromise<void>();
- this._page.frameManager._consoleMessageTags.set(tag, () => {
- // Clear lifecycle right after document.open() - see 'tag' below.
- this._onClearLifecycle();
- tagPromise.resolve();
- });
- const lifecyclePromise = progress.race(tagPromise).then(() => this.waitForLoadState(progress, waitUntil));
- const contentPromise = progress.race(context.evaluate(({ html, tag }) => {
- document.open();
- console.debug(tag); // eslint-disable-line no-console
- document.write(html);
- document.close();
- }, { html, tag }));
- await Promise.all([contentPromise, lifecyclePromise]);
- return null;
- }).finally(() => {
- this._page.frameManager._consoleMessageTags.delete(tag);
- });
+
+ await this.raceNavigationAction(progress, async () => {
+ const waitUntil = options.waitUntil === void 0 ? "load" : options.waitUntil;
+ progress.log(`setting frame content, waiting until "${waitUntil}"`);
+ const lifecyclePromise = new Promise((resolve, reject) => {
+ this._onClearLifecycle();
+ this.waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
+ });
+ const setContentPromise = this._page.delegate._sessionForFrame(this)._client.send("Page.setDocumentContent", {
+ frameId: this._id,
+ html
+ });
+ await Promise.all([setContentPromise, lifecyclePromise]);
+
+ return null;
+ });
+
}
name(): string {
@@ -1101,60 +1240,14 @@
progress: Progress,
selector: string,
options: { strict?: boolean, noAutoWaiting?: boolean, force?: boolean, performActionPreChecks?: boolean },
- action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
- progress.log(`waiting for ${this._asLocator(selector)}`);
- const noAutoWaiting = (options as any).__testHookNoAutoWaiting ?? options.noAutoWaiting;
- const performActionPreChecks = (options.performActionPreChecks ?? !options.force) && !noAutoWaiting;
- return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
- if (performActionPreChecks)
- await this._page.performActionPreChecks(progress);
+ action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>, returnAction: 'returnOnNotResolved' | 'returnAll' | undefined): Promise<R> {
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, { strict: options.strict }));
- if (!resolved) {
- if (noAutoWaiting)
- throw new dom.NonRecoverableDOMError('Element(s) not found');
- return continuePolling;
- }
- const result = await progress.race(resolved.injected.evaluateHandle((injected, { info, callId }) => {
- const elements = injected.querySelectorAll(info.parsed, document);
- if (callId)
- injected.markTargetElements(new Set(elements), callId);
- const element = elements[0] as Element | undefined;
- let log = '';
- if (elements.length > 1) {
- if (info.strict)
- throw injected.strictModeViolationError(info.parsed, elements);
- log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
- } else if (element) {
- log = ` locator resolved to ${injected.previewNode(element)}`;
- }
- injected.checkDeprecatedSelectorUsage(info.parsed, elements);
- return { log, success: !!element, element };
- }, { info: resolved.info, callId: progress.metadata.id }));
- const { log, success } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success })));
- if (log)
- progress.log(log);
- if (!success) {
- if (noAutoWaiting)
- throw new dom.NonRecoverableDOMError('Element(s) not found');
- result.dispose();
- return continuePolling;
- }
- const element = await progress.race(result.evaluateHandle(r => r.element)) as dom.ElementHandle<Element>;
- result.dispose();
- try {
- const result = await action(element);
- if (result === 'error:notconnected') {
- if (noAutoWaiting)
- throw new dom.NonRecoverableDOMError('Element is not attached to the DOM');
- progress.log('element was detached from the DOM, retrying');
- return continuePolling;
- }
- return result;
- } finally {
- element?.dispose();
- }
- });
+ if (!(options as any)?.__patchrightSkipRetryLogWaiting)
+ progress.log("waiting for " + this._asLocator(selector));
+ return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
+ return this._retryWithoutProgress(progress, selector, options as any, action as any, returnAction, continuePolling);
+ });
+
}
async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise<Buffer> {
@@ -1307,20 +1400,61 @@
}
async isVisibleInternal(progress: Progress, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
- try {
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope));
- if (!resolved)
- return false;
- return await progress.race(resolved.injected.evaluate((injected, { info, root }) => {
- const element = injected.querySelector(info.parsed, root || document, info.strict);
- const state = element ? injected.elementState(element, 'visible') : { matches: false, received: 'error:notconnected' };
- return state.matches;
- }, { info: resolved.info, root: resolved.frame === this ? scope : undefined }));
- } catch (e) {
- if (this.isNonRetriableError(e))
- throw e;
- return false;
- }
+
+ try {
+ const metadata = { internal: false, log: [], method: "isVisible" };
+ const progress = {
+ log: message => metadata.log.push(message),
+ metadata,
+ race: (promise) => Promise.race(Array.isArray(promise) ? promise : [promise])
+ }
+ progress.log("waiting for " + this._asLocator(selector));
+ if (selector === ":scope") {
+ const scopeParentNode = scope.parentNode || scope;
+ if (scopeParentNode instanceof dom.ElementHandle) {
+ return await scopeParentNode.evaluateInUtility(([injected, node, { scope: handle2 }]) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { scope });
+ } else {
+ return await scopeParentNode.evaluate((injected, node, { scope: handle2 }) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { scope });
+ }
+ } else {
+ return await this._retryWithoutProgress(progress, selector, { ...options, performActionPreChecks: false}, async (handle) => {
+ if (!handle) return false;
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ return await handle.parentNode.evaluateInUtility(([injected, node, { handle: handle2 }]) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { handle });
+ } else {
+ return await handle.parentNode.evaluate((injected, { handle: handle2 }) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { handle });
+ }
+ }, "returnOnNotResolved", null);
+ }
+ } catch (e) {
+ if (this.isNonRetriableError(e)) throw e;
+ return false;
+ }
+
}
async isHidden(progress: Progress, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
@@ -1439,47 +1573,96 @@
}
private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean) {
- // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted.
- const race = <T>(p: Promise<T>) => noAbort ? p : progress.race(p);
- const selectorInFrame = selector ? await race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined;
- const { frame, info } = selectorInFrame || { frame: this, info: undefined };
- const world = options.expression === 'to.have.property' ? 'main' : (info?.world ?? 'utility');
- const context = await race(frame._context(world));
- const injected = await race(context.injectedScript());
+ // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted.
+ const race = (p) => noAbort ? p : progress.race(p);
+ const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
+ var log, matches, received, missingReceived;
+ if (selector) {
+ var frame, info;
+ try {
+ var { frame, info } = await race(this.selectors.resolveFrameForSelector(selector, { strict: true }));
+ } catch (e) { }
+ const action = async result => {
+ if (!result) {
+ if (options.expectedNumber === 0)
+ return { matches: true };
+ if (options.isNot && options.expectedNumber)
+ return { matches: false, received: 0 };
+ // expect(locator).toBeHidden() passes when there is no element.
+ if (!options.isNot && options.expression === 'to.be.hidden')
+ return { matches: true };
+ // expect(locator).not.toBeVisible() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.visible')
+ return { matches: false };
+ // expect(locator).toBeAttached({ attached: false }) passes when there is no element.
+ if (!options.isNot && options.expression === 'to.be.detached')
+ return { matches: true };
+ // expect(locator).not.toBeAttached() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.attached')
+ return { matches: false };
+ // expect(locator).not.toBeInViewport() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.in.viewport')
+ return { matches: false };
+ // expect(locator).toHaveText([]) pass when there is no element.
+ if (options.expression === "to.have.text.array") {
+ if (options.expectedText.length === 0)
+ return { matches: true, received: [] };
+ if (options.isNot && options.expectedText.length !== 0)
+ return { matches: false, received: [] };
+ }
+ // When none of the above applies, expect does not match.
+ return { matches: options.isNot, missingReceived: true };
+ }
- const { log, matches, received, missingReceived } = await race(injected.evaluate(async (injected, { info, options, callId }) => {
- const elements = info ? injected.querySelectorAll(info.parsed, document) : [];
- if (callId)
- injected.markTargetElements(new Set(elements), callId);
- const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
- let log = '';
- if (isArray)
- log = ` locator resolved to ${elements.length} element${elements.length === 1 ? '' : 's'}`;
- else if (elements.length > 1)
- throw injected.strictModeViolationError(info!.parsed, elements);
- else if (elements.length)
- log = ` locator resolved to ${injected.previewNode(elements[0])}`;
- if (info)
- injected.checkDeprecatedSelectorUsage(info.parsed, elements);
- return { log, ...await injected.expect(elements[0], options, elements) };
- }, { info, options, callId: progress.metadata.id }));
+ const handle = result[0];
+ const handles = result[1];
- if (log)
- progress.log(log);
- // Note: missingReceived avoids `unexpected value "undefined"` when element was not found.
- if (matches === options.isNot) {
- if (missingReceived) {
- lastIntermediateResult.errorMessage = 'Error: element(s) not found';
- } else {
- lastIntermediateResult.errorMessage = undefined;
- lastIntermediateResult.received = received;
- }
- lastIntermediateResult.isSet = true;
- if (!missingReceived && !Array.isArray(received))
- progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`);
- }
- return { matches, received };
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ return await handle.parentNode.evaluateInUtility(async ([injected, node, { handle, options, handles }]) => {
+ return await injected.expect(handle, options, handles);
+ }, { handle, options, handles });
+ } else {
+ return await handle.parentNode.evaluate(async (injected, { handle, options, handles }) => {
+ return await injected.expect(handle, options, handles);
+ }, { handle, options, handles });
+ }
+ }
+
+ if (noAbort) {
+ var { log, matches, received, missingReceived } = await this._retryWithoutProgress(progress, selector, {strict: !isArray, performActionPreChecks: false}, action, 'returnAll', null);
+ } else {
+ var { log, matches, received, missingReceived } = await race(this._retryWithProgressIfNotConnected(progress, selector, { strict: !isArray, performActionPreChecks: false, __patchrightSkipRetryLogWaiting: true } as any, action, 'returnAll'));
+ }
+ } else {
+ const world = options.expression === 'to.have.property' ? 'main' : 'utility';
+ const context = await race(this._context(world));
+ const injected = await race(context.injectedScript());
+ var { matches, received, missingReceived } = await race(injected.evaluate(async (injected, { options, callId }) => {
+ return { ...await injected.expect(undefined, options, []) };
+ }, { options, callId: progress.metadata.id }));
+ }
+
+
+ if (log)
+ progress.log(log);
+ // Note: missingReceived avoids `unexpected value "undefined"` when element was not found.
+ if (matches === options.isNot) {
+ if (missingReceived) {
+ lastIntermediateResult.errorMessage = 'Error: element(s) not found';
+ } else {
+ lastIntermediateResult.errorMessage = undefined;
+ lastIntermediateResult.received = received;
+ }
+ lastIntermediateResult.isSet = true;
+ if (!missingReceived) {
+ const rendered = renderUnexpectedValue(options.expression, received);
+ if (rendered !== undefined)
+ progress.log(' unexpected value "' + rendered + '"');
+ }
+ }
+ return { matches, received };
+
}
async waitForFunctionExpression<R>(progress: Progress, expression: string, isFunction: boolean | undefined, arg: any, options: { pollingInterval?: number }, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
@@ -1538,7 +1721,7 @@
return { result, abort: () => aborted = true };
}, { expression, isFunction, polling: options.pollingInterval, arg }));
try {
- return await progress.race(handle.evaluateHandle(h => h.result));
+ return await progress.race(this._detachedScope.race(handle.evaluateHandle(h => h.result)));
} catch (error) {
// Note: it is important to await "abort()" to prevent any side effects
// after this method returns.
@@ -1591,42 +1774,129 @@
}
_onDetached() {
- this._stopNetworkIdleTimer();
- this._detachedScope.close(new Error('Frame was detached'));
- for (const data of this._contextData.values()) {
- if (data.context)
- data.context.contextDestroyed('Frame was detached');
- data.contextPromise.resolve({ destroyedReason: 'Frame was detached' });
- }
- if (this._parentFrame)
- this._parentFrame._childFrames.delete(this);
- this._parentFrame = null;
+
+ this._stopNetworkIdleTimer();
+ this._detachedScope.close(new Error('Frame was detached'));
+ for (const data of this._contextData.values()) {
+ if (data.context)
+ data.context.contextDestroyed('Frame was detached');
+ data.contextPromise.resolve({ destroyedReason: 'Frame was detached' });
+ }
+ if (this._mainWorld)
+ this._mainWorld.contextDestroyed('Frame was detached');
+ if (this._iframeWorld)
+ this._iframeWorld.contextDestroyed('Frame was detached');
+ if (this._isolatedWorld)
+ this._isolatedWorld.contextDestroyed('Frame was detached');
+ if (this._parentFrame)
+ this._parentFrame._childFrames.delete(this);
+ this._parentFrame = null;
+
}
private async _callOnElementOnceMatches<T, R>(progress: Progress, selector: string, body: ElementCallback<T, R>, taskData: T, options: types.StrictOptions & { mainWorld?: boolean }, scope?: dom.ElementHandle): Promise<R> {
- const callbackText = body.toString();
- progress.log(`waiting for ${this._asLocator(selector)}`);
- const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope));
- if (!resolved)
- return continuePolling;
- const { log, success, value } = await progress.race(resolved.injected.evaluate((injected, { info, callbackText, taskData, callId, root }) => {
- const callback = injected.eval(callbackText) as ElementCallback<T, R>;
- const element = injected.querySelector(info.parsed, root || document, info.strict);
- if (!element)
- return { success: false };
- const log = ` locator resolved to ${injected.previewNode(element)}`;
- if (callId)
- injected.markTargetElements(new Set([element]), callId);
- return { log, success: true, value: callback(injected, element, taskData as T) };
- }, { info: resolved.info, callbackText, taskData, callId: progress.metadata.id, root: resolved.frame === this ? scope : undefined }));
- if (log)
- progress.log(log);
- if (!success)
- return continuePolling;
- return value!;
- });
- return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+
+ const callbackText = body.toString();
+ progress.log("waiting for " + this._asLocator(selector));
+ var promise;
+ if (selector === ":scope") {
+ const scopeParentNode = scope.parentNode || scope;
+ if (scopeParentNode instanceof dom.ElementHandle) {
+ if (options?.mainWorld) {
+ promise = (async () => {
+ const mainContext = await this._mainContext();
+ const adoptedScope = await this._page.delegate.adoptElementHandle(scope, mainContext);
+ try {
+ return await mainContext.evaluate(([injected, node, { callbackText: callbackText2, scope: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, [
+ await mainContext.injectedScript(),
+ adoptedScope,
+ { callbackText, scope: adoptedScope, taskData },
+ ]);
+ } finally {
+ adoptedScope.dispose();
+ }
+ })();
+ } else {
+ promise = scopeParentNode.evaluateInUtility(([injected, node, { callbackText: callbackText2, scope: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ scope,
+ taskData
+ });
+ }
+ } else {
+ promise = scopeParentNode.evaluate((injected, { callbackText: callbackText2, scope: handle2, taskData: taskData2 }) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ scope,
+ taskData
+ });
+ }
+ } else {
+
+ promise = this._retryWithProgressIfNotConnected(progress, selector, { ...options, performActionPreChecks: false }, async (handle) => {
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ if (options?.mainWorld) {
+ const mainContext = await handle._frame._mainContext();
+ const adoptedHandle = await this._page.delegate.adoptElementHandle(handle, mainContext);
+ try {
+ return await mainContext.evaluate(([injected, node, { callbackText: callbackText2, handle: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, [
+ await mainContext.injectedScript(),
+ adoptedHandle,
+ { callbackText, handle: adoptedHandle, taskData },
+ ]);
+ } finally {
+ adoptedHandle.dispose();
+ }
+ }
+
+ // Handling dispatch_event's in isolated and Main Contexts
+ const [taskScope] = Object.values(taskData?.eventInit ?? {});
+ if (taskScope) {
+ const taskScopeContext = taskScope._context;
+ const adoptedHandle = await handle._adoptTo(taskScopeContext);
+ return await taskScopeContext.evaluate(([injected, node, { callbackText: callbackText2, adoptedHandle: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, [
+ await taskScopeContext.injectedScript(),
+ adoptedHandle,
+ { callbackText, adoptedHandle, taskData },
+ ]);
+ }
+
+ return await handle.parentNode.evaluateInUtility(([injected, node, { callbackText: callbackText2, handle: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ handle,
+ taskData
+ });
+ } else {
+ return await handle.parentNode.evaluate((injected, { callbackText: callbackText2, handle: handle2, taskData: taskData2 }) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ handle,
+ taskData
+ });
+ }
+ })
+ }
+ return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+
}
private _setContext(world: types.World, context: dom.FrameExecutionContext | null) {
@@ -1722,6 +1992,325 @@
private _asLocator(selector: string) {
return asLocator(this._page.browserContext._browser.sdkLanguage(), selector);
}
+
+ _isolatedWorld: dom.FrameExecutionContext;
+ _mainWorld: dom.FrameExecutionContext;
+ _iframeWorld: dom.FrameExecutionContext;
+
+ async _getFrameMainFrameContextId(client: CRSession): Promise<number> {
+
+ try {
+ const frameOwner = await client._sendMayFail("DOM.getFrameOwner", { frameId: this._id });
+ if (!frameOwner?.nodeId)
+ return 0;
+
+ const describedNode = await client._sendMayFail("DOM.describeNode", { backendNodeId: frameOwner.backendNodeId });
+ if (!describedNode?.node.contentDocument)
+ return 0;
+
+ const resolvedNode = await client._sendMayFail("DOM.resolveNode", { backendNodeId: describedNode.node.contentDocument.backendNodeId });
+ if (!resolvedNode?.object?.objectId)
+ return 0;
+
+ return parseInt(resolvedNode.object.objectId.split(".")[1], 10);
+ } catch (e) {}
+ return 0;
+
+ }
+
+ async _retryWithoutProgress(progress: Progress, selector: string, options: { performActionPreChecks: boolean; strict?: boolean | null; state?: 'attached' | 'detached' | 'visible' | 'hidden'; noAutoWaiting?: boolean; __testHookNoAutoWaiting?: boolean; __patchrightWaitForSelector?: boolean; __patchrightInitialScope?: dom.ElementHandle; __patchrightSkipRetryLogWaiting?: boolean }, action: (result: dom.ElementHandle | [dom.ElementHandle, dom.ElementHandle[]] | null) => Promise<unknown>, returnAction: 'returnOnNotResolved' | 'returnAll' | undefined, continuePolling: symbol) {
+
+ if (options.performActionPreChecks)
+ await this._page.performActionPreChecks(progress);
+
+ const resolved = await this.selectors.resolveInjectedForSelector(
+ selector,
+ { strict: options.strict },
+ (options as any).__patchrightInitialScope
+ );
+
+ if (!resolved) {
+ if (returnAction === 'returnOnNotResolved' || returnAction === 'returnAll') {
+ const result = await action(null);
+ return result === "internal:continuepolling" ? continuePolling : result;
+ }
+ return continuePolling;
+ }
+
+ const utilityContext = await resolved.frame._utilityContext();
+ const mainContext = await resolved.frame._mainContext();
+ let client;
+ try {
+ client = this._page.delegate._sessionForFrame(resolved.frame)._client;
+ } catch (e) {
+ client = this._page.delegate._mainFrameSession._client;
+ }
+
+ const documentNode = await client._sendMayFail('Runtime.evaluate', {
+ expression: "document",
+ serializationOptions: { serialization: "idOnly" },
+ contextId: utilityContext.delegate._contextId,
+ });
+ if (!documentNode)
+ return continuePolling;
+
+ let initialScope = new dom.ElementHandle(utilityContext, documentNode.result.objectId);
+
+ if ((resolved as any).scope) {
+ const scopeObjectId = (resolved as any).scope._objectId;
+ if (scopeObjectId) {
+ const describeResult = await client._sendMayFail('DOM.describeNode', {
+ objectId: scopeObjectId,
+ });
+ const backendNodeId = describeResult?.node?.backendNodeId;
+
+ if (backendNodeId) {
+ const scopeInUtility = await client._sendMayFail('DOM.resolveNode', {
+ backendNodeId,
+ executionContextId: utilityContext.delegate._contextId
+ });
+
+ if (scopeInUtility?.object?.objectId) {
+ initialScope = new dom.ElementHandle(utilityContext, scopeInUtility.object.objectId);
+ }
+ }
+ }
+ }
+ (progress as any).__patchrightInitialScope = (resolved as any).scope;
+
+ // Save parsed selector before _customFindElementsByParsed mutates it via parts.shift()
+ const parsedSnapshot = (options as any).__patchrightWaitForSelector ? JSON.parse(JSON.stringify(resolved.info.parsed)) : null;
+ let currentScopingElements;
+ try {
+ currentScopingElements = await this._customFindElementsByParsed(resolved, client, mainContext, initialScope, progress, resolved.info.parsed);
+ } catch (e) {
+ if ("JSHandles can be evaluated only in the context they were created!" === e.message)
+ return continuePolling;
+ if (e instanceof TypeError && e.message.includes("is not a function"))
+ return continuePolling;
+ await progress.race(resolved.injected.evaluateHandle((injected, { error }) => { throw error }, { error: e }));
+ }
+
+ if (currentScopingElements.length === 0) {
+ if ((options as any).__testHookNoAutoWaiting || (options as any).noAutoWaiting)
+ throw new dom.NonRecoverableDOMError('Element(s) not found');
+
+ // CDP-based element search is non-atomic and can temporarily miss
+ // elements during DOM mutations. Verify element absence in-page before reporting
+ // "not found" to the waitForSelector callback.
+ if (parsedSnapshot && (returnAction === 'returnOnNotResolved' || returnAction === 'returnAll')) {
+ const elementCount = await resolved.injected.evaluate((injected, { parsed }) => {
+ return injected.querySelectorAll(parsed, document).length;
+ }, { parsed: parsedSnapshot }).catch(() => 0);
+ if (elementCount > 0)
+ return continuePolling;
+ }
+ if (returnAction === 'returnOnNotResolved' || returnAction === 'returnAll') {
+ const result = await action(null);
+ return result === "internal:continuepolling" ? continuePolling : result;
+ }
+ return continuePolling;
+ }
+
+ const resultElement = currentScopingElements[0];
+ await resultElement._initializePreview().catch(() => {});
+
+ let visibilityQualifier = '';
+ if (options && (options as any).__patchrightWaitForSelector) {
+ visibilityQualifier = await resultElement.evaluateInUtility(([injected, node]) => injected.utils.isElementVisible(node) ? 'visible' : 'hidden', {}).catch(() => '');
+ }
+
+ if (currentScopingElements.length > 1) {
+ if (resolved.info.strict) {
+ await progress.race(resolved.injected.evaluateHandle((injected, {
+ info,
+ elements
+ }) => {
+ throw injected.strictModeViolationError(info.parsed, elements);
+ }, {
+ info: resolved.info,
+ elements: currentScopingElements
+ }));
+ }
+ progress.log(" locator resolved to " + currentScopingElements.length + " elements. Proceeding with the first one: " + resultElement.preview());
+ } else if (resultElement) {
+ progress.log(" locator resolved to " + (visibilityQualifier ? visibilityQualifier + " " : "") + resultElement.preview().replace("JSHandle@", ""));
+ }
+
+ try {
+ var result = null;
+ if (returnAction === 'returnAll') {
+ result = await action([resultElement, currentScopingElements]);
+ } else {
+ result = await action(resultElement);
+ }
+ if (result === 'error:notconnected') {
+ progress.log('element was detached from the DOM, retrying');
+ return continuePolling;
+ } else if (result === 'internal:continuepolling') {
+ return continuePolling;
+ }
+ // Verify no visible elements exist before accepting a null result to avoid stale CDP handles during mutations.
+ if (parsedSnapshot && result === null && ((options as any).state === 'hidden' || (options as any).state === 'detached')) {
+ const visibleCount = await resolved.injected.evaluate((injected, { parsed }) => {
+ const elements = injected.querySelectorAll(parsed, document);
+ return elements.filter(e => injected.utils.isElementVisible(e)).length;
+ }, { parsed: parsedSnapshot }).catch(() => 0);
+ if (visibleCount > 0)
+ return continuePolling;
+ }
+ return result;
+ } finally {}
+
+ }
+
+ async _customFindElementsByParsed(resolved: { injected: js.JSHandle<InjectedScript>, info: { parsed: ParsedSelector, strict: boolean }, frame: Frame, scope?: dom.ElementHandle }, client: CRSession, context: dom.FrameExecutionContext, documentScope: dom.ElementHandle, progress: Progress, parsed: ParsedSelector) {
+
+ var parsedEdits = { ...parsed };
+ // Note: We start scoping at document level
+ var currentScopingElements = [documentScope];
+
+ for (const part of [...parsed.parts]) {
+ parsedEdits.parts = [part];
+ var elements = [];
+
+ if (part.name === "nth") {
+ const partNth = Number(part.body);
+ // Check if any Elements are currently scoped, else return empty array to continue polling
+ if (currentScopingElements.length == 0)
+ return [];
+
+ if (partNth > currentScopingElements.length-1 || partNth < -(currentScopingElements.length-1)) {
+ if (parsed.capture !== undefined)
+ throw new Error("Can't query n-th element in a request with the capture.");
+ return [];
+ }
+ currentScopingElements = [currentScopingElements.at(partNth)];
+ continue;
+ } else if (part.name === "internal:or") {
+ var orredElements = await this._customFindElementsByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ elements = [...currentScopingElements, ...orredElements];
+ } else if (part.name == "internal:and") {
+ var andedElements = await this._customFindElementsByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ const backendNodeIds = new Set(andedElements.map(elem => elem.backendNodeId));
+ elements = currentScopingElements.filter(elem => backendNodeIds.has(elem.backendNodeId));
+ } else {
+ for (const scope of currentScopingElements) {
+ const describedScope = await client.send("DOM.describeNode", {
+ objectId: scope._objectId,
+ depth: -1,
+ pierce: true
+ });
+
+ let findClosedShadowRoots = function(node, results = []) {
+ if (!node || typeof node !== "object") return results;
+ if (node.shadowRoots && Array.isArray(node.shadowRoots)) {
+ for (const shadowRoot of node.shadowRoots) {
+ if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) {
+ results.push(shadowRoot.backendNodeId);
+ }
+ findClosedShadowRoots(shadowRoot, results);
+ }
+ }
+ if (node.nodeName !== "IFRAME" && node.children && Array.isArray(node.children)) {
+ for (const child of node.children) {
+ findClosedShadowRoots(child, results);
+ }
+ }
+ return results;
+ };
+ var shadowRootBackendIds = findClosedShadowRoots(describedScope.node);
+
+ const shadowRoots = await Promise.all(
+ shadowRootBackendIds.map(async backendNodeId => {
+ const resolved = await client.send("DOM.resolveNode", {
+ backendNodeId,
+ contextId: context.delegate._contextId,
+ });
+ return new dom.ElementHandle(context, resolved.object.objectId);
+ })
+ );
+
+ // Elements Queryed in the "current round"
+ const queryGroups: { handles: any; parentNode: any }[] = [];
+ for (var shadowRoot of shadowRoots) {
+ const shadowHandles = await shadowRoot.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId: progress.metadata.id
+ }
+ );
+ queryGroups.push({ handles: shadowHandles, parentNode: shadowRoot });
+ }
+
+ // Document Root Elements (not in CSR)
+ const rootHandles = await scope.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId: progress.metadata.id
+ }
+ );
+ queryGroups.push({ handles: rootHandles, parentNode: scope });
+
+ // Querying and Sorting the elements by their backendNodeId
+ for (const { handles, parentNode } of queryGroups) {
+ const handlesAmount = await (await handles.getProperty("length")).jsonValue();
+ for (var i = 0; i < handlesAmount; i++) {
+ if (parentNode instanceof dom.ElementHandle) {
+ var element = await parentNode.evaluateHandleInUtility(
+ ([injected, node, { i, handles: elems }]) => elems[i],
+ { i, handles }
+ );
+ } else {
+ var element = await parentNode.evaluateHandle(
+ (injected, { i, handles: elems }) => elems[i],
+ { i, handles }
+ );
+ }
+
+ // For other Functions/Utilities
+ element.parentNode = parentNode;
+ const resolvedElement = await client.send("DOM.describeNode", { objectId: element._objectId, depth: -1 });
+ element.backendNodeId = resolvedElement.node.backendNodeId;
+ element.nodePosition = await this.selectors._findElementPositionInDomTree(element, describedScope.node, context, "");
+ elements.push(element);
+ }
+ }
+ }
+ }
+
+ // Sorting elements by their nodePosition, which is a index to the Element in the DOM tree
+ const getParts = (pos) => (pos || '').split('.').filter(Boolean).map(Number);
+ elements.sort((a, b) => {
+ const partsA = getParts(a.nodePosition);
+ const partsB = getParts(b.nodePosition);
+
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
+ const diff = (partsA[i] ?? -1) - (partsB[i] ?? -1);
+ if (diff !== 0) return diff;
+ }
+ return 0;
+ });
+
+ // Remove duplicates by backendNodeId, keeping the first occurrence
+ currentScopingElements = Array.from(
+ new Map(elements.map(e => [e.backendNodeId, e])).values()
+ );
+ }
+
+ return currentScopingElements;
+
+ }
}
class SignalBarrier {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/javascript.ts patchright/node_modules/playwright-core/src/server/javascript.ts
---
+++
@@ -22,6 +22,7 @@
import type * as dom from './dom';
import type { UtilityScript } from '@injected/utilityScript';
+import * as domValue from "./dom";
interface TaggedAsJSHandle<T> {
__jshandle: T;
@@ -149,17 +150,45 @@
return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg);
}
- async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg: any) {
- const value = await evaluateExpression(this._context, expression, { ...options, returnByValue: true }, this, arg);
- await this._context.doSlowMo();
- return value;
- }
+ async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg: any, isolatedContext?: boolean) {
- async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg: any): Promise<JSHandle<any>> {
- const value = await evaluateExpression(this._context, expression, { ...options, returnByValue: false }, this, arg);
- await this._context.doSlowMo();
- return value;
- }
+ let context = this._context;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext === true)
+ context = await frame._utilityContext();
+ else if (isolatedContext === false)
+ context = await frame._mainContext();
+ }
+ }
+ if (context !== this._context && context.adoptIfNeeded(this) === null)
+ context = this._context;
+
+ const value = await evaluateExpression(context, expression, { ...options, returnByValue: true }, this, arg);
+ await context.doSlowMo();
+ return value;
+ }
+
+ async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg: any, isolatedContext?: boolean): Promise<JSHandle<any>> {
+
+ let context = this._context;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext === true)
+ context = await frame._utilityContext();
+ else if (isolatedContext === false)
+ context = await frame._mainContext();
+ }
+ }
+ if (context !== this._context && context.adoptIfNeeded(this) === null)
+ context = this._context;
+
+ const value = await evaluateExpression(context, expression, { ...options, returnByValue: false }, this, arg);
+ await context.doSlowMo();
+ return value;
+ }
async getProperty(propertyName: string): Promise<JSHandle> {
const objectHandle = await this.evaluateHandle((object: any, propertyName) => {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/launchApp.ts patchright/node_modules/playwright-core/src/server/launchApp.ts
---
+++
@@ -117,7 +117,8 @@
return;
Object.entries(settings).map(([k, v]) => localStorage[k] = v);
(window as any).saveSettings = () => {
- (window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage }));
+ if (typeof (window as any)._saveSerializedSettings === 'function')
+ (window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage }));
};
})})(${settings});
`);
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/page.ts patchright/node_modules/playwright-core/src/server/page.ts
---
+++
@@ -51,6 +51,8 @@
import type * as channels from '@protocol/channels';
import type { BindingPayload } from '@injected/bindingsController';
import type { SelectorInfo } from './frameSelectors';
+import * as domValue from "./dom";
+import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from "./pageBinding";
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@@ -332,21 +334,16 @@
}
async exposeBinding(progress: Progress, name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<PageBinding> {
- if (this._pageBindings.has(name))
- throw new Error(`Function "${name}" has been already registered`);
- if (this.browserContext._pageBindings.has(name))
- throw new Error(`Function "${name}" has been already registered in the browser context`);
- await progress.race(this.browserContext.exposePlaywrightBindingIfNeeded());
- const binding = new PageBinding(this, name, playwrightBinding, needsHandle);
- this._pageBindings.set(name, binding);
- try {
- await progress.race(this.delegate.addInitScript(binding.initScript));
- await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
- return binding;
- } catch (error) {
- this._pageBindings.delete(name);
- throw error;
- }
+
+ if (this._pageBindings.has(name))
+ throw new Error(`Function "${name}" has been already registered`);
+ if (this.browserContext._pageBindings.has(name))
+ throw new Error(`Function "${name}" has been already registered in the browser context`);
+ const binding = new PageBinding(this, name, playwrightBinding, needsHandle);
+ this._pageBindings.set(name, binding);
+ await this.delegate.exposeBinding(binding);
+ return binding;
+
}
async removeExposedBinding(binding: PageBinding) {
@@ -870,13 +867,6 @@
}
}
- allInitScripts() {
- const bindings = [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()].map(binding => binding.initScript);
- if (this.browserContext.bindingsInitScript)
- bindings.unshift(this.browserContext.bindingsInitScript);
- return [...bindings, ...this.browserContext.initScripts, ...this.initScripts];
- }
-
getBinding(name: string) {
return this._pageBindings.get(name) || this.browserContext._pageBindings.get(name);
}
@@ -899,6 +889,12 @@
async setDockTile(image: Buffer) {
await this.delegate.setDockTile(image);
}
+
+ allBindings() {
+
+ return [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()];
+
+ }
}
export const WorkerEvent = {
@@ -953,83 +949,119 @@
this.openScope.close(new Error('Worker closed'));
}
- async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
- return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg);
- }
+ async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, isolatedContext?: boolean): Promise<any> {
- async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
- return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg);
- }
+ let context = await this._executionContextPromise;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext) context = await frame._utilityContext();
+ else if (!isolatedContext) context = await frame._mainContext();
+ }
+ }
+
+ return js.evaluateExpression(context, expression, { returnByValue: true, isFunction }, arg);
+ }
+
+ async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any, isolatedContext?: boolean): Promise<any> {
+
+ let context = await this._executionContextPromise;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext) context = await frame._utilityContext();
+ else if (!isolatedContext) context = await frame._mainContext();
+ }
+ }
+
+ return js.evaluateExpression(context, expression, { returnByValue: false, isFunction }, arg);
+ }
}
-export class PageBinding extends DisposableObject {
- private static kController = '__playwright__binding__controller__';
- static kBindingName = '__playwright__binding__';
- static createInitScript(browserContext: BrowserContext): InitScript {
- return new InitScript(browserContext, `
- (() => {
- const module = {};
- ${rawBindingsControllerSource.source}
- const property = '${PageBinding.kController}';
- if (!globalThis[property])
- globalThis[property] = new (module.exports.BindingsController())(globalThis, '${PageBinding.kBindingName}');
- })();
- `);
- }
+ export class PageBinding extends DisposableObject {
+ readonly source: string;
+ readonly name: string;
+ readonly playwrightFunction: frames.FunctionWithSource;
+ readonly initScript: InitScript;
+ readonly needsHandle: boolean;
+ readonly cleanupScript: string;
+ forClient?: unknown;
- readonly name: string;
- readonly playwrightFunction: frames.FunctionWithSource;
- readonly initScript: InitScript;
- readonly needsHandle: boolean;
- readonly cleanupScript: string;
- forClient?: unknown;
+ constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
+ super(parent);
+ this.name = name;
+ this.playwrightFunction = playwrightFunction;
+ this.initScript = new InitScript(parent, createPageBindingScript(name, needsHandle));
+ this.source = this.initScript.source;
+ this.cleanupScript = `delete globalThis[${JSON.stringify(name)}];`;
+ this.needsHandle = needsHandle;
+ }
- constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
- super(parent);
- this.name = name;
- this.playwrightFunction = playwrightFunction;
- this.initScript = new InitScript(parent, `globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)}, ${needsHandle})`);
- this.needsHandle = needsHandle;
- this.cleanupScript = `globalThis['${PageBinding.kController}'].removeBinding(${JSON.stringify(name)})`;
- }
+ static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
+ const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
- static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
- const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
- try {
- assert(context.world);
- const binding = page.getBinding(name);
- if (!binding)
- throw new Error(`Function "${name}" is not exposed`);
- let result: any;
- if (binding.needsHandle) {
- const handle = await context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].takeBindingHandle(arg)`, { isFunction: true }, { name, seq }).catch(e => null);
- result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, handle);
- } else {
- if (!Array.isArray(serializedArgs))
- throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
- const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
- result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args);
- }
- context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
- } catch (error) {
- context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
- }
- }
+ const deliver = async (deliverPayload: any) => {
+ let deliveryError: any;
+ try {
+ await context.evaluate(deliverBindingResult, deliverPayload);
+ return;
+ } catch (e) {
+ deliveryError = e;
+ }
+ const frame = context.frame;
+ if (!frame) {
+ debugLogger.log('error', deliveryError);
+ return;
+ }
+ const mainContext = await frame._mainContext().catch(() => null);
+ const utilityContext = await frame._utilityContext().catch(() => null);
+ for (const ctx of [mainContext, utilityContext]) {
+ if (!ctx || ctx === context)
+ continue;
+ try {
+ await ctx.evaluate(deliverBindingResult, deliverPayload);
+ return;
+ } catch {
+ }
+ }
+ debugLogger.log('error', deliveryError);
+ };
- override async dispose(): Promise<void> {
- await this.parent.removeExposedBinding(this);
- }
-}
+ try {
+ assert(context.world);
+ const binding = page.getBinding(name);
+ if (!binding)
+ throw new Error(`Function "${name}" is not exposed`);
+
+ let result: any;
+ if (binding.needsHandle) {
+ const handle = await context.evaluateHandle(takeBindingHandle, { name, seq }).catch(e => null);
+ result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle);
+ } else {
+ if (!Array.isArray(serializedArgs))
+ throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
+ const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
+ result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
+ }
+ await deliver({ name, seq, result });
+ } catch (error) {
+ await deliver({ name, seq, error });
+ }
+ }
+
+ override async dispose(): Promise<void> {
+ await this.parent.removeExposedBinding(this);
+ }
+ }
+
export class InitScript extends DisposableObject {
readonly source: string;
constructor(owner: BrowserContext | Page, source: string) {
super(owner);
- this.source = `(() => {
- ${source}
- })();`;
+ this.source = `(() => { ${source} })();`;
}
async dispose() {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/pageBinding.ts patchright/node_modules/playwright-core/src/server/pageBinding.ts
---
+++
@@ -0,0 +1,91 @@
+;
+ /**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * 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 { source } from '../utils/isomorphic/oldUtilityScriptSerializers';
+
+ import type { SerializedValue } from '../utils/isomorphic/oldUtilityScriptSerializers';
+
+ export type BindingPayload = {
+ name: string;
+ seq: number;
+ serializedArgs?: SerializedValue[],
+ };
+
+ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScriptSerializersFactory: typeof source) {
+ const { serializeAsCallArgument } = utilityScriptSerializersFactory;
+ // eslint-disable-next-line no-restricted-globals
+ const binding = (globalThis as any)[bindingName];
+ if (!binding || binding.toString().startsWith("(...args) => {")) return
+ // eslint-disable-next-line no-restricted-globals
+ (globalThis as any)[bindingName] = (...args: any[]) => {
+ // eslint-disable-next-line no-restricted-globals
+ const me = (globalThis as any)[bindingName];
+ if (needsHandle && args.slice(1).some(arg => arg !== undefined))
+ throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
+ let callbacks = me['callbacks'];
+ if (!callbacks) {
+ callbacks = new Map();
+ me['callbacks'] = callbacks;
+ }
+ const seq: number = (me['lastSeq'] || 0) + 1;
+ me['lastSeq'] = seq;
+ let handles = me['handles'];
+ if (!handles) {
+ handles = new Map();
+ me['handles'] = handles;
+ }
+ const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject }));
+ let payload: BindingPayload;
+ if (needsHandle) {
+ handles.set(seq, args[0]);
+ payload = { name: bindingName, seq };
+ } else {
+ const serializedArgs = [];
+ for (let i = 0; i < args.length; i++) {
+ serializedArgs[i] = serializeAsCallArgument(args[i], v => {
+ return { fallThrough: v };
+ });
+ }
+ payload = { name: bindingName, seq, serializedArgs };
+ }
+ binding(JSON.stringify(payload));
+ return promise;
+ };
+ // eslint-disable-next-line no-restricted-globals
+ }
+
+ export function takeBindingHandle(arg: { name: string, seq: number }) {
+ // eslint-disable-next-line no-restricted-globals
+ const handles = (globalThis as any)[arg.name]['handles'];
+ const handle = handles.get(arg.seq);
+ handles.delete(arg.seq);
+ return handle;
+ }
+
+ export function deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) {
+ // eslint-disable-next-line no-restricted-globals
+ const callbacks = (globalThis as any)[arg.name]['callbacks'];
+ if ('error' in arg)
+ callbacks.get(arg.seq).reject(arg.error);
+ else
+ callbacks.get(arg.seq).resolve(arg.result);
+ callbacks.delete(arg.seq);
+ }
+
+ export function createPageBindingScript(name: string, needsHandle: boolean) {
+ return `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle}, (${source})())`;
+ }
+
\ No newline at end of file
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/registry/index.ts patchright/node_modules/playwright-core/src/server/registry/index.ts
---
+++
@@ -1,20 +1,3 @@
-/**
- * Copyright 2017 Google Inc. All rights reserved.
- * Modifications copyright (c) Microsoft Corporation.
- *
- * 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 fs from 'fs';
import os from 'os';
import path from 'path';
@@ -600,7 +583,7 @@
`- current: ${currentDockerVersion.dockerImageName}`,
`- required: ${preferredDockerVersion.dockerImageName}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
} else if (isFfmpeg) {
prettyMessage = [
@@ -610,7 +593,7 @@
``,
` ${buildPlaywrightCLICommand(sdkLanguage, 'install ffmpeg')}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
} else {
prettyMessage = [
@@ -619,7 +602,7 @@
``,
` ${installCommand}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
}
throw new Error(`Executable doesn't exist at ${e}\n${wrapInASCIIBox(prettyMessage, 1)}`);
@@ -1083,7 +1066,7 @@
// Install browsers for this package.
for (const executable of executables) {
if (!executable._install)
- throw new Error(`ERROR: Playwright does not support installing ${executable.name}`);
+ throw new Error(`ERROR: Patchright does not support installing ${executable.name}`);
if (!getAsBooleanFromENV('CI') && !executable._isHermeticInstallation && !options?.force && executable.executablePath()) {
const { embedderName } = getEmbedderName();
@@ -1102,7 +1085,7 @@
``,
` ${command}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n'), 1) + '\n\n');
return;
}
@@ -1117,12 +1100,12 @@
` ${lockfilePath}`,
``,
`Either:`,
- `- wait a few minutes if other Playwright is installing browsers in parallel`,
+ `- wait a few minutes if other Patchright is installing browsers in parallel`,
`- remove lock manually with:`,
``,
` ${rmCommand} ${lockfilePath}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n'), 1));
} else {
throw e;
@@ -1217,13 +1200,13 @@
private async _downloadExecutable(descriptor: BrowsersJSONDescriptor, force: boolean, executablePath?: string) {
const downloadURLs = this._downloadURLs(descriptor);
if (!downloadURLs.length)
- throw new Error(`ERROR: Playwright does not support ${descriptor.name} on ${hostPlatform}`);
+ throw new Error(`ERROR: Patchright does not support ${descriptor.name} on ${hostPlatform}`);
if (!isOfficiallySupportedPlatform)
- logPolitely(`BEWARE: your OS is not officially supported by Playwright; downloading fallback build for ${hostPlatform}.`);
+ logPolitely(`BEWARE: your OS is not officially supported by Patchright; downloading fallback build for ${hostPlatform}.`);
if (descriptor.hasRevisionOverride) {
const message = `You are using a frozen ${descriptor.name} browser which does not receive updates anymore on ${hostPlatform}. Please update to the latest version of your operating system to test up-to-date browsers.`;
if (process.env.GITHUB_ACTIONS)
- console.log(`::warning title=Playwright::${message}`); // eslint-disable-line no-console
+ console.log(`::warning title=Patchright::${message}`); // eslint-disable-line no-console
else
logPolitely(message);
}
@@ -1444,7 +1427,7 @@
export function buildPlaywrightCLICommand(sdkLanguage: string, parameters: string): string {
switch (sdkLanguage) {
case 'python':
- return `playwright ${parameters}`;
+ return `patchright ${parameters}`;
case 'java':
return `mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="${parameters}"`;
case 'csharp':
@@ -1497,7 +1480,7 @@
``,
` ${installCommand}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
throw new Error('\n' + wrapInASCIIBox(prettyMessage, 1));
}
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/screenshotter.ts patchright/node_modules/playwright-core/src/server/screenshotter.ts
---
+++
@@ -254,6 +254,11 @@
if (disableAnimations)
progress.log(' disabled all CSS animations');
const syncAnimations = this._page.delegate.shouldToggleStyleSheetToSyncAnimations();
+
+ await Promise.all(this._page.frames().map(async (f: any) => {
+ try { await f._utilityContext(); } catch {}
+ }));
+
await progress.race(this._page.safeNonStallingEvaluateInAllFrames('(' + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${hideCaret}, ${disableAnimations}, ${syncAnimations})`, 'utility'));
try {
if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/trace/recorder/snapshotter.ts patchright/node_modules/playwright-core/src/server/trace/recorder/snapshotter.ts
---
+++
@@ -26,7 +26,6 @@
import type { SnapshotData } from './snapshotterInjected';
import type { RegisteredListener } from '../../utils/eventsHelper';
import type { Frame } from '../../frames';
-import type { InitScript } from '../../page';
import type { FrameSnapshot } from '@trace/snapshot';
export type SnapshotterBlob = {
@@ -44,7 +43,7 @@
private _delegate: SnapshotterDelegate;
private _eventListeners: RegisteredListener[] = [];
private _snapshotStreamer: string;
- private _initScript: InitScript | undefined;
+ private _initScript: boolean | undefined;
private _started = false;
constructor(context: BrowserContext, delegate: SnapshotterDelegate) {
@@ -67,7 +66,7 @@
async reset() {
if (this._started)
- await this._context.safeNonStallingEvaluateInAllFrames(`window["${this._snapshotStreamer}"].reset()`, 'main');
+ await this._context.safeNonStallingEvaluateInAllFrames(`window["${this._snapshotStreamer}"].reset()`, 'utility');
}
stop() {
@@ -75,25 +74,27 @@
}
async resetForReuse() {
- // Next time we start recording, we will call addInitScript again.
- if (this._initScript) {
- eventsHelper.removeEventListeners(this._eventListeners);
- await this._initScript.dispose();
- this._initScript = undefined;
- }
+
+ if (this._initScript) {
+ eventsHelper.removeEventListeners(this._eventListeners);
+ this._initScript = undefined;
+ this._initScriptSource = undefined;
+ }
+
}
async _initialize() {
- for (const page of this._context.pages())
- this._onPage(page);
- this._eventListeners = [
- eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
- ];
- const { javaScriptEnabled } = this._context._options;
- const initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`;
- this._initScript = await this._context.addInitScript(initScriptSource);
- await this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main');
+ const { javaScriptEnabled } = this._context._options;
+ this._initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`;
+ this._initScript = true;
+ for (const page of this._context.pages())
+ this._onPage(page);
+ this._eventListeners = [
+ eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
+ ];
+ await this._context.safeNonStallingEvaluateInAllFrames(this._initScriptSource, 'utility');
+
}
dispose() {
@@ -106,7 +107,7 @@
(frame as any)[kNeedsResetSymbol] = false;
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${needsReset ? 'true' : 'false'})`;
try {
- return await frame.nonStallingRawEvaluateInExistingMainContext(expression);
+ return await frame.nonStallingEvaluateInExistingContext(expression, 'utility');
} catch (e) {
// If we fail to capture snapshot in this frame, we cannot rely on the snapshot index
// being the same here and in snapshotter injected script.
@@ -159,6 +160,7 @@
for (const frame of page.frames())
this._annotateFrameHierarchy(frame);
this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame)));
+ this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.InternalFrameNavigatedToNewDocument, (frame: Frame) => this._onFrameNavigated(frame)));
}
private async _annotateFrameHierarchy(frame: Frame) {
@@ -167,7 +169,7 @@
const parent = frame.parentFrame();
if (!parent)
return;
- const context = await parent._mainContext();
+ const context = await parent._utilityContext();
await context?.evaluate(({ snapshotStreamer, frameElement, frameId }) => {
(window as any)[snapshotStreamer].markIframe(frameElement, frameId);
}, { snapshotStreamer: this._snapshotStreamer, frameElement, frameId: frame.guid });
@@ -175,6 +177,18 @@
} catch (e) {
}
}
+
+ _initScriptSource: string | undefined;
+
+ async _onFrameNavigated(frame: Frame) {
+
+ if (!this._initScriptSource)
+ return;
+ try {
+ await frame.nonStallingEvaluateInExistingContext(this._initScriptSource, 'utility');
+ } catch (e) {}
+
+ }
}
const kNeedsResetSymbol = Symbol('kNeedsReset');
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/trace/recorder/snapshotterInjected.ts patchright/node_modules/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
---
+++
@@ -85,33 +85,15 @@
class Streamer {
private _lastSnapshotNumber = 0;
- private _staleStyleSheets = new Set<CSSStyleSheet>();
private _modifiedStyleSheets = new Set<CSSStyleSheet>();
- private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
private _fakeBase: HTMLBaseElement;
private _observer: MutationObserver;
constructor() {
- const invalidateCSSGroupingRule = (rule: CSSGroupingRule) => {
- if (rule.parentStyleSheet)
- this._invalidateStyleSheet(rule.parentStyleSheet);
- };
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'deleteRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'addRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'removeRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'rules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'cssRules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'replaceSync', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSGroupingRule.prototype, 'insertRule', invalidateCSSGroupingRule);
- this._interceptNativeMethod(window.CSSGroupingRule.prototype, 'deleteRule', invalidateCSSGroupingRule);
- this._interceptNativeGetter(window.CSSGroupingRule.prototype, 'cssRules', invalidateCSSGroupingRule);
this._interceptNativeSetter(window.StyleSheet.prototype, 'disabled', (sheet: StyleSheet) => {
if (sheet instanceof CSSStyleSheet)
this._invalidateStyleSheet(sheet as CSSStyleSheet);
});
- this._interceptNativeAsyncMethod(window.CSSStyleSheet.prototype, 'replace', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
-
this._fakeBase = document.createElement('base');
this._observer = new MutationObserver(list => this._handleMutations(list));
@@ -162,40 +144,6 @@
});
}
- private _interceptNativeMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
- const native = obj[method] as Function;
- if (!native)
- return;
- obj[method] = function(...args: any[]) {
- const result = native.call(this, ...args);
- cb(this, result);
- return result;
- };
- }
-
- private _interceptNativeAsyncMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
- const native = obj[method] as Function;
- if (!native)
- return;
- obj[method] = async function(...args: any[]) {
- const result = await native.call(this, ...args);
- cb(this, result);
- return result;
- };
- }
-
- private _interceptNativeGetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
- const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
- Object.defineProperty(obj, prop, {
- ...descriptor,
- get: function() {
- const result = descriptor.get!.call(this);
- cb(this, result);
- return result;
- },
- });
- }
-
private _interceptNativeSetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
Object.defineProperty(obj, prop, {
@@ -213,42 +161,37 @@
ensureCachedData(mutation.target).attributesCached = undefined;
}
- private _invalidateStyleSheet(sheet: CSSStyleSheet) {
- if (this._readingStyleSheet)
- return;
- this._staleStyleSheets.add(sheet);
- if (sheet.href !== null)
- this._modifiedStyleSheets.add(sheet);
- }
-
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet, forceText?: boolean): string | undefined {
- const data = ensureCachedData(sheet);
- if (this._staleStyleSheets.has(sheet) || (forceText && data.cssText === undefined)) {
- this._staleStyleSheets.delete(sheet);
- try {
- data.cssText = this._getSheetText(sheet);
- } catch (e) {
- // Sometimes we cannot access cross-origin stylesheets.
- data.cssText = '';
- }
- }
- return data.cssText;
+
+ const data = ensureCachedData(sheet);
+ try {
+ data.cssText = this._getSheetText(sheet);
+ } catch (e) {
+ data.cssText = '';
+ }
+ return data.cssText;
+
}
// Returns either content, ref, or no override.
private _updateLinkStyleSheetTextIfNeeded(sheet: CSSStyleSheet, snapshotNumber: number): string | number | undefined {
- const data = ensureCachedData(sheet);
- if (this._staleStyleSheets.has(sheet)) {
- this._staleStyleSheets.delete(sheet);
- try {
- data.cssText = this._getSheetText(sheet);
- data.cssRef = snapshotNumber;
- return data.cssText;
- } catch (e) {
- // Sometimes we cannot access cross-origin stylesheets.
- }
- }
- return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
+
+ const data = ensureCachedData(sheet);
+ try {
+ const currentText = this._getSheetText(sheet);
+ if (data.cssText === undefined) {
+ data.cssText = currentText;
+ return undefined;
+ }
+ if (currentText === data.cssText)
+ return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
+ data.cssText = currentText;
+ data.cssRef = snapshotNumber;
+ return data.cssText;
+ } catch (e) {
+ return undefined;
+ }
+
}
markIframe(iframeElement: HTMLIFrameElement | HTMLFrameElement, frameId: string) {
@@ -256,8 +199,6 @@
}
reset() {
- this._staleStyleSheets.clear();
-
const visitNode = (node: Node | ShadowRoot) => {
resetCachedData(node);
if (node.nodeType === Node.ELEMENT_NODE) {
@@ -326,17 +267,12 @@
}
private _getSheetText(sheet: CSSStyleSheet): string {
- this._readingStyleSheet = true;
- try {
- if (sheet.disabled)
- return '';
- const rules: string[] = [];
- for (const rule of sheet.cssRules)
- rules.push(rule.cssText);
- return rules.join('\n');
- } finally {
- this._readingStyleSheet = false;
- }
+
+ const rules: string[] = [];
+ for (const rule of sheet.cssRules)
+ rules.push(rule.cssText);
+ return rules.join('\n');
+
}
captureSnapshot(needsReset: boolean): SnapshotData | undefined {
@@ -635,18 +571,18 @@
collectionTime: 0,
};
- for (const sheet of this._modifiedStyleSheets) {
- if (sheet.href === null)
- continue;
- const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
- if (content === undefined) {
- // Unable to capture stylesheet contents.
- continue;
- }
- const base = this._getSheetBase(sheet);
- const url = removeHash(this._resolveUrl(base, sheet.href!));
- result.resourceOverrides.push({ url, content, contentType: 'text/css' },);
- }
+ for (const sheet of document.styleSheets) {
+ if (sheet.href === null)
+ continue;
+ const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
+ if (content === undefined) {
+ // Unable to capture stylesheet contents.
+ continue;
+ }
+ const base = this._getSheetBase(sheet);
+ const url = removeHash(this._resolveUrl(base, sheet.href!));
+ result.resourceOverrides.push({ url, content, contentType: 'text/css' },);
+ }
result.collectionTime = performance.now() - timestamp;
return result;
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/trace/recorder/tracing.ts patchright/node_modules/playwright-core/src/server/trace/recorder/tracing.ts
---
+++
@@ -670,6 +670,11 @@
}
function createBeforeActionTraceEvent(metadata: CallMetadata, parentId?: string): trace.BeforeActionTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
const event: trace.BeforeActionTraceEvent = {
@@ -689,6 +694,11 @@
}
function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
@@ -699,6 +709,11 @@
}
function createActionLogTraceEvent(metadata: CallMetadata, message: string): trace.LogTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
@@ -710,6 +725,11 @@
}
function createAfterActionTraceEvent(metadata: CallMetadata): trace.AfterActionTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/server/utils/expectUtils.ts patchright/node_modules/playwright-core/src/server/utils/expectUtils.ts
---
+++
@@ -116,8 +116,7 @@
details.printedDiff = undefined;
}
- const align = !details.errorMessage && details.printedExpected?.startsWith('Expected:')
- && (!details.printedReceived || details.printedReceived.startsWith('Received:'));
+ const align = !details.errorMessage && details.printedExpected?.startsWith('Expected:') && (!details.printedReceived || details.printedReceived.startsWith('Received:'));
if (details.locator)
message += `Locator: ${align ? ' ' : ''}${details.locator}\n`;
if (details.printedExpected)
diff -ruN -x protocol.yml --minimal playwright/node_modules/playwright-core/src/utils/isomorphic/oldUtilityScriptSerializers.ts patchright/node_modules/playwright-core/src/utils/isomorphic/oldUtilityScriptSerializers.ts
---
+++
@@ -0,0 +1,292 @@
+;
+ /**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * 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.
+ */
+
+ type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64';
+
+ export type SerializedValue =
+ undefined | boolean | number | string |
+ { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } |
+ { d: string } |
+ { u: string } |
+ { bi: string } |
+ { e: { n: string, m: string, s: string } } |
+ { r: { p: string, f: string } } |
+ { a: SerializedValue[], id: number } |
+ { o: { k: string, v: SerializedValue }[], id: number } |
+ { ref: number } |
+ { h: number } |
+ { ta: { b: string, k: TypedArrayKind } };
+
+ type HandleOrValue = { h: number } | { fallThrough: any };
+
+ type VisitorInfo = {
+ visited: Map<object, number>;
+ lastId: number;
+ };
+
+ export function source() {
+
+ function isRegExp(obj: any): obj is RegExp {
+ try {
+ return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isDate(obj: any): obj is Date {
+ try {
+ return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]';
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isURL(obj: any): obj is URL {
+ try {
+ return obj instanceof URL || Object.prototype.toString.call(obj) === '[object URL]';
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isError(obj: any): obj is Error {
+ try {
+ return obj instanceof Error || (obj && Object.getPrototypeOf(obj)?.name === 'Error');
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isTypedArray(obj: any, constructor: Function): boolean {
+ try {
+ return obj instanceof constructor || Object.prototype.toString.call(obj) === `[object ${constructor.name}]`;
+ } catch (error) {
+ return false;
+ }
+ }
+
+ const typedArrayConstructors: Record<TypedArrayKind, Function> = {
+ i8: Int8Array,
+ ui8: Uint8Array,
+ ui8c: Uint8ClampedArray,
+ i16: Int16Array,
+ ui16: Uint16Array,
+ i32: Int32Array,
+ ui32: Uint32Array,
+ // TODO: add Float16Array once it's in baseline
+ f32: Float32Array,
+ f64: Float64Array,
+ bi64: BigInt64Array,
+ bui64: BigUint64Array,
+ };
+
+ function typedArrayToBase64(array: any) {
+ /**
+ * Firefox does not support iterating over typed arrays, so we use `.toBase64`.
+ * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().'
+ */
+ if ('toBase64' in array)
+ return array.toBase64();
+ const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join('');
+ return btoa(binary);
+ }
+
+ function base64ToTypedArray(base64: string, TypedArrayConstructor: any) {
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++)
+ bytes[i] = binary.charCodeAt(i);
+ return new TypedArrayConstructor(bytes.buffer);
+ }
+
+ function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map<number, object> = new Map()): any {
+ if (Object.is(value, undefined))
+ return undefined;
+ if (typeof value === 'object' && value) {
+ if ('ref' in value)
+ return refs.get(value.ref);
+ if ('v' in value) {
+ if (value.v === 'undefined')
+ return undefined;
+ if (value.v === 'null')
+ return null;
+ if (value.v === 'NaN')
+ return NaN;
+ if (value.v === 'Infinity')
+ return Infinity;
+ if (value.v === '-Infinity')
+ return -Infinity;
+ if (value.v === '-0')
+ return -0;
+ return undefined;
+ }
+ if ('d' in value)
+ return new Date(value.d);
+ if ('u' in value)
+ return new URL(value.u);
+ if ('bi' in value)
+ return BigInt(value.bi);
+ if ('e' in value) {
+ const error = new Error(value.e.m);
+ error.name = value.e.n;
+ error.stack = value.e.s;
+ return error;
+ }
+ if ('r' in value)
+ return new RegExp(value.r.p, value.r.f);
+ if ('a' in value) {
+ const result: any[] = [];
+ refs.set(value.id, result);
+ for (const a of value.a)
+ result.push(parseEvaluationResultValue(a, handles, refs));
+ return result;
+ }
+ if ('o' in value) {
+ const result: any = {};
+ refs.set(value.id, result);
+ for (const { k, v } of value.o)
+ result[k] = parseEvaluationResultValue(v, handles, refs);
+ return result;
+ }
+ if ('h' in value)
+ return handles[value.h];
+ if ('ta' in value)
+ return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]);
+ }
+ return value;
+ }
+
+ function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
+ return serialize(value, handleSerializer, { visited: new Map(), lastId: 0 });
+ }
+
+ function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
+ if (value && typeof value === 'object') {
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window)
+ return 'ref: <Window>';
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document)
+ return 'ref: <Document>';
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node)
+ return 'ref: <Node>';
+ }
+ return innerSerialize(value, handleSerializer, visitorInfo);
+ }
+
+ function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
+ const result = handleSerializer(value);
+ if ('fallThrough' in result)
+ value = result.fallThrough;
+ else
+ return result;
+
+ if (typeof value === 'symbol')
+ return { v: 'undefined' };
+ if (Object.is(value, undefined))
+ return { v: 'undefined' };
+ if (Object.is(value, null))
+ return { v: 'null' };
+ if (Object.is(value, NaN))
+ return { v: 'NaN' };
+ if (Object.is(value, Infinity))
+ return { v: 'Infinity' };
+ if (Object.is(value, -Infinity))
+ return { v: '-Infinity' };
+ if (Object.is(value, -0))
+ return { v: '-0' };
+
+ if (typeof value === 'boolean')
+ return value;
+ if (typeof value === 'number')
+ return value;
+ if (typeof value === 'string')
+ return value;
+ if (typeof value === 'bigint')
+ return { bi: value.toString() };
+
+ if (isError(value)) {
+ let stack;
+ if (value.stack?.startsWith(value.name + ': ' + value.message)) {
+ // v8
+ stack = value.stack;
+ } else {
+ stack = `${value.name}: ${value.message}
+${value.stack}`;
+ }
+ return { e: { n: value.name, m: value.message, s: stack } };
+ }
+ if (isDate(value))
+ return { d: value.toJSON() };
+ if (isURL(value))
+ return { u: value.toJSON() };
+ if (isRegExp(value))
+ return { r: { p: value.source, f: value.flags } };
+ for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) {
+ if (isTypedArray(value, ctor))
+ return { ta: { b: typedArrayToBase64(value), k } };
+ }
+
+ const id = visitorInfo.visited.get(value);
+ if (id)
+ return { ref: id };
+
+ if (Array.isArray(value)) {
+ const a = [];
+ const id = ++visitorInfo.lastId;
+ visitorInfo.visited.set(value, id);
+ for (let i = 0; i < value.length; ++i)
+ a.push(serialize(value[i], handleSerializer, visitorInfo));
+ return { a, id };
+ }
+
+ if (typeof value === 'object') {
+ const o: { k: string, v: SerializedValue }[] = [];
+ const id = ++visitorInfo.lastId;
+ visitorInfo.visited.set(value, id);
+ for (const name of Object.keys(value)) {
+ let item;
+ try {
+ item = value[name];
+ } catch (e) {
+ continue; // native bindings will throw sometimes;
+ }
+ if (name === 'toJSON' && typeof item === 'function')
+ o.push({ k: name, v: { o: [], id: 0 } });
+ else
+ o.push({ k: name, v: serialize(item, handleSerializer, visitorInfo) });
+ }
+
+ let jsonWrapper;
+ try {
+ // If Object.keys().length === 0 we fall back to toJSON if it exists
+ if (o.length === 0 && value.toJSON && typeof value.toJSON === 'function')
+ jsonWrapper = { value: value.toJSON() };
+ } catch (e) {
+ }
+ if (jsonWrapper)
+ return innerSerialize(jsonWrapper.value, handleSerializer, visitorInfo);
+
+ return { o, id };
+ }
+ }
+
+ return { parseEvaluationResultValue, serializeAsCallArgument };
+ }
+
\ No newline at end of file
diff -ruN -x protocol.yml --minimal playwright/node_modules/recorder/src/recorder.tsx patchright/node_modules/recorder/src/recorder.tsx
---
+++
@@ -101,7 +101,7 @@
}, [backend, mode, selectedTab, setSelectedTab, source]);
React.useEffect(() => {
- backend.setAutoExpect({ autoExpect });
+ try { window.dispatch({ event: 'setAutoExpect', params: { autoExpect } }); } catch {}
}, [autoExpect, backend]);
React.useLayoutEffect(() => {
diff -ruN -x protocol.yml --minimal playwright/packages/injected/src/xpathSelectorEngine.ts patchright/packages/injected/src/xpathSelectorEngine.ts
---
+++
@@ -18,6 +18,43 @@
export const XPathEngine: SelectorEngine = {
queryAll(root: SelectorRoot, selector: string): Element[] {
+
+ if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+ const result: Element[] = [];
+ // Custom ClosedShadowRoot XPath Engine
+ const parser = new DOMParser();
+ // Function to (recursively) get all elements in the shadowRoot
+ function getAllChildElements(node) {
+ const elements = [];
+ const traverse = (currentNode) => {
+ if (currentNode.nodeType === Node.ELEMENT_NODE) elements.push(currentNode);
+ currentNode.childNodes?.forEach(traverse);
+ };
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE) {
+ traverse(node);
+ }
+ return elements;
+ }
+ // Setting innerHTMl and childElements (all, recursive) to avoid race conditions
+ const csrHTMLContent = root.innerHTML;
+ const csrChildElements = getAllChildElements(root);
+ const htmlDoc = parser.parseFromString(csrHTMLContent, 'text/html');
+ const rootDiv = htmlDoc.body;
+ const rootDivChildElements = getAllChildElements(rootDiv);
+ // Use the namespace prefix in the XPath expression
+ const it = htmlDoc.evaluate(selector, htmlDoc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
+ for (let node = it.iterateNext(); node; node = it.iterateNext()) {
+ // -1 for the body element
+ const nodeIndex = rootDivChildElements.indexOf(node) - 1;
+ if (nodeIndex >= 0) {
+ const originalNode = csrChildElements[nodeIndex];
+ if (originalNode.nodeType === Node.ELEMENT_NODE)
+ result.push(originalNode as Element);
+ }
+ }
+ return result;
+ }
+
if (selector.startsWith('/') && root.nodeType !== Node.DOCUMENT_NODE)
selector = '.' + selector;
const result: Element[] = [];
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/cli/program.ts patchright/packages/playwright-core/src/cli/program.ts
---
+++
@@ -1,21 +1,3 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * 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.
- */
-
-/* eslint-disable no-console */
-
import '../bootstrap';
import { gracefullyProcessExitDoNotHang, getPackageManagerExecCommand } from '../utils';
import { addTraceCommands } from '../tools/trace/traceCli';
@@ -69,10 +51,10 @@
program
.command('install [browser...]')
- .description('ensure browsers necessary for this version of Playwright are installed')
+ .description('ensure browsers necessary for this version of Patchright are installed')
.option('--with-deps', 'install system dependencies for browsers')
.option('--dry-run', 'do not execute installation, only print information')
- .option('--list', 'prints list of browsers from all playwright installations')
+ .option('--list', 'prints list of browsers from all patchright installations')
.option('--force', 'force reinstall of already installed browsers')
.option('--only-shell', 'only install headless shell when installing chromium')
.option('--no-shell', 'do not install chromium headless shell')
@@ -95,8 +77,8 @@
program
.command('uninstall')
- .description('Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.')
- .option('--all', 'Removes all browsers used by any Playwright installation from the system.')
+ .description('Removes browsers used by this installation of Patchright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.')
+ .option('--all', 'Removes all browsers used by any Patchright installation from the system.')
.action(async (options: { all?: boolean }) => {
const { uninstallBrowsers } = await import('./installActions');
uninstallBrowsers(options).catch(logErrorAndExit);
@@ -284,7 +266,7 @@
.option('--save-har-glob <glob pattern>', 'filter entries in the HAR by matching url against this glob pattern')
.option('--save-storage <filename>', 'save context storage state at the end, for later use with --load-storage')
.option('--timezone <time zone>', 'time zone to emulate, for example "Europe/Rome"')
- .option('--timeout <timeout>', 'timeout for Playwright actions in milliseconds, no timeout by default')
+ .option('--timeout <timeout>', 'timeout for Patchright actions in milliseconds, no timeout by default')
.option('--user-agent <ua string>', 'specify user agent string')
.option('--user-data-dir <directory>', 'use the specified user data directory instead of a new context')
.option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"');
@@ -293,14 +275,14 @@
function buildBasePlaywrightCLICommand(cliTargetLang: string | undefined): string {
switch (cliTargetLang) {
case 'python':
- return `playwright`;
+ return `patchright`;
case 'java':
return `mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="...options.."`;
case 'csharp':
return `pwsh bin/Debug/netX/playwright.ps1`;
default: {
const packageManagerCommand = getPackageManagerExecCommand();
- return `${packageManagerCommand} playwright`;
+ return `${packageManagerCommand} patchright`;
}
}
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/cli/programWithTestStub.ts patchright/packages/playwright-core/src/cli/programWithTestStub.ts
---
+++
@@ -1,21 +1,3 @@
-/**
- * Copyright (c) Microsoft Corporation.
- *
- * 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.
- */
-
-/* eslint-disable no-console */
-
import { gracefullyProcessExitDoNotHang } from '../server/utils/processLauncher';
import { getPackageManager } from '../utils';
import { program } from './program';
@@ -34,24 +16,24 @@
packages.push('playwright');
const packageManager = getPackageManager();
if (packageManager === 'yarn') {
- console.error(`Please install @playwright/test package before running "yarn playwright ${command}"`);
+ console.error(`Please install @playwright/test package before running "yarn patchright ${command}"`);
console.error(` yarn remove ${packages.join(' ')}`);
console.error(' yarn add -D @playwright/test');
} else if (packageManager === 'pnpm') {
- console.error(`Please install @playwright/test package before running "pnpm exec playwright ${command}"`);
+ console.error(`Please install @playwright/test package before running "pnpm exec patchright ${command}"`);
console.error(` pnpm remove ${packages.join(' ')}`);
console.error(' pnpm add -D @playwright/test');
} else {
- console.error(`Please install @playwright/test package before running "npx playwright ${command}"`);
+ console.error(`Please install @playwright/test package before running "npx patchright ${command}"`);
console.error(` npm uninstall ${packages.join(' ')}`);
console.error(' npm install -D @playwright/test');
}
}
const kExternalPlaywrightTestCommands = [
- ['test', 'Run tests with Playwright Test.'],
- ['show-report', 'Show Playwright Test HTML report.'],
- ['merge-reports', 'Merge Playwright Test Blob reports'],
+ ['test', 'Run tests with Patchright Test.'],
+ ['show-report', 'Show Patchright Test HTML report.'],
+ ['merge-reports', 'Merge Patchright Test Blob reports'],
];
function addExternalPlaywrightTestCommands() {
for (const [command, description] of kExternalPlaywrightTestCommands) {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/browserContext.ts patchright/packages/playwright-core/src/client/browserContext.ts
---
+++
@@ -146,9 +146,9 @@
// a) removing "dialog" listener subscription (client->server)
// b) actual "dialog" event (server->client)
if (dialogObject.type() === 'beforeunload')
- dialog.accept({}).catch(() => {});
+ dialogObject._wrapApiCall(() => dialog.accept({}).catch(() => {}), { internal: true });
else
- dialog.dismiss().catch(() => {});
+ dialogObject._wrapApiCall(() => dialog.dismiss().catch(() => {}), { internal: true });
}
});
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
@@ -356,17 +356,20 @@
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
+ await this.installInjectRoute();
const source = await evaluationScript(this._platform, script, arg);
return DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
}
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}): Promise<DisposableObject> {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
this._bindings.set(name, callback);
return DisposableObject.from(result.disposable);
}
async exposeFunction(name: string, callback: Function): Promise<DisposableObject> {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name });
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
this._bindings.set(name, binding);
@@ -560,6 +563,25 @@
await this._channel.exposeConsoleApi();
}
+ routeInjecting: boolean = false;
+
+ async installInjectRoute() {
+
+ if (this.routeInjecting) return;
+ await this.route('**/*', async route => {
+ try {
+ if (route.request().resourceType() === 'document' && route.request().url().startsWith('http')) {
+ await route.fallback({ patchrightInitScript: true } as any);
+ } else {
+ await route.fallback();
+ }
+ } catch (error) {
+ await route.fallback();
+ }
+ });
+ this.routeInjecting = true;
+
+ }
}
async function prepareStorageState(platform: Platform, storageState: string | SetStorageState): Promise<NonNullable<channels.BrowserNewContextParams['storageState']>> {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/clientHelper.ts patchright/packages/playwright-core/src/client/clientHelper.ts
---
+++
@@ -50,5 +50,5 @@
}
export function addSourceUrlToScript(source: string, path: string): string {
- return `${source}\n//# sourceURL=${path.replace(/\n/g, '')}`;
+ return source
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/clock.ts patchright/packages/playwright-core/src/client/clock.ts
---
+++
@@ -25,6 +25,7 @@
}
async install(options: { time?: number | string | Date } = { }) {
+ await this._browserContext.installInjectRoute()
await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/frame.ts patchright/packages/playwright-core/src/client/frame.ts
---
+++
@@ -177,25 +177,34 @@
}
async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise<void> {
- if (urlMatches(this._page?.context()._options.baseURL, this.url(), url))
- return await this.waitForLoadState(options.waitUntil, options);
- await this.waitForNavigation({ url, ...options });
+ if (urlMatches(this._page?.context()._options.baseURL, this.url(), url))
+ return await this.waitForLoadState(options.waitUntil, options);
+ try {
+ await this.waitForNavigation({ url, ...options });
+ } catch (error) {
+ if (urlMatches(this._page?.context()._options.baseURL, this.url(), url)) {
+ await this.waitForLoadState(options.waitUntil, options);
+ return;
+ }
+ throw error;
+ }
+
}
async frameElement(): Promise<ElementHandle> {
return ElementHandle.from((await this._channel.frameElement()).element);
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return parseResult(result.value);
}
@@ -231,9 +240,9 @@
return parseResult(result.value);
}
- async $$eval<R, Arg>(selector: string, pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 3);
- const result = await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async $$eval<R, Arg>(selector: string, pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 4);
+ const result = await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return parseResult(result.value);
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/jsHandle.ts patchright/packages/playwright-core/src/client/jsHandle.ts
---
+++
@@ -36,14 +36,28 @@
this._channel.on('previewUpdated', ({ preview }) => this._preview = preview);
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg): Promise<R> {
- const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
- return parseResult(result.value);
+ async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+
+ const result = await this._channel.evaluateExpression({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === 'function',
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ });
+ return parseResult(result.value);
+
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
- return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<T, Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+
+ const result = await this._channel.evaluateExpressionHandle({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === 'function',
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ });
+ return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
+
}
async getProperty(propertyName: string): Promise<JSHandle> {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/locator.ts patchright/packages/playwright-core/src/client/locator.ts
---
+++
@@ -27,7 +27,7 @@
import type * as api from '../../types/types';
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
import type * as channels from '@protocol/channels';
-
+import { JSHandle, parseResult, serializeArgument } from "./jsHandle";
export type LocatorOptions = {
hasText?: string | RegExp;
@@ -124,16 +124,56 @@
});
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<R> {
- return await this._withElement(h => h.evaluate(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout });
+ async evaluate<R, Arg>(pageFunction: structs.PageFunctionOn<SVGElement | HTMLElement, Arg, R>, arg?: Arg, options?: TimeoutOptions, isolatedContext: boolean = true): Promise<R> {
+
+ if (typeof options === 'boolean') {
+ isolatedContext = options;
+ options = undefined;
+ }
+ return await this._withElement(
+ async (h) =>
+ parseResult(
+ (
+ await h._channel.evaluateExpression({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === "function",
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ })
+ ).value
+ ),
+ { title: "Evaluate", timeout: options?.timeout }
+ );
+
}
- async evaluateAll<R, Arg>(pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg): Promise<R> {
- return await this._frame.$$eval(this._selector, pageFunction, arg);
+ async evaluateAll<R, Arg>(pageFunction: structs.PageFunctionOn<Element[], Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+
+ return await this._frame.$$eval(this._selector, pageFunction, arg, isolatedContext);
+
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<any, Arg, R>, arg?: Arg, options?: TimeoutOptions): Promise<structs.SmartHandle<R>> {
- return await this._withElement(h => h.evaluateHandle(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout });
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunctionOn<any, Arg, R>, arg?: Arg, options?: TimeoutOptions, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+
+ if (typeof options === 'boolean') {
+ isolatedContext = options;
+ options = undefined;
+ }
+ return await this._withElement(
+ async (h) =>
+ JSHandle.from(
+ (
+ await h._channel.evaluateExpressionHandle({
+ expression: String(pageFunction),
+ isFunction: typeof pageFunction === "function",
+ arg: serializeArgument(arg),
+ isolatedContext: isolatedContext,
+ })
+ ).handle
+ ) as any as structs.SmartHandle<R>,
+ { title: "Evaluate", timeout: options?.timeout }
+ );
+
}
async fill(value: string, options: channels.ElementHandleFillOptions & TimeoutOptions = {}): Promise<void> {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/network.ts patchright/packages/playwright-core/src/client/network.ts
---
+++
@@ -15,7 +15,7 @@
*/
import { ChannelOwner } from './channelOwner';
-import { isTargetClosedError } from './errors';
+import { isTargetClosedError, TargetClosedError } from './errors';
import { Events } from './events';
import { APIResponse } from './fetch';
import { Frame } from './frame';
@@ -183,7 +183,13 @@
}
async allHeaders(): Promise<Headers> {
- return (await this._actualHeaders()).headers();
+
+ const headers = await this._actualHeaders();
+ const page = this._safePage();
+ if (page?._closeWasCalled)
+ throw new TargetClosedError();
+ return headers.headers();
+
}
async headersArray(): Promise<HeadersArray> {
@@ -272,19 +278,22 @@
}
_applyFallbackOverrides(overrides: FallbackOverrides) {
- if (overrides.url)
- this._fallbackOverrides.url = overrides.url;
- if (overrides.method)
- this._fallbackOverrides.method = overrides.method;
- if (overrides.headers)
- this._fallbackOverrides.headers = overrides.headers;
- if (isString(overrides.postData))
- this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, 'utf-8');
- else if (overrides.postData instanceof Buffer)
- this._fallbackOverrides.postDataBuffer = overrides.postData;
- else if (overrides.postData)
- this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), 'utf-8');
+ if (overrides.url)
+ this._fallbackOverrides.url = overrides.url;
+ if (overrides.method)
+ this._fallbackOverrides.method = overrides.method;
+ if (overrides.headers)
+ this._fallbackOverrides.headers = overrides.headers;
+ if ((overrides as any).patchrightInitScript)
+ (this._fallbackOverrides as any).patchrightInitScript = true;
+ if (isString(overrides.postData))
+ this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, "utf-8");
+ else if (overrides.postData instanceof Buffer)
+ this._fallbackOverrides.postDataBuffer = overrides.postData;
+ else if (overrides.postData)
+ this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), "utf-8");
+
}
_fallbackOverridesForContinue() {
@@ -449,6 +458,7 @@
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
postData: options.postDataBuffer,
isFallback,
+ patchrightInitScript: (options as any).patchrightInitScript
}));
}
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/page.ts patchright/packages/playwright-core/src/client/page.ts
---
+++
@@ -313,9 +313,9 @@
return await this._mainFrame.dispatchEvent(selector, type, eventInit, options);
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- assertMaxArguments(arguments.length, 2);
- return await this._mainFrame.evaluateHandle(pageFunction, arg);
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+ assertMaxArguments(arguments.length, 3);
+ return await this._mainFrame.evaluateHandle(pageFunction, arg, isolatedContext);
}
async $eval<R, Arg>(selector: string, pageFunction: structs.PageFunctionOn<Element, Arg, R>, arg?: Arg): Promise<R> {
@@ -341,6 +341,7 @@
}
async exposeFunction(name: string, callback: Function) {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name });
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
this._bindings.set(name, binding);
@@ -348,6 +349,7 @@
}
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}) {
+ await this.installInjectRoute();
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
this._bindings.set(name, callback);
return DisposableObject.from(result.disposable);
@@ -507,12 +509,13 @@
return this._viewportSize || null;
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 2);
- return await this._mainFrame.evaluate(pageFunction, arg);
+ async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 3);
+ return await this._mainFrame.evaluate(pageFunction, arg, isolatedContext);
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
+ await this.installInjectRoute();
const source = await evaluationScript(this._platform, script, arg);
return DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
}
@@ -864,6 +867,26 @@
async _setDockTile(image: Buffer) {
await this._channel.setDockTile({ image });
}
+
+ routeInjecting: boolean = false;
+
+ async installInjectRoute() {
+
+ if (this.routeInjecting || this.context().routeInjecting) return;
+ await this.route('**/*', async route => {
+ try {
+ if (route.request().resourceType() === 'document' && route.request().url().startsWith('http')) {
+ await route.fallback({ patchrightInitScript: true } as any);
+ } else {
+ await route.fallback();
+ }
+ } catch (error) {
+ await route.fallback();
+ }
+ });
+ this.routeInjecting = true;
+
+ }
}
export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/tracing.ts patchright/packages/playwright-core/src/client/tracing.ts
---
+++
@@ -38,6 +38,7 @@
}
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, live?: boolean } = {}) {
+ if (typeof this._parent.installInjectRoute === 'function') await this._parent.installInjectRoute();
await this._wrapApiCall(async () => {
this._includeSources = !!options.sources;
this._isLive = !!options.live;
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/client/worker.ts patchright/packages/playwright-core/src/client/worker.ts
---
+++
@@ -62,15 +62,15 @@
return this._initializer.url;
}
- async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<R> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluate<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<R> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return parseResult(result.value);
}
- async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg): Promise<structs.SmartHandle<R>> {
- assertMaxArguments(arguments.length, 2);
- const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) });
+ async evaluateHandle<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, isolatedContext: boolean = true): Promise<structs.SmartHandle<R>> {
+ assertMaxArguments(arguments.length, 3);
+ const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg), isolatedContext: isolatedContext });
return JSHandle.from(result.handle) as any as structs.SmartHandle<R>;
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/android/android.ts patchright/packages/playwright-core/src/server/android/android.ts
---
+++
@@ -1,19 +1,3 @@
-/**
- * Copyright 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 { EventEmitter } from 'events';
import fs from 'fs';
import os from 'os';
@@ -184,7 +168,7 @@
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
const fullName = path.join(executable.directory!, file);
if (!fs.existsSync(fullName))
- throw new Error(`Please install Android driver apk using '${packageManagerCommand} playwright install android'`);
+ throw new Error(`Please install Android driver apk using '${packageManagerCommand} patchright install android'`);
await this.installApk(progress, await progress.race(fs.promises.readFile(fullName)));
}
} else {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/browserContext.ts patchright/packages/playwright-core/src/server/browserContext.ts
---
+++
@@ -167,7 +167,7 @@
await this.exposeConsoleApi();
if (this._options.serviceWorkers === 'block')
- await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
+ await this.addInitScript(`if (navigator.serviceWorker) navigator.serviceWorker.register = async () => { };`);
if (this._options.permissions)
await this.grantPermissions(this._options.permissions);
@@ -352,18 +352,13 @@
if (page.getBinding(name))
throw new Error(`Function "${name}" has been already registered in one of the pages`);
}
- await progress.race(this.exposePlaywrightBindingIfNeeded());
const binding = new PageBinding(this, name, playwrightBinding, needsHandle);
binding.forClient = forClient;
this._pageBindings.set(name, binding);
- try {
- await progress.race(this.doAddInitScript(binding.initScript));
- await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
- return binding;
- } catch (error) {
- this._pageBindings.delete(name);
- throw error;
- }
+
+ await this.doExposeBinding(binding);
+ return binding;
+
}
async removeExposedBinding(binding: PageBinding) {
@@ -881,4 +876,5 @@
strictSelectors: false,
serviceWorkers: 'allow',
locale: 'en-US',
+ focusControl: false
};
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/chromium.ts patchright/packages/playwright-core/src/server/chromium/chromium.ts
---
+++
@@ -320,8 +320,6 @@
const chromeArguments = [...chromiumSwitches(options.assistantMode, options.channel)];
// See https://issues.chromium.org/issues/40277080
- chromeArguments.push('--enable-unsafe-swiftshader');
-
if (options.headless) {
chromeArguments.push('--headless');
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/chromiumSwitches.ts patchright/packages/playwright-core/src/server/chromium/chromiumSwitches.ts
---
+++
@@ -51,26 +51,16 @@
'--disable-field-trial-config', // https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
'--disable-background-networking',
'--disable-background-timer-throttling',
- '--disable-backgrounding-occluded-windows',
- '--disable-back-forward-cache', // Avoids surprises like main request not being intercepted during page.goBack().
- '--disable-breakpad',
- '--disable-client-side-phishing-detection',
- '--disable-component-extensions-with-background-pages',
- '--disable-component-update', // Avoids unneeded network activity after startup.
+ '--disable-backgrounding-occluded-windows', // Avoids surprises like main request not being intercepted during page.goBack().
+ '--disable-breakpad', // Avoids unneeded network activity after startup.
'--no-default-browser-check',
- '--disable-default-apps',
'--disable-dev-shm-usage',
- '--disable-extensions',
'--disable-features=' + disabledFeatures(assistantMode).join(','),
process.env.PLAYWRIGHT_LEGACY_SCREENSHOT ? '' : '--enable-features=CDPScreenshotNewSurface',
- '--allow-pre-commit-input',
'--disable-hang-monitor',
- '--disable-ipc-flooding-protection',
- '--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--force-color-profile=srgb',
- '--metrics-recording-only',
'--no-first-run',
'--password-store=basic',
'--use-mock-keychain',
@@ -79,11 +69,8 @@
'--export-tagged-pdf',
// https://chromium-review.googlesource.com/c/chromium/src/+/4853540
'--disable-search-engine-choice-screen',
- // https://issues.chromium.org/41491762
- '--unsafely-disable-devtools-self-xss-warnings',
// Edge can potentially restart on Windows (msRelaunchNoCompatLayer) which looses its file descriptors (stdout/stderr) and CDP (3/4). Disable until fixed upstream.
'--edge-skip-compat-layer-relaunch',
- assistantMode ? '' : '--enable-automation',
// This disables Chrome for Testing infobar that is visible in the persistent context.
// The switch is ignored everywhere else, including Chromium/Chrome/Edge.
'--disable-infobars',
@@ -91,4 +78,5 @@
'--disable-search-engine-choice-screen',
// Prevents the "three dots" menu crash in IdentityManager::HasPrimaryAccount for ephemeral contexts.
android ? '' : '--disable-sync',
+ '--disable-blink-features=AutomationControlled'
].filter(Boolean);
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/crBrowser.ts patchright/packages/playwright-core/src/server/chromium/crBrowser.ts
---
+++
@@ -522,8 +522,10 @@
}
async doRemoveInitScripts(initScripts: InitScript[]) {
- for (const page of this.pages())
- await (page.delegate as CRPage).removeInitScripts(initScripts);
+
+ for (const page of this.pages())
+ await (page.delegate as CRPage).removeInitScripts(initScripts);
+
}
async doUpdateRequestInterception(): Promise<void> {
@@ -611,6 +613,20 @@
const rootSession = await this._browser._clientRootSession();
return rootSession.attachToTarget(targetId);
}
+
+ async doExposeBinding(binding: PageBinding) {
+
+ for (const page of this.pages())
+ await (page.delegate as CRPage).exposeBinding(binding);
+
+ }
+
+ async doRemoveExposedBindings() {
+
+ for (const page of this.pages())
+ await (page.delegate as CRPage).removeExposedBindings();
+
+ }
}
export function shouldProxyLoopback(bypass: string | undefined) {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/crCoverage.ts patchright/packages/playwright-core/src/server/chromium/crCoverage.ts
---
+++
@@ -82,10 +82,13 @@
this._scriptIds.clear();
this._scriptSources.clear();
this._eventListeners = [
- eventsHelper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
- eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
- eventsHelper.addEventListener(this._client, 'Debugger.paused', this._onDebuggerPaused.bind(this)),
- ];
+ eventsHelper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
+
+ eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
+ eventsHelper.addEventListener(this._client, 'Page.frameNavigated', this._onFrameNavigated.bind(this)),
+
+ eventsHelper.addEventListener(this._client, 'Debugger.paused', this._onDebuggerPaused.bind(this)),
+ ];
await Promise.all([
this._client.send('Profiler.enable'),
this._client.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }),
@@ -142,6 +145,11 @@
}
return coverage;
}
+
+ _onFrameNavigated(event: Protocol.Page.frameNavigatedPayload) {
+ if (event.frame.parentId) return;
+ this._onExecutionContextsCleared();
+ }
}
class CSSCoverage {
@@ -169,9 +177,12 @@
this._stylesheetURLs.clear();
this._stylesheetSources.clear();
this._eventListeners = [
- eventsHelper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
- eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
- ];
+ eventsHelper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
+
+ eventsHelper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
+ eventsHelper.addEventListener(this._client, 'Page.frameNavigated', this._onFrameNavigated.bind(this)),
+
+ ];
await Promise.all([
this._client.send('DOM.enable'),
this._client.send('CSS.enable'),
@@ -235,6 +246,11 @@
return coverage;
}
+
+ _onFrameNavigated(event: Protocol.Page.frameNavigatedPayload) {
+ if (event.frame.parentId) return;
+ this._onExecutionContextsCleared();
+ }
}
function convertToDisjointRanges(nestedRanges: {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/crDevTools.ts patchright/packages/playwright-core/src/server/chromium/crDevTools.ts
---
+++
@@ -64,7 +64,6 @@
}).catch(e => null);
});
Promise.all([
- session.send('Runtime.enable'),
session.send('Runtime.addBinding', { name: kBindingName }),
session.send('Page.enable'),
session.send('Page.addScriptToEvaluateOnNewDocument', { source: `
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/crNetworkManager.ts patchright/packages/playwright-core/src/server/chromium/crNetworkManager.ts
---
+++
@@ -30,7 +30,7 @@
import type * as types from '../types';
import type { CRPage } from './crPage';
import type { CRServiceWorker } from './crServiceWorker';
-
+import crypto from "crypto";
type SessionInfo = {
session: CRSession;
@@ -97,6 +97,7 @@
if (info)
eventsHelper.removeEventListeners(info.eventListeners);
this._sessions.delete(session);
+ if (!this._sessions.size) this._alreadyTrackedNetworkIds.clear();
}
private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
@@ -142,6 +143,10 @@
async setRequestInterception(value: boolean) {
this._userRequestInterceptionEnabled = value;
await this._updateProtocolRequestInterception();
+
+ if (this._page)
+ await this._forEachSession(info => info.session.send('Network.setCacheDisabled', { cacheDisabled: this._page.needsRequestInterception() }));
+
}
async _updateProtocolRequestInterception() {
@@ -156,7 +161,11 @@
const enabled = this._protocolRequestInterceptionEnabled;
if (initial && !enabled)
return;
- const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
+
+ const hasHarRecorders = !!this._page?.browserContext?._harRecorders?.size;
+ const userInterception = this._page ? this._page.needsRequestInterception() : false;
+ const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: userInterception || hasHarRecorders });
+
let fetchPromise = Promise.resolve<any>(undefined);
if (!info.workerFrame) {
if (enabled)
@@ -238,6 +247,7 @@
}
_onRequestPaused(sessionInfo: SessionInfo, event: Protocol.Fetch.requestPausedPayload) {
+ if (this._alreadyTrackedNetworkIds.has(event.networkId)) return;
if (!event.networkId) {
// Fetch without networkId means that request was not recognized by inspector, and
// it will never receive Network.requestWillBeSent. Continue the request to not affect it.
@@ -276,6 +286,10 @@
}
_onRequest(requestWillBeSentSessionInfo: SessionInfo, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedSessionInfo: SessionInfo | undefined, requestPausedEvent: Protocol.Fetch.requestPausedPayload | undefined) {
+
+ if (this._alreadyTrackedNetworkIds.has(requestWillBeSentEvent.requestId))
+ return;
+
if (requestWillBeSentEvent.request.url.startsWith('data:'))
return;
let redirectedFrom: InterceptableRequest | null = null;
@@ -287,6 +301,13 @@
redirectedFrom = request;
}
}
+ const isInterceptedOptionsPreflight = !!requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && requestWillBeSentEvent.initiator.type === 'preflight';
+
+ if (isInterceptedOptionsPreflight && !(this._page || this._serviceWorker).needsRequestInterception()) {
+ requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent!.requestId });
+ return;
+ }
+
let frame = requestWillBeSentEvent.frameId ? this._page?.frameManager.frame(requestWillBeSentEvent.frameId) : requestWillBeSentSessionInfo.workerFrame;
// Requests from workers lack frameId, because we receive Network.requestWillBeSent
// on the worker target. However, we receive Fetch.requestPaused on the page target,
@@ -306,7 +327,6 @@
// we accept all CORS options, assuming that this was intended when setting route.
//
// Note: it would be better to match the URL against interception patterns.
- const isInterceptedOptionsPreflight = !!requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && requestWillBeSentEvent.initiator.type === 'preflight';
if (isInterceptedOptionsPreflight && (this._page || this._serviceWorker)!.needsRequestInterception()) {
const requestHeaders = requestPausedEvent.request.headers;
const responseHeaders: Protocol.Fetch.HeaderEntry[] = [
@@ -346,7 +366,7 @@
}
requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers: headersOverride });
} else {
- route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId);
+ route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId, this._page, requestPausedEvent.networkId ?? requestPausedEvent.requestId, this);
}
}
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
@@ -558,6 +578,8 @@
if (request.session !== sessionInfo.session && !sessionInfo.isMain && (request._documentId === request._requestId || sessionInfo.workerFrame))
request.session = sessionInfo.session;
}
+
+ _alreadyTrackedNetworkIds: Set<string> = new Set();
}
class InterceptableRequest {
@@ -614,38 +636,149 @@
_alreadyContinuedParams: Protocol.Fetch.continueRequestParameters | undefined;
_fulfilled: boolean = false;
- constructor(session: CRSession, interceptionId: string) {
+ constructor(session: CRSession, interceptionId: string, page: Page | null, networkId: string, sessionManager: CRNetworkManager) {
+ this._page = void 0;
+ this._networkId = void 0;
+ this._sessionManager = void 0;
this._session = session;
this._interceptionId = interceptionId;
+ this._page = page;
+ this._networkId = networkId;
+ this._sessionManager = sessionManager;
+ eventsHelper.addEventListener(this._session, 'Fetch.requestPaused', async e => await this._networkRequestIntercepted(e));
}
async continue(overrides: types.NormalizedContinueOverrides): Promise<void> {
- this._alreadyContinuedParams = {
- requestId: this._interceptionId!,
- url: overrides.url,
- headers: overrides.headers,
- method: overrides.method,
- postData: overrides.postData ? overrides.postData.toString('base64') : undefined
- };
- await catchDisallowedErrors(async () => {
- await this._session.send('Fetch.continueRequest', this._alreadyContinuedParams);
- });
+ ;
+ const patchrightInitScript = !!(overrides as any).patchrightInitScript;
+ this._alreadyContinuedParams = {
+ requestId: this._interceptionId,
+ url: overrides.url,
+ headers: overrides.headers,
+ method: overrides.method,
+ postData: overrides.postData?.toString('base64'),
+ };
+ if (patchrightInitScript) {
+ await catchDisallowedErrors(async () => {
+ this._sessionManager._alreadyTrackedNetworkIds.add(this._networkId);
+ try {
+ await this._session.send('Fetch.continueRequest', { requestId: this._interceptionId, interceptResponse: true });
+ } catch (e) {
+ this._sessionManager._alreadyTrackedNetworkIds.delete(this._networkId);
+ throw e;
+ }
+ });
+ } else {
+ await catchDisallowedErrors(async () => {
+ await this._session.send('Fetch.continueRequest', this._alreadyContinuedParams);
+ });
+ }
+
}
async fulfill(response: types.NormalizedFulfillResponse) {
- this._fulfilled = true;
- const body = response.isBase64 ? response.body : Buffer.from(response.body).toString('base64');
- const responseHeaders = splitSetCookieHeader(response.headers);
- await catchDisallowedErrors(async () => {
- await this._session.send('Fetch.fulfillRequest', {
- requestId: this._interceptionId!,
- responseCode: response.status,
- responsePhrase: network.statusText(response.status),
- responseHeaders,
- body,
- });
- });
+ const isTextHtml = response.headers.some((header) => header.name.toLowerCase() === "content-type" && header.value.includes("text/html"));
+ const pageDelegate = this._page?.delegate ?? null;
+ const initScriptTag = pageDelegate?.initScriptTag ?? "";
+ const allInjections = pageDelegate
+ ? [...pageDelegate._mainFrameSession._evaluateOnNewDocumentScripts]
+ : [];
+
+ if (isTextHtml && allInjections.length && initScriptTag) {
+ // Decode body if needed
+ if (response.isBase64) {
+ response.isBase64 = false;
+ response.body = Buffer.from(response.body, "base64").toString("utf-8");
+ }
+
+ // CSP Detection and Fixing
+ const cspHeaderNames = ["content-security-policy", "content-security-policy-report-only"];
+ const extractNonce = (cspValue) => {
+ const match = cspValue.match(/script-src[^;]*'nonce-([^'"s;]+)'/i);
+ return match?.[1] ?? null;
+ };
+ let useNonce = false;
+ let scriptNonce = null;
+
+ // Fix CSP in headers
+ for (const header of response.headers) {
+ if (cspHeaderNames.includes(header.name.toLowerCase())) {
+ const originalCsp = header.value ?? "";
+ // Extract nonce if present
+ const nonce = !useNonce && extractNonce(originalCsp);
+ if (nonce) {
+ scriptNonce = nonce;
+ useNonce = true;
+ }
+
+ header.value = this._fixCSP(originalCsp, scriptNonce);
+ }
+ }
+
+ // Fix CSP in meta tags
+ if (typeof response.body === "string" && response.body.length) {
+ response.body = response.body.replace(
+ /<meta[^>]*http-equiv=(?:"|')?Content-Security-Policy(?:"|')?[^>]*>/gi,
+ (match) => {
+ const contentMatch = match.match(/content=(?:"|')([^"']*)(?:"|')/i);
+ if (!contentMatch)
+ return match;
+
+ let originalCsp = contentMatch[1];
+ // Decode HTML entities
+ originalCsp = originalCsp
+ .replace(/&amp;/g, '&') // Must be first!
+ .replace(/&lt;/g, '<')
+ .replace(/&gt;/g, '>')
+ .replace(/&quot;/g, '"')
+ .replace(/&#x27;/g, "'")
+ .replace(/&#x22;/g, '"')
+ .replace(/&nbsp;/g, ' ')
+ .replace(/&#(d+);/g, (match, dec) => String.fromCharCode(dec))
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
+
+ // Extract nonce if present
+ const nonce = !useNonce && extractNonce(originalCsp);
+ if (nonce) {
+ scriptNonce = nonce;
+ useNonce = true;
+ }
+
+ const fixedCsp = this._fixCSP(originalCsp, scriptNonce);
+ // Re-encode for HTML
+ const encodedCsp = fixedCsp.replace(/'/g, '&#x27;').replace(/"/g, '&#x22;');
+ return match.replace(contentMatch[1], encodedCsp);
+ }
+ );
+ }
+
+ // Build injection HTML - only use nonce if one was found in existing CSP
+ const nonceAttr = useNonce ? `nonce="${scriptNonce}"` : '';
+ let injectionHTML = "";
+ allInjections.forEach((script) => {
+ let scriptId = crypto.randomBytes(22).toString("hex");
+ let scriptSource = script.source ?? script;
+ injectionHTML += `<script class="${initScriptTag}" ${nonceAttr} id="${scriptId}" type="text/javascript">document.getElementById("${scriptId}")?.remove();${scriptSource}</script>`;
+ });
+
+ // Inject at END of <head>
+ response.body = this._injectIntoHead(response.body, injectionHTML);
+ }
+
+ this._fulfilled = true;
+ const body = response.isBase64 ? response.body : Buffer.from(response.body).toString("base64");
+ const responseHeaders = splitSetCookieHeader(response.headers);
+ await catchDisallowedErrors(async () => {
+ await this._session.send("Fetch.fulfillRequest", {
+ requestId: response.interceptionId ? response.interceptionId : this._interceptionId,
+ responseCode: response.status,
+ responsePhrase: network.statusText(response.status),
+ responseHeaders,
+ body
+ });
+ });
+
}
async abort(errorCode: string = 'failed') {
@@ -658,6 +791,211 @@
});
});
}
+
+ _fixCSP(csp: string | null, scriptNonce: string | null) {
+
+ if (!csp || typeof csp !== 'string')
+ return csp;
+
+ // Split by semicolons and clean up
+ const directives = csp.split(';')
+ .map(d => d.trim())
+ .filter(Boolean);
+
+ const fixedDirectives = [];
+ let hasScriptSrc = false;
+
+ const addIfMissing = (values: string[], ...items: string[]) => {
+ for (const item of items)
+ if (!values.includes(item))
+ values.push(item);
+ };
+
+
+ for (let directive of directives) {
+ // Improved directive parsing to handle more edge cases
+ const directiveMatch = directive.match(/^([a-zA-Z-]+)\s+(.*)$/);
+ if (!directiveMatch) {
+ fixedDirectives.push(directive);
+ continue;
+ }
+
+ const directiveName = directiveMatch[1].toLowerCase();
+ const directiveValues = directiveMatch[2].split(/\s+/).filter(Boolean);
+
+ switch (directiveName) {
+ case 'script-src':
+ hasScriptSrc = true;
+
+ // Add nonce if we have one and it's not already present
+ if (scriptNonce && !directiveValues.some(v => v.includes(`nonce-${scriptNonce}`)))
+ directiveValues.push(`'nonce-${scriptNonce}'`);
+
+ // Add 'unsafe-eval' if not present
+ addIfMissing(directiveValues, "'unsafe-eval'");
+
+ // Add unsafe-inline if not present and no nonce is being used
+ if (!scriptNonce)
+ addIfMissing(directiveValues, "'unsafe-inline'");
+
+ // Add wildcard for external scripts if not already present
+ if (!directiveValues.includes("*") && !directiveValues.includes("'self'") && !directiveValues.some(v => v.includes("https:")))
+ directiveValues.push("*");
+
+ fixedDirectives.push(`script-src ${directiveValues.join(' ')}`);
+ break;
+
+ case 'style-src':
+ // Add 'unsafe-inline' for styles if not present
+ addIfMissing(directiveValues, "'unsafe-inline'");
+ fixedDirectives.push(`style-src ${directiveValues.join(' ')}`);
+ break;
+
+ case 'img-src':
+ case 'font-src':
+ // Allow data: URLs for images/fonts if not already allowed
+ if (!directiveValues.includes('*'))
+ addIfMissing(directiveValues, 'data:');
+ fixedDirectives.push(`${directiveName} ${directiveValues.join(' ')}`);
+ break;
+
+ case 'connect-src':
+ // Allow WebSocket connections if not already allowed
+ if (!directiveValues.some(v => v.includes('ws:') || v.includes('wss:') || v === '*'))
+ addIfMissing(directiveValues, 'ws:', 'wss:');
+ fixedDirectives.push(`connect-src ${directiveValues.join(' ')}`);
+ break;
+
+ case 'frame-ancestors':
+ // If completely blocked with 'none', allow 'self' at least
+ let frameAncestorValues = directiveValues.includes("'none'") ? "'self'" : directiveValues.join(' ');
+ fixedDirectives.push(`frame-ancestors ${frameAncestorValues}`);
+ break;
+
+ default:
+ // Keep other directives as-is
+ fixedDirectives.push(directive);
+ }
+ }
+
+ // Add script-src if it doesn't exist (for our injected scripts)
+ if (!hasScriptSrc) {
+ fixedDirectives.push(
+ scriptNonce
+ ? `script-src 'self' 'unsafe-eval' 'nonce-${scriptNonce}' *`
+ : `script-src 'self' 'unsafe-eval' 'unsafe-inline' *`
+ );
+ }
+
+ return fixedDirectives.join('; ');
+
+ }
+
+ _injectIntoHead(body: string, injectionHTML: string) {
+
+ // Inject at END of <head>
+ const lower = body.toLowerCase();
+ const headStartIndex = lower.indexOf("<head");
+
+ if (headStartIndex !== -1) {
+ const headStartTagEndIndex = lower.indexOf(">", headStartIndex) + 1;
+ const headEndTagIndex = lower.indexOf("</head>", headStartIndex);
+
+ if (headEndTagIndex !== -1) {
+ // Find the first <script> tag in <head>, skipping HTML comments
+ const headContent = lower.slice(headStartTagEndIndex, headEndTagIndex);
+
+ // Look for the first <script> tag in the head content but ignore comments
+ let firstScriptIndex = -1;
+ let searchPos = 0;
+
+ while (searchPos < headContent.length) {
+ const commentStart = headContent.indexOf("<!--", searchPos);
+ const scriptStart = headContent.indexOf("<script", searchPos);
+
+ // No more script tags, inject at the end of head content
+ if (scriptStart === -1)
+ break;
+
+ if (commentStart !== -1 && commentStart < scriptStart) {
+ const commentEnd = headContent.indexOf("-->", commentStart);
+ if (commentEnd === -1)
+ break;
+
+ // Skip past the comment and keep searching
+ searchPos = commentEnd + 3;
+ } else {
+ // Found a script tag outside a comment
+ firstScriptIndex = scriptStart;
+ break;
+ }
+ }
+
+ const insertAt =
+ firstScriptIndex !== -1
+ ? headStartTagEndIndex + firstScriptIndex // Before first <script>
+ : headEndTagIndex; // Before </head>
+
+ return body.slice(0, insertAt) + injectionHTML + body.slice(insertAt);
+ }
+
+ // No </head> found — inject right after the opening <head> tag
+ return body.slice(0, headStartTagEndIndex) + injectionHTML + body.slice(headStartTagEndIndex);
+ }
+
+ // No <head> — try after <!DOCTYPE>
+ const doctypeIndex = lower.indexOf("<!doctype");
+ if (doctypeIndex === 0) {
+ const doctypeEnd = body.indexOf(">", doctypeIndex) + 1;
+ return body.slice(0, doctypeEnd) + injectionHTML + body.slice(doctypeEnd);
+ }
+
+ // Try after <html>
+ const htmlTagIndex = lower.indexOf("<html");
+ if (htmlTagIndex !== -1) {
+ const htmlTagEnd = body.indexOf(">", htmlTagIndex) + 1;
+ return body.slice(0, htmlTagEnd) + `<head>${injectionHTML}</head>` + body.slice(htmlTagEnd);
+ }
+
+ // Last resort — prepend to body
+ return injectionHTML + body;
+
+ }
+
+ async _networkRequestIntercepted(event: Protocol.Fetch.requestPausedPayload) {
+
+ if (this._networkId != event.networkId || !this._sessionManager._alreadyTrackedNetworkIds.has(event.networkId))
+ return;
+
+ const trackedNetworkId = event.networkId;
+ try {
+ if (event.resourceType !== 'Document')
+ return;
+
+ if (event.responseStatusCode >= 301 && event.responseStatusCode <= 308 || (event.redirectedRequestId && !event.responseStatusCode)) {
+ await this._session.send('Fetch.continueRequest', { requestId: event.requestId, interceptResponse: true });
+ } else {
+ const responseBody = await this._session.send('Fetch.getResponseBody', { requestId: event.requestId });
+ await this.fulfill({
+ headers: event.responseHeaders,
+ isBase64: true,
+ body: responseBody.body,
+ status: event.responseStatusCode,
+ interceptionId: event.requestId,
+ resourceType: event.resourceType,
+ });
+ }
+ } catch (error) {
+ if (error.message.includes("Can only get response body on HeadersReceived pattern matched requests.")) {
+ await this._session.send("Fetch.continueRequest", { requestId: event.requestId, interceptResponse: true });
+ } else {
+ await this._session._sendMayFail("Fetch.continueRequest", { requestId: event.requestId });
+ }
+ } finally {
+ this._sessionManager._alreadyTrackedNetworkIds.delete(trackedNetworkId);
+ }
+
+ }
}
// In certain cases, protocol will return error if the request was already canceled
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/crPage.ts patchright/packages/playwright-core/src/server/chromium/crPage.ts
---
+++
@@ -45,7 +45,7 @@
import type { Progress } from '../progress';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
-
+import crypto from "crypto";
export type WindowBounds = { top?: number, left?: number, width?: number, height?: number };
@@ -96,7 +96,10 @@
this.updateOffline();
this.updateExtraHTTPHeaders();
this.updateHttpCredentials();
- this.updateRequestInterception();
+
+ this._networkManager.setRequestInterception(true);
+ this.initScriptTag = crypto.randomBytes(20).toString('hex');
+
this._mainFrameSession = new FrameSession(this, client, targetId, null);
this._sessions.set(targetId, this._mainFrameSession);
if (opener && !browserContext._options.noDefaultViewport) {
@@ -126,15 +129,15 @@
}
_sessionForFrame(frame: frames.Frame): FrameSession {
- // Frame id equals target id.
- while (!this._sessions.has(frame._id)) {
- const parent = frame.parentFrame();
- if (!parent)
- throw new Error(`Frame has been detached.`);
- frame = parent;
+ // Frame id equals target id.
+ while (!this._sessions.has(frame._id)) {
+ const parent = frame.parentFrame();
+ if (!parent)
+ throw new Error(`Frame was detached`);
+ frame = parent;
+ }
+ return this._sessions.get(frame._id)!;
}
- return this._sessions.get(frame._id)!;
- }
private _sessionForHandle(handle: dom.ElementHandle): FrameSession {
const frame = handle._context.frame;
@@ -225,6 +228,7 @@
}
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
+ this._page.initScripts.push(initScript);
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
}
@@ -364,6 +368,19 @@
async setDockTile(image: Buffer): Promise<void> {
await this._mainFrameSession._client.send('Browser.setDockTile', { image: image.toString('base64') });
}
+
+ async exposeBinding(binding: PageBinding) {
+
+ await this._forAllFrameSessions(frame => frame._initBinding(binding));
+ await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source).catch(e => {})));
+
+ }
+
+ async removeExposedBindings() {
+
+ await this._forAllFrameSessions(frame => frame._removeExposedBindings());
+
+ }
}
class FrameSession {
@@ -438,6 +455,7 @@
}
async _initialize(hasUIWindow: boolean) {
+ const pageEnablePromise = this._client.send('Page.enable');
if (!this._page.isStorageStatePage && hasUIWindow &&
!this._crPage._browserContext._browser.isClank() &&
!this._crPage._browserContext._options.noDefaultViewport) {
@@ -451,6 +469,11 @@
let lifecycleEventsEnabled: Promise<any>;
if (!this._isMainFrame())
this._addRendererListeners();
+
+ let bufferedDialogEvents: any[] | undefined = this._isMainFrame() ? [] : undefined;
+ if (bufferedDialogEvents)
+ this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.javascriptDialogOpening', (event: any) => bufferedDialogEvents ? bufferedDialogEvents.push(event) : undefined));
+
this._addBrowserListeners();
// Buffer attachedToTarget events until we receive the frame tree.
@@ -460,11 +483,18 @@
this._bufferedAttachedToTargetEvents = [];
const promises: Promise<any>[] = [
- this._client.send('Page.enable'),
+ pageEnablePromise,
this._client.send('Page.getFrameTree').then(({ frameTree }) => {
if (this._isMainFrame()) {
this._handleFrameTree(frameTree);
this._addRendererListeners();
+
+ // Replay any dialog events that arrived before _addRendererListeners
+ const pendingDialogEvents = bufferedDialogEvents || [];
+ bufferedDialogEvents = undefined;
+ for (const event of pendingDialogEvents)
+ this._onDialog(event);
+
}
// Now that we have the frame tree, it is possible to insert oopif targets at the right place.
@@ -472,17 +502,6 @@
this._bufferedAttachedToTargetEvents = undefined;
for (const event of attachedToTargetEvents)
this._onAttachedToTarget(event);
-
- const localFrames = this._isMainFrame() ? this._page.frames() : [this._page.frameManager.frame(this._targetId)!];
- for (const frame of localFrames) {
- // Note: frames might be removed before we send these.
- this._client._sendMayFail('Page.createIsolatedWorld', {
- frameId: frame._id,
- grantUniveralAccess: true,
- worldName: this._crPage.utilityWorldName,
- });
- }
-
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) {
// Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page
@@ -492,13 +511,24 @@
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
});
} else {
+
+ const localFrames = this._isMainFrame() ? this._page.frames() : [this._page.frameManager.frame(this._targetId)!];
+ for (const frame of localFrames) {
+ this._page.frameManager.frame(frame._id)._context("utility").catch(() => {});
+ for (const binding of this._crPage._browserContext._pageBindings.values())
+ frame.evaluateExpression(binding.source).catch(e => {});
+ for (const source of this._crPage._browserContext.initScripts)
+ frame.evaluateExpression(source.source).catch(e => {});
+ for (const source of this._crPage._page.initScripts)
+ frame.evaluateExpression(source.source).catch(e => {});
+ }
+
this._firstNonInitialNavigationCommittedFulfill();
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
}
}),
this._client.send('Log.enable', {}),
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
- this._client.send('Runtime.enable', {}),
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: '',
worldName: this._crPage.utilityWorldName,
@@ -509,8 +539,10 @@
if (!this._page.isStorageStatePage) {
if (this._crPage._browserContext.needsPlaywrightBinding())
promises.push(this.exposePlaywrightBinding());
- if (this._isMainFrame())
- promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true }));
+
+ if (this._isMainFrame() && !this._crPage._browserContext._options.focusControl)
+ promises.push(this._client.send("Emulation.setFocusEmulationEnabled", { enabled: true }));
+
const options = this._crPage._browserContext._options;
if (options.bypassCSP)
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
@@ -533,12 +565,22 @@
promises.push(this._updateGeolocation(true));
promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true));
- for (const initScript of this._crPage._page.allInitScripts())
- promises.push(this._evaluateOnNewDocument(initScript, 'main', true /* runImmediately */));
+
+ for (const binding of this._crPage._page.allBindings()) promises.push(this._initBinding(binding));
+ for (const initScript of this._crPage._browserContext.initScripts) promises.push(this._evaluateOnNewDocument(initScript, 'main'));
+ for (const initScript of this._crPage._page.initScripts) promises.push(this._evaluateOnNewDocument(initScript, 'main'));
+
}
- promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
+
+ if (!(this._crPage._page._pageBindings.size || this._crPage._browserContext._pageBindings.size))
+ promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
+
promises.push(this._firstNonInitialNavigationCommittedPromise);
await Promise.all(promises);
+
+ if (this._crPage._page._pageBindings.size || this._crPage._browserContext._pageBindings.size)
+ await this._client.send('Runtime.runIfWaitingForDebugger');
+
}
dispose() {
@@ -562,13 +604,33 @@
return { newDocumentId: response.loaderId };
}
- _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
+ async _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
if (this._eventBelongsToStaleFrame(event.frameId))
return;
if (event.name === 'load')
this._page.frameManager.frameLifecycleEvent(event.frameId, 'load');
else if (event.name === 'DOMContentLoaded')
this._page.frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');
+
+ // Only do full init script cleanup on load to reduce CDP round-trip pressure.
+ // Other lifecycle events just get a minimal runIfWaitingForDebugger call.
+ if (event.name !== "load") {
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ return;
+ }
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ var document = await this._client._sendMayFail("DOM.getDocument");
+ if (!document) return
+ var query = await this._client._sendMayFail("DOM.querySelectorAll", {
+ nodeId: document.root.nodeId,
+ selector: "[class=" + this._crPage.initScriptTag + "]"
+ });
+ if (!query) return
+ for (const nodeId of query.nodeIds) await this._client._sendMayFail("DOM.removeNode", { nodeId: nodeId });
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ // ensuring execution context
+ try { await this._page.frameManager.frame(this._targetId)._context("utility") } catch { };
+
}
_handleFrameTree(frameTree: Protocol.Page.FrameTree) {
@@ -615,12 +677,33 @@
this._page.frameManager.frameAttached(frameId, parentFrameId);
}
- _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
+ async _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
if (this._eventBelongsToStaleFrame(framePayload.id))
return;
this._page.frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url + (framePayload.urlFragment || ''), framePayload.name || '', framePayload.loaderId, initial);
if (!initial)
this._firstNonInitialNavigationCommittedFulfill();
+
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ // patchright: For non-initial navigations, skip DOM cleanup since the document just changed
+ // and init script tags haven't been re-added yet. The _onLifecycleEvent("load") handler
+ // will perform cleanup after the page finishes loading.
+ if (!initial) {
+ try { await this._page.frameManager.frame(this._targetId)._context("utility") } catch { };
+ return;
+ }
+ var document = await this._client._sendMayFail("DOM.getDocument");
+ if (!document) return
+ var query = await this._client._sendMayFail("DOM.querySelectorAll", {
+ nodeId: document.root.nodeId,
+ selector: "[class=" + this._crPage.initScriptTag + "]"
+ });
+ if (!query) return
+ for (const nodeId of query.nodeIds) await this._client._sendMayFail("DOM.removeNode", { nodeId: nodeId });
+ await this._client._sendMayFail('Runtime.runIfWaitingForDebugger');
+ // ensuring execution context
+ try { await this._page.frameManager.frame(this._targetId)._context("utility") } catch { };
+
}
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
@@ -657,19 +740,33 @@
}
_onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription) {
+
+ for (const name of this._exposedBindingNames)
+ this._client._sendMayFail('Runtime.addBinding', { name: name, executionContextId: contextPayload.id });
+
const frame = contextPayload.auxData ? this._page.frameManager.frame(contextPayload.auxData.frameId) : null;
+
+ if (contextPayload.auxData?.type === "worker") throw new Error("ExecutionContext is worker");
+
if (!frame || this._eventBelongsToStaleFrame(frame._id))
return;
const delegate = new CRExecutionContext(this._client, contextPayload);
- let worldName: types.World|null = null;
- if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
- worldName = 'main';
- else if (contextPayload.name === this._crPage.utilityWorldName)
- worldName = 'utility';
+ let worldName = contextPayload.name;
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
- if (worldName)
- frame._contextCreated(worldName, context);
+
+ if (worldName && (worldName === 'main' || worldName === 'utility'))
+ frame._contextCreated(worldName, context);
+
this._contextIdToContext.set(contextPayload.id, context);
+
+ for (const source of this._exposedBindingScripts) {
+ this._client._sendMayFail("Runtime.evaluate", {
+ expression: source,
+ contextId: contextPayload.id,
+ awaitPromise: true,
+ })
+ }
+
}
_onExecutionContextDestroyed(executionContextId: number) {
@@ -685,7 +782,7 @@
this._onExecutionContextDestroyed(contextId);
}
- _onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
+ async _onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
if (this._bufferedAttachedToTargetEvents) {
this._bufferedAttachedToTargetEvents.push(event);
return;
@@ -727,12 +824,22 @@
session.once('Runtime.executionContextCreated', async event => {
worker.createExecutionContext(new CRExecutionContext(session, event.context));
});
+
+ var globalThis = await session._sendMayFail('Runtime.evaluate', {
+ expression: "globalThis",
+ serializationOptions: { serialization: "idOnly" }
+ });
+ if (globalThis && globalThis.result) {
+ var globalThisObjId = globalThis.result.objectId;
+ var executionContextId = parseInt(globalThisObjId.split('.')[1], 10);
+ worker.createExecutionContext(new CRExecutionContext(session, { id: executionContextId }));
+ }
+
if (this._crPage._browserContext._browser.majorVersion() >= 143)
session.on('Inspector.workerScriptLoaded', () => worker.workerScriptLoaded());
else
worker.workerScriptLoaded();
// This might fail if the target is closed before we initialize.
- session._sendMayFail('Runtime.enable');
// TODO: attribute workers to the right frame.
this._crPage._networkManager.addSession(session, this._page.frameManager.frame(this._targetId) ?? undefined).catch(() => {});
session._sendMayFail('Runtime.runIfWaitingForDebugger');
@@ -811,8 +918,10 @@
const pageOrError = await this._crPage._page.waitForInitializedOrError();
if (!(pageOrError instanceof Error)) {
const context = this._contextIdToContext.get(event.executionContextId);
- if (context)
- await this._page.onBindingCalled(event.payload, context);
+
+ if (context) await this._page.onBindingCalled(event.payload, context);
+ else await this._page._onBindingCalled(event.payload, (await this._page.mainFrame()._mainContext())) // This might be a bit sketchy but it works for now
+
}
}
@@ -1008,20 +1117,14 @@
}
async _evaluateOnNewDocument(initScript: InitScript, world: types.World, runImmediately?: boolean): Promise<void> {
- const worldName = world === 'utility' ? this._crPage.utilityWorldName : undefined;
- const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName, runImmediately });
- this._initScriptIds.set(initScript, identifier);
+ this._evaluateOnNewDocumentScripts.push(initScript);
}
async _removeEvaluatesOnNewDocument(initScripts: InitScript[]): Promise<void> {
- const ids: string[] = [];
- for (const script of initScripts) {
- const id = this._initScriptIds.get(script);
- if (id)
- ids.push(id);
- this._initScriptIds.delete(script);
- }
- await Promise.all(ids.map(identifier => this._client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier }).catch(() => {}))); // target can be closed
+
+ const toRemove = new Set(initScripts);
+ this._evaluateOnNewDocumentScripts = this._evaluateOnNewDocumentScripts.filter(script => !toRemove.has(script));
+
}
async exposePlaywrightBinding() {
@@ -1126,12 +1229,53 @@
async _adoptBackendNodeId(backendNodeId: Protocol.DOM.BackendNodeId, to: dom.FrameExecutionContext): Promise<dom.ElementHandle> {
const result = await this._client._sendMayFail('DOM.resolveNode', {
backendNodeId,
- executionContextId: (to.delegate as CRExecutionContext)._contextId,
+ executionContextId: to.delegate._contextId,
});
if (!result || result.object.subtype === 'null')
throw new Error(dom.kUnableToAdoptErrorMessage);
return createHandle(to, result.object).asElement()!;
}
+
+ _exposedBindingNames: string[] = [];
+ _evaluateOnNewDocumentScripts: InitScript[] = [];
+ _parsedExecutionContextIds: number[] = [];
+ _exposedBindingScripts: string[] = [];
+
+ async _initBinding(binding = PageBinding) {
+
+ // Remember this binding so future execution contexts get it in _onExecutionContextCreated.
+ this._exposedBindingNames.push(binding.name);
+ this._exposedBindingScripts.push(binding.source);
+
+ // Install binding in all existing execution contexts.
+ const contextIds = Array.from(this._contextIdToContext.keys());
+ await Promise.all([
+ this._client._sendMayFail('Runtime.addBinding', { name: binding.name }),
+ ...contextIds.map(executionContextId => this._client._sendMayFail('Runtime.addBinding', { name: binding.name, executionContextId })),
+ ]);
+
+ // Evaluate binding bootstrap in all existing execution contexts.
+ const evaluationPromises = contextIds.map(contextId =>
+ this._client._sendMayFail('Runtime.evaluate', {
+ expression: binding.source,
+ contextId,
+ awaitPromise: true,
+ }).catch(e => { }),
+ );
+ await Promise.all(evaluationPromises);
+
+ }
+
+ async _removeExposedBindings() {
+
+ const toRetain: string[] = [];
+ const toRemove: string[] = [];
+ for (const name of this._exposedBindingNames)
+ (name.startsWith('__pw_') ? toRetain : toRemove).push(name);
+ this._exposedBindingNames = toRetain;
+ await Promise.all(toRemove.map(name => this._client.send('Runtime.removeBinding', { name })));
+
+ }
}
async function emulateLocale(session: CRSession, locale: string) {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/chromium/crServiceWorker.ts patchright/packages/playwright-core/src/server/chromium/crServiceWorker.ts
---
+++
@@ -63,13 +63,23 @@
const message = new ConsoleMessage(null, this, event.type, undefined, args, toConsoleMessageLocation(event.stackTrace), event.timestamp);
this.browserContext.emit(BrowserContext.Events.Console, message);
});
-
- session.send('Runtime.enable', {}).catch(e => {});
session.send('Runtime.runIfWaitingForDebugger').catch(e => {});
session.on('Inspector.targetReloadedAfterCrash', () => {
// Resume service worker after restart.
session._sendMayFail('Runtime.runIfWaitingForDebugger', {});
});
+
+ session._sendMayFail("Runtime.evaluate", {
+ expression: "globalThis",
+ serializationOptions: { serialization: "idOnly" }
+ }).then(globalThis => {
+ if (globalThis && globalThis.result) {
+ var globalThisObjId = globalThis.result.objectId;
+ var executionContextId = parseInt(globalThisObjId.split(".")[1], 10);
+ this.createExecutionContext(new CRExecutionContext(session, { id: executionContextId }));
+ }
+ });
+
}
override didClose() {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/clock.ts patchright/packages/playwright-core/src/server/clock.ts
---
+++
@@ -92,8 +92,18 @@
}
private async _installIfNeeded() {
- if (this._initScripts.length)
- return;
+
+ if (this._initScripts.length) {
+ const initScriptSources = JSON.stringify(this._initScripts.map((initScript) => initScript.source));
+ await this._evaluateInFrames(`(() => {
+ if (globalThis.__pwClock?.controller)
+ return;
+ for (const source of ${initScriptSources})
+ (0, eval)(source);
+ })();`);
+ return;
+ }
+
const script = `(() => {
const module = {};
${rawClockSource.source}
@@ -106,6 +116,15 @@
}
private async _evaluateInFrames(script: string) {
+
+ // Dont ask me why this works
+ const frames = this._browserContext.pages().flatMap((page) => page.frames());
+ await Promise.all(frames.map(async (frame) => {
+ try {
+ await frame.evaluateExpression("");
+ } catch {}
+ }));
+
await this._browserContext.safeNonStallingEvaluateInAllFrames(script, 'main', { throwOnJSErrors: true });
}
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts patchright/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts
---
+++
@@ -124,12 +124,12 @@
});
}
});
- this._dialogHandler = dialog => {
- if (!this._shouldDispatchEvent(dialog.page(), 'dialog'))
- return false;
- this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) });
- return true;
- };
+
+ this._dialogHandler = dialog => {
+ this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this, dialog) });
+ return true;
+ };
+
context.dialogManager.addDialogHandler(this._dialogHandler);
if (context._browser.options.name === 'chromium' && this._object._browser instanceof CRBrowser) {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts patchright/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
---
+++
@@ -84,11 +84,36 @@
}
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, progress: Progress): Promise<channels.FrameEvaluateExpressionResult> {
- return { value: serializeResult(await progress.race(this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)))) };
+
+ return {
+ value: serializeResult(
+ await progress.race(
+ this._frame.evaluateExpression(
+ params.expression,
+ { isFunction: params.isFunction, world: params.isolatedContext ? 'utility': 'main' },
+ parseArgument(params.arg)
+ )
+ )
+ )
+ };
+
}
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, progress: Progress): Promise<channels.FrameEvaluateExpressionHandleResult> {
- return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await progress.race(this._frame.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)))) };
+
+ return {
+ handle: ElementHandleDispatcher.fromJSOrElementHandle(
+ this,
+ await progress.race(
+ this._frame.evaluateExpressionHandle(
+ params.expression,
+ { isFunction: params.isFunction, world: params.isolatedContext ? 'utility': 'main' },
+ parseArgument(params.arg)
+ )
+ )
+ )
+ };
+
}
async waitForSelector(params: channels.FrameWaitForSelectorParams, progress: Progress): Promise<channels.FrameWaitForSelectorResult> {
@@ -104,7 +129,20 @@
}
async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, progress: Progress): Promise<channels.FrameEvalOnSelectorAllResult> {
- return { value: serializeResult(await progress.race(this._frame.evalOnSelectorAll(params.selector, params.expression, params.isFunction, parseArgument(params.arg)))) };
+
+ return {
+ value: serializeResult(
+ await this._frame.evalOnSelectorAll(
+ params.selector,
+ params.expression,
+ params.isFunction,
+ parseArgument(params.arg),
+ null,
+ params.isolatedContext
+ )
+ )
+ };
+
}
async querySelector(params: channels.FrameQuerySelectorParams, progress: Progress): Promise<channels.FrameQuerySelectorResult> {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts patchright/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts
---
+++
@@ -43,12 +43,12 @@
}
async evaluateExpression(params: channels.JSHandleEvaluateExpressionParams, progress: Progress): Promise<channels.JSHandleEvaluateExpressionResult> {
- const jsHandle = await progress.race(this._object.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)));
+ const jsHandle = await progress.race(this._object.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg), params.isolatedContext));
return { value: serializeResult(jsHandle) };
}
async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams, progress: Progress): Promise<channels.JSHandleEvaluateExpressionHandleResult> {
- const jsHandle = await progress.race(this._object.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)));
+ const jsHandle = await progress.race(this._object.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg), params.isolatedContext));
// If "jsHandle" is an ElementHandle, it belongs to the same frame as "this".
return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this.parentScope() as FrameDispatcher, jsHandle) };
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts patchright/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts
---
+++
@@ -153,6 +153,7 @@
headers: params.headers,
postData: params.postData,
isFallback: params.isFallback,
+ patchrightInitScript: (params as any).patchrightInitScript
});
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/frameSelectors.ts patchright/packages/playwright-core/src/server/frameSelectors.ts
---
+++
@@ -23,7 +23,10 @@
import type { JSHandle } from './javascript';
import type * as types from './types';
import type { ParsedSelector } from '../utils/isomorphic/selectorParser';
-
+import { ElementHandle } from "./dom";
+import type { CRSession } from "./chromium/crConnection";
+import type { Progress } from "./progress";
+import type { Protocol } from "./chromium/protocol";
export type SelectorInfo = {
parsed: ParsedSelector,
@@ -65,8 +68,8 @@
return adoptIfNeeded(elementHandle, await resolved.frame._mainContext());
}
- async queryArrayInMainWorld(selector: string, scope?: ElementHandle): Promise<JSHandle<Element[]>> {
- const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: true }, scope);
+ async queryArrayInMainWorld(selector: string, scope?: ElementHandle, isolatedContext?: boolean): Promise<JSHandle<Element[]>> {
+ const resolved = await this.resolveInjectedForSelector(selector, { mainWorld: !isolatedContext }, scope);
// Be careful, |this.frame| can be different from |resolved.frame|.
if (!resolved)
throw new Error(`Failed to find frame for selector "${selector}"`);
@@ -156,9 +159,26 @@
throw injected.createStacklessError(`Selector "${selectorString}" resolved to ${injected.previewNode(element)}, <iframe> was expected`);
return element;
}, { info, scope: i === 0 ? scope : undefined, selectorString: stringifySelector(info.parsed) });
- const element = handle.asElement() as ElementHandle<Element> | null;
- if (!element)
- return null;
+ let element = handle.asElement() as ElementHandle<Element> | null;
+
+ if (!element) {
+ try {
+ var client = frame._page.delegate._sessionForFrame(frame)._client;
+ } catch (e) {
+ var client = frame._page.delegate._mainFrameSession._client;
+ }
+ var mainContext = await frame._context("main");
+ const documentNode = await client.send("Runtime.evaluate", {
+ expression: "document",
+ serializationOptions: { serialization: "idOnly" },
+ contextId: mainContext.delegate._contextId
+ });
+ const documentScope = new ElementHandle(mainContext, documentNode.result.objectId);
+ var check = await this._customFindFramesByParsed(injectedScript, client, mainContext, documentScope, undefined, info.parsed);
+ if (check.length === 0) return null;
+ element = check[0];
+ }
+
const maybeFrame = await frame._page.delegate.getContentFrame(element);
element.dispose();
if (!maybeFrame)
@@ -179,9 +199,194 @@
if (!resolved)
return;
const context = await resolved.frame._context(options?.mainWorld ? 'main' : resolved.info.world);
+ if (!context) throw new Error("Frame was detached");
const injected = await context.injectedScript();
return { injected, info: resolved.info, frame: resolved.frame, scope: resolved.scope };
}
+
+ async _customFindFramesByParsed(resolved: JSHandle<InjectedScript>, client: CRSession, context: FrameExecutionContext, documentScope: ElementHandle, progress: Progress | undefined, parsed: ParsedSelector) {
+
+ var parsedEdits = { ...parsed };
+ const callId = progress?.metadata.id;
+ // Note: We start scoping at document level
+ var currentScopingElements = [documentScope];
+
+ for (const part of [...parsed.parts]) {
+ parsedEdits.parts = [part];
+ var elements = [];
+
+ if (part.name === "nth") {
+ const partNth = Number(part.body);
+ // Check if any Elements are currently scoped, else return empty array to continue polling
+ if (currentScopingElements.length == 0)
+ return [];
+
+ if (partNth > currentScopingElements.length-1 || partNth < -(currentScopingElements.length-1)) {
+ if (parsed.capture !== undefined)
+ throw new Error("Can't query n-th element in a request with the capture.");
+ return [];
+ }
+ currentScopingElements = [currentScopingElements.at(partNth)];
+ continue;
+ } else if (part.name === "internal:or") {
+ var orredElements = await this._customFindFramesByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ elements = [...currentScopingElements, ...orredElements];
+ } else if (part.name == "internal:and") {
+ var andedElements = await this._customFindFramesByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ const backendNodeIds = new Set(andedElements.map(elem => elem.backendNodeId));
+ elements = currentScopingElements.filter(elem => backendNodeIds.has(elem.backendNodeId));
+ } else {
+ for (const scope of currentScopingElements) {
+ const describedScope = await client.send("DOM.describeNode", {
+ objectId: scope._objectId,
+ depth: -1,
+ pierce: true
+ });
+
+ let findClosedShadowRoots = function(node, results = []) {
+ if (!node || typeof node !== "object") return results;
+ if (node.shadowRoots && Array.isArray(node.shadowRoots)) {
+ for (const shadowRoot of node.shadowRoots) {
+ if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) {
+ results.push(shadowRoot.backendNodeId);
+ }
+ findClosedShadowRoots(shadowRoot, results);
+ }
+ }
+ if (node.nodeName !== "IFRAME" && node.children && Array.isArray(node.children)) {
+ for (const child of node.children) {
+ findClosedShadowRoots(child, results);
+ }
+ }
+ return results;
+ };
+ var shadowRootBackendIds = findClosedShadowRoots(describedScope.node);
+
+ const shadowRoots = await Promise.all(
+ shadowRootBackendIds.map(async backendNodeId => {
+ const resolved = await client.send("DOM.resolveNode", {
+ backendNodeId,
+ contextId: context.delegate._contextId,
+ });
+ return new ElementHandle(context, resolved.object.objectId);
+ })
+ );
+
+ // Elements Queryed in the "current round"
+ const queryGroups: { handles: any; parentNode: any }[] = [];
+ for (var shadowRoot of shadowRoots) {
+ const shadowHandles = await shadowRoot.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId
+ }
+ );
+ queryGroups.push({ handles: shadowHandles, parentNode: shadowRoot });
+ }
+
+ // Document Root Elements (not in CSR)
+ const rootHandles = await scope.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId
+ }
+ );
+ queryGroups.push({ handles: rootHandles, parentNode: scope });
+
+ // Querying and Sorting the elements by their backendNodeId
+ for (const { handles, parentNode } of queryGroups) {
+ const handlesAmount = await (await handles.getProperty("length")).jsonValue();
+ for (var i = 0; i < handlesAmount; i++) {
+ if (parentNode instanceof ElementHandle) {
+ var element = await parentNode.evaluateHandleInUtility(
+ ([injected, node, { i, handles: elems }]) => elems[i],
+ { i, handles }
+ );
+ } else {
+ var element = await parentNode.evaluateHandle(
+ (injected, { i, handles: elems }) => elems[i],
+ { i, handles }
+ );
+ }
+
+ // For other Functions/Utilities
+ element.parentNode = parentNode;
+ const resolvedElement = await client.send("DOM.describeNode", { objectId: element._objectId, depth: -1 });
+ element.backendNodeId = resolvedElement.node.backendNodeId;
+ element.nodePosition = await this._findElementPositionInDomTree(element, describedScope.node, context, "");
+ elements.push(element);
+ }
+ }
+ }
+ }
+
+ // Sorting elements by their nodePosition, which is a index to the Element in the DOM tree
+ const getParts = (pos) => (pos || '').split('.').filter(Boolean).map(Number);
+ elements.sort((a, b) => {
+ const partsA = getParts(a.nodePosition);
+ const partsB = getParts(b.nodePosition);
+
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
+ const diff = (partsA[i] ?? -1) - (partsB[i] ?? -1);
+ if (diff !== 0) return diff;
+ }
+ return 0;
+ });
+
+ // Remove duplicates by backendNodeId, keeping the first occurrence
+ currentScopingElements = Array.from(
+ new Map(elements.map(e => [e.backendNodeId, e])).values()
+ );
+ }
+
+ return currentScopingElements;
+
+ }
+
+ async _findElementPositionInDomTree(element: { backendNodeId: number }, queryingElement: Protocol.DOM.Node, context: FrameExecutionContext, currentIndex: string) {
+
+ // Get Element Position in DOM Tree by Indexing it via their children indexes, like a search tree index
+ // Check if backendNodeId matches, if so, return currentIndex
+ if (element.backendNodeId === queryingElement.backendNodeId)
+ return currentIndex;
+
+ // Iterating through children of queryingElement
+ for (const [childrenNodeIndex, child] of (queryingElement.children || []).entries()) {
+ // Further querying the child recursively and appending the children index to the currentIndex
+ const childIndex = await this._findElementPositionInDomTree(element, child, context, currentIndex + "." + childrenNodeIndex.toString());
+ if (childIndex !== null) return childIndex;
+ }
+
+ for (const shadowRoot of queryingElement.shadowRoots || []) {
+ // For CSRs, we dont have to append its index because patchright treats CSRs like they dont exist
+ if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) {
+ // Resolve the CDP client for the current context so closed shadow roots can be traversed safely.
+ const client = context.frame._page.delegate._sessionForFrame(context.frame)._client;
+ const describedShadowRoot = await client.send("DOM.describeNode", { backendNodeId: shadowRoot.backendNodeId, depth: -1, pierce: true });
+ if (describedShadowRoot && describedShadowRoot.node) {
+ const childIndex = await this._findElementPositionInDomTree(element, describedShadowRoot.node, context, currentIndex);
+ if (childIndex !== null) return childIndex;
+ }
+ }
+ // Traverse into shadow root children (open and closed) to properly position elements inside shadow DOMs
+ for (const [shadowChildIndex, shadowChild] of (shadowRoot.children || []).entries()) {
+ const childIndex = await this._findElementPositionInDomTree(element, shadowChild, context, currentIndex + "." + shadowChildIndex.toString());
+ if (childIndex !== null) return childIndex;
+ }
+ }
+ return null;
+
+ }
}
async function adoptIfNeeded<T extends Node>(handle: ElementHandle<T>, context: FrameExecutionContext): Promise<ElementHandle<T>> {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/frames.ts patchright/packages/playwright-core/src/server/frames.ts
---
+++
@@ -42,6 +42,10 @@
import type { RegisteredListener } from './utils/eventsHelper';
import type { ParsedSelector } from '../utils/isomorphic/selectorParser';
import type * as channels from '@protocol/channels';
+import { CRExecutionContext } from "./chromium/crExecutionContext";
+import { FrameExecutionContext } from "./dom";
+import type { CRSession } from "./chromium/crConnection";
+import crypto from "crypto";
type ContextData = {
contextPromise: ManualPromise<dom.FrameExecutionContext | { destroyedReason: string }>;
@@ -239,6 +243,9 @@
// No pending - just commit a new document.
frame._currentDocument = { documentId, request: undefined };
}
+ frame._iframeWorld = undefined;
+ frame._mainWorld = undefined;
+ frame._isolatedWorld = undefined;
frame._onClearLifecycle();
const navigationEvent: NavigationEvent = { url, name, newDocument: frame._currentDocument, isPublic: true };
@@ -572,12 +579,15 @@
}
nonStallingEvaluateInExistingContext(expression: string, world: types.World): Promise<any> {
- return this.raceAgainstEvaluationStallingEvents(() => {
- const context = this._contextData.get(world)?.context;
- if (!context)
- throw new Error('Frame does not yet have the execution context');
- return context.evaluateExpression(expression, { isFunction: false });
- });
+
+ return this.raceAgainstEvaluationStallingEvents(async () => {
+ try { await this._context(world); } catch {}
+ const context = this._contextData.get(world)?.context;
+ if (!context)
+ throw new Error('Frame does not yet have the execution context');
+ return context.evaluateExpression(expression, { isFunction: false });
+ });
+
}
_recalculateNetworkIdle(frameThatAllowsRemovingNetworkIdle?: Frame) {
@@ -722,12 +732,69 @@
return this._page.delegate.getFrameElement(this);
}
- _context(world: types.World): Promise<dom.FrameExecutionContext> {
- return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => {
- if (contextOrDestroyedReason instanceof js.ExecutionContext)
- return contextOrDestroyedReason;
- throw new Error(contextOrDestroyedReason.destroyedReason);
- });
+ async _context(world: types.World): Promise<dom.FrameExecutionContext> {
+
+ if (this.isDetached())
+ throw new Error('Frame was detached');
+
+ let client;
+ try {
+ client = this._page.delegate._sessionForFrame(this)._client;
+ } catch (e) {
+ client = this._page.delegate._mainFrameSession._client;
+ }
+
+ var iframeExecutionContextId = await this._getFrameMainFrameContextId(client);
+ const isMainFrame = this === this._page.mainFrame();
+ const session = this._page.delegate._sessionForFrame(this);
+
+ const registerContext = (executionContextId: number, worldName: string) => {
+ const crContext = new CRExecutionContext(client, { id: executionContextId }, this._id);
+ const frameContext = new FrameExecutionContext(crContext, this, worldName);
+ session._onExecutionContextCreated({
+ id: executionContextId,
+ origin: worldName,
+ name: worldName,
+ auxData: { isDefault: isMainFrame, type: 'isolated', frameId: this._id },
+ });
+ return frameContext;
+ };
+
+ if (world === "main") {
+ // Iframe Only
+ if (!isMainFrame && iframeExecutionContextId && this._iframeWorld === undefined) {
+ this._iframeWorld = registerContext(iframeExecutionContextId, world);
+ } else if (this._mainWorld === undefined) {
+ const globalThis = await client._sendMayFail('Runtime.evaluate', {
+ expression: "globalThis",
+ serializationOptions: { serialization: "idOnly" },
+ });
+ if (!globalThis) {
+ if (this.isDetached()) throw new Error('Frame was detached');
+ return;
+ }
+ const executionContextId = parseInt(globalThis.result.objectId.split('.')[1], 10);
+ this._mainWorld = registerContext(executionContextId, world);
+ }
+ }
+
+ if (world !== "main" && this._isolatedWorld === undefined) {
+ const result = await client._sendMayFail('Page.createIsolatedWorld', {
+ frameId: this._id, grantUniveralAccess: true, worldName: world,
+ });
+ if (!result) {
+ if (this.isDetached()) throw new Error("Frame was detached");
+ return;
+ }
+ this._isolatedWorld = registerContext(result.executionContextId, "utility");
+ }
+
+ if (world !== "main")
+ return this._isolatedWorld;
+ if (!isMainFrame && this._iframeWorld)
+ return this._iframeWorld;
+ return this._mainWorld;
+
}
_mainContext(): Promise<dom.FrameExecutionContext> {
@@ -743,107 +810,168 @@
}
async evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<any> {
- const context = await this._context(options.world ?? 'main');
- const value = await context.evaluateExpression(expression, options, arg);
- return value;
+
+ const context = await this._detachedScope.race(this._context(options.world ?? "main"));
+ return await this._detachedScope.race(context.evaluateExpression(expression, options, arg));
+
}
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise<js.JSHandle<any>> {
- const context = await this._context(options.world ?? 'main');
- const value = await context.evaluateExpressionHandle(expression, options, arg);
- return value;
+
+ const context = await this._detachedScope.race(this._context(options.world ?? "utility"));
+ return await this._detachedScope.race(context.evaluateExpressionHandle(expression, options, arg));
+
}
async querySelector(selector: string, options: types.StrictOptions): Promise<dom.ElementHandle<Element> | null> {
- this.apiLog(` finding element using the selector "${selector}"`);
- return this.selectors.query(selector, options);
+
+ return this.querySelectorAll(selector, options).then((handles) => {
+ if (handles.length === 0)
+ return null;
+ if (handles.length > 1 && options?.strict)
+ throw new Error(`Strict mode: expected one element matching selector "${selector}", found ${handles.length}`);
+ return handles[0];
+ });
+
}
async waitForSelector(progress: Progress, selector: string, performActionPreChecksAndLog: boolean, options: types.WaitForElementOptions, scope?: dom.ElementHandle): Promise<dom.ElementHandle<Element> | null> {
- if ((options as any).visibility)
- throw new Error('options.visibility is not supported, did you mean options.state?');
- if ((options as any).waitFor && (options as any).waitFor !== 'visible')
- throw new Error('options.waitFor is not supported, did you mean options.state?');
- const { state = 'visible' } = options;
- if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
- throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
- if (performActionPreChecksAndLog)
- progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
- const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
- if (performActionPreChecksAndLog)
- await this._page.performActionPreChecks(progress);
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope));
- if (!resolved) {
- if (state === 'hidden' || state === 'detached')
- return null;
- return continuePolling;
- }
- const result = await progress.race(resolved.injected.evaluateHandle((injected, { info, root }) => {
- if (root && !root.isConnected)
- throw injected.createStacklessError('Element is not attached to the DOM');
- const elements = injected.querySelectorAll(info.parsed, root || document);
- const element: Element | undefined = elements[0];
- const visible = element ? injected.utils.isElementVisible(element) : false;
- let log = '';
- if (elements.length > 1) {
- if (info.strict)
- throw injected.strictModeViolationError(info.parsed, elements);
- log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
- } else if (element) {
- log = ` locator resolved to ${visible ? 'visible' : 'hidden'} ${injected.previewNode(element)}`;
- }
- injected.checkDeprecatedSelectorUsage(info.parsed, elements);
- return { log, element, visible, attached: !!element };
- }, { info: resolved.info, root: resolved.frame === this ? scope : undefined }));
- const { log, visible, attached } = await progress.race(result.evaluate(r => ({ log: r.log, visible: r.visible, attached: r.attached })));
- if (log)
- progress.log(log);
- const success = { attached, detached: !attached, visible, hidden: !visible }[state];
- if (!success) {
- result.dispose();
- return continuePolling;
- }
- if (options.omitReturnValue) {
- result.dispose();
- return null;
- }
- const element = state === 'attached' || state === 'visible' ? await progress.race(result.evaluateHandle(r => r.element)) : null;
- result.dispose();
- if (!element)
- return null;
- if ((options as any).__testHookBeforeAdoptNode)
- await progress.race((options as any).__testHookBeforeAdoptNode());
- try {
- const mainContext = await progress.race(resolved.frame._mainContext());
- return await progress.race(element._adoptTo(mainContext));
- } catch (e) {
- return continuePolling;
- }
- });
- return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+ if ((options as any).visibility)
+ throw new Error('options.visibility is not supported, did you mean options.state?');
+ if ((options as any).waitFor && (options as any).waitFor !== 'visible')
+ throw new Error('options.waitFor is not supported, did you mean options.state?');
+
+ const { state = 'visible' } = options;
+ if (!['attached', 'detached', 'visible', 'hidden'].includes(state))
+ throw new Error(`state: expected one of (attached|detached|visible|hidden)`);
+
+ if (performActionPreChecksAndLog)
+ progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`);
+
+ const promise = this._retryWithProgressIfNotConnected(progress, selector, { ...options, performActionPreChecks: true, __patchrightWaitForSelector: true, __patchrightInitialScope: scope }, async handle => {
+ if (scope) {
+ const scopeIsConnected = await scope.evaluateInUtility(([injected, node]) => node.isConnected, {}).catch(() => false);
+ if (scopeIsConnected !== true) {
+ if (state === 'hidden' || state === 'detached')
+ return null;
+ throw new dom.NonRecoverableDOMError('Element is not attached to the DOM');
+ }
+ }
+
+ const attached = !!handle;
+ var visible = false;
+
+ if (attached) {
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ visible = await handle.parentNode.evaluateInUtility(([injected, node, { handle }]) => {
+ return handle ? injected.utils.isElementVisible(handle) : false;
+ }, { handle });
+ } else {
+ visible = await handle.parentNode.evaluate((injected, { handle }) => {
+ return handle ? injected.utils.isElementVisible(handle) : false;
+ }, { handle });
+ }
+ }
+
+ const success = {
+ attached,
+ detached: !attached,
+ visible,
+ hidden: !visible
+ }[state];
+ if (!success) return "internal:continuepolling";
+ if (options.omitReturnValue) return null;
+
+ const element = state === 'attached' || state === 'visible' ? handle : null;
+ if (!element) return null;
+ if (options.__testHookBeforeAdoptNode) await options.__testHookBeforeAdoptNode();
+ try {
+ return element;
+ } catch (e) {
+ return "internal:continuepolling";
+ }
+ }, "returnOnNotResolved");
+
+ const resultPromise = scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+ return resultPromise.catch(e => {
+ if (this.isDetached() && (e as any)?.message?.includes('Execution context was destroyed'))
+ throw new Error('Frame was detached');
+ throw e;
+ });
+
}
async dispatchEvent(progress: Progress, selector: string, type: string, eventInit: Object = {}, options: types.QueryOnSelectorOptions, scope?: dom.ElementHandle): Promise<void> {
- await this._callOnElementOnceMatches(progress, selector, (injectedScript, element, data) => {
- injectedScript.dispatchEvent(element, data.type, data.eventInit);
- }, { type, eventInit }, { mainWorld: true, ...options }, scope);
+
+ const eventInitHandles: js.JSHandle[] = [];
+ const visited = new WeakSet();
+ const collectHandles = (value: any) => {
+ if (!value || typeof value !== "object")
+ return;
+ if (value instanceof js.JSHandle) {
+ eventInitHandles.push(value);
+ return;
+ }
+ if (visited.has(value))
+ return;
+ visited.add(value);
+ if (Array.isArray(value)) {
+ for (const item of value)
+ collectHandles(item);
+ return;
+ }
+ for (const propertyValue of Object.values(value))
+ collectHandles(propertyValue);
+ };
+ collectHandles(eventInit);
+
+ const handlesFrame = eventInitHandles[0]?._context?.frame;
+ const allHandlesFromSameFrame = eventInitHandles.length > 0 && eventInitHandles.every(handle => handle._context?.frame === handlesFrame);
+ const canRetryInSecondaryContext = allHandlesFromSameFrame && (handlesFrame !== this || !selector.includes("internal:control=enter-frame"));
+ const callback = (injectedScript, element, data) => {
+ injectedScript.dispatchEvent(element, data.type, data.eventInit);
+ };
+ try {
+ await this._callOnElementOnceMatches(progress, selector, callback, { type, eventInit }, { mainWorld: true, ...options }, scope);
+ } catch (e) {
+ if ("JSHandles can be evaluated only in the context they were created!" === e.message && canRetryInSecondaryContext) {
+ await this._callOnElementOnceMatches(progress, selector, callback, { type, eventInit }, { ...options }, scope);
+ return;
+ }
+ throw e;
+ }
+
}
async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
- const handle = await this.selectors.query(selector, { strict }, scope);
- if (!handle)
- throw new Error(`Failed to find element matching selector "${selector}"`);
- const result = await handle.evaluateExpression(expression, { isFunction }, arg);
- handle.dispose();
- return result;
+
+ const handle = await this.selectors.query(selector, { strict }, scope);
+ if (!handle)
+ throw new Error('Failed to find element matching selector "' + selector + '"');
+ const result = await handle.evaluateExpression(expression, { isFunction }, arg, true);
+ handle.dispose();
+ return result;
+
}
- async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle): Promise<any> {
- const arrayHandle = await this.selectors.queryArrayInMainWorld(selector, scope);
- const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
- arrayHandle.dispose();
- return result;
+ async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any, scope?: dom.ElementHandle, isolatedContext?: boolean): Promise<any> {
+
+ const maxAttempts = 3;
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ isolatedContext = this.selectors._parseSelector(selector, { strict: false }).world !== "main" && isolatedContext;
+ const arrayHandle = await this.selectors.queryArrayInMainWorld(selector, scope, isolatedContext);
+ const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg, isolatedContext);
+ arrayHandle.dispose();
+ return result;
+ } catch (e) {
+ // Retry only on specific context mismatch errors, and only a bounded number of times.
+ if ("JSHandles can be evaluated only in the context they were created!" !== e.message || attempt === maxAttempts) throw e;
+ await new Promise(resolve => setTimeout(resolve, 50 * attempt));
+ }
+ }
+
}
async maskSelectors(selectors: ParsedSelector[], color: string): Promise<void> {
@@ -855,17 +983,34 @@
}
async querySelectorAll(selector: string): Promise<dom.ElementHandle<Element>[]> {
- return this.selectors.queryAll(selector);
+
+ const metadata = { internal: false, log: [], method: "querySelectorAll" };
+ const progress = {
+ log: message => metadata.log.push(message),
+ metadata,
+ race: (promise) => Promise.race(Array.isArray(promise) ? promise : [promise])
+ }
+ return await this._retryWithoutProgress(progress, selector, {strict: null, performActionPreChecks: false}, async (result) => {
+ if (!result || !result[0]) return [];
+ return Array.isArray(result[1]) ? result[1] : [];
+ }, 'returnAll', null);
+
}
async queryCount(selector: string, options: any): Promise<number> {
- try {
- return await this.selectors.queryCount(selector, options);
- } catch (e) {
- if (this.isNonRetriableError(e))
- throw e;
- return 0;
- }
+
+ const metadata = { internal: false, log: [], method: "queryCount" };
+ const progress = {
+ log: message => metadata.log.push(message),
+ metadata,
+ race: (promise) => Promise.race(Array.isArray(promise) ? promise : [promise])
+ }
+ return await this._retryWithoutProgress(progress, selector, {strict: null, performActionPreChecks: false }, async (result) => {
+ if (!result || !result[0])
+ return 0;
+ return Array.isArray(result[1]) ? result[1].length : 0;
+ }, 'returnAll', null);
+
}
async content(): Promise<string> {
@@ -887,29 +1032,23 @@
}
async setContent(progress: Progress, html: string, options: types.NavigateOptions): Promise<void> {
- const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
- await this.raceNavigationAction(progress, async () => {
- const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
- progress.log(`setting frame content, waiting until "${waitUntil}"`);
- const context = await progress.race(this._utilityContext());
- const tagPromise = new ManualPromise<void>();
- this._page.frameManager._consoleMessageTags.set(tag, () => {
- // Clear lifecycle right after document.open() - see 'tag' below.
- this._onClearLifecycle();
- tagPromise.resolve();
- });
- const lifecyclePromise = progress.race(tagPromise).then(() => this.waitForLoadState(progress, waitUntil));
- const contentPromise = progress.race(context.evaluate(({ html, tag }) => {
- document.open();
- console.debug(tag); // eslint-disable-line no-console
- document.write(html);
- document.close();
- }, { html, tag }));
- await Promise.all([contentPromise, lifecyclePromise]);
- return null;
- }).finally(() => {
- this._page.frameManager._consoleMessageTags.delete(tag);
- });
+
+ await this.raceNavigationAction(progress, async () => {
+ const waitUntil = options.waitUntil === void 0 ? "load" : options.waitUntil;
+ progress.log(`setting frame content, waiting until "${waitUntil}"`);
+ const lifecyclePromise = new Promise((resolve, reject) => {
+ this._onClearLifecycle();
+ this.waitForLoadState(progress, waitUntil).then(resolve).catch(reject);
+ });
+ const setContentPromise = this._page.delegate._sessionForFrame(this)._client.send("Page.setDocumentContent", {
+ frameId: this._id,
+ html
+ });
+ await Promise.all([setContentPromise, lifecyclePromise]);
+
+ return null;
+ });
+
}
name(): string {
@@ -1101,60 +1240,14 @@
progress: Progress,
selector: string,
options: { strict?: boolean, noAutoWaiting?: boolean, force?: boolean, performActionPreChecks?: boolean },
- action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>): Promise<R> {
- progress.log(`waiting for ${this._asLocator(selector)}`);
- const noAutoWaiting = (options as any).__testHookNoAutoWaiting ?? options.noAutoWaiting;
- const performActionPreChecks = (options.performActionPreChecks ?? !options.force) && !noAutoWaiting;
- return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
- if (performActionPreChecks)
- await this._page.performActionPreChecks(progress);
+ action: (handle: dom.ElementHandle<Element>) => Promise<R | 'error:notconnected'>, returnAction: 'returnOnNotResolved' | 'returnAll' | undefined): Promise<R> {
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, { strict: options.strict }));
- if (!resolved) {
- if (noAutoWaiting)
- throw new dom.NonRecoverableDOMError('Element(s) not found');
- return continuePolling;
- }
- const result = await progress.race(resolved.injected.evaluateHandle((injected, { info, callId }) => {
- const elements = injected.querySelectorAll(info.parsed, document);
- if (callId)
- injected.markTargetElements(new Set(elements), callId);
- const element = elements[0] as Element | undefined;
- let log = '';
- if (elements.length > 1) {
- if (info.strict)
- throw injected.strictModeViolationError(info.parsed, elements);
- log = ` locator resolved to ${elements.length} elements. Proceeding with the first one: ${injected.previewNode(elements[0])}`;
- } else if (element) {
- log = ` locator resolved to ${injected.previewNode(element)}`;
- }
- injected.checkDeprecatedSelectorUsage(info.parsed, elements);
- return { log, success: !!element, element };
- }, { info: resolved.info, callId: progress.metadata.id }));
- const { log, success } = await progress.race(result.evaluate(r => ({ log: r.log, success: r.success })));
- if (log)
- progress.log(log);
- if (!success) {
- if (noAutoWaiting)
- throw new dom.NonRecoverableDOMError('Element(s) not found');
- result.dispose();
- return continuePolling;
- }
- const element = await progress.race(result.evaluateHandle(r => r.element)) as dom.ElementHandle<Element>;
- result.dispose();
- try {
- const result = await action(element);
- if (result === 'error:notconnected') {
- if (noAutoWaiting)
- throw new dom.NonRecoverableDOMError('Element is not attached to the DOM');
- progress.log('element was detached from the DOM, retrying');
- return continuePolling;
- }
- return result;
- } finally {
- element?.dispose();
- }
- });
+ if (!(options as any)?.__patchrightSkipRetryLogWaiting)
+ progress.log("waiting for " + this._asLocator(selector));
+ return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
+ return this._retryWithoutProgress(progress, selector, options as any, action as any, returnAction, continuePolling);
+ });
+
}
async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise<Buffer> {
@@ -1307,20 +1400,61 @@
}
async isVisibleInternal(progress: Progress, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
- try {
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope));
- if (!resolved)
- return false;
- return await progress.race(resolved.injected.evaluate((injected, { info, root }) => {
- const element = injected.querySelector(info.parsed, root || document, info.strict);
- const state = element ? injected.elementState(element, 'visible') : { matches: false, received: 'error:notconnected' };
- return state.matches;
- }, { info: resolved.info, root: resolved.frame === this ? scope : undefined }));
- } catch (e) {
- if (this.isNonRetriableError(e))
- throw e;
- return false;
- }
+
+ try {
+ const metadata = { internal: false, log: [], method: "isVisible" };
+ const progress = {
+ log: message => metadata.log.push(message),
+ metadata,
+ race: (promise) => Promise.race(Array.isArray(promise) ? promise : [promise])
+ }
+ progress.log("waiting for " + this._asLocator(selector));
+ if (selector === ":scope") {
+ const scopeParentNode = scope.parentNode || scope;
+ if (scopeParentNode instanceof dom.ElementHandle) {
+ return await scopeParentNode.evaluateInUtility(([injected, node, { scope: handle2 }]) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { scope });
+ } else {
+ return await scopeParentNode.evaluate((injected, node, { scope: handle2 }) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { scope });
+ }
+ } else {
+ return await this._retryWithoutProgress(progress, selector, { ...options, performActionPreChecks: false}, async (handle) => {
+ if (!handle) return false;
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ return await handle.parentNode.evaluateInUtility(([injected, node, { handle: handle2 }]) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { handle });
+ } else {
+ return await handle.parentNode.evaluate((injected, { handle: handle2 }) => {
+ const state = handle2 ? injected.elementState(handle2, "visible") : {
+ matches: false,
+ received: "error:notconnected"
+ };
+ return state.matches;
+ }, { handle });
+ }
+ }, "returnOnNotResolved", null);
+ }
+ } catch (e) {
+ if (this.isNonRetriableError(e)) throw e;
+ return false;
+ }
+
}
async isHidden(progress: Progress, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
@@ -1439,47 +1573,96 @@
}
private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean) {
- // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted.
- const race = <T>(p: Promise<T>) => noAbort ? p : progress.race(p);
- const selectorInFrame = selector ? await race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined;
- const { frame, info } = selectorInFrame || { frame: this, info: undefined };
- const world = options.expression === 'to.have.property' ? 'main' : (info?.world ?? 'utility');
- const context = await race(frame._context(world));
- const injected = await race(context.injectedScript());
+ // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted.
+ const race = (p) => noAbort ? p : progress.race(p);
+ const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
+ var log, matches, received, missingReceived;
+ if (selector) {
+ var frame, info;
+ try {
+ var { frame, info } = await race(this.selectors.resolveFrameForSelector(selector, { strict: true }));
+ } catch (e) { }
+ const action = async result => {
+ if (!result) {
+ if (options.expectedNumber === 0)
+ return { matches: true };
+ if (options.isNot && options.expectedNumber)
+ return { matches: false, received: 0 };
+ // expect(locator).toBeHidden() passes when there is no element.
+ if (!options.isNot && options.expression === 'to.be.hidden')
+ return { matches: true };
+ // expect(locator).not.toBeVisible() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.visible')
+ return { matches: false };
+ // expect(locator).toBeAttached({ attached: false }) passes when there is no element.
+ if (!options.isNot && options.expression === 'to.be.detached')
+ return { matches: true };
+ // expect(locator).not.toBeAttached() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.attached')
+ return { matches: false };
+ // expect(locator).not.toBeInViewport() passes when there is no element.
+ if (options.isNot && options.expression === 'to.be.in.viewport')
+ return { matches: false };
+ // expect(locator).toHaveText([]) pass when there is no element.
+ if (options.expression === "to.have.text.array") {
+ if (options.expectedText.length === 0)
+ return { matches: true, received: [] };
+ if (options.isNot && options.expectedText.length !== 0)
+ return { matches: false, received: [] };
+ }
+ // When none of the above applies, expect does not match.
+ return { matches: options.isNot, missingReceived: true };
+ }
- const { log, matches, received, missingReceived } = await race(injected.evaluate(async (injected, { info, options, callId }) => {
- const elements = info ? injected.querySelectorAll(info.parsed, document) : [];
- if (callId)
- injected.markTargetElements(new Set(elements), callId);
- const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
- let log = '';
- if (isArray)
- log = ` locator resolved to ${elements.length} element${elements.length === 1 ? '' : 's'}`;
- else if (elements.length > 1)
- throw injected.strictModeViolationError(info!.parsed, elements);
- else if (elements.length)
- log = ` locator resolved to ${injected.previewNode(elements[0])}`;
- if (info)
- injected.checkDeprecatedSelectorUsage(info.parsed, elements);
- return { log, ...await injected.expect(elements[0], options, elements) };
- }, { info, options, callId: progress.metadata.id }));
+ const handle = result[0];
+ const handles = result[1];
- if (log)
- progress.log(log);
- // Note: missingReceived avoids `unexpected value "undefined"` when element was not found.
- if (matches === options.isNot) {
- if (missingReceived) {
- lastIntermediateResult.errorMessage = 'Error: element(s) not found';
- } else {
- lastIntermediateResult.errorMessage = undefined;
- lastIntermediateResult.received = received;
- }
- lastIntermediateResult.isSet = true;
- if (!missingReceived && !Array.isArray(received))
- progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`);
- }
- return { matches, received };
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ return await handle.parentNode.evaluateInUtility(async ([injected, node, { handle, options, handles }]) => {
+ return await injected.expect(handle, options, handles);
+ }, { handle, options, handles });
+ } else {
+ return await handle.parentNode.evaluate(async (injected, { handle, options, handles }) => {
+ return await injected.expect(handle, options, handles);
+ }, { handle, options, handles });
+ }
+ }
+
+ if (noAbort) {
+ var { log, matches, received, missingReceived } = await this._retryWithoutProgress(progress, selector, {strict: !isArray, performActionPreChecks: false}, action, 'returnAll', null);
+ } else {
+ var { log, matches, received, missingReceived } = await race(this._retryWithProgressIfNotConnected(progress, selector, { strict: !isArray, performActionPreChecks: false, __patchrightSkipRetryLogWaiting: true } as any, action, 'returnAll'));
+ }
+ } else {
+ const world = options.expression === 'to.have.property' ? 'main' : 'utility';
+ const context = await race(this._context(world));
+ const injected = await race(context.injectedScript());
+ var { matches, received, missingReceived } = await race(injected.evaluate(async (injected, { options, callId }) => {
+ return { ...await injected.expect(undefined, options, []) };
+ }, { options, callId: progress.metadata.id }));
+ }
+
+
+ if (log)
+ progress.log(log);
+ // Note: missingReceived avoids `unexpected value "undefined"` when element was not found.
+ if (matches === options.isNot) {
+ if (missingReceived) {
+ lastIntermediateResult.errorMessage = 'Error: element(s) not found';
+ } else {
+ lastIntermediateResult.errorMessage = undefined;
+ lastIntermediateResult.received = received;
+ }
+ lastIntermediateResult.isSet = true;
+ if (!missingReceived) {
+ const rendered = renderUnexpectedValue(options.expression, received);
+ if (rendered !== undefined)
+ progress.log(' unexpected value "' + rendered + '"');
+ }
+ }
+ return { matches, received };
+
}
async waitForFunctionExpression<R>(progress: Progress, expression: string, isFunction: boolean | undefined, arg: any, options: { pollingInterval?: number }, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
@@ -1538,7 +1721,7 @@
return { result, abort: () => aborted = true };
}, { expression, isFunction, polling: options.pollingInterval, arg }));
try {
- return await progress.race(handle.evaluateHandle(h => h.result));
+ return await progress.race(this._detachedScope.race(handle.evaluateHandle(h => h.result)));
} catch (error) {
// Note: it is important to await "abort()" to prevent any side effects
// after this method returns.
@@ -1591,42 +1774,129 @@
}
_onDetached() {
- this._stopNetworkIdleTimer();
- this._detachedScope.close(new Error('Frame was detached'));
- for (const data of this._contextData.values()) {
- if (data.context)
- data.context.contextDestroyed('Frame was detached');
- data.contextPromise.resolve({ destroyedReason: 'Frame was detached' });
- }
- if (this._parentFrame)
- this._parentFrame._childFrames.delete(this);
- this._parentFrame = null;
+
+ this._stopNetworkIdleTimer();
+ this._detachedScope.close(new Error('Frame was detached'));
+ for (const data of this._contextData.values()) {
+ if (data.context)
+ data.context.contextDestroyed('Frame was detached');
+ data.contextPromise.resolve({ destroyedReason: 'Frame was detached' });
+ }
+ if (this._mainWorld)
+ this._mainWorld.contextDestroyed('Frame was detached');
+ if (this._iframeWorld)
+ this._iframeWorld.contextDestroyed('Frame was detached');
+ if (this._isolatedWorld)
+ this._isolatedWorld.contextDestroyed('Frame was detached');
+ if (this._parentFrame)
+ this._parentFrame._childFrames.delete(this);
+ this._parentFrame = null;
+
}
private async _callOnElementOnceMatches<T, R>(progress: Progress, selector: string, body: ElementCallback<T, R>, taskData: T, options: types.StrictOptions & { mainWorld?: boolean }, scope?: dom.ElementHandle): Promise<R> {
- const callbackText = body.toString();
- progress.log(`waiting for ${this._asLocator(selector)}`);
- const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => {
- const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope));
- if (!resolved)
- return continuePolling;
- const { log, success, value } = await progress.race(resolved.injected.evaluate((injected, { info, callbackText, taskData, callId, root }) => {
- const callback = injected.eval(callbackText) as ElementCallback<T, R>;
- const element = injected.querySelector(info.parsed, root || document, info.strict);
- if (!element)
- return { success: false };
- const log = ` locator resolved to ${injected.previewNode(element)}`;
- if (callId)
- injected.markTargetElements(new Set([element]), callId);
- return { log, success: true, value: callback(injected, element, taskData as T) };
- }, { info: resolved.info, callbackText, taskData, callId: progress.metadata.id, root: resolved.frame === this ? scope : undefined }));
- if (log)
- progress.log(log);
- if (!success)
- return continuePolling;
- return value!;
- });
- return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+
+ const callbackText = body.toString();
+ progress.log("waiting for " + this._asLocator(selector));
+ var promise;
+ if (selector === ":scope") {
+ const scopeParentNode = scope.parentNode || scope;
+ if (scopeParentNode instanceof dom.ElementHandle) {
+ if (options?.mainWorld) {
+ promise = (async () => {
+ const mainContext = await this._mainContext();
+ const adoptedScope = await this._page.delegate.adoptElementHandle(scope, mainContext);
+ try {
+ return await mainContext.evaluate(([injected, node, { callbackText: callbackText2, scope: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, [
+ await mainContext.injectedScript(),
+ adoptedScope,
+ { callbackText, scope: adoptedScope, taskData },
+ ]);
+ } finally {
+ adoptedScope.dispose();
+ }
+ })();
+ } else {
+ promise = scopeParentNode.evaluateInUtility(([injected, node, { callbackText: callbackText2, scope: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ scope,
+ taskData
+ });
+ }
+ } else {
+ promise = scopeParentNode.evaluate((injected, { callbackText: callbackText2, scope: handle2, taskData: taskData2 }) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ scope,
+ taskData
+ });
+ }
+ } else {
+
+ promise = this._retryWithProgressIfNotConnected(progress, selector, { ...options, performActionPreChecks: false }, async (handle) => {
+ if (handle.parentNode instanceof dom.ElementHandle) {
+ if (options?.mainWorld) {
+ const mainContext = await handle._frame._mainContext();
+ const adoptedHandle = await this._page.delegate.adoptElementHandle(handle, mainContext);
+ try {
+ return await mainContext.evaluate(([injected, node, { callbackText: callbackText2, handle: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, [
+ await mainContext.injectedScript(),
+ adoptedHandle,
+ { callbackText, handle: adoptedHandle, taskData },
+ ]);
+ } finally {
+ adoptedHandle.dispose();
+ }
+ }
+
+ // Handling dispatch_event's in isolated and Main Contexts
+ const [taskScope] = Object.values(taskData?.eventInit ?? {});
+ if (taskScope) {
+ const taskScopeContext = taskScope._context;
+ const adoptedHandle = await handle._adoptTo(taskScopeContext);
+ return await taskScopeContext.evaluate(([injected, node, { callbackText: callbackText2, adoptedHandle: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, [
+ await taskScopeContext.injectedScript(),
+ adoptedHandle,
+ { callbackText, adoptedHandle, taskData },
+ ]);
+ }
+
+ return await handle.parentNode.evaluateInUtility(([injected, node, { callbackText: callbackText2, handle: handle2, taskData: taskData2 }]) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ handle,
+ taskData
+ });
+ } else {
+ return await handle.parentNode.evaluate((injected, { callbackText: callbackText2, handle: handle2, taskData: taskData2 }) => {
+ const callback = injected.eval(callbackText2);
+ return callback(injected, handle2, taskData2);
+ }, {
+ callbackText,
+ handle,
+ taskData
+ });
+ }
+ })
+ }
+ return scope ? scope._context._raceAgainstContextDestroyed(promise) : promise;
+
}
private _setContext(world: types.World, context: dom.FrameExecutionContext | null) {
@@ -1722,6 +1992,325 @@
private _asLocator(selector: string) {
return asLocator(this._page.browserContext._browser.sdkLanguage(), selector);
}
+
+ _isolatedWorld: dom.FrameExecutionContext;
+ _mainWorld: dom.FrameExecutionContext;
+ _iframeWorld: dom.FrameExecutionContext;
+
+ async _getFrameMainFrameContextId(client: CRSession): Promise<number> {
+
+ try {
+ const frameOwner = await client._sendMayFail("DOM.getFrameOwner", { frameId: this._id });
+ if (!frameOwner?.nodeId)
+ return 0;
+
+ const describedNode = await client._sendMayFail("DOM.describeNode", { backendNodeId: frameOwner.backendNodeId });
+ if (!describedNode?.node.contentDocument)
+ return 0;
+
+ const resolvedNode = await client._sendMayFail("DOM.resolveNode", { backendNodeId: describedNode.node.contentDocument.backendNodeId });
+ if (!resolvedNode?.object?.objectId)
+ return 0;
+
+ return parseInt(resolvedNode.object.objectId.split(".")[1], 10);
+ } catch (e) {}
+ return 0;
+
+ }
+
+ async _retryWithoutProgress(progress: Progress, selector: string, options: { performActionPreChecks: boolean; strict?: boolean | null; state?: 'attached' | 'detached' | 'visible' | 'hidden'; noAutoWaiting?: boolean; __testHookNoAutoWaiting?: boolean; __patchrightWaitForSelector?: boolean; __patchrightInitialScope?: dom.ElementHandle; __patchrightSkipRetryLogWaiting?: boolean }, action: (result: dom.ElementHandle | [dom.ElementHandle, dom.ElementHandle[]] | null) => Promise<unknown>, returnAction: 'returnOnNotResolved' | 'returnAll' | undefined, continuePolling: symbol) {
+
+ if (options.performActionPreChecks)
+ await this._page.performActionPreChecks(progress);
+
+ const resolved = await this.selectors.resolveInjectedForSelector(
+ selector,
+ { strict: options.strict },
+ (options as any).__patchrightInitialScope
+ );
+
+ if (!resolved) {
+ if (returnAction === 'returnOnNotResolved' || returnAction === 'returnAll') {
+ const result = await action(null);
+ return result === "internal:continuepolling" ? continuePolling : result;
+ }
+ return continuePolling;
+ }
+
+ const utilityContext = await resolved.frame._utilityContext();
+ const mainContext = await resolved.frame._mainContext();
+ let client;
+ try {
+ client = this._page.delegate._sessionForFrame(resolved.frame)._client;
+ } catch (e) {
+ client = this._page.delegate._mainFrameSession._client;
+ }
+
+ const documentNode = await client._sendMayFail('Runtime.evaluate', {
+ expression: "document",
+ serializationOptions: { serialization: "idOnly" },
+ contextId: utilityContext.delegate._contextId,
+ });
+ if (!documentNode)
+ return continuePolling;
+
+ let initialScope = new dom.ElementHandle(utilityContext, documentNode.result.objectId);
+
+ if ((resolved as any).scope) {
+ const scopeObjectId = (resolved as any).scope._objectId;
+ if (scopeObjectId) {
+ const describeResult = await client._sendMayFail('DOM.describeNode', {
+ objectId: scopeObjectId,
+ });
+ const backendNodeId = describeResult?.node?.backendNodeId;
+
+ if (backendNodeId) {
+ const scopeInUtility = await client._sendMayFail('DOM.resolveNode', {
+ backendNodeId,
+ executionContextId: utilityContext.delegate._contextId
+ });
+
+ if (scopeInUtility?.object?.objectId) {
+ initialScope = new dom.ElementHandle(utilityContext, scopeInUtility.object.objectId);
+ }
+ }
+ }
+ }
+ (progress as any).__patchrightInitialScope = (resolved as any).scope;
+
+ // Save parsed selector before _customFindElementsByParsed mutates it via parts.shift()
+ const parsedSnapshot = (options as any).__patchrightWaitForSelector ? JSON.parse(JSON.stringify(resolved.info.parsed)) : null;
+ let currentScopingElements;
+ try {
+ currentScopingElements = await this._customFindElementsByParsed(resolved, client, mainContext, initialScope, progress, resolved.info.parsed);
+ } catch (e) {
+ if ("JSHandles can be evaluated only in the context they were created!" === e.message)
+ return continuePolling;
+ if (e instanceof TypeError && e.message.includes("is not a function"))
+ return continuePolling;
+ await progress.race(resolved.injected.evaluateHandle((injected, { error }) => { throw error }, { error: e }));
+ }
+
+ if (currentScopingElements.length === 0) {
+ if ((options as any).__testHookNoAutoWaiting || (options as any).noAutoWaiting)
+ throw new dom.NonRecoverableDOMError('Element(s) not found');
+
+ // CDP-based element search is non-atomic and can temporarily miss
+ // elements during DOM mutations. Verify element absence in-page before reporting
+ // "not found" to the waitForSelector callback.
+ if (parsedSnapshot && (returnAction === 'returnOnNotResolved' || returnAction === 'returnAll')) {
+ const elementCount = await resolved.injected.evaluate((injected, { parsed }) => {
+ return injected.querySelectorAll(parsed, document).length;
+ }, { parsed: parsedSnapshot }).catch(() => 0);
+ if (elementCount > 0)
+ return continuePolling;
+ }
+ if (returnAction === 'returnOnNotResolved' || returnAction === 'returnAll') {
+ const result = await action(null);
+ return result === "internal:continuepolling" ? continuePolling : result;
+ }
+ return continuePolling;
+ }
+
+ const resultElement = currentScopingElements[0];
+ await resultElement._initializePreview().catch(() => {});
+
+ let visibilityQualifier = '';
+ if (options && (options as any).__patchrightWaitForSelector) {
+ visibilityQualifier = await resultElement.evaluateInUtility(([injected, node]) => injected.utils.isElementVisible(node) ? 'visible' : 'hidden', {}).catch(() => '');
+ }
+
+ if (currentScopingElements.length > 1) {
+ if (resolved.info.strict) {
+ await progress.race(resolved.injected.evaluateHandle((injected, {
+ info,
+ elements
+ }) => {
+ throw injected.strictModeViolationError(info.parsed, elements);
+ }, {
+ info: resolved.info,
+ elements: currentScopingElements
+ }));
+ }
+ progress.log(" locator resolved to " + currentScopingElements.length + " elements. Proceeding with the first one: " + resultElement.preview());
+ } else if (resultElement) {
+ progress.log(" locator resolved to " + (visibilityQualifier ? visibilityQualifier + " " : "") + resultElement.preview().replace("JSHandle@", ""));
+ }
+
+ try {
+ var result = null;
+ if (returnAction === 'returnAll') {
+ result = await action([resultElement, currentScopingElements]);
+ } else {
+ result = await action(resultElement);
+ }
+ if (result === 'error:notconnected') {
+ progress.log('element was detached from the DOM, retrying');
+ return continuePolling;
+ } else if (result === 'internal:continuepolling') {
+ return continuePolling;
+ }
+ // Verify no visible elements exist before accepting a null result to avoid stale CDP handles during mutations.
+ if (parsedSnapshot && result === null && ((options as any).state === 'hidden' || (options as any).state === 'detached')) {
+ const visibleCount = await resolved.injected.evaluate((injected, { parsed }) => {
+ const elements = injected.querySelectorAll(parsed, document);
+ return elements.filter(e => injected.utils.isElementVisible(e)).length;
+ }, { parsed: parsedSnapshot }).catch(() => 0);
+ if (visibleCount > 0)
+ return continuePolling;
+ }
+ return result;
+ } finally {}
+
+ }
+
+ async _customFindElementsByParsed(resolved: { injected: js.JSHandle<InjectedScript>, info: { parsed: ParsedSelector, strict: boolean }, frame: Frame, scope?: dom.ElementHandle }, client: CRSession, context: dom.FrameExecutionContext, documentScope: dom.ElementHandle, progress: Progress, parsed: ParsedSelector) {
+
+ var parsedEdits = { ...parsed };
+ // Note: We start scoping at document level
+ var currentScopingElements = [documentScope];
+
+ for (const part of [...parsed.parts]) {
+ parsedEdits.parts = [part];
+ var elements = [];
+
+ if (part.name === "nth") {
+ const partNth = Number(part.body);
+ // Check if any Elements are currently scoped, else return empty array to continue polling
+ if (currentScopingElements.length == 0)
+ return [];
+
+ if (partNth > currentScopingElements.length-1 || partNth < -(currentScopingElements.length-1)) {
+ if (parsed.capture !== undefined)
+ throw new Error("Can't query n-th element in a request with the capture.");
+ return [];
+ }
+ currentScopingElements = [currentScopingElements.at(partNth)];
+ continue;
+ } else if (part.name === "internal:or") {
+ var orredElements = await this._customFindElementsByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ elements = [...currentScopingElements, ...orredElements];
+ } else if (part.name == "internal:and") {
+ var andedElements = await this._customFindElementsByParsed(resolved, client, context, documentScope, progress, part.body.parsed);
+ const backendNodeIds = new Set(andedElements.map(elem => elem.backendNodeId));
+ elements = currentScopingElements.filter(elem => backendNodeIds.has(elem.backendNodeId));
+ } else {
+ for (const scope of currentScopingElements) {
+ const describedScope = await client.send("DOM.describeNode", {
+ objectId: scope._objectId,
+ depth: -1,
+ pierce: true
+ });
+
+ let findClosedShadowRoots = function(node, results = []) {
+ if (!node || typeof node !== "object") return results;
+ if (node.shadowRoots && Array.isArray(node.shadowRoots)) {
+ for (const shadowRoot of node.shadowRoots) {
+ if (shadowRoot.shadowRootType === "closed" && shadowRoot.backendNodeId) {
+ results.push(shadowRoot.backendNodeId);
+ }
+ findClosedShadowRoots(shadowRoot, results);
+ }
+ }
+ if (node.nodeName !== "IFRAME" && node.children && Array.isArray(node.children)) {
+ for (const child of node.children) {
+ findClosedShadowRoots(child, results);
+ }
+ }
+ return results;
+ };
+ var shadowRootBackendIds = findClosedShadowRoots(describedScope.node);
+
+ const shadowRoots = await Promise.all(
+ shadowRootBackendIds.map(async backendNodeId => {
+ const resolved = await client.send("DOM.resolveNode", {
+ backendNodeId,
+ contextId: context.delegate._contextId,
+ });
+ return new dom.ElementHandle(context, resolved.object.objectId);
+ })
+ );
+
+ // Elements Queryed in the "current round"
+ const queryGroups: { handles: any; parentNode: any }[] = [];
+ for (var shadowRoot of shadowRoots) {
+ const shadowHandles = await shadowRoot.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId: progress.metadata.id
+ }
+ );
+ queryGroups.push({ handles: shadowHandles, parentNode: shadowRoot });
+ }
+
+ // Document Root Elements (not in CSR)
+ const rootHandles = await scope.evaluateHandleInUtility(
+ ([injected, node, { parsed, callId }]) => {
+ const elements = injected.querySelectorAll(parsed, node);
+ if (callId)
+ injected.markTargetElements(new Set(elements), callId);
+ return elements;
+ }, {
+ parsed: parsedEdits,
+ callId: progress.metadata.id
+ }
+ );
+ queryGroups.push({ handles: rootHandles, parentNode: scope });
+
+ // Querying and Sorting the elements by their backendNodeId
+ for (const { handles, parentNode } of queryGroups) {
+ const handlesAmount = await (await handles.getProperty("length")).jsonValue();
+ for (var i = 0; i < handlesAmount; i++) {
+ if (parentNode instanceof dom.ElementHandle) {
+ var element = await parentNode.evaluateHandleInUtility(
+ ([injected, node, { i, handles: elems }]) => elems[i],
+ { i, handles }
+ );
+ } else {
+ var element = await parentNode.evaluateHandle(
+ (injected, { i, handles: elems }) => elems[i],
+ { i, handles }
+ );
+ }
+
+ // For other Functions/Utilities
+ element.parentNode = parentNode;
+ const resolvedElement = await client.send("DOM.describeNode", { objectId: element._objectId, depth: -1 });
+ element.backendNodeId = resolvedElement.node.backendNodeId;
+ element.nodePosition = await this.selectors._findElementPositionInDomTree(element, describedScope.node, context, "");
+ elements.push(element);
+ }
+ }
+ }
+ }
+
+ // Sorting elements by their nodePosition, which is a index to the Element in the DOM tree
+ const getParts = (pos) => (pos || '').split('.').filter(Boolean).map(Number);
+ elements.sort((a, b) => {
+ const partsA = getParts(a.nodePosition);
+ const partsB = getParts(b.nodePosition);
+
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
+ const diff = (partsA[i] ?? -1) - (partsB[i] ?? -1);
+ if (diff !== 0) return diff;
+ }
+ return 0;
+ });
+
+ // Remove duplicates by backendNodeId, keeping the first occurrence
+ currentScopingElements = Array.from(
+ new Map(elements.map(e => [e.backendNodeId, e])).values()
+ );
+ }
+
+ return currentScopingElements;
+
+ }
}
class SignalBarrier {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/javascript.ts patchright/packages/playwright-core/src/server/javascript.ts
---
+++
@@ -22,6 +22,7 @@
import type * as dom from './dom';
import type { UtilityScript } from '@injected/utilityScript';
+import * as domValue from "./dom";
interface TaggedAsJSHandle<T> {
__jshandle: T;
@@ -149,17 +150,45 @@
return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg);
}
- async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg: any) {
- const value = await evaluateExpression(this._context, expression, { ...options, returnByValue: true }, this, arg);
- await this._context.doSlowMo();
- return value;
- }
+ async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg: any, isolatedContext?: boolean) {
- async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg: any): Promise<JSHandle<any>> {
- const value = await evaluateExpression(this._context, expression, { ...options, returnByValue: false }, this, arg);
- await this._context.doSlowMo();
- return value;
- }
+ let context = this._context;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext === true)
+ context = await frame._utilityContext();
+ else if (isolatedContext === false)
+ context = await frame._mainContext();
+ }
+ }
+ if (context !== this._context && context.adoptIfNeeded(this) === null)
+ context = this._context;
+
+ const value = await evaluateExpression(context, expression, { ...options, returnByValue: true }, this, arg);
+ await context.doSlowMo();
+ return value;
+ }
+
+ async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg: any, isolatedContext?: boolean): Promise<JSHandle<any>> {
+
+ let context = this._context;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext === true)
+ context = await frame._utilityContext();
+ else if (isolatedContext === false)
+ context = await frame._mainContext();
+ }
+ }
+ if (context !== this._context && context.adoptIfNeeded(this) === null)
+ context = this._context;
+
+ const value = await evaluateExpression(context, expression, { ...options, returnByValue: false }, this, arg);
+ await context.doSlowMo();
+ return value;
+ }
async getProperty(propertyName: string): Promise<JSHandle> {
const objectHandle = await this.evaluateHandle((object: any, propertyName) => {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/launchApp.ts patchright/packages/playwright-core/src/server/launchApp.ts
---
+++
@@ -117,7 +117,8 @@
return;
Object.entries(settings).map(([k, v]) => localStorage[k] = v);
(window as any).saveSettings = () => {
- (window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage }));
+ if (typeof (window as any)._saveSerializedSettings === 'function')
+ (window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage }));
};
})})(${settings});
`);
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/page.ts patchright/packages/playwright-core/src/server/page.ts
---
+++
@@ -51,6 +51,8 @@
import type * as channels from '@protocol/channels';
import type { BindingPayload } from '@injected/bindingsController';
import type { SelectorInfo } from './frameSelectors';
+import * as domValue from "./dom";
+import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from "./pageBinding";
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@@ -332,21 +334,16 @@
}
async exposeBinding(progress: Progress, name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<PageBinding> {
- if (this._pageBindings.has(name))
- throw new Error(`Function "${name}" has been already registered`);
- if (this.browserContext._pageBindings.has(name))
- throw new Error(`Function "${name}" has been already registered in the browser context`);
- await progress.race(this.browserContext.exposePlaywrightBindingIfNeeded());
- const binding = new PageBinding(this, name, playwrightBinding, needsHandle);
- this._pageBindings.set(name, binding);
- try {
- await progress.race(this.delegate.addInitScript(binding.initScript));
- await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main'));
- return binding;
- } catch (error) {
- this._pageBindings.delete(name);
- throw error;
- }
+
+ if (this._pageBindings.has(name))
+ throw new Error(`Function "${name}" has been already registered`);
+ if (this.browserContext._pageBindings.has(name))
+ throw new Error(`Function "${name}" has been already registered in the browser context`);
+ const binding = new PageBinding(this, name, playwrightBinding, needsHandle);
+ this._pageBindings.set(name, binding);
+ await this.delegate.exposeBinding(binding);
+ return binding;
+
}
async removeExposedBinding(binding: PageBinding) {
@@ -870,13 +867,6 @@
}
}
- allInitScripts() {
- const bindings = [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()].map(binding => binding.initScript);
- if (this.browserContext.bindingsInitScript)
- bindings.unshift(this.browserContext.bindingsInitScript);
- return [...bindings, ...this.browserContext.initScripts, ...this.initScripts];
- }
-
getBinding(name: string) {
return this._pageBindings.get(name) || this.browserContext._pageBindings.get(name);
}
@@ -899,6 +889,12 @@
async setDockTile(image: Buffer) {
await this.delegate.setDockTile(image);
}
+
+ allBindings() {
+
+ return [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()];
+
+ }
}
export const WorkerEvent = {
@@ -953,83 +949,119 @@
this.openScope.close(new Error('Worker closed'));
}
- async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
- return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: true, isFunction }, arg);
- }
+ async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, isolatedContext?: boolean): Promise<any> {
- async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
- return js.evaluateExpression(await this._executionContextPromise, expression, { returnByValue: false, isFunction }, arg);
- }
+ let context = await this._executionContextPromise;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext) context = await frame._utilityContext();
+ else if (!isolatedContext) context = await frame._mainContext();
+ }
+ }
+
+ return js.evaluateExpression(context, expression, { returnByValue: true, isFunction }, arg);
+ }
+
+ async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any, isolatedContext?: boolean): Promise<any> {
+
+ let context = await this._executionContextPromise;
+ if (context instanceof domValue.FrameExecutionContext) {
+ const frame = context.frame;
+ if (frame) {
+ if (isolatedContext) context = await frame._utilityContext();
+ else if (!isolatedContext) context = await frame._mainContext();
+ }
+ }
+
+ return js.evaluateExpression(context, expression, { returnByValue: false, isFunction }, arg);
+ }
}
-export class PageBinding extends DisposableObject {
- private static kController = '__playwright__binding__controller__';
- static kBindingName = '__playwright__binding__';
- static createInitScript(browserContext: BrowserContext): InitScript {
- return new InitScript(browserContext, `
- (() => {
- const module = {};
- ${rawBindingsControllerSource.source}
- const property = '${PageBinding.kController}';
- if (!globalThis[property])
- globalThis[property] = new (module.exports.BindingsController())(globalThis, '${PageBinding.kBindingName}');
- })();
- `);
- }
+ export class PageBinding extends DisposableObject {
+ readonly source: string;
+ readonly name: string;
+ readonly playwrightFunction: frames.FunctionWithSource;
+ readonly initScript: InitScript;
+ readonly needsHandle: boolean;
+ readonly cleanupScript: string;
+ forClient?: unknown;
- readonly name: string;
- readonly playwrightFunction: frames.FunctionWithSource;
- readonly initScript: InitScript;
- readonly needsHandle: boolean;
- readonly cleanupScript: string;
- forClient?: unknown;
+ constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
+ super(parent);
+ this.name = name;
+ this.playwrightFunction = playwrightFunction;
+ this.initScript = new InitScript(parent, createPageBindingScript(name, needsHandle));
+ this.source = this.initScript.source;
+ this.cleanupScript = `delete globalThis[${JSON.stringify(name)}];`;
+ this.needsHandle = needsHandle;
+ }
- constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
- super(parent);
- this.name = name;
- this.playwrightFunction = playwrightFunction;
- this.initScript = new InitScript(parent, `globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)}, ${needsHandle})`);
- this.needsHandle = needsHandle;
- this.cleanupScript = `globalThis['${PageBinding.kController}'].removeBinding(${JSON.stringify(name)})`;
- }
+ static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
+ const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
- static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
- const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
- try {
- assert(context.world);
- const binding = page.getBinding(name);
- if (!binding)
- throw new Error(`Function "${name}" is not exposed`);
- let result: any;
- if (binding.needsHandle) {
- const handle = await context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].takeBindingHandle(arg)`, { isFunction: true }, { name, seq }).catch(e => null);
- result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, handle);
- } else {
- if (!Array.isArray(serializedArgs))
- throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
- const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
- result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args);
- }
- context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
- } catch (error) {
- context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
- }
- }
+ const deliver = async (deliverPayload: any) => {
+ let deliveryError: any;
+ try {
+ await context.evaluate(deliverBindingResult, deliverPayload);
+ return;
+ } catch (e) {
+ deliveryError = e;
+ }
+ const frame = context.frame;
+ if (!frame) {
+ debugLogger.log('error', deliveryError);
+ return;
+ }
+ const mainContext = await frame._mainContext().catch(() => null);
+ const utilityContext = await frame._utilityContext().catch(() => null);
+ for (const ctx of [mainContext, utilityContext]) {
+ if (!ctx || ctx === context)
+ continue;
+ try {
+ await ctx.evaluate(deliverBindingResult, deliverPayload);
+ return;
+ } catch {
+ }
+ }
+ debugLogger.log('error', deliveryError);
+ };
- override async dispose(): Promise<void> {
- await this.parent.removeExposedBinding(this);
- }
-}
+ try {
+ assert(context.world);
+ const binding = page.getBinding(name);
+ if (!binding)
+ throw new Error(`Function "${name}" is not exposed`);
+
+ let result: any;
+ if (binding.needsHandle) {
+ const handle = await context.evaluateHandle(takeBindingHandle, { name, seq }).catch(e => null);
+ result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle);
+ } else {
+ if (!Array.isArray(serializedArgs))
+ throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`);
+ const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
+ result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
+ }
+ await deliver({ name, seq, result });
+ } catch (error) {
+ await deliver({ name, seq, error });
+ }
+ }
+
+ override async dispose(): Promise<void> {
+ await this.parent.removeExposedBinding(this);
+ }
+ }
+
export class InitScript extends DisposableObject {
readonly source: string;
constructor(owner: BrowserContext | Page, source: string) {
super(owner);
- this.source = `(() => {
- ${source}
- })();`;
+ this.source = `(() => { ${source} })();`;
}
async dispose() {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/pageBinding.ts patchright/packages/playwright-core/src/server/pageBinding.ts
---
+++
@@ -0,0 +1,91 @@
+;
+ /**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * 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 { source } from '../utils/isomorphic/oldUtilityScriptSerializers';
+
+ import type { SerializedValue } from '../utils/isomorphic/oldUtilityScriptSerializers';
+
+ export type BindingPayload = {
+ name: string;
+ seq: number;
+ serializedArgs?: SerializedValue[],
+ };
+
+ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScriptSerializersFactory: typeof source) {
+ const { serializeAsCallArgument } = utilityScriptSerializersFactory;
+ // eslint-disable-next-line no-restricted-globals
+ const binding = (globalThis as any)[bindingName];
+ if (!binding || binding.toString().startsWith("(...args) => {")) return
+ // eslint-disable-next-line no-restricted-globals
+ (globalThis as any)[bindingName] = (...args: any[]) => {
+ // eslint-disable-next-line no-restricted-globals
+ const me = (globalThis as any)[bindingName];
+ if (needsHandle && args.slice(1).some(arg => arg !== undefined))
+ throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
+ let callbacks = me['callbacks'];
+ if (!callbacks) {
+ callbacks = new Map();
+ me['callbacks'] = callbacks;
+ }
+ const seq: number = (me['lastSeq'] || 0) + 1;
+ me['lastSeq'] = seq;
+ let handles = me['handles'];
+ if (!handles) {
+ handles = new Map();
+ me['handles'] = handles;
+ }
+ const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject }));
+ let payload: BindingPayload;
+ if (needsHandle) {
+ handles.set(seq, args[0]);
+ payload = { name: bindingName, seq };
+ } else {
+ const serializedArgs = [];
+ for (let i = 0; i < args.length; i++) {
+ serializedArgs[i] = serializeAsCallArgument(args[i], v => {
+ return { fallThrough: v };
+ });
+ }
+ payload = { name: bindingName, seq, serializedArgs };
+ }
+ binding(JSON.stringify(payload));
+ return promise;
+ };
+ // eslint-disable-next-line no-restricted-globals
+ }
+
+ export function takeBindingHandle(arg: { name: string, seq: number }) {
+ // eslint-disable-next-line no-restricted-globals
+ const handles = (globalThis as any)[arg.name]['handles'];
+ const handle = handles.get(arg.seq);
+ handles.delete(arg.seq);
+ return handle;
+ }
+
+ export function deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) {
+ // eslint-disable-next-line no-restricted-globals
+ const callbacks = (globalThis as any)[arg.name]['callbacks'];
+ if ('error' in arg)
+ callbacks.get(arg.seq).reject(arg.error);
+ else
+ callbacks.get(arg.seq).resolve(arg.result);
+ callbacks.delete(arg.seq);
+ }
+
+ export function createPageBindingScript(name: string, needsHandle: boolean) {
+ return `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle}, (${source})())`;
+ }
+
\ No newline at end of file
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/registry/index.ts patchright/packages/playwright-core/src/server/registry/index.ts
---
+++
@@ -1,20 +1,3 @@
-/**
- * Copyright 2017 Google Inc. All rights reserved.
- * Modifications copyright (c) Microsoft Corporation.
- *
- * 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 fs from 'fs';
import os from 'os';
import path from 'path';
@@ -600,7 +583,7 @@
`- current: ${currentDockerVersion.dockerImageName}`,
`- required: ${preferredDockerVersion.dockerImageName}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
} else if (isFfmpeg) {
prettyMessage = [
@@ -610,7 +593,7 @@
``,
` ${buildPlaywrightCLICommand(sdkLanguage, 'install ffmpeg')}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
} else {
prettyMessage = [
@@ -619,7 +602,7 @@
``,
` ${installCommand}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
}
throw new Error(`Executable doesn't exist at ${e}\n${wrapInASCIIBox(prettyMessage, 1)}`);
@@ -1083,7 +1066,7 @@
// Install browsers for this package.
for (const executable of executables) {
if (!executable._install)
- throw new Error(`ERROR: Playwright does not support installing ${executable.name}`);
+ throw new Error(`ERROR: Patchright does not support installing ${executable.name}`);
if (!getAsBooleanFromENV('CI') && !executable._isHermeticInstallation && !options?.force && executable.executablePath()) {
const { embedderName } = getEmbedderName();
@@ -1102,7 +1085,7 @@
``,
` ${command}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n'), 1) + '\n\n');
return;
}
@@ -1117,12 +1100,12 @@
` ${lockfilePath}`,
``,
`Either:`,
- `- wait a few minutes if other Playwright is installing browsers in parallel`,
+ `- wait a few minutes if other Patchright is installing browsers in parallel`,
`- remove lock manually with:`,
``,
` ${rmCommand} ${lockfilePath}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n'), 1));
} else {
throw e;
@@ -1217,13 +1200,13 @@
private async _downloadExecutable(descriptor: BrowsersJSONDescriptor, force: boolean, executablePath?: string) {
const downloadURLs = this._downloadURLs(descriptor);
if (!downloadURLs.length)
- throw new Error(`ERROR: Playwright does not support ${descriptor.name} on ${hostPlatform}`);
+ throw new Error(`ERROR: Patchright does not support ${descriptor.name} on ${hostPlatform}`);
if (!isOfficiallySupportedPlatform)
- logPolitely(`BEWARE: your OS is not officially supported by Playwright; downloading fallback build for ${hostPlatform}.`);
+ logPolitely(`BEWARE: your OS is not officially supported by Patchright; downloading fallback build for ${hostPlatform}.`);
if (descriptor.hasRevisionOverride) {
const message = `You are using a frozen ${descriptor.name} browser which does not receive updates anymore on ${hostPlatform}. Please update to the latest version of your operating system to test up-to-date browsers.`;
if (process.env.GITHUB_ACTIONS)
- console.log(`::warning title=Playwright::${message}`); // eslint-disable-line no-console
+ console.log(`::warning title=Patchright::${message}`); // eslint-disable-line no-console
else
logPolitely(message);
}
@@ -1444,7 +1427,7 @@
export function buildPlaywrightCLICommand(sdkLanguage: string, parameters: string): string {
switch (sdkLanguage) {
case 'python':
- return `playwright ${parameters}`;
+ return `patchright ${parameters}`;
case 'java':
return `mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="${parameters}"`;
case 'csharp':
@@ -1497,7 +1480,7 @@
``,
` ${installCommand}`,
``,
- `<3 Playwright Team`,
+ `<3 Patchright Team`,
].join('\n');
throw new Error('\n' + wrapInASCIIBox(prettyMessage, 1));
}
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/screenshotter.ts patchright/packages/playwright-core/src/server/screenshotter.ts
---
+++
@@ -254,6 +254,11 @@
if (disableAnimations)
progress.log(' disabled all CSS animations');
const syncAnimations = this._page.delegate.shouldToggleStyleSheetToSyncAnimations();
+
+ await Promise.all(this._page.frames().map(async (f: any) => {
+ try { await f._utilityContext(); } catch {}
+ }));
+
await progress.race(this._page.safeNonStallingEvaluateInAllFrames('(' + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${hideCaret}, ${disableAnimations}, ${syncAnimations})`, 'utility'));
try {
if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/trace/recorder/snapshotter.ts patchright/packages/playwright-core/src/server/trace/recorder/snapshotter.ts
---
+++
@@ -26,7 +26,6 @@
import type { SnapshotData } from './snapshotterInjected';
import type { RegisteredListener } from '../../utils/eventsHelper';
import type { Frame } from '../../frames';
-import type { InitScript } from '../../page';
import type { FrameSnapshot } from '@trace/snapshot';
export type SnapshotterBlob = {
@@ -44,7 +43,7 @@
private _delegate: SnapshotterDelegate;
private _eventListeners: RegisteredListener[] = [];
private _snapshotStreamer: string;
- private _initScript: InitScript | undefined;
+ private _initScript: boolean | undefined;
private _started = false;
constructor(context: BrowserContext, delegate: SnapshotterDelegate) {
@@ -67,7 +66,7 @@
async reset() {
if (this._started)
- await this._context.safeNonStallingEvaluateInAllFrames(`window["${this._snapshotStreamer}"].reset()`, 'main');
+ await this._context.safeNonStallingEvaluateInAllFrames(`window["${this._snapshotStreamer}"].reset()`, 'utility');
}
stop() {
@@ -75,25 +74,27 @@
}
async resetForReuse() {
- // Next time we start recording, we will call addInitScript again.
- if (this._initScript) {
- eventsHelper.removeEventListeners(this._eventListeners);
- await this._initScript.dispose();
- this._initScript = undefined;
- }
+
+ if (this._initScript) {
+ eventsHelper.removeEventListeners(this._eventListeners);
+ this._initScript = undefined;
+ this._initScriptSource = undefined;
+ }
+
}
async _initialize() {
- for (const page of this._context.pages())
- this._onPage(page);
- this._eventListeners = [
- eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
- ];
- const { javaScriptEnabled } = this._context._options;
- const initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`;
- this._initScript = await this._context.addInitScript(initScriptSource);
- await this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main');
+ const { javaScriptEnabled } = this._context._options;
+ this._initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`;
+ this._initScript = true;
+ for (const page of this._context.pages())
+ this._onPage(page);
+ this._eventListeners = [
+ eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
+ ];
+ await this._context.safeNonStallingEvaluateInAllFrames(this._initScriptSource, 'utility');
+
}
dispose() {
@@ -106,7 +107,7 @@
(frame as any)[kNeedsResetSymbol] = false;
const expression = `window["${this._snapshotStreamer}"].captureSnapshot(${needsReset ? 'true' : 'false'})`;
try {
- return await frame.nonStallingRawEvaluateInExistingMainContext(expression);
+ return await frame.nonStallingEvaluateInExistingContext(expression, 'utility');
} catch (e) {
// If we fail to capture snapshot in this frame, we cannot rely on the snapshot index
// being the same here and in snapshotter injected script.
@@ -159,6 +160,7 @@
for (const frame of page.frames())
this._annotateFrameHierarchy(frame);
this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame)));
+ this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.InternalFrameNavigatedToNewDocument, (frame: Frame) => this._onFrameNavigated(frame)));
}
private async _annotateFrameHierarchy(frame: Frame) {
@@ -167,7 +169,7 @@
const parent = frame.parentFrame();
if (!parent)
return;
- const context = await parent._mainContext();
+ const context = await parent._utilityContext();
await context?.evaluate(({ snapshotStreamer, frameElement, frameId }) => {
(window as any)[snapshotStreamer].markIframe(frameElement, frameId);
}, { snapshotStreamer: this._snapshotStreamer, frameElement, frameId: frame.guid });
@@ -175,6 +177,18 @@
} catch (e) {
}
}
+
+ _initScriptSource: string | undefined;
+
+ async _onFrameNavigated(frame: Frame) {
+
+ if (!this._initScriptSource)
+ return;
+ try {
+ await frame.nonStallingEvaluateInExistingContext(this._initScriptSource, 'utility');
+ } catch (e) {}
+
+ }
}
const kNeedsResetSymbol = Symbol('kNeedsReset');
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts patchright/packages/playwright-core/src/server/trace/recorder/snapshotterInjected.ts
---
+++
@@ -85,33 +85,15 @@
class Streamer {
private _lastSnapshotNumber = 0;
- private _staleStyleSheets = new Set<CSSStyleSheet>();
private _modifiedStyleSheets = new Set<CSSStyleSheet>();
- private _readingStyleSheet = false; // To avoid invalidating due to our own reads.
private _fakeBase: HTMLBaseElement;
private _observer: MutationObserver;
constructor() {
- const invalidateCSSGroupingRule = (rule: CSSGroupingRule) => {
- if (rule.parentStyleSheet)
- this._invalidateStyleSheet(rule.parentStyleSheet);
- };
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'insertRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'deleteRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'addRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'removeRule', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'rules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeGetter(window.CSSStyleSheet.prototype, 'cssRules', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSStyleSheet.prototype, 'replaceSync', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
- this._interceptNativeMethod(window.CSSGroupingRule.prototype, 'insertRule', invalidateCSSGroupingRule);
- this._interceptNativeMethod(window.CSSGroupingRule.prototype, 'deleteRule', invalidateCSSGroupingRule);
- this._interceptNativeGetter(window.CSSGroupingRule.prototype, 'cssRules', invalidateCSSGroupingRule);
this._interceptNativeSetter(window.StyleSheet.prototype, 'disabled', (sheet: StyleSheet) => {
if (sheet instanceof CSSStyleSheet)
this._invalidateStyleSheet(sheet as CSSStyleSheet);
});
- this._interceptNativeAsyncMethod(window.CSSStyleSheet.prototype, 'replace', (sheet: CSSStyleSheet) => this._invalidateStyleSheet(sheet));
-
this._fakeBase = document.createElement('base');
this._observer = new MutationObserver(list => this._handleMutations(list));
@@ -162,40 +144,6 @@
});
}
- private _interceptNativeMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
- const native = obj[method] as Function;
- if (!native)
- return;
- obj[method] = function(...args: any[]) {
- const result = native.call(this, ...args);
- cb(this, result);
- return result;
- };
- }
-
- private _interceptNativeAsyncMethod(obj: any, method: string, cb: (thisObj: any, result: any) => void) {
- const native = obj[method] as Function;
- if (!native)
- return;
- obj[method] = async function(...args: any[]) {
- const result = await native.call(this, ...args);
- cb(this, result);
- return result;
- };
- }
-
- private _interceptNativeGetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
- const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
- Object.defineProperty(obj, prop, {
- ...descriptor,
- get: function() {
- const result = descriptor.get!.call(this);
- cb(this, result);
- return result;
- },
- });
- }
-
private _interceptNativeSetter(obj: any, prop: string, cb: (thisObj: any, result: any) => void) {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop)!;
Object.defineProperty(obj, prop, {
@@ -213,42 +161,37 @@
ensureCachedData(mutation.target).attributesCached = undefined;
}
- private _invalidateStyleSheet(sheet: CSSStyleSheet) {
- if (this._readingStyleSheet)
- return;
- this._staleStyleSheets.add(sheet);
- if (sheet.href !== null)
- this._modifiedStyleSheets.add(sheet);
- }
-
private _updateStyleElementStyleSheetTextIfNeeded(sheet: CSSStyleSheet, forceText?: boolean): string | undefined {
- const data = ensureCachedData(sheet);
- if (this._staleStyleSheets.has(sheet) || (forceText && data.cssText === undefined)) {
- this._staleStyleSheets.delete(sheet);
- try {
- data.cssText = this._getSheetText(sheet);
- } catch (e) {
- // Sometimes we cannot access cross-origin stylesheets.
- data.cssText = '';
- }
- }
- return data.cssText;
+
+ const data = ensureCachedData(sheet);
+ try {
+ data.cssText = this._getSheetText(sheet);
+ } catch (e) {
+ data.cssText = '';
+ }
+ return data.cssText;
+
}
// Returns either content, ref, or no override.
private _updateLinkStyleSheetTextIfNeeded(sheet: CSSStyleSheet, snapshotNumber: number): string | number | undefined {
- const data = ensureCachedData(sheet);
- if (this._staleStyleSheets.has(sheet)) {
- this._staleStyleSheets.delete(sheet);
- try {
- data.cssText = this._getSheetText(sheet);
- data.cssRef = snapshotNumber;
- return data.cssText;
- } catch (e) {
- // Sometimes we cannot access cross-origin stylesheets.
- }
- }
- return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
+
+ const data = ensureCachedData(sheet);
+ try {
+ const currentText = this._getSheetText(sheet);
+ if (data.cssText === undefined) {
+ data.cssText = currentText;
+ return undefined;
+ }
+ if (currentText === data.cssText)
+ return data.cssRef === undefined ? undefined : snapshotNumber - data.cssRef;
+ data.cssText = currentText;
+ data.cssRef = snapshotNumber;
+ return data.cssText;
+ } catch (e) {
+ return undefined;
+ }
+
}
markIframe(iframeElement: HTMLIFrameElement | HTMLFrameElement, frameId: string) {
@@ -256,8 +199,6 @@
}
reset() {
- this._staleStyleSheets.clear();
-
const visitNode = (node: Node | ShadowRoot) => {
resetCachedData(node);
if (node.nodeType === Node.ELEMENT_NODE) {
@@ -326,17 +267,12 @@
}
private _getSheetText(sheet: CSSStyleSheet): string {
- this._readingStyleSheet = true;
- try {
- if (sheet.disabled)
- return '';
- const rules: string[] = [];
- for (const rule of sheet.cssRules)
- rules.push(rule.cssText);
- return rules.join('\n');
- } finally {
- this._readingStyleSheet = false;
- }
+
+ const rules: string[] = [];
+ for (const rule of sheet.cssRules)
+ rules.push(rule.cssText);
+ return rules.join('\n');
+
}
captureSnapshot(needsReset: boolean): SnapshotData | undefined {
@@ -635,18 +571,18 @@
collectionTime: 0,
};
- for (const sheet of this._modifiedStyleSheets) {
- if (sheet.href === null)
- continue;
- const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
- if (content === undefined) {
- // Unable to capture stylesheet contents.
- continue;
- }
- const base = this._getSheetBase(sheet);
- const url = removeHash(this._resolveUrl(base, sheet.href!));
- result.resourceOverrides.push({ url, content, contentType: 'text/css' },);
- }
+ for (const sheet of document.styleSheets) {
+ if (sheet.href === null)
+ continue;
+ const content = this._updateLinkStyleSheetTextIfNeeded(sheet, snapshotNumber);
+ if (content === undefined) {
+ // Unable to capture stylesheet contents.
+ continue;
+ }
+ const base = this._getSheetBase(sheet);
+ const url = removeHash(this._resolveUrl(base, sheet.href!));
+ result.resourceOverrides.push({ url, content, contentType: 'text/css' },);
+ }
result.collectionTime = performance.now() - timestamp;
return result;
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/trace/recorder/tracing.ts patchright/packages/playwright-core/src/server/trace/recorder/tracing.ts
---
+++
@@ -670,6 +670,11 @@
}
function createBeforeActionTraceEvent(metadata: CallMetadata, parentId?: string): trace.BeforeActionTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
const event: trace.BeforeActionTraceEvent = {
@@ -689,6 +694,11 @@
}
function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
@@ -699,6 +709,11 @@
}
function createActionLogTraceEvent(metadata: CallMetadata, message: string): trace.LogTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
@@ -710,6 +725,11 @@
}
function createAfterActionTraceEvent(metadata: CallMetadata): trace.AfterActionTraceEvent | null {
+
+ // Filter out internal fallback Route.continue calls from Patchright's inject routing
+ if (metadata.type === 'Route' && metadata.method === 'continue' && metadata.params?.isFallback)
+ return null;
+
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/server/utils/expectUtils.ts patchright/packages/playwright-core/src/server/utils/expectUtils.ts
---
+++
@@ -116,8 +116,7 @@
details.printedDiff = undefined;
}
- const align = !details.errorMessage && details.printedExpected?.startsWith('Expected:')
- && (!details.printedReceived || details.printedReceived.startsWith('Received:'));
+ const align = !details.errorMessage && details.printedExpected?.startsWith('Expected:') && (!details.printedReceived || details.printedReceived.startsWith('Received:'));
if (details.locator)
message += `Locator: ${align ? ' ' : ''}${details.locator}\n`;
if (details.printedExpected)
diff -ruN -x protocol.yml --minimal playwright/packages/playwright-core/src/utils/isomorphic/oldUtilityScriptSerializers.ts patchright/packages/playwright-core/src/utils/isomorphic/oldUtilityScriptSerializers.ts
---
+++
@@ -0,0 +1,292 @@
+;
+ /**
+ * Copyright (c) Microsoft Corporation.
+ *
+ * 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.
+ */
+
+ type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64';
+
+ export type SerializedValue =
+ undefined | boolean | number | string |
+ { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } |
+ { d: string } |
+ { u: string } |
+ { bi: string } |
+ { e: { n: string, m: string, s: string } } |
+ { r: { p: string, f: string } } |
+ { a: SerializedValue[], id: number } |
+ { o: { k: string, v: SerializedValue }[], id: number } |
+ { ref: number } |
+ { h: number } |
+ { ta: { b: string, k: TypedArrayKind } };
+
+ type HandleOrValue = { h: number } | { fallThrough: any };
+
+ type VisitorInfo = {
+ visited: Map<object, number>;
+ lastId: number;
+ };
+
+ export function source() {
+
+ function isRegExp(obj: any): obj is RegExp {
+ try {
+ return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isDate(obj: any): obj is Date {
+ try {
+ return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]';
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isURL(obj: any): obj is URL {
+ try {
+ return obj instanceof URL || Object.prototype.toString.call(obj) === '[object URL]';
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isError(obj: any): obj is Error {
+ try {
+ return obj instanceof Error || (obj && Object.getPrototypeOf(obj)?.name === 'Error');
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isTypedArray(obj: any, constructor: Function): boolean {
+ try {
+ return obj instanceof constructor || Object.prototype.toString.call(obj) === `[object ${constructor.name}]`;
+ } catch (error) {
+ return false;
+ }
+ }
+
+ const typedArrayConstructors: Record<TypedArrayKind, Function> = {
+ i8: Int8Array,
+ ui8: Uint8Array,
+ ui8c: Uint8ClampedArray,
+ i16: Int16Array,
+ ui16: Uint16Array,
+ i32: Int32Array,
+ ui32: Uint32Array,
+ // TODO: add Float16Array once it's in baseline
+ f32: Float32Array,
+ f64: Float64Array,
+ bi64: BigInt64Array,
+ bui64: BigUint64Array,
+ };
+
+ function typedArrayToBase64(array: any) {
+ /**
+ * Firefox does not support iterating over typed arrays, so we use `.toBase64`.
+ * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().'
+ */
+ if ('toBase64' in array)
+ return array.toBase64();
+ const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join('');
+ return btoa(binary);
+ }
+
+ function base64ToTypedArray(base64: string, TypedArrayConstructor: any) {
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++)
+ bytes[i] = binary.charCodeAt(i);
+ return new TypedArrayConstructor(bytes.buffer);
+ }
+
+ function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map<number, object> = new Map()): any {
+ if (Object.is(value, undefined))
+ return undefined;
+ if (typeof value === 'object' && value) {
+ if ('ref' in value)
+ return refs.get(value.ref);
+ if ('v' in value) {
+ if (value.v === 'undefined')
+ return undefined;
+ if (value.v === 'null')
+ return null;
+ if (value.v === 'NaN')
+ return NaN;
+ if (value.v === 'Infinity')
+ return Infinity;
+ if (value.v === '-Infinity')
+ return -Infinity;
+ if (value.v === '-0')
+ return -0;
+ return undefined;
+ }
+ if ('d' in value)
+ return new Date(value.d);
+ if ('u' in value)
+ return new URL(value.u);
+ if ('bi' in value)
+ return BigInt(value.bi);
+ if ('e' in value) {
+ const error = new Error(value.e.m);
+ error.name = value.e.n;
+ error.stack = value.e.s;
+ return error;
+ }
+ if ('r' in value)
+ return new RegExp(value.r.p, value.r.f);
+ if ('a' in value) {
+ const result: any[] = [];
+ refs.set(value.id, result);
+ for (const a of value.a)
+ result.push(parseEvaluationResultValue(a, handles, refs));
+ return result;
+ }
+ if ('o' in value) {
+ const result: any = {};
+ refs.set(value.id, result);
+ for (const { k, v } of value.o)
+ result[k] = parseEvaluationResultValue(v, handles, refs);
+ return result;
+ }
+ if ('h' in value)
+ return handles[value.h];
+ if ('ta' in value)
+ return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]);
+ }
+ return value;
+ }
+
+ function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
+ return serialize(value, handleSerializer, { visited: new Map(), lastId: 0 });
+ }
+
+ function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
+ if (value && typeof value === 'object') {
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window)
+ return 'ref: <Window>';
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document)
+ return 'ref: <Document>';
+ // eslint-disable-next-line no-restricted-globals
+ if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node)
+ return 'ref: <Node>';
+ }
+ return innerSerialize(value, handleSerializer, visitorInfo);
+ }
+
+ function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
+ const result = handleSerializer(value);
+ if ('fallThrough' in result)
+ value = result.fallThrough;
+ else
+ return result;
+
+ if (typeof value === 'symbol')
+ return { v: 'undefined' };
+ if (Object.is(value, undefined))
+ return { v: 'undefined' };
+ if (Object.is(value, null))
+ return { v: 'null' };
+ if (Object.is(value, NaN))
+ return { v: 'NaN' };
+ if (Object.is(value, Infinity))
+ return { v: 'Infinity' };
+ if (Object.is(value, -Infinity))
+ return { v: '-Infinity' };
+ if (Object.is(value, -0))
+ return { v: '-0' };
+
+ if (typeof value === 'boolean')
+ return value;
+ if (typeof value === 'number')
+ return value;
+ if (typeof value === 'string')
+ return value;
+ if (typeof value === 'bigint')
+ return { bi: value.toString() };
+
+ if (isError(value)) {
+ let stack;
+ if (value.stack?.startsWith(value.name + ': ' + value.message)) {
+ // v8
+ stack = value.stack;
+ } else {
+ stack = `${value.name}: ${value.message}
+${value.stack}`;
+ }
+ return { e: { n: value.name, m: value.message, s: stack } };
+ }
+ if (isDate(value))
+ return { d: value.toJSON() };
+ if (isURL(value))
+ return { u: value.toJSON() };
+ if (isRegExp(value))
+ return { r: { p: value.source, f: value.flags } };
+ for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) {
+ if (isTypedArray(value, ctor))
+ return { ta: { b: typedArrayToBase64(value), k } };
+ }
+
+ const id = visitorInfo.visited.get(value);
+ if (id)
+ return { ref: id };
+
+ if (Array.isArray(value)) {
+ const a = [];
+ const id = ++visitorInfo.lastId;
+ visitorInfo.visited.set(value, id);
+ for (let i = 0; i < value.length; ++i)
+ a.push(serialize(value[i], handleSerializer, visitorInfo));
+ return { a, id };
+ }
+
+ if (typeof value === 'object') {
+ const o: { k: string, v: SerializedValue }[] = [];
+ const id = ++visitorInfo.lastId;
+ visitorInfo.visited.set(value, id);
+ for (const name of Object.keys(value)) {
+ let item;
+ try {
+ item = value[name];
+ } catch (e) {
+ continue; // native bindings will throw sometimes;
+ }
+ if (name === 'toJSON' && typeof item === 'function')
+ o.push({ k: name, v: { o: [], id: 0 } });
+ else
+ o.push({ k: name, v: serialize(item, handleSerializer, visitorInfo) });
+ }
+
+ let jsonWrapper;
+ try {
+ // If Object.keys().length === 0 we fall back to toJSON if it exists
+ if (o.length === 0 && value.toJSON && typeof value.toJSON === 'function')
+ jsonWrapper = { value: value.toJSON() };
+ } catch (e) {
+ }
+ if (jsonWrapper)
+ return innerSerialize(jsonWrapper.value, handleSerializer, visitorInfo);
+
+ return { o, id };
+ }
+ }
+
+ return { parseEvaluationResultValue, serializeAsCallArgument };
+ }
+
\ No newline at end of file
diff -ruN -x protocol.yml --minimal playwright/packages/recorder/src/recorder.tsx patchright/packages/recorder/src/recorder.tsx
---
+++
@@ -101,7 +101,7 @@
}, [backend, mode, selectedTab, setSelectedTab, source]);
React.useEffect(() => {
- backend.setAutoExpect({ autoExpect });
+ try { window.dispatch({ event: 'setAutoExpect', params: { autoExpect } }); } catch {}
}, [autoExpect, backend]);
React.useLayoutEffect(() => {