mirror of
https://github.com/hanzoai/playwright.git
synced 2026-09-24 17:01:53 +00:00
feat(addInitScript): allow passing functions as init script arguments (#41921)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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]>
|
||||
|
||||
@@ -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]>
|
||||
|
||||
@@ -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
@@ -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
@@ -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, '')}`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
Vendored
+2
-2
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user