PRACTICAL GUIDE / Playwright addInitScript ordering

Stop depending on Playwright init-script order

Remove flaky dependencies between Playwright init scripts, prove when each document is patched, and choose the right scope for pages, frames, and popups.

By The Testing AcademyUpdated August 4, 202623 min read
All field guides
In this guide6 sections
  1. Know which sequence Playwright actually guarantees
  2. Replace dependencies with one synchronous bootstrap
  3. Make timing and scope visible in the failure
  4. Distinguish ordering bugs from nearby lifecycle bugs
  5. Move an existing suite to a deterministic fixture
  6. Pay for determinism only where it protects the product

What you will learn

  • Know which sequence Playwright actually guarantees
  • Replace dependencies with one synchronous bootstrap
  • Make timing and scope visible in the failure
  • Distinguish ordering bugs from nearby lifecycle bugs

A browser mock works locally, then CI reports that its configuration object is undefined. Both setup calls were awaited before navigation, and the trace shows them in the expected call order. The missing fact is that Playwright does not define the evaluation order of multiple context and page init scripts.

Know which sequence Playwright actually guarantees

That failure is not a race to solve with a delay. The scripts run in a new document before the application's scripts, but their order relative to one another is outside the API contract. Any dependency between them must be removed, composed into one initializer, or represented by a single generated preload file.

page.addInitScript() registers JavaScript for a page. Playwright evaluates it whenever that page navigates and whenever one of its child frames is attached or navigated. browserContext.addInitScript() has broader scope: it applies when a page is created in the context or navigated, and when child frames in those pages are attached or navigated. In both cases, the documented timing is after a document has been created and before that document's own scripts run.

That guarantee is strong enough to replace a browser API before application startup. It is also narrow. The documentation explicitly says the order of multiple scripts installed through the context and page methods is not defined. There is no exception for calls that were awaited in a particular order, calls made in the same hook, or a context registration followed by a page registration.

Awaiting the methods proves that Playwright accepted each registration before the test continued. It does not mean the first function was executed at registration time. Execution belongs to the lifecycle of a new document. When navigation creates that document, Playwright arranges for the registered functions to run before page scripts, but it does not promise which registered function goes first.

This difference is easy to miss because one browser build may produce the same order for months. A fixture registers a base object at context scope, then a test registers a page-level extension. Every local run happens to produce base followed by extension. Someone writes an assertion against the combined object, and the pattern spreads. A browser update, a Playwright update, or a different engine can expose the unsupported assumption without changing the test code.

The common broken shape is simple. Script A creates window.__qaConfig. Script B reads it and installs a mock. If B runs first, it either throws or falls back to defaults. Reversing the registrations is not a fix because either order remains allowed. Adding a timeout inside B is worse: page scripts are not obligated to wait for that timeout, so the application may capture the unmocked API first.

The guarantee is per document, not a once-per-test setup event. A reload creates a new document and runs the registered initializer again. A main-frame navigation does the same. Child frames receive execution in their own JavaScript environment when they attach or navigate. State written to one document's window does not become shared context storage. If a test sees the marker in the main frame, it has not proved that a child frame read the same object.

Scope also matters for new pages. A page-level initializer belongs to that Page and its child frames. A popup is another page, even though it was opened by the first one. A context-level initializer is the appropriate tool when every page in the context, including future popups, must start with the same browser patch. Choosing context scope solely to avoid thinking about ownership can be too broad, especially when third-party frames are present.

Registration timing is separate from evaluation order. Calling page.addInitScript() after the application has already loaded does not rewrite the current document's startup. The script will be available for a later navigation and for future child-frame events covered by the API. A test that starts passing only after page.reload() usually has a late-registration problem, not a mysterious order problem.

The function argument is another boundary. Playwright supports passing a serializable argument when the script is a function. Use it for seeds, feature states, and test identifiers. Do not let the browser function close over variables from the Node.js test process. The function is evaluated in the browser environment, where the test runner's lexical scope does not exist. Keeping all dependencies inside the function or its explicit argument also makes review much easier.

Replace dependencies with one synchronous bootstrap

