feat(addInitScript): allow passing functions as init script arguments (#41921)

This commit is contained in:
Pavel Feldman
2026-07-21 15:44:04 -07:00
committed by GitHub
parent 6d487d67c9
commit f39623b16d
19 changed files with 306 additions and 40 deletions
+3
View File
@@ -476,6 +476,9 @@ Path to the JavaScript file. If `path` is a relative path, then it is resolved r
Script to be evaluated in all pages in the browser context. Optional.
### option: BrowserContext.addInitScript.exposeFunctions = %%-js-init-script-expose-functions-%%
* since: v1.62
## method: BrowserContext.backgroundPages
* since: v1.11
* deprecated: Background pages have been removed from Chromium together with Manifest V2 extensions.
+3
View File
@@ -651,6 +651,9 @@ Path to the JavaScript file. If `path` is a relative path, then it is resolved r
Script to be evaluated in all pages in the browser context. Optional.
### option: Page.addInitScript.exposeFunctions = %%-js-init-script-expose-functions-%%
* since: v1.62
## async method: Page.addScriptTag
* since: v1.8
- returns: <[ElementHandle]>
+6
View File
@@ -590,6 +590,12 @@ Function to be evaluated in the page context.
When set to `true`, functions passed inside [`param: arg`] are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [`method: Page.exposeFunction`], so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
## js-init-script-expose-functions
* langs: js
- `exposeFunctions` <[boolean]>
When set to `true`, functions passed inside [`param: arg`] are exposed in the page and can be called from the init script. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [`method: Page.exposeFunction`], so it is technically accessible from all frames and worlds of the page. Unlike functions passed to [`method: Page.evaluate`], functions passed to an init script are exposed in every new document, so they survive navigations. Defaults to `false`, in which case functions are not serializable and are silently dropped.
## js-evalonselector-pagefunction
* langs: js
- `pageFunction` <[function]\([Element]\)|[string]>
+7 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
import { parseEvaluationResultValue, serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
import type { SerializedValue } from '@isomorphic/utilityScriptSerializers';
@@ -68,6 +68,12 @@ export class BindingsController {
return promise;
}
parseInitScriptArg(value: SerializedValue): any {
// Functions serialized as { fn } deserialize into wrappers
// that route the call through this controller.
return parseEvaluationResultValue(value);
}
removeBinding(bindingName: string) {
const data = this._bindings.get(bindingName);
if (data)
@@ -39,7 +39,7 @@ export type SerializedValue =
{ ta: { b: string, k: TypedArrayKind } } |
{ ab: { b: string } };
type HandleOrValue = { h: number } | { fallThrough: any };
type HandleOrValue = { h: number } | { fn: string } | { fallThrough: any };
type VisitorInfo = {
visited: Map<object, number>;
+15 -12
View File
@@ -314,16 +314,17 @@ export interface Page {
* ```
*
* **NOTE** The order of evaluation of multiple scripts installed via
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined.
* [browserContext.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script)
* is not defined.
*
* @param script Script to be evaluated in the page.
* @param arg Optional argument to pass to
* [`script`](https://playwright.dev/docs/api/class-page#page-add-init-script-option-script) (only supported when
* passing a function).
* @param options
*/
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise<Disposable>;
/**
* **NOTE** Use locator-based [page.locator(selector[, options])](https://playwright.dev/docs/api/class-page#page-locator)
@@ -9090,16 +9091,17 @@ export interface BrowserContext {
* ```
*
* **NOTE** The order of evaluation of multiple scripts installed via
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined.
* [browserContext.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script)
* is not defined.
*
* @param script Script to be evaluated in all pages in the browser context.
* @param arg Optional argument to pass to
* [`script`](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script-option-script)
* (only supported when passing a function).
* @param options
*/
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise<Disposable>;
/**
* Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
@@ -20795,14 +20797,15 @@ export interface Dialog {
/**
* [Disposable](https://playwright.dev/docs/api/class-disposable) is returned from various methods to allow undoing
* the corresponding action. For example,
* [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) returns a
* [Disposable](https://playwright.dev/docs/api/class-disposable) that can be used to remove the init script.
* [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script)
* returns a [Disposable](https://playwright.dev/docs/api/class-disposable) that can be used to remove the init
* script.
*/
export interface Disposable {
/**
* Removes the associated resource. For example, removes the init script installed via
* [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) or
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script).
* [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script) or
* [browserContext.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script).
*/
dispose(): Promise<void>;
@@ -37,8 +37,9 @@ import { Events } from './events';
import { APIRequestContext } from './fetch';
import { Frame } from './frame';
import { HarRouter } from './harRouter';
import { assertEvaluateOptions } from './jsHandle';
import * as network from './network';
import { BindingCall, Page } from './page';
import { BindingCall, Page, addInitScriptWithExposedFunctions } from './page';
import { Tracing } from './tracing';
import { Waiter } from './waiter';
import { WebError } from './webError';
@@ -46,6 +47,7 @@ import { Worker } from './worker';
import { TimeoutSettings, kNoTimeout } from './timeoutSettings';
import { mkdirIfNeeded } from './fileUtils';
import type { EvaluateOptions } from './jsHandle';
import type { BrowserContextOptions, Headers, SetStorageState, StorageState, WaitForEventOptions } from './types';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
@@ -358,7 +360,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.setHTTPCredentials({ httpCredentials: httpCredentials || undefined }, kNoTimeout);
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any, options?: EvaluateOptions) {
assertEvaluateOptions(options);
if (options?.exposeFunctions)
return await addInitScriptWithExposedFunctions(this, script, arg);
const source = await evaluationScript(script, arg);
return DisposableObject.from((await this._channel.addInitScript({ source }, kNoTimeout)).disposable);
}
@@ -369,6 +374,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
return DisposableObject.from(result.disposable);
}
async _exposeCallbackBinding(name: string, callback: Function): Promise<DisposableObject> {
this._bindings.set(name, (source, ...args) => callback(...args));
const result = await this._channel.exposeBinding({ name, noGlobal: true }, kNoTimeout);
return DisposableObject.from(result.disposable);
}
async exposeFunction(name: string, callback: Function): Promise<DisposableObject> {
const result = await this._channel.exposeBinding({ name }, kNoTimeout);
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
+2 -1
View File
@@ -1425,9 +1425,10 @@ export type BrowserContextCookiesResult = {
};
export type BrowserContextExposeBindingParams = {
name: string,
noGlobal?: boolean,
};
export type BrowserContextExposeBindingOptions = {
noGlobal?: boolean,
};
export type BrowserContextExposeBindingResult = {
disposable: DisposableChannel,
@@ -18,6 +18,8 @@
import fs from 'fs';
import { isString } from '@isomorphic/rtti';
import { kBindingsControllerProperty, kFunctionBindingPrefix, serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers';
import { createGuid } from '@utils/crypto';
export function envObjectToArray(env: NodeJS.ProcessEnv): { name: string, value: string }[] {
const result: { name: string, value: string }[] = [];
@@ -49,6 +51,22 @@ export async function evaluationScript(fun: Function | string | { path?: string,
throw new Error('Either path or content property must be present');
}
export async function initScriptSourceWithExposedFunctions(fun: Function, arg: any, expose: (name: string, callback: Function) => Promise<void>): Promise<string> {
const exposePromises: Promise<void>[] = [];
const serialized = serializeAsCallArgument(arg, value => {
if (typeof value === 'function') {
const name = kFunctionBindingPrefix + createGuid();
exposePromises.push(expose(name, value));
return { fn: name };
}
return { fallThrough: value };
});
await Promise.all(exposePromises);
// Bindings backing the functions are registered through their own init scripts
// that are guaranteed to run first, so the controller is available here.
return `(${fun.toString()})(globalThis['${kBindingsControllerProperty}'].parseInitScriptArg(${JSON.stringify(serialized)}))`;
}
export function addSourceUrlToScript(source: string, path: string): string {
return `${source}\n//# sourceURL=${path.replace(/\n/g, '')}`;
}
+33 -5
View File
@@ -28,7 +28,7 @@ import { LongStandingScope } from '@isomorphic/manualPromise';
import { isObject, isRegExp, isString } from '@isomorphic/rtti';
import { Artifact } from './artifact';
import { ChannelOwner } from './channelOwner';
import { evaluationScript } from './clientHelper';
import { evaluationScript, initScriptSourceWithExposedFunctions } from './clientHelper';
import { Coverage } from './coverage';
import { DisposableObject, DisposableStub } from './disposable';
import { Download } from './download';
@@ -40,7 +40,7 @@ import { Frame, verifyLoadState } from './frame';
import { HarRouter } from './harRouter';
import { Keyboard, Mouse, Touchscreen } from './input';
import { WebStorage } from './webStorage';
import { assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { assertEvaluateOptions, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { Request, Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
import { Video } from './video';
import { Screencast } from './screencast';
@@ -376,10 +376,15 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return DisposableObject.from(result.disposable);
}
async _exposeEvaluateCallback(name: string, callback: Function) {
async _exposeCallbackBinding(name: string, callback: Function): Promise<DisposableObject> {
this._bindings.set(name, (source, ...args) => callback(...args));
const result = await this._channel.exposeBinding({ name, noGlobal: true }, kNoTimeout);
this._evaluateCallbacks.push({ name, disposable: DisposableObject.from(result.disposable) });
return DisposableObject.from(result.disposable);
}
async _exposeEvaluateCallback(name: string, callback: Function) {
const disposable = await this._exposeCallbackBinding(name, callback);
this._evaluateCallbacks.push({ name, disposable });
}
_eraseEvaluateCallbacks() {
@@ -549,7 +554,10 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return await this._mainFrame.evaluate(pageFunction, arg, options);
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any, options?: EvaluateOptions) {
assertEvaluateOptions(options);
if (options?.exposeFunctions)
return await addInitScriptWithExposedFunctions(this, script, arg);
const source = await evaluationScript(script, arg);
return DisposableObject.from((await this._channel.addInitScript({ source }, kNoTimeout)).disposable);
}
@@ -947,3 +955,23 @@ function trimUrl(param: any): string | undefined {
if (isString(param))
return `"${trimStringWithEllipsis(param, 50)}"`;
}
export async function addInitScriptWithExposedFunctions(owner: Page | BrowserContext, script: Function | string | { path?: string, content?: string }, arg: any): Promise<DisposableStub> {
if (typeof script !== 'function')
throw new Error('Passing functions requires the init script to be a function');
const callbacks: { name: string, disposable: DisposableObject }[] = [];
const source = await owner._wrapApiCall(async () => {
return await initScriptSourceWithExposedFunctions(script, arg, async (name, callback) => {
const disposable = await owner._exposeCallbackBinding(name, callback);
callbacks.push({ name, disposable });
});
}, { internal: true });
const initScriptDisposable = DisposableObject.from((await owner._channel.addInitScript({ source }, kNoTimeout)).disposable);
return new DisposableStub(async () => {
for (const { name, disposable } of callbacks) {
owner._bindings.delete(name);
disposable.dispose().catch(() => {});
}
await initScriptDisposable.dispose();
});
}
@@ -374,7 +374,7 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
return this._playwrightBindingExposed !== undefined;
}
async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource, forClient?: unknown): Promise<PageBinding> {
async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource, forClient?: unknown, noGlobal?: boolean): Promise<PageBinding> {
if (this._pageBindings.has(name))
throw new Error(`Function "${name}" has been already registered`);
for (const page of this.pages()) {
@@ -382,7 +382,7 @@ export abstract class BrowserContext<EM extends EventMap = EventMap> extends Sdk
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);
const binding = new PageBinding(this, name, playwrightBinding, noGlobal);
binding.forClient = forClient;
this._pageBindings.set(name, binding);
try {
+2 -1
View File
@@ -1426,9 +1426,10 @@ export type BrowserContextCookiesResult = {
};
export type BrowserContextExposeBindingParams = {
name: string,
noGlobal?: boolean,
};
export type BrowserContextExposeBindingOptions = {
noGlobal?: boolean,
};
export type BrowserContextExposeBindingResult = {
disposable: DisposableChannel,
@@ -253,7 +253,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const binding = new BindingCallDispatcher(pageDispatcher, params.name, source, args);
this._dispatchEvent('bindingCall', { binding });
return binding.promise();
});
}, undefined, params.noGlobal);
this._disposables.push(binding);
return { disposable: new DisposableDispatcher(this, binding) };
}
+15 -12
View File
@@ -314,16 +314,17 @@ export interface Page {
* ```
*
* **NOTE** The order of evaluation of multiple scripts installed via
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined.
* [browserContext.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script)
* is not defined.
*
* @param script Script to be evaluated in the page.
* @param arg Optional argument to pass to
* [`script`](https://playwright.dev/docs/api/class-page#page-add-init-script-option-script) (only supported when
* passing a function).
* @param options
*/
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise<Disposable>;
/**
* **NOTE** Use locator-based [page.locator(selector[, options])](https://playwright.dev/docs/api/class-page#page-locator)
@@ -9090,16 +9091,17 @@ export interface BrowserContext {
* ```
*
* **NOTE** The order of evaluation of multiple scripts installed via
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined.
* [browserContext.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script)
* is not defined.
*
* @param script Script to be evaluated in all pages in the browser context.
* @param arg Optional argument to pass to
* [`script`](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script-option-script)
* (only supported when passing a function).
* @param options
*/
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise<Disposable>;
/**
* Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
@@ -20795,14 +20797,15 @@ export interface Dialog {
/**
* [Disposable](https://playwright.dev/docs/api/class-disposable) is returned from various methods to allow undoing
* the corresponding action. For example,
* [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) returns a
* [Disposable](https://playwright.dev/docs/api/class-disposable) that can be used to remove the init script.
* [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script)
* returns a [Disposable](https://playwright.dev/docs/api/class-disposable) that can be used to remove the init
* script.
*/
export interface Disposable {
/**
* Removes the associated resource. For example, removes the init script installed via
* [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) or
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script).
* [page.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-page#page-add-init-script) or
* [browserContext.addInitScript(script[, arg, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script).
*/
dispose(): Promise<void>;
@@ -84,6 +84,7 @@ BrowserContext:
group: configuration
parameters:
name: string
noGlobal: boolean?
returns:
disposable: Disposable
+1
View File
@@ -773,6 +773,7 @@ scheme.BrowserContextCookiesResult = tObject({
});
scheme.BrowserContextExposeBindingParams = tObject({
name: tString,
noGlobal: tOptional(tBoolean),
});
scheme.BrowserContextExposeBindingResult = tObject({
disposable: tChannel(['Disposable']),
@@ -86,6 +86,54 @@ it('should remove context init script and keep working in new pages', async ({ c
expect(await page.evaluate(() => (window as any)['temp'])).toBe(undefined);
});
it('should expose functions passed as arguments', async ({ context, server }) => {
const received: string[] = [];
await context.addInitScript(async ({ cb }) => {
await cb(location.href);
}, { cb: async (href: string) => { received.push(href); } }, { exposeFunctions: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => received).toContain(server.EMPTY_PAGE);
});
it('should expose functions that survive navigation', async ({ context, server }) => {
const received: number[] = [];
await context.addInitScript(({ cb }) => {
(window as any).cb = cb;
}, { cb: (n: number) => { received.push(n); return n * 2; } }, { exposeFunctions: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => (window as any).cb(1))).toBe(2);
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
expect(await page.evaluate(() => (window as any).cb(2))).toBe(4);
expect(received).toEqual([1, 2]);
});
it('should expose functions in popups', async ({ context, server }) => {
await context.addInitScript(({ mul }) => {
(window as any).mul = mul;
}, { mul: (a: number, b: number) => a * b }, { exposeFunctions: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => window.open('about:blank')),
]);
expect(await popup.evaluate(() => (window as any).mul(6, 7))).toBe(42);
});
it('should remove exposed functions after dispose', async ({ context, server }) => {
const disposable = await context.addInitScript(({ cb }) => {
(window as any).cb = cb;
}, { cb: (n: number) => n * 2 }, { exposeFunctions: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => (window as any).cb(21))).toBe(42);
await disposable.dispose();
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => typeof (window as any).cb)).toBe('undefined');
});
it('init script should run only once in popup', async ({ context }) => {
await context.addInitScript(() => {
window['callCount'] = (window['callCount'] || 0) + 1;
@@ -0,0 +1,133 @@
/**
* 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 { test as it, expect } from './pageTest';
it('should drop functions without the exposeFunctions option', async ({ page, server }) => {
await page.addInitScript(({ cb }) => {
(window as any).cbType = typeof cb;
}, { cb: () => {} });
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => (window as any).cbType)).toBe('undefined');
});
it('should throw when the script is not a function', async ({ page }) => {
await expect(page.addInitScript({ content: 'window.foo = 1;' }, undefined, { exposeFunctions: true }))
.rejects.toThrow('Passing functions requires the init script to be a function');
});
it('should call a function passed as an argument', async ({ page, server }) => {
const received: number[] = [];
await page.addInitScript(async ({ cb }) => {
await cb(1);
await cb(2);
}, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => received).toEqual([1, 2]);
});
it('should accept a function as the whole argument', async ({ page, server }) => {
await page.addInitScript(async cb => {
(window as any).result = await cb('a');
}, async (s: string) => s + 'b', { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.evaluate(() => (window as any).result)).toBe('ab');
});
it('should pass arguments to the callback', async ({ page, server }) => {
const argsPromise = new Promise<any[]>(resolve => {
void page.addInitScript(({ cb }) => cb(1, 'two', { three: 3 }, [4]), {
cb: (...a: any[]) => resolve(a),
}, { exposeFunctions: true }).then(() => page.goto(server.EMPTY_PAGE));
});
expect(await argsPromise).toEqual([1, 'two', { three: 3 }, [4]]);
});
it('should return the callback result to the page', async ({ page, server }) => {
await page.addInitScript(async ({ double }) => {
(window as any).result = await double(21);
}, { double: async (n: number) => n * 2 }, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.evaluate(() => (window as any).result)).toBe(42);
});
it('should propagate callback errors to the page', async ({ page, server }) => {
await page.addInitScript(async ({ cb }) => {
try {
await cb();
(window as any).result = 'no error';
} catch (e) {
(window as any).result = (e as Error).message;
}
}, { cb: async () => { throw new Error('boom'); } }, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.evaluate(() => (window as any).result)).toContain('boom');
});
it('should support multiple callbacks', async ({ page, server }) => {
await page.addInitScript(async ({ add, mul }) => {
(window as any).result = (await add(2, 3)) + (await mul(2, 3));
}, {
add: async (a: number, b: number) => a + b,
mul: async (a: number, b: number) => a * b,
}, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.evaluate(() => (window as any).result)).toBe(11);
});
it('should survive a navigation and keep working', async ({ page, server }) => {
const received: number[] = [];
await page.addInitScript(({ cb }) => {
(window as any).cb = cb;
}, { cb: (n: number) => { received.push(n); return n * 2; } }, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => (window as any).cb(1))).toBe(2);
await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
expect(await page.evaluate(() => (window as any).cb(2))).toBe(4);
expect(received).toEqual([1, 2]);
});
it('should work in a child frame', async ({ page, server }) => {
const received: number[] = [];
await page.addInitScript(async ({ cb }) => {
await cb(42);
}, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true });
await page.goto(server.PREFIX + '/frames/one-frame.html');
// Once for the main frame document, once for the child frame document.
await expect.poll(() => received).toEqual([42, 42]);
});
it('should not register the callback on the global object', async ({ page, server }) => {
await page.addInitScript(async ({ cb }) => {
await cb();
(window as any).result = Object.getOwnPropertyNames(globalThis).filter(name => name.startsWith('__pw_fn_'));
}, { cb: async () => {} }, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await expect.poll(() => page.evaluate(() => (window as any).result)).toEqual([]);
});
it('should remove exposed functions after dispose', async ({ page, server }) => {
const received: number[] = [];
const disposable = await page.addInitScript(({ cb }) => {
(window as any).cb = cb;
}, { cb: (n: number) => { received.push(n); } }, { exposeFunctions: true });
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => (window as any).cb(1));
await disposable.dispose();
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => typeof (window as any).cb)).toBe('undefined');
expect(received).toEqual([1]);
});
+2 -2
View File
@@ -44,7 +44,7 @@ export interface Page {
evaluateHandle<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg, options?: { exposeFunctions?: boolean }): Promise<SmartHandle<R>>;
evaluateHandle<R>(pageFunction: PageFunction<void, R>, arg?: any, options?: { exposeFunctions?: boolean }): Promise<SmartHandle<R>>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise<Disposable>;
$<K extends keyof HTMLElementTagNameMap>(selector: K, options?: { strict: boolean }): Promise<ElementHandleForTag<K> | null>;
$(selector: string, options?: { strict: boolean }): Promise<ElementHandle<SVGElement | HTMLElement> | null>;
@@ -119,7 +119,7 @@ export interface Frame {
export interface BrowserContext {
exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<Disposable>;
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise<Disposable>;
removeAllListeners(type?: string): this;
removeAllListeners(type: string | undefined, options: {