Implementation:Microsoft Playwright Page AddInitScript
| Knowledge Sources | |
|---|---|
| Domains | Network_Testing, Mocking, Browser_API_Injection |
| Last Updated | 2026-02-11 00:00 GMT |
Overview
Concrete tool for injecting JavaScript into the browser context to mock or override browser APIs before page scripts execute, provided by the Playwright library.
Description
Playwright provides three complementary methods for injecting test-controlled code into the browser environment:
page.addInitScript() (packages/playwright-core/src/client/page.ts:L501-504) registers a script that runs in every new document context created within the page. The script executes before any page-authored JavaScript, making it ideal for overriding browser APIs. The method accepts a function, a string of JavaScript code, or an object with path or content properties. An optional arg parameter can be passed and is serialized into the script context. Internally, the script is converted to a source string via evaluationScript() from clientHelper and sent to the browser through the channel protocol.
page.exposeFunction() (packages/playwright-core/src/client/page.ts:L327-331) adds a function to the browser's window object that, when called from page scripts, bridges to a Node.js callback in the test process. The function name is registered via the channel's exposeBinding command, and the callback is stored in the page's _bindings map. When the browser-side function is called, arguments are serialized, sent to Node.js, the callback executes, and the result is returned to the browser.
page.exposeBinding() (packages/playwright-core/src/client/page.ts:L333-336) is similar to exposeFunction() but provides additional context to the callback. The first argument received by the callback is a source object containing { context, page, frame }, allowing the handler to know which frame triggered the call. The options.handle parameter, when set to true, passes a JSHandle instead of serialized values, enabling manipulation of browser-side objects directly.
The BindingCall class (packages/playwright-core/src/client/page.ts:L864-891) manages the cross-boundary function invocation, handling argument deserialization via parseResult(), result serialization via serializeArgument(), and error propagation via serializeError().
Usage
Use these methods when:
- You need to mock
navigator.geolocation,navigator.permissions, or other non-HTTP browser APIs. - You want to freeze or control time with
Date.now()overrides. - You need to inject feature flags or configuration before application code reads them.
- You want to set up
window.fetchorXMLHttpRequestinterceptors at the JavaScript level. - You need to bridge browser events to the test process for assertion (via
exposeFunction). - You want to inject a mock service worker registration script.
Code Reference
Source Location
- Repository: playwright
- addInitScript:
packages/playwright-core/src/client/page.ts:L501-504 - exposeFunction:
packages/playwright-core/src/client/page.ts:L327-331 - exposeBinding:
packages/playwright-core/src/client/page.ts:L333-336 - BindingCall:
packages/playwright-core/src/client/page.ts:L864-891
Signature
// Add an initialization script
page.addInitScript(
script: Function | string | { path?: string; content?: string },
arg?: any
): Promise<void>;
// Expose a function to the browser window object
page.exposeFunction(
name: string,
callback: Function
): Promise<void>;
// Expose a binding with source context
page.exposeBinding(
name: string,
callback: (source: { context: BrowserContext; page: Page; frame: Frame }, ...args: any[]) => any,
options?: { handle?: boolean }
): Promise<void>;
Import
// Playwright Test (recommended)
import { test, expect } from '@playwright/test';
// Library mode
import { chromium } from 'playwright';
I/O Contract
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
| script (addInitScript) | string | { path?, content? } | Yes | JavaScript to evaluate in every new document. Functions are serialized; strings are evaluated directly; objects specify a file path or inline content. |
| arg (addInitScript) | any |
No | Argument passed to the script function. Must be JSON-serializable. |
| name (exposeFunction) | string |
Yes | The name of the function to add to window. Must not conflict with existing properties.
|
| callback (exposeFunction) | Function |
Yes | Node.js function to invoke when the browser-side function is called. Arguments are deserialized from the browser context. |
| name (exposeBinding) | string |
Yes | The name of the function to add to window.
|
| callback (exposeBinding) | (source, ...args) => any |
Yes | Node.js function receiving a source object ({ context, page, frame }) as the first argument, followed by deserialized browser arguments.
|
| options.handle (exposeBinding) | boolean |
No | When true, the callback receives a JSHandle instead of serialized argument values. Useful for complex or non-serializable browser objects.
|
Outputs
| Name | Type | Description |
|---|---|---|
| addInitScript return | Promise<void> |
Resolves when the init script is registered. The script will execute in every subsequent document context. |
| exposeFunction return | Promise<void> |
Resolves when the function is exposed. window[name] becomes callable from browser code and bridges to the Node.js callback.
|
| exposeBinding return | Promise<void> |
Resolves when the binding is exposed. Same as exposeFunction but callback receives source context. |
| Browser-side function return | Promise<any> |
When the exposed function is called from browser code, it returns a Promise that resolves with the serialized return value of the Node.js callback. |
Usage Examples
Basic Example: Mock Geolocation
import { test, expect } from '@playwright/test';
test('mock geolocation API', async ({ page }) => {
// Override navigator.geolocation before page scripts run
await page.addInitScript(() => {
const mockPosition = {
coords: {
latitude: 48.8566,
longitude: 2.3522,
accuracy: 100,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
},
timestamp: Date.now(),
};
navigator.geolocation.getCurrentPosition = (success) => {
success(mockPosition);
};
navigator.geolocation.watchPosition = (success) => {
success(mockPosition);
return 1;
};
});
await page.goto('https://example.com/map');
// Page will see Paris coordinates
});
Mock with Parameters
import { test, expect } from '@playwright/test';
test('inject feature flags via init script', async ({ page }) => {
const featureFlags = {
newDashboard: true,
darkMode: false,
betaFeatures: true,
};
await page.addInitScript((flags) => {
window.__FEATURE_FLAGS__ = flags;
}, featureFlags);
await page.goto('https://example.com');
// Application code can read window.__FEATURE_FLAGS__
});
Expose Function: Bridge Browser to Node.js
import { test, expect } from '@playwright/test';
test('bridge browser analytics to test', async ({ page }) => {
const analyticsEvents: Array<{ name: string; data: any }> = [];
// Expose a function that page code can call
await page.exposeFunction('__trackEvent', (name: string, data: any) => {
analyticsEvents.push({ name, data });
});
// Override the analytics library to use our bridge
await page.addInitScript(() => {
window.analytics = {
track: (name: string, data: any) => {
(window as any).__trackEvent(name, data);
},
};
});
await page.goto('https://example.com');
await page.click('#purchase-button');
// Assert analytics events were fired
expect(analyticsEvents).toContainEqual(
expect.objectContaining({ name: 'purchase_clicked' })
);
});
Expose Binding with Source Context
import { test, expect } from '@playwright/test';
test('expose binding with frame context', async ({ page }) => {
await page.exposeBinding('getFrameUrl', (source) => {
// source.frame tells us which frame called this function
return source.frame.url();
});
await page.addInitScript(() => {
// Page scripts can call this to get their frame URL from Node.js
(window as any).getFrameUrl().then((url: string) => {
document.title = `Frame: ${url}`;
});
});
await page.goto('https://example.com');
});
Mock Date.now for Deterministic Tests
import { test, expect } from '@playwright/test';
test('freeze time for deterministic testing', async ({ page }) => {
const frozenTime = new Date('2025-01-15T10:00:00Z').getTime();
await page.addInitScript((timestamp) => {
const OriginalDate = Date;
const MockDate = class extends OriginalDate {
constructor(...args: any[]) {
if (args.length === 0) {
super(timestamp);
} else {
// @ts-ignore
super(...args);
}
}
static now() { return timestamp; }
};
(window as any).Date = MockDate;
}, frozenTime);
await page.goto('https://example.com');
const displayedDate = await page.textContent('.current-date');
expect(displayedDate).toContain('January 15, 2025');
});