chore: implement exposeFunction
(#10860)
This commit is contained in:
parent
acdd7d3cd5
commit
253d469515
@ -18,7 +18,7 @@ Functions installed via `page.exposeFunction` survive navigations.
|
||||
|
||||
```typescript
|
||||
class Page {
|
||||
exposeFunction(
|
||||
abstract exposeFunction(
|
||||
name: string,
|
||||
pptrFunction:
|
||||
| Function
|
||||
|
@ -15,7 +15,7 @@ One Browser instance might have multiple Page instances.
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
export declare class Page extends EventEmitter implements AsyncDisposable, Disposable
|
||||
export declare abstract class Page extends EventEmitter implements AsyncDisposable, Disposable
|
||||
```
|
||||
|
||||
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)
|
||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -3434,9 +3434,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "0.4.25",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.25.tgz",
|
||||
"integrity": "sha512-wQOIgYulshTLpZtuDO/eKFfKqVtpS2UwFVVqi/9q5rX/VXVkYNb/0mZ5l479W24A5ogYKBKEIb6BxMlhMcpXFw==",
|
||||
"version": "0.4.26",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.26.tgz",
|
||||
"integrity": "sha512-lukBGfogAI4T0y3acc86RaacqgKQve47/8pV2c+Hr1PjcICj2K4OkL3qfX3qrqxxnd4ddurFC0WBA3VCQqYeUQ==",
|
||||
"dependencies": {
|
||||
"mitt": "3.0.1"
|
||||
},
|
||||
@ -11115,7 +11115,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "1.7.0",
|
||||
"chromium-bidi": "0.4.25",
|
||||
"chromium-bidi": "0.4.26",
|
||||
"cross-fetch": "4.0.0",
|
||||
"debug": "4.3.4",
|
||||
"devtools-protocol": "0.0.1159816",
|
||||
@ -13466,9 +13466,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"chromium-bidi": {
|
||||
"version": "0.4.25",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.25.tgz",
|
||||
"integrity": "sha512-wQOIgYulshTLpZtuDO/eKFfKqVtpS2UwFVVqi/9q5rX/VXVkYNb/0mZ5l479W24A5ogYKBKEIb6BxMlhMcpXFw==",
|
||||
"version": "0.4.26",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.26.tgz",
|
||||
"integrity": "sha512-lukBGfogAI4T0y3acc86RaacqgKQve47/8pV2c+Hr1PjcICj2K4OkL3qfX3qrqxxnd4ddurFC0WBA3VCQqYeUQ==",
|
||||
"requires": {
|
||||
"mitt": "3.0.1"
|
||||
}
|
||||
@ -17257,7 +17257,7 @@
|
||||
"version": "file:packages/puppeteer-core",
|
||||
"requires": {
|
||||
"@puppeteer/browsers": "1.7.0",
|
||||
"chromium-bidi": "0.4.25",
|
||||
"chromium-bidi": "0.4.26",
|
||||
"cross-fetch": "4.0.0",
|
||||
"debug": "4.3.4",
|
||||
"devtools-protocol": "0.0.1159816",
|
||||
|
@ -141,7 +141,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "1.7.0",
|
||||
"chromium-bidi": "0.4.25",
|
||||
"chromium-bidi": "0.4.26",
|
||||
"cross-fetch": "4.0.0",
|
||||
"debug": "4.3.4",
|
||||
"devtools-protocol": "0.0.1159816",
|
||||
|
@ -1107,4 +1107,15 @@ export abstract class Frame extends EventEmitter {
|
||||
waitForDevicePrompt(): Promise<DeviceRequestPrompt> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
exposeFunction<Args extends unknown[], Ret>(
|
||||
name: string,
|
||||
fn: (...args: Args) => Awaitable<Ret>
|
||||
): Promise<void>;
|
||||
exposeFunction(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
@ -478,7 +478,10 @@ export interface NewDocumentScriptEvaluation {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class Page extends EventEmitter implements AsyncDisposable, Disposable {
|
||||
export abstract class Page
|
||||
extends EventEmitter
|
||||
implements AsyncDisposable, Disposable
|
||||
{
|
||||
#handlerMap = new WeakMap<Handler<any>, Handler<any>>();
|
||||
|
||||
/**
|
||||
@ -1327,13 +1330,10 @@ export class Page extends EventEmitter implements AsyncDisposable, Disposable {
|
||||
* @param pptrFunction - Callback function which will be called in Puppeteer's
|
||||
* context.
|
||||
*/
|
||||
async exposeFunction(
|
||||
abstract exposeFunction(
|
||||
name: string,
|
||||
pptrFunction: Function | {default: Function}
|
||||
): Promise<void>;
|
||||
async exposeFunction(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* The method removes a previously added function via ${@link Page.exposeFunction}
|
||||
|
276
packages/puppeteer-core/src/common/bidi/ExposedFunction.ts
Normal file
276
packages/puppeteer-core/src/common/bidi/ExposedFunction.ts
Normal file
@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Copyright 2023 Google Inc. 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 * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {assert} from '../../util/assert.js';
|
||||
import {Deferred} from '../../util/Deferred.js';
|
||||
import {interpolateFunction, stringifyFunction} from '../../util/Function.js';
|
||||
import {Awaitable, FlattenHandle} from '../types.js';
|
||||
|
||||
import {Connection} from './Connection.js';
|
||||
import {BidiFrame} from './Frame.js';
|
||||
import {BidiSerializer} from './Serializer.js';
|
||||
import {debugError} from './utils.js';
|
||||
|
||||
type SendArgsChannel<Args> = (value: [id: number, args: Args]) => void;
|
||||
type SendResolveChannel<Ret> = (
|
||||
value: [id: number, resolve: (ret: FlattenHandle<Awaited<Ret>>) => void]
|
||||
) => void;
|
||||
type SendRejectChannel = (
|
||||
value: [id: number, reject: (error: unknown) => void]
|
||||
) => void;
|
||||
|
||||
interface RemotePromiseCallbacks {
|
||||
resolve: Deferred<Bidi.Script.RemoteValue>;
|
||||
reject: Deferred<Bidi.Script.RemoteValue>;
|
||||
}
|
||||
|
||||
export class ExposeableFunction<Args extends unknown[], Ret> {
|
||||
readonly #frame;
|
||||
|
||||
readonly name;
|
||||
readonly #apply;
|
||||
|
||||
readonly #channels;
|
||||
readonly #callerInfos = new Map<
|
||||
string,
|
||||
Map<number, RemotePromiseCallbacks>
|
||||
>();
|
||||
|
||||
constructor(
|
||||
frame: BidiFrame,
|
||||
name: string,
|
||||
apply: (...args: Args) => Awaitable<Ret>
|
||||
) {
|
||||
this.#frame = frame;
|
||||
this.name = name;
|
||||
this.#apply = apply;
|
||||
|
||||
this.#channels = {
|
||||
args: `__puppeteer__${this.#frame._id}_page_exposeFunction_${
|
||||
this.name
|
||||
}_args`,
|
||||
resolve: `__puppeteer__${this.#frame._id}_page_exposeFunction_${
|
||||
this.name
|
||||
}_resolve`,
|
||||
reject: `__puppeteer__${this.#frame._id}_page_exposeFunction_${
|
||||
this.name
|
||||
}_reject`,
|
||||
};
|
||||
}
|
||||
|
||||
async expose(): Promise<void> {
|
||||
const connection = this.#connection;
|
||||
const channelArguments = this.#channelArguments;
|
||||
const {name} = this;
|
||||
|
||||
// TODO(jrandolf): Implement cleanup with removePreloadScript.
|
||||
connection.on(
|
||||
Bidi.ChromiumBidi.Script.EventNames.Message,
|
||||
this.#handleArgumentsMessage
|
||||
);
|
||||
connection.on(
|
||||
Bidi.ChromiumBidi.Script.EventNames.Message,
|
||||
this.#handleResolveMessage
|
||||
);
|
||||
connection.on(
|
||||
Bidi.ChromiumBidi.Script.EventNames.Message,
|
||||
this.#handleRejectMessage
|
||||
);
|
||||
|
||||
const functionDeclaration = stringifyFunction(
|
||||
interpolateFunction(
|
||||
(
|
||||
sendArgs: SendArgsChannel<Args>,
|
||||
sendResolve: SendResolveChannel<Ret>,
|
||||
sendReject: SendRejectChannel
|
||||
) => {
|
||||
let id = 0;
|
||||
Object.assign(globalThis, {
|
||||
[PLACEHOLDER('name') as string]: function (...args: Args) {
|
||||
return new Promise<FlattenHandle<Awaited<Ret>>>(
|
||||
(resolve, reject) => {
|
||||
sendArgs([id, args]);
|
||||
sendResolve([id, resolve]);
|
||||
sendReject([id, reject]);
|
||||
++id;
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
{name: JSON.stringify(name)}
|
||||
)
|
||||
);
|
||||
|
||||
await connection.send('script.addPreloadScript', {
|
||||
functionDeclaration,
|
||||
arguments: channelArguments,
|
||||
});
|
||||
|
||||
await connection.send('script.callFunction', {
|
||||
functionDeclaration,
|
||||
arguments: channelArguments,
|
||||
awaitPromise: false,
|
||||
target: this.#frame.mainRealm().realm.target,
|
||||
});
|
||||
}
|
||||
|
||||
#handleArgumentsMessage = async (params: Bidi.Script.MessageParameters) => {
|
||||
if (params.channel !== this.#channels.args) {
|
||||
return;
|
||||
}
|
||||
const connection = this.#connection;
|
||||
const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
|
||||
const args = remoteValue.value?.[1];
|
||||
assert(args);
|
||||
try {
|
||||
const result = await this.#apply(...BidiSerializer.deserialize(args));
|
||||
await connection.send('script.callFunction', {
|
||||
functionDeclaration: stringifyFunction(([_, resolve]: any, result) => {
|
||||
resolve(result);
|
||||
}),
|
||||
arguments: [
|
||||
(await callbacks.resolve.valueOrThrow()) as Bidi.Script.LocalValue,
|
||||
BidiSerializer.serializeRemoteValue(result),
|
||||
],
|
||||
awaitPromise: false,
|
||||
target: params.source,
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
if (error instanceof Error) {
|
||||
await connection.send('script.callFunction', {
|
||||
functionDeclaration: stringifyFunction(
|
||||
(
|
||||
[_, reject]: any,
|
||||
name: string,
|
||||
message: string,
|
||||
stack?: string
|
||||
) => {
|
||||
const error = new Error(message);
|
||||
error.name = name;
|
||||
if (stack) {
|
||||
error.stack = stack;
|
||||
}
|
||||
reject(error);
|
||||
}
|
||||
),
|
||||
arguments: [
|
||||
(await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
|
||||
BidiSerializer.serializeRemoteValue(error.name),
|
||||
BidiSerializer.serializeRemoteValue(error.message),
|
||||
BidiSerializer.serializeRemoteValue(error.stack),
|
||||
],
|
||||
awaitPromise: false,
|
||||
target: params.source,
|
||||
});
|
||||
} else {
|
||||
await connection.send('script.callFunction', {
|
||||
functionDeclaration: stringifyFunction(
|
||||
([_, reject]: any, error: unknown) => {
|
||||
reject(error);
|
||||
}
|
||||
),
|
||||
arguments: [
|
||||
(await callbacks.reject.valueOrThrow()) as Bidi.Script.LocalValue,
|
||||
BidiSerializer.serializeRemoteValue(error),
|
||||
],
|
||||
awaitPromise: false,
|
||||
target: params.source,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
debugError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
get #connection(): Connection {
|
||||
return this.#frame.context().connection;
|
||||
}
|
||||
|
||||
get #channelArguments() {
|
||||
return [
|
||||
{
|
||||
type: 'channel' as const,
|
||||
value: {
|
||||
channel: this.#channels.args,
|
||||
ownership: Bidi.Script.ResultOwnership.Root,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'channel' as const,
|
||||
value: {
|
||||
channel: this.#channels.resolve,
|
||||
ownership: Bidi.Script.ResultOwnership.Root,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'channel' as const,
|
||||
value: {
|
||||
channel: this.#channels.reject,
|
||||
ownership: Bidi.Script.ResultOwnership.Root,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#handleResolveMessage = (params: Bidi.Script.MessageParameters) => {
|
||||
if (params.channel !== this.#channels.resolve) {
|
||||
return;
|
||||
}
|
||||
const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
|
||||
callbacks.resolve.resolve(remoteValue);
|
||||
};
|
||||
|
||||
#handleRejectMessage = (params: Bidi.Script.MessageParameters) => {
|
||||
if (params.channel !== this.#channels.reject) {
|
||||
return;
|
||||
}
|
||||
const {callbacks, remoteValue} = this.#getCallbacksAndRemoteValue(params);
|
||||
callbacks.reject.resolve(remoteValue);
|
||||
};
|
||||
|
||||
#getCallbacksAndRemoteValue(params: Bidi.Script.MessageParameters) {
|
||||
const {data, source} = params;
|
||||
assert(data.type === 'array');
|
||||
assert(data.value);
|
||||
|
||||
const callerIdRemote = data.value[0];
|
||||
assert(callerIdRemote);
|
||||
assert(callerIdRemote.type === 'number');
|
||||
assert(typeof callerIdRemote.value === 'number');
|
||||
|
||||
let bindingMap = this.#callerInfos.get(source.realm);
|
||||
if (!bindingMap) {
|
||||
bindingMap = new Map();
|
||||
this.#callerInfos.set(source.realm, bindingMap);
|
||||
}
|
||||
|
||||
const callerId = callerIdRemote.value;
|
||||
let callbacks = bindingMap.get(callerId);
|
||||
if (!callbacks) {
|
||||
callbacks = {
|
||||
resolve: new Deferred(),
|
||||
reject: new Deferred(),
|
||||
};
|
||||
bindingMap.set(callerId, callbacks);
|
||||
}
|
||||
return {callbacks, remoteValue: data};
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import {CDPSession} from '../Connection.js';
|
||||
import {UTILITY_WORLD_NAME} from '../FrameManager.js';
|
||||
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
|
||||
import {TimeoutSettings} from '../TimeoutSettings.js';
|
||||
import {Awaitable} from '../types.js';
|
||||
import {waitForEvent} from '../util.js';
|
||||
|
||||
import {
|
||||
@ -29,6 +30,7 @@ import {
|
||||
getWaitUntilSingle,
|
||||
lifeCycleToSubscribedEvent,
|
||||
} from './BrowsingContext.js';
|
||||
import {ExposeableFunction} from './ExposedFunction.js';
|
||||
import {HTTPResponse} from './HTTPResponse.js';
|
||||
import {BidiPage} from './Page.js';
|
||||
import {
|
||||
@ -207,4 +209,24 @@ export class BidiFrame extends Frame {
|
||||
this.sandboxes[MAIN_SANDBOX][Symbol.dispose]();
|
||||
this.sandboxes[PUPPETEER_SANDBOX][Symbol.dispose]();
|
||||
}
|
||||
|
||||
#exposedFunctions = new Map<string, ExposeableFunction<never[], unknown>>();
|
||||
override async exposeFunction<Args extends unknown[], Ret>(
|
||||
name: string,
|
||||
apply: (...args: Args) => Awaitable<Ret>
|
||||
): Promise<void> {
|
||||
if (this.#exposedFunctions.has(name)) {
|
||||
throw new Error(
|
||||
`Failed to add page binding with name ${name}: globalThis['${name}'] already exists!`
|
||||
);
|
||||
}
|
||||
const exposeable = new ExposeableFunction(this, name, apply);
|
||||
this.#exposedFunctions.set(name, exposeable);
|
||||
try {
|
||||
await exposeable.expose();
|
||||
} catch (error) {
|
||||
this.#exposedFunctions.delete(name);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import {PDFOptions} from '../PDFOptions.js';
|
||||
import {Viewport} from '../PuppeteerViewport.js';
|
||||
import {TimeoutSettings} from '../TimeoutSettings.js';
|
||||
import {Tracing} from '../Tracing.js';
|
||||
import {Awaitable} from '../types.js';
|
||||
import {
|
||||
debugError,
|
||||
evaluationString,
|
||||
@ -674,6 +675,18 @@ export class BidiPage extends Page {
|
||||
script: id,
|
||||
});
|
||||
}
|
||||
|
||||
override async exposeFunction<Args extends unknown[], Ret>(
|
||||
name: string,
|
||||
pptrFunction:
|
||||
| ((...args: Args) => Awaitable<Ret>)
|
||||
| {default: (...args: Args) => Awaitable<Ret>}
|
||||
): Promise<void> {
|
||||
return await this.mainFrame().exposeFunction(
|
||||
name,
|
||||
'default' in pptrFunction ? pptrFunction.default : pptrFunction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isConsoleLogEntry(
|
||||
|
@ -765,7 +765,7 @@
|
||||
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument *",
|
||||
@ -1367,6 +1367,12 @@
|
||||
"parameters": ["webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[page.spec] Page Page.exposeFunction *",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[page.spec] Page Page.pdf should respect timeout",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -2099,6 +2105,12 @@
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -2285,6 +2297,12 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "TIMEOUT"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
@ -2979,7 +2997,7 @@
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["firefox", "webDriverBiDi"],
|
||||
"expectations": ["PASS"]
|
||||
"expectations": ["FAIL", "PASS"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[navigation.spec] navigation Page.goto should send referer",
|
||||
@ -3701,6 +3719,12 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"expectations": ["SKIP"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[page.spec] Page Page.exposeFunction should be callable from-inside evaluateOnNewDocument",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
"parameters": ["chrome", "webDriverBiDi"],
|
||||
"expectations": ["FAIL", "TIMEOUT"]
|
||||
},
|
||||
{
|
||||
"testIdPattern": "[page.spec] Page Page.exposeFunction should be callable from-inside evaluateOnNewDocument",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
Loading…
Reference in New Issue
Block a user