9407 lines
404 KiB
Diff
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
# 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(() => {