When an API Is Frozen, Mock the Transport Layer Instead
Some runtimes inject high-level APIs into the page and freeze them before your code runs. When that happens, monkey-patching the API directly will not work, even in tests.
The practical fallback is to drop one level lower: find the transport the API uses internally and intercept that instead.
The symptom
Your test or automation code tries to wrap a runtime API:
const originalInvoke = window.__APP_INTERNALS__.invoke;
window.__APP_INTERNALS__.invoke = async (command, args) => {
if (command === "load_projects") {
return [{ id: "p1", name: "Example" }];
}
return originalInvoke(command, args);
};
But calls still go to the real backend. Or Object.defineProperty throws. Or the assignment appears to succeed but has no effect.
Check the descriptor:
console.log(
Object.getOwnPropertyDescriptor(window.__APP_INTERNALS__, "invoke")
);
If it says writable: false and configurable: false, that property is frozen. You cannot replace it at that layer.
Find the transport
Most high-level APIs eventually use a lower-level transport:
runtime.invoke(command, args)
-> fetch(...)
runtime.send(message)
-> postMessage(...)
client.call(payload)
-> WebSocket.send(...)
Use DevTools, source inspection, or a temporary network logger to confirm the transport.
For example, an IPC-style API might send requests through fetch() to an internal URL:
http://ipc.localhost/load_projects
If the high-level API is frozen but fetch is still mutable, intercept fetch.
Intercept fetch with fallthrough
Keep the mock narrow and pass through everything else:
const originalFetch = window.fetch.bind(window);
window.fetch = async (input, init) => {
const url = typeof input === "string" ? input : input.url;
if (url.startsWith("http://ipc.localhost/load_projects")) {
return new Response(
JSON.stringify([{ id: "p1", name: "Example" }]),
{
status: 200,
headers: { "content-type": "application/json" },
}
);
}
return originalFetch(input, init);
};
The fallthrough is important. Tests should mock only the command under test and preserve real behavior for unrelated requests.
Install the interception early
Transport interception often needs to happen before the app code initializes. In browser automation, use a preload or init script:
await page.addInitScript(() => {
const originalFetch = window.fetch.bind(window);
window.fetch = async (input, init) => {
const url = typeof input === "string" ? input : input.url;
if (url.startsWith("http://ipc.localhost/load_projects")) {
return new Response(JSON.stringify([]), {
status: 200,
headers: { "content-type": "application/json" },
});
}
return originalFetch(input, init);
};
});
If the app captures the original transport before your mock is installed, later replacement may not affect the call path.
Decision checklist
Use this sequence:
- Confirm the high-level API is actually frozen with
Object.getOwnPropertyDescriptor. - Identify the real transport:
fetch,postMessage, WebSocket, custom protocol, or native bridge. - Confirm the transport can be wrapped in the test context.
- Install the wrapper before app initialization.
- Mock only the target route or message.
- Pass through all unknown requests to the original transport.
- Add logging during test development, then remove noisy logs from final tests.
Limits
Transport-layer mocking is powerful, but it is not a license to hide integration bugs.
Use it when:
- the runtime blocks high-level API replacement
- the test target is frontend behavior
- you need deterministic backend responses
- the real backend is unavailable in CI
Prefer real integration when:
- the transport protocol itself is what you are testing
- auth, permissions, or security policy is the subject
- request serialization is the suspected bug
References
Summary
When a runtime freezes a high-level API, direct monkey-patching is the wrong layer. Confirm the freeze, find the transport, intercept narrowly, and preserve fallthrough. That gives you deterministic tests without fighting the runtime's security model.