When two setup operations require an order, put them in one init function. Normal statement order inside one function is under your control. Build the base state, validate it, install the dependent API, and publish a readiness marker before returning. The page then receives either the complete synchronous setup or a recorded setup failure, rather than whichever half happened to run first.

The marker should describe a product-relevant contract. A boolean called loaded says little when CI fails. A small object with a schema version, build identifier, status, and sanitized error gives the test something specific to assert and attach. Keep secrets out of it because page code can read values placed on the global object.

This complete example installs configuration and a small browser-facing feature client in one registration. The application page reads the client during its own startup script. The test would fail if the initializer were moved after navigation, if the published client disappeared, or if the enabled feature value changed.

TypeScript
// tests/deterministic-bootstrap.spec.ts
import { expect, type Page, test } from '@playwright/test';

type BootstrapInput = {
  buildId: string;
  features: Record<string, boolean>;
};

async function installBootstrap(page: Page, input: BootstrapInput) {
  await page.addInitScript((config: BootstrapInput) => {
    type Harness =
      | {
          buildId: string;
          status: 'ready';
          isEnabled(name: string): boolean;
        }
      | {
          buildId: string;
          status: 'failed';
          error: string;
        };

    const root = globalThis as typeof globalThis & {
      __qaHarness?: Harness;
    };

    if (root.__qaHarness?.buildId === config.buildId) return;

    const publish = (harness: Harness) => {
      Object.defineProperty(root, '__qaHarness', {
        configurable: true,
        writable: true,
        value: Object.freeze(harness),
      });
    };

    try {
      if (!config.buildId || !config.features) {
        throw new Error('bootstrap requires buildId and features');
      }
      const featureState = Object.freeze({ ...config.features });
      publish({
        buildId: config.buildId,
        status: 'ready',
        isEnabled: (name: string) => featureState[name] === true,
      });
    } catch (error) {
      publish({
        buildId: config.buildId || 'missing',
        status: 'failed',
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }, input);
}

test('application reads the complete bootstrap during startup', async ({ page }) => {
  await installBootstrap(page, {
    buildId: 'checkout-contract-v1',
    features: { expressCheckout: true },
  });

  const html = `
    <p id="variant"></p>
    <script>
      const harness = window.__qaHarness;
      document.querySelector('#variant').textContent =
        harness?.status === 'ready' && harness.isEnabled('expressCheckout')
          ? 'express'
          : 'standard';
    </script>
  `;
  await page.goto(`data:text/html,${encodeURIComponent(html)}`);

  await expect(page.locator('#variant')).toHaveText('express');
  await expect
    .poll(() =>
      page.evaluate(
        () =>
          (
            globalThis as typeof globalThis & {
              __qaHarness?: { buildId: string };
            }
          ).__qaHarness?.buildId,
      ),
    )
    .toBe('checkout-contract-v1');
});

The optional chaining in the application fixture is not what makes the test safe. It makes the failure observable as standard instead of stopping the sample page with an unrelated null-reference error. The assertion still fails if the setup is absent. In production code, follow the application's intended failure policy rather than silently defaulting a security-sensitive or billing-sensitive feature.

The guard handles accidental duplicate registration in the same document when the build identifier matches. It is not cross-navigation state. A navigation creates a fresh global object, so the bootstrap runs again and publishes a fresh harness. That is the desired behavior for a startup patch. If the initializer has effects outside the document, such as calling an external service, it no longer fits this simple model and should not assume exactly-once execution.

Keep the initializer synchronous. The documented promise is that the script is evaluated before the page's scripts; it is not a general application boot scheduler for arbitrary network work. If setup needs remote data, retrieve and validate that data in the test process first, reduce it to a serializable value, then pass it as the init-script argument. That gives the page a complete input without making its startup depend on an unobserved request.

Do not freeze or redefine more browser surface than the scenario needs. A non-writable global may prevent the application from performing a legitimate update and can turn a useful test double into a different browser contract. The example freezes its test-owned harness data but leaves the global property writable. If you replace a real Web API, preserve the parts of its shape and error behavior that the application relies on, and test a real-browser path elsewhere.

Composition has a maintenance cost. A single initializer can become a dumping ground for clock control, feature flags, analytics spies, storage seeds, and permission shims. Keep one bootstrap per coherent contract and expose explicit input. If two mocks have no dependency, they may remain separate, provided no assertion or product code depends on their relative order.

Make timing and scope visible in the failure

An observed order is useful forensic data, but it must not become the oracle. The following test registers one independent context marker and one independent page marker. Each script initializes the shared array if necessary and appends its own name. The assertion sorts a copy before comparing, so either evaluation order passes while a missing or duplicate marker fails.

TypeScript
// tests/init-script-presence.spec.ts
import { expect, test } from '@playwright/test';

test('installs both independent markers without asserting their order', async (
  { context, page },
  testInfo,
) => {
  await context.addInitScript(() => {
    const root = globalThis as typeof globalThis & { __qaMarks?: string[] };
    root.__qaMarks ??= [];
    root.__qaMarks.push('context');
  });

  await page.addInitScript(() => {
    const root = globalThis as typeof globalThis & { __qaMarks?: string[] };
    root.__qaMarks ??= [];
    root.__qaMarks.push('page');
  });

  await page.goto('data:text/html,<title>init marker check</title>');
  const observed = await page.evaluate(
    () =>
      (globalThis as typeof globalThis & { __qaMarks?: string[] }).__qaMarks ??
      [],
  );

  await testInfo.attach('init-script-markers.json', {
    body: Buffer.from(JSON.stringify({ observed }, null, 2)),
    contentType: 'application/json',
  });

  expect(observed).toHaveLength(2);
  expect([...observed].sort()).toEqual(['context', 'page']);
});

This oracle can fail for meaningful changes. Remove a registration and the length or membership check fails. Register one script twice and the length fails. Rename a marker and membership fails. Swapping the observed order does not fail because the API never promised that order. The attachment retains what this attempt did without presenting it as a future guarantee.

For a product mock, use a richer marker. Record a stable schema version, scope, and status. Avoid timestamps as pass criteria because they create comparisons with no product meaning. Avoid serializing functions or full configuration containing credentials. A marker is evidence for the test harness, not a general debug dump of the application.

Capture the marker immediately after navigation and before the action that depends on it. If it is absent at that checkpoint, the test should stop on a setup assertion. Continuing until a button times out throws away the most specific evidence. If it is present and the product later behaves incorrectly, the investigation can move to whether the application read a different realm, replaced the mocked property, or cached a reference before the expected patch.

Trace Viewer helps with lifecycle evidence. The action list can show whether navigation happened before the registration call in a particular attempt, while snapshots and source links show which product action failed. The marker attachment records what the new document actually received. A trace that happens to show context before page is not proof that the ordering is supported. It is one observation under an API that says the order is undefined.

Console logging can supplement the marker, but it is easy to overuse. An init script runs before page scripts, so a namespaced line containing the schema version and frame URL can help identify execution in an unexpected document. Logs from multiple frames interleave, and ordering in collected output can be affected by transport and reporting. Use a state assertion within the target frame as the decision point; use logs to find where to look.

Frame evidence must be gathered in the frame that owns the behavior. Reading window.__qaHarness from the main page tells you nothing about a checkout widget inside an iframe. Locate the relevant frame, evaluate its marker there, and attach its URL with the sanitized state. Cross-origin boundaries prevent the parent application's JavaScript from reading arbitrary child state, but Playwright can evaluate in the selected frame. The test should still respect the product's trust boundary and avoid injecting into a third party unless that is explicitly the scenario.

A setup record can distinguish five useful states. Missing means the initializer did not execute in this document or wrote somewhere else. Ready with the expected schema means bootstrap completed. Ready with an old schema points to a stale fixture or project. Failed with a captured message points inside the initializer. Ready initially but missing later suggests the application replaced the property or navigation created another document. Those categories lead to different fixes and should not be collapsed into “addInitScript flaky.”

Distinguish ordering bugs from nearby lifecycle bugs

Late registration produces the most similar symptom. The app sees the real API, the marker is absent, and a reload makes the test pass. Look at the trace or test source: if page.goto() precedes page.addInitScript(), move the registration before navigation. Do not keep the reload as a repair. It adds latency, exercises a different application path, and can hide startup behavior that occurs only on the first document.

DOM access is another common impostor. The init function runs after the document object exists but before the page's scripts. That does not mean the parser has built the body or the element your code wants. An initializer that calls document.querySelector('#app').append(...) may fail because the element is not present yet, even when there is only one init script. Use an init script for browser-environment patches. Use normal page synchronization, a locator assertion, or application code for work that requires parsed DOM.

Wrong scope looks like missing order when the main page and popup behave differently. A page-level patch correctly reaches the page and its child frames, but a newly opened popup is a separate page. If the popup needs the same startup environment, register the patch on the browser context before opening it. Evidence is straightforward: the main page marker is ready, the popup marker is absent, and both documents otherwise load. Adding a second page script after the popup opens repeats the late-registration mistake.

The inverse scope error is installing a context-level patch that should affect only one page. The script also runs in other pages and child frames covered by the context method. A third-party sign-in frame may see a modified global it never expected, or an administrative popup may receive a customer-specific feature state. If the failure appears outside the page under test and its marker reports the shared context patch, narrow the registration or use a separate context.

An iframe can reveal realm confusion. Both the main frame and child frame may have run the initializer, but they have separate global objects. The child does not extend the main frame's array, and an object identity comparison across the two is meaningless. Evaluate the schema and values independently in each target. If the product uses postMessage to transfer state, test that product protocol rather than expecting init-script globals to become shared memory.

Application overwrite is a different failure again. The marker is present just after navigation, but the real API or property reappears before the action. Search page scripts for assignments and capture the value at the product boundary. Making the test's property non-configurable can stop the overwrite, but that also changes behavior and may hide an application defect. Use a locked property only when immutability is part of the intended mock contract.

Cached references create a subtler variant. The initializer replaces a method correctly, but a module in an earlier document, worker, or different frame captured the original reference. Confirm which realm owns the call. The documented init-script scope covers pages and child frames; it does not promise to initialize service workers. If the behavior is executed by a service worker, do not keep rearranging page init scripts. Test or control that worker through an appropriate boundary, or disable it only when the scenario explicitly excludes service-worker behavior.

A malformed serialized argument is not ordering. The API accepts serializable input for a function script. Class instances, Node-only objects, and functions are poor inputs because the browser does not share the test process's object graph. Reduce data to strings, numbers, booleans, arrays, and plain records needed by the patch. Validate required fields inside the initializer and publish a failed status rather than allowing a downstream null error to become the first clue.

Repeated navigation can be mistaken for duplicate execution within one document. The marker's counter returns to one because the new document has a new global, yet external logs show several bootstrap messages over the test. Check navigation events and frame URLs. A login redirect, client-triggered full navigation, or popup can legitimately create multiple documents. If the test requires exactly one navigation, assert that product behavior separately instead of using init-script log count as a proxy.

Finally, two scripts that are genuinely independent may still produce nondeterministic log order with no product defect. Do not merge them solely to make a trace aesthetically consistent. Assert presence and individual outcomes. Composition is required when one consumes the other's state, when the application observes an intermediate state, or when review cannot establish independence.

Move an existing suite to a deterministic fixture

Begin with an inventory of registrations and consumers. Search context fixtures, page objects, beforeEach hooks, and individual specs. For every init script, write down its scope, the global or API it owns, its input, and any other setup it reads. A dependency is present even if it is expressed only as optional chaining with a default. Defaults can hide reversed order by producing a valid but wrong product path.

Shell
rg -n "addInitScript\(|__qa|window\.[A-Za-z0-9_]+\s*=" \
  playwright.config.ts tests

pnpm exec playwright test tests/init-script-presence.spec.ts \
  --project=chromium \
  --workers=1 \
  --trace=on

Freeze the browser-facing contract before refactoring. Write one focused test that loads a self-contained page whose first script reads the patched surface. Assert the application-visible result and the readiness marker. This case should fail if the initializer is absent or late. Do not assert the previous incidental order, even if the old implementation exposes it in an array.

Compose dependent operations in one function. If several teams own pieces, they can still supply plain configuration to a single bootstrap builder in the test code. The final function sent to the browser must contain everything it calls, because Node-side closures do not travel with it. Keep the public schema small enough that ownership is visible during review.

Choose page or context scope from the user journey. A mock needed only on a single document belongs on the page. A startup patch required by popups created during the test belongs on the context. If two pages need different values, they should usually live in different contexts or receive separate page registrations before their first navigation. One context-wide mutable global configuration is a source of cross-page confusion.

An automatic test fixture makes registration consistent without relying on every spec author to remember a hook. This fixture installs one context bootstrap before the test body can navigate. It passes a project identifier as plain data and publishes the same schema to every covered page and frame. Tests import this test object instead of the base one.

TypeScript
// tests/fixtures/qa-test.ts
import { expect, test as base } from '@playwright/test';

type Fixtures = {
  qaBootstrap: void;
};

export const test = base.extend<Fixtures>({
  qaBootstrap: [
    async ({ context }, use, testInfo) => {
      await context.addInitScript(
        (input: { project: string; schema: number }) => {
          const root = globalThis as typeof globalThis & {
            __qaBootstrap?: {
              project: string;
              schema: number;
              status: 'ready';
            };
          };

          root.__qaBootstrap ??= Object.freeze({
            project: input.project,
            schema: input.schema,
            status: 'ready' as const,
          });
        },
        { project: testInfo.project.name, schema: 1 },
      );

      await use();
    },
    { auto: true },
  ],
});

export { expect };

The fixture owns registration, not the product assertion. Each spec still checks the behavior it needs. A smoke test can additionally assert schema === 1 to catch an old import or fixture. Avoid placing dozens of product-specific flags in the automatic fixture, because every test and third-party frame in that context then runs under a modified environment.

Prove context scope with a popup case if popups are part of the contract. This test creates a link that opens a new page, waits for the context's page event, and reads the bootstrap from the popup after its document loads. If someone narrows the fixture to page.addInitScript(), the assertion has a clear failure path.

TypeScript
// tests/popup-bootstrap.spec.ts
import { expect, test } from './fixtures/qa-test';

test('installs the bootstrap in a newly opened popup', async (
  { context, page },
  testInfo,
) => {
  const html = `
    <a target="_blank" href="about:blank">Open child</a>
  `;
  await page.goto(`data:text/html,${encodeURIComponent(html)}`);

  const popupPromise = context.waitForEvent('page');
  await page.getByRole('link', { name: 'Open child' }).click();
  const popup = await popupPromise;
  await popup.waitForLoadState('domcontentloaded');

  const state = await popup.evaluate(
    () =>
      (
        globalThis as typeof globalThis & {
          __qaBootstrap?: {
            project: string;
            schema: number;
            status: 'ready';
          };
        }
      ).__qaBootstrap,
  );
  expect(state).toEqual({
    project: testInfo.project.name,
    schema: 1,
    status: 'ready',
  });
});

The project assertion follows testInfo.project.name, so the same contract remains valid in a multi-project suite. The important oracle is that the popup receives the selected project's current schema and ready state, not that a popup merely opened.

Migrate one fixture consumer group at a time. Remove its old page-level registrations after switching imports so the composed initializer is not installed twice. Run the focused startup test, a navigation or reload case, an iframe case if the product owns one, and a popup case if scope requires it. Then run the group's product tests. Failures in the first set belong to the harness; failures with a ready marker belong further downstream.

During a short transition, attach both the old and new sanitized states without letting both drive the application. A shadow comparison can reveal missing fields, but two active bootstraps recreate the ordering problem. Set an explicit removal condition for the legacy fixture. Carrying both indefinitely doubles diagnostics and leaves future authors unsure which contract is authoritative.

In CI, retain a trace on the first failure and keep the readiness attachment. Run the startup contract in every browser project that uses the fixture, because an observed order or browser-global shape in Chromium does not establish the same outcome elsewhere. There is no need to run dozens of application flows for this gate. A self-contained page checks timing and schema quickly, while selected product tests check whether the mock remains faithful enough.

Pay for determinism only where it protects the product

One composed bootstrap trades modular registration for deterministic dependency order. The file becomes easier to reason about at execution time but may be harder for separate teams to own. Keep pure configuration producers outside the browser function, then pass their combined serializable result into a small installer. Review any change to the browser-facing schema as test infrastructure, not as a harmless helper edit.

Context scope trades consistency for reach. It is ideal when every page and popup in an isolated test context needs the same patch. It is risky when the context contains unrelated pages or third-party frames. A broad analytics mock can suppress a failure in a payment frame; a feature harness can leak a test-only global to content that should never see it. Page scope narrows interference but requires deliberate handling for popups.

Instrumentation has a fidelity cost. Readiness globals, console markers, and frozen test objects are visible to application code. Use a namespaced property unlikely to collide, keep its contents non-sensitive, and avoid making product behavior depend on the diagnostic marker. If production code checks __qaBootstrap to decide what to do, the test harness has become a product feature flag and no longer validates the normal startup path.

Mocks themselves reduce realism. A stable clock, feature client, or storage adapter can make a boundary deterministic, but it can also hide integration breakage. Pair a deep deterministic test with a smaller unmocked contract or staging check where the real dependency matters. The split should be explicit: one test explains application behavior under controlled inputs, and the other checks that the real integration still speaks the expected protocol.

Do not use an init script when Playwright already has a context option for the behavior. Locale, timezone, geolocation, permissions, color scheme, touch, and user agent have supported configuration paths. Reimplementing them by overwriting browser globals can create contradictions between JavaScript-visible values, HTTP headers, browser internals, and permission state. A first-class option expresses the browser contract more accurately.

Do not use one to wait for the DOM. Its timing is intentionally before page scripts, which is too early for many elements. If the test needs to click a control after rendering, use locators and web-first assertions. If the product needs code to run at DOMContentLoaded, that listener belongs to the application or to a test page designed for the scenario, not to a browser API mock pretending the DOM already exists.

Do not patch over a product initialization bug. If two application bundles race to create shared state, composing test-side init scripts can make the suite green without repairing the customer path. Remove the test patch, reproduce the product order with application diagnostics, and fix ownership in the app. Init scripts are appropriate for controlled browser inputs, not for secretly supplying state production forgot to create.

Do not use page init scripts to control service-worker logic. The documented scope covers page documents and child frames. A worker has its own lifecycle and environment. If the business behavior is served from a worker cache, test that boundary directly or create a scenario that explicitly blocks or unregisters the worker with a supported design. Rearranging page registrations cannot establish a guarantee the API does not offer.

Separate scripts are fine when they are truly independent. An accessibility marker and a deterministic random source may have no shared state and no observable intermediate dependency. Assert each outcome separately and ignore their log order. Merging every initializer creates coupling and makes unrelated changes share one failure surface.

The review question is concrete: what would break if these two functions swapped? If the answer names missing state, a default branch, a thrown exception, or an application-visible intermediate value, compose them. If the answer is only that the trace would look different, preserve the modules and fix the assertion. That distinction keeps determinism focused on user-facing risk instead of cosmetic test-run consistency.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official playwright.dev reference

    playwright.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Does Playwright guarantee context init scripts run before page init scripts?

No. Playwright explicitly documents that the evaluation order of multiple scripts installed with `browserContext.addInitScript()` and `page.addInitScript()` is not defined. Registration calls finishing in sequence do not create an execution-order contract.

Why did addInitScript after page.goto not change the current page?

Registration applies when the page is next navigated and when child frames are attached or navigated; it does not retroactively restart scripts in the document that already loaded. Install the script before the navigation that starts the application.

Should an initialization script be registered on the context or the page?

Choose scope from ownership. Context registration covers pages in that context, including future popups and their frames, while page registration is appropriate when only one page and its child frames should receive the patch.

Can a Playwright trace prove init-script order is safe?

A trace can show what happened in one attempt and whether registration preceded navigation. It cannot turn an order that the API leaves undefined into a guarantee for the next browser, worker, or Playwright version.

How should two dependent browser mocks be installed?

Put their dependent setup inside one synchronous init function, or generate one preload artifact whose internal statement order you control. Pass test-specific data through the documented serializable argument instead of relying on closures or a second script.