9407 lines
404 KiB
Diff
9407 lines
404 KiB
Diff
|
|
# 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(/&/g, '&') // Must be first!
|
|||
|
|
+ .replace(/</g, '<')
|
|||
|
|
+ .replace(/>/g, '>')
|
|||
|
|
+ .replace(/"/g, '"')
|
|||
|
|
+ .replace(/'/g, "'")
|
|||
|
|
+ .replace(/"/g, '"')
|
|||
|
|
+ .replace(/ /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, ''').replace(/"/g, '"');
|
|||
|
|
+ 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(/&/g, '&') // Must be first!
|
|||
|
|
+ .replace(/</g, '<')
|
|||
|
|
+ .replace(/>/g, '>')
|
|||
|
|
+ .replace(/"/g, '"')
|
|||
|
|
+ .replace(/'/g, "'")
|
|||
|
|
+ .replace(/"/g, '"')
|
|||
|
|
+ .replace(/ /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, ''').replace(/"/g, '"');
|
|||
|
|
+ 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(() => {
|