Principle:Microsoft Playwright Inject Browser API Mocks
| Knowledge Sources | |
|---|---|
| Domains | Network_Testing, Mocking, Browser_API_Injection |
| Last Updated | 2026-02-11 00:00 GMT |
Overview
Injecting JavaScript into the browser context to mock or override browser APIs before page scripts execute.
Description
While network route handlers intercept HTTP traffic at the protocol level, many testing scenarios require mocking at the browser API level. Browser APIs such as geolocation, notifications, battery status, media devices, WebRTC, File API, and others cannot be intercepted via network routes because they are built-in browser interfaces, not HTTP endpoints.
Inject Browser API Mocks is the principle of using script injection to replace or augment browser-provided JavaScript APIs with test-controlled implementations. The critical requirement is that mock scripts must execute before any page scripts run, ensuring that the application always sees the mocked API rather than the real one.
This principle covers two complementary approaches:
- Init scripts: JavaScript code injected into every document context (including iframes and navigations) that runs before any page-authored scripts. Init scripts operate entirely within the browser's JavaScript environment and are ideal for overriding globals like
navigator.geolocation,Date.now(), orwindow.fetch.
- Exposed functions: Bridge functions that connect the browser's JavaScript environment to the test process (e.g., Node.js). When called from browser code, these functions serialize their arguments, send them to the test process, execute a callback there, and return the result to the browser. This enables mocks that depend on test-side state or logic.
The key characteristics of effective browser API mocking:
- Timing guarantee: Mocks are installed before any page code executes.
- Scope isolation: Mocks apply only to the test's browser context, not to Playwright's internal operations.
- Persistence across navigation: Init scripts re-execute on every page load and navigation.
- Transparency: The application cannot distinguish mocked APIs from real ones when mocks are well-implemented.
This principle is library-agnostic. The concept of script injection for API mocking applies to any browser automation tool, including Puppeteer's evaluateOnNewDocument, Cypress's cy.stub()/cy.clock(), and Selenium's executeScript.
Usage
Apply this principle when:
- You need to mock browser APIs that are not HTTP-based (geolocation, permissions, media devices).
- You want to control time-related APIs (
Date.now(),performance.now(),setTimeout) for deterministic testing. - You need to override
window.fetchorXMLHttpRequestat the JavaScript level rather than the protocol level. - You want to inject polyfills or shims for APIs not supported in the test browser.
- You need to set up global test state accessible to page scripts (feature flags, configuration).
- You want to bridge browser-side events to test-side assertions via exposed functions.
Theoretical Basis
Browser API mocking relies on JavaScript's prototype chain and property descriptor mechanisms. Most browser APIs are properties of global objects (window, navigator, document) that can be redefined:
// Mocking pattern: Override a read-only browser API
Object.defineProperty(navigator, 'languages', {
get: function() { return ['fr-FR', 'fr']; }
});
// Mocking pattern: Replace a global function
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (shouldMock(url)) {
return Promise.resolve(new Response(mockBody, { status: 200 }));
}
return originalFetch.call(window, url, options);
};
The script injection lifecycle:
1. Browser creates new document context (navigation, iframe, popup)
2. INIT SCRIPTS EXECUTE (before any page-authored scripts)
-> Override global APIs
-> Install mock implementations
-> Register exposed function bridges
3. Page scripts execute (see mocked APIs)
4. Page scripts call mocked APIs (receive mock responses)
5. [If exposed functions used] Calls bridge to test process
-> Serialize arguments
-> Send to test runner (Node.js)
-> Execute callback
-> Return result to browser
The exposed function bridge pattern:
// Test-side (Node.js):
expose("mockGeolocation", () => {
return { latitude: 48.8566, longitude: 2.3522 }; // Paris
})
// Browser-side (automatic):
window.mockGeolocation() // Calls Node.js, returns { latitude: 48.8566, ... }
Init scripts and exposed functions are complementary: init scripts are best for pure browser-side mocking (replacing APIs with static or computed values), while exposed functions are best for mocks that need test-side state or complex logic.