chore: extract page bindings controller from `UtilityScript` (#35996)
This commit is contained in:
parent
a88aa92d9d
commit
d57f194693
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* 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 { serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
|
||||
|
||||
import type { SerializedValue } from '@isomorphic/utilityScriptSerializers';
|
||||
|
||||
// This runtime guid is replaced by the actual guid at runtime in all generated sources.
|
||||
const kRuntimeGuid = '$runtime_guid$';
|
||||
|
||||
// The name of the global playwright binding, referenced in Node.js.
|
||||
const kPlaywrightBinding = `__playwright__binding__${kRuntimeGuid}`;
|
||||
const kPlaywrightBindingController = `__playwright__binding__controller__${kRuntimeGuid}`;
|
||||
|
||||
export type BindingPayload = {
|
||||
name: string;
|
||||
seq: number;
|
||||
serializedArgs?: SerializedValue[],
|
||||
};
|
||||
|
||||
type BindingData = {
|
||||
callbacks: Map<number, { resolve: (value: any) => void, reject: (error: Error) => void }>;
|
||||
lastSeq: number;
|
||||
handles: Map<number, any>;
|
||||
};
|
||||
|
||||
class BindingsController {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
private _global: typeof globalThis;
|
||||
private _bindings = new Map<string, BindingData>();
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
constructor(global: typeof globalThis) {
|
||||
this._global = global;
|
||||
}
|
||||
|
||||
addBinding(bindingName: string, needsHandle: boolean) {
|
||||
const data: BindingData = {
|
||||
callbacks: new Map(),
|
||||
lastSeq: 0,
|
||||
handles: new Map(),
|
||||
};
|
||||
this._bindings.set(bindingName, data);
|
||||
(this._global as any)[bindingName] = (...args: any[]) => {
|
||||
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
|
||||
throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
|
||||
const seq = ++data.lastSeq;
|
||||
const promise = new Promise((resolve, reject) => data.callbacks.set(seq, { resolve, reject }));
|
||||
let payload: BindingPayload;
|
||||
if (needsHandle) {
|
||||
data.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 };
|
||||
}
|
||||
(this._global as any)[kPlaywrightBinding](JSON.stringify(payload));
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
takeBindingHandle(arg: { name: string, seq: number }) {
|
||||
const handles = this._bindings.get(arg.name)!.handles;
|
||||
const handle = handles.get(arg.seq);
|
||||
handles.delete(arg.seq);
|
||||
return handle;
|
||||
}
|
||||
|
||||
deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) {
|
||||
const callbacks = this._bindings.get(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 ensureBindingsController() {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const global = globalThis;
|
||||
if (!(global as any)[kPlaywrightBindingController])
|
||||
(global as any)[kPlaywrightBindingController] = new BindingsController(global);
|
||||
}
|
||||
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
import { parseEvaluationResultValue, serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
|
||||
|
||||
import type { SerializedValue } from '@isomorphic/utilityScriptSerializers';
|
||||
|
||||
// --- This section should match javascript.ts and generated_injected_builtins.js ---
|
||||
|
||||
// This runtime guid is replaced by the actual guid at runtime in all generated sources.
|
||||
|
|
@ -25,9 +23,6 @@ const kRuntimeGuid = '$runtime_guid$';
|
|||
// This flag is replaced by true/false at runtime in all generated sources.
|
||||
const kUtilityScriptIsUnderTest = false;
|
||||
|
||||
// The name of the global playwright binding, referenced in Node.js.
|
||||
const kPlaywrightBinding = `__playwright__binding__${kRuntimeGuid}`;
|
||||
|
||||
// The name of the global property that stores the UtilityScript instance,
|
||||
// referenced by generated_injected_builtins.js.
|
||||
const kUtilityScriptGlobalProperty = `__playwright_utility_script__${kRuntimeGuid}`;
|
||||
|
|
@ -56,26 +51,12 @@ export type Builtins = {
|
|||
|
||||
// --- End of the matching section ---
|
||||
|
||||
export type BindingPayload = {
|
||||
name: string;
|
||||
seq: number;
|
||||
serializedArgs?: SerializedValue[],
|
||||
};
|
||||
|
||||
type BindingData = {
|
||||
callbacks: Map<number, { resolve: (value: any) => void, reject: (error: Error) => void }>;
|
||||
lastSeq: number;
|
||||
handles: Map<number, any>;
|
||||
};
|
||||
|
||||
export class UtilityScript {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
readonly global: typeof globalThis;
|
||||
readonly builtins: Builtins;
|
||||
readonly isUnderTest: boolean;
|
||||
|
||||
private _bindings = new Map<string, BindingData>();
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
constructor(global: typeof globalThis) {
|
||||
this.global = global;
|
||||
|
|
@ -134,52 +115,6 @@ export class UtilityScript {
|
|||
return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }));
|
||||
}
|
||||
|
||||
addBinding(bindingName: string, needsHandle: boolean) {
|
||||
const data: BindingData = {
|
||||
callbacks: new Map(),
|
||||
lastSeq: 0,
|
||||
handles: new Map(),
|
||||
};
|
||||
this._bindings.set(bindingName, data);
|
||||
(this.global as any)[bindingName] = (...args: any[]) => {
|
||||
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
|
||||
throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
|
||||
const seq = ++data.lastSeq;
|
||||
const promise = new Promise((resolve, reject) => data.callbacks.set(seq, { resolve, reject }));
|
||||
let payload: BindingPayload;
|
||||
if (needsHandle) {
|
||||
data.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 };
|
||||
}
|
||||
(this.global as any)[kPlaywrightBinding](JSON.stringify(payload));
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
takeBindingHandle(arg: { name: string, seq: number }) {
|
||||
const handles = this._bindings.get(arg.name)!.handles;
|
||||
const handle = handles.get(arg.seq);
|
||||
handles.delete(arg.seq);
|
||||
return handle;
|
||||
}
|
||||
|
||||
deliverBindingResult(arg: { name: string, seq: number, result?: any, error?: any }) {
|
||||
const callbacks = this._bindings.get(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);
|
||||
}
|
||||
|
||||
private _promiseAwareJsonValueNoThrow(value: any) {
|
||||
const safeJson = (value: any) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { RecorderApp } from './recorder/recorderApp';
|
|||
import { Tracing } from './trace/recorder/tracing';
|
||||
import * as js from './javascript';
|
||||
import * as rawStorageSource from '../generated/storageScriptSource';
|
||||
import * as rawBindingsControllerSource from '../generated/bindingsControllerSource';
|
||||
|
||||
import type { Artifact } from './artifact';
|
||||
import type { Browser, BrowserOptions } from './browser';
|
||||
|
|
@ -90,6 +91,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
private _customCloseHandler?: () => Promise<any>;
|
||||
readonly _tempDirs: string[] = [];
|
||||
private _settingStorageState = false;
|
||||
bindingsInitScript?: InitScript;
|
||||
initScripts: InitScript[] = [];
|
||||
private _routesInFlight = new Set<network.Route>();
|
||||
private _debugger!: Debugger;
|
||||
|
|
@ -330,6 +332,17 @@ export abstract class BrowserContext extends SdkObject {
|
|||
return;
|
||||
this._playwrightBindingExposed = true;
|
||||
await this.doExposePlaywrightBinding();
|
||||
|
||||
this.bindingsInitScript = new InitScript(`
|
||||
(() => {
|
||||
const module = {};
|
||||
${js.prepareGeneratedScript(rawBindingsControllerSource.source)}
|
||||
(module.exports.ensureBindingsController())();
|
||||
})();
|
||||
`, true /* internal */);
|
||||
this.initScripts.push(this.bindingsInitScript);
|
||||
await this.doAddInitScript(this.bindingsInitScript);
|
||||
await this.safeNonStallingEvaluateInAllFrames(this.bindingsInitScript.source, 'main');
|
||||
}
|
||||
|
||||
needsPlaywrightBinding() {
|
||||
|
|
@ -347,8 +360,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this.doAddInitScript(binding.initScript);
|
||||
const frames = this.pages().map(page => page.frames()).flat();
|
||||
await Promise.all(frames.map(frame => frame.evaluateExpression(binding.initScript.source).catch(e => {})));
|
||||
await this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main');
|
||||
}
|
||||
|
||||
async _removeExposedBindings() {
|
||||
|
|
|
|||
|
|
@ -380,6 +380,8 @@ export class FFBrowserContext extends BrowserContext {
|
|||
|
||||
private async _updateInitScripts() {
|
||||
const bindingScripts = [...this._pageBindings.values()].map(binding => binding.initScript.source);
|
||||
if (this.bindingsInitScript)
|
||||
bindingScripts.unshift(this.bindingsInitScript.source);
|
||||
const initScripts = this.initScripts.map(script => script.source);
|
||||
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [kUtilityInitScript.source, ...bindingScripts, ...initScripts].map(script => ({ script })) });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ export function accessUtilityScript() {
|
|||
// The name of the global playwright binding, accessed by UtilityScript.
|
||||
export const kPlaywrightBinding = '__playwright__binding__' + runtimeGuid;
|
||||
|
||||
// Include this code in any evaluated source to get access to the BindingsController instance.
|
||||
export function accessBindingsController() {
|
||||
return `globalThis['__playwright__binding__controller__${runtimeGuid}']`;
|
||||
}
|
||||
|
||||
// --- End of the matching section ---
|
||||
|
||||
interface TaggedAsJSHandle<T> {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import type * as types from './types';
|
|||
import type { TimeoutOptions } from '../utils/isomorphic/types';
|
||||
import type { ImageComparatorOptions } from './utils/comparators';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { BindingPayload, UtilityScript } from '@injected/utilityScript';
|
||||
import type { BindingPayload } from '@injected/bindingsController';
|
||||
|
||||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
|
|
@ -349,7 +349,7 @@ export class Page extends SdkObject {
|
|||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this.delegate.addInitScript(binding.initScript);
|
||||
await Promise.all(this.frames().map(frame => frame.evaluateExpression(binding.initScript.source).catch(e => {})));
|
||||
await this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main');
|
||||
}
|
||||
|
||||
private async _removeExposedBindings() {
|
||||
|
|
@ -770,8 +770,10 @@ export class Page extends SdkObject {
|
|||
}
|
||||
|
||||
allInitScripts() {
|
||||
const bindings = [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()];
|
||||
return [kUtilityInitScript, ...bindings.map(binding => binding.initScript), ...this.browserContext.initScripts, ...this.initScripts];
|
||||
const bindings = [...this.browserContext._pageBindings.values(), ...this._pageBindings.values()].map(binding => binding.initScript);
|
||||
if (this.browserContext.bindingsInitScript)
|
||||
bindings.unshift(this.browserContext.bindingsInitScript);
|
||||
return [kUtilityInitScript, ...bindings, ...this.browserContext.initScripts, ...this.initScripts];
|
||||
}
|
||||
|
||||
getBinding(name: string) {
|
||||
|
|
@ -870,23 +872,21 @@ export class PageBinding {
|
|||
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
|
||||
this.name = name;
|
||||
this.playwrightFunction = playwrightFunction;
|
||||
this.initScript = new InitScript(`${js.accessUtilityScript()}.addBinding(${JSON.stringify(name)}, ${needsHandle})`, true /* internal */);
|
||||
this.initScript = new InitScript(`${js.accessBindingsController()}.addBinding(${JSON.stringify(name)}, ${needsHandle})`, true /* internal */);
|
||||
this.needsHandle = needsHandle;
|
||||
this.internal = name.startsWith('__pw');
|
||||
}
|
||||
|
||||
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
|
||||
const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
|
||||
let utilityScript: js.JSHandle<UtilityScript> | undefined;
|
||||
try {
|
||||
utilityScript = await context.utilityScript();
|
||||
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 utilityScript.evaluateHandle((utility, arg) => utility.takeBindingHandle(arg), { name, seq }).catch(e => null);
|
||||
const handle = await context.evaluateExpressionHandle(`arg => ${js.accessBindingsController()}.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))
|
||||
|
|
@ -894,9 +894,9 @@ export class PageBinding {
|
|||
const args = serializedArgs!.map(a => parseEvaluationResultValue(a));
|
||||
result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args);
|
||||
}
|
||||
utilityScript.evaluate((utility, arg) => utility.deliverBindingResult(arg), { name, seq, result }).catch(e => debugLogger.log('error', e));
|
||||
context.evaluateExpressionHandle(`arg => ${js.accessBindingsController()}.deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e));
|
||||
} catch (error) {
|
||||
utilityScript?.evaluate((utility, arg) => utility.deliverBindingResult(arg), { name, seq, error }).catch(e => debugLogger.log('error', e));
|
||||
context.evaluateExpressionHandle(`arg => ${js.accessBindingsController()}.deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ const injectedScripts = [
|
|||
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
|
||||
true,
|
||||
],
|
||||
[
|
||||
path.join(ROOT, 'packages', 'injected', 'src', 'bindingsController.ts'),
|
||||
path.join(ROOT, 'packages', 'injected', 'lib'),
|
||||
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
|
||||
true,
|
||||
],
|
||||
[
|
||||
path.join(ROOT, 'packages', 'injected', 'src', 'webSocketMock.ts'),
|
||||
path.join(ROOT, 'packages', 'injected', 'lib'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue