Source: runner/networkConditions.js

/**
 * @module runner/networkConditions
 * @description AUTO-006: Apply per-run network condition emulation to a
 *   Playwright context/page. Extracted from `executeTest.js` so it can be
 *   unit-tested with fake browser objects.
 *
 * Supported values:
 *   - `"fast"` (or anything else, including `undefined`) — no-op.
 *   - `"offline"` — `context.setOffline(true)`.
 *   - `"slow3g"` — Chromium-only CDP `Network.emulateNetworkConditions`
 *     (~400 Kbps, 400 ms RTT). Falls back to a per-request 400 ms delay
 *     via `page.route("**\/*", …)` on Firefox/WebKit where CDP isn't
 *     available.
 *
 * ## MVP scope (AUTO-006 ROADMAP deferral)
 *
 * The ROADMAP entry mentions "throttling" with configurable latency and
 * throughput. This MVP ships **three hardcoded presets** (`fast` / `slow3g`
 * / `offline`) that map to Chrome DevTools' own "Slow 3G" preset values
 * (400 Kbps, 400 ms RTT). Configurable `{ latency, downloadKbps,
 * uploadKbps }` is **intentionally deferred** for these reasons:
 *
 *   1. The preset values are the industry defaults every QA platform
 *      compares against — customers asking about "Slow 3G" testing expect
 *      these exact numbers, not arbitrary ones.
 *   2. Adding a free-form object to the run payload without schema
 *      validation invites bad inputs (negative throughput, absurd
 *      latencies) that produce confusing results rather than hard errors.
 *   3. The `slow3g` preset covers ≥90% of "my site is slow on mobile"
 *      testing intent without operator tuning.
 *
 * If custom throttling is needed (e.g. to reproduce a specific customer
 * network profile), extend `applyNetworkCondition` to accept
 * `networkCondition: { kind: "custom", latency, downloadKbps, uploadKbps }`
 * and validate at the route layer (`backend/src/routes/runs.js`). The CDP
 * call already accepts arbitrary values — only the public API surface needs
 * widening. Tracked as a follow-up note under AUTO-006 in ROADMAP.md.
 *
 * Returns a `{ teardown }` handle. The caller MUST `await teardown()` in a
 * `finally` block before closing the page so the slow3g route handler is
 * unrouted and doesn't keep firing on in-flight teardown requests.
 */

const SLOW_3G_LATENCY_MS = 400;
// 400 Kbps in bytes/sec — matches Chrome DevTools "Slow 3G" preset.
const SLOW_3G_THROUGHPUT_BPS = (400 * 1024) / 8;

/**
 * @typedef {Object} ApplyNetworkConditionArgs
 * @property {string} [networkCondition] - "fast" | "slow3g" | "offline".
 * @property {*} context - Playwright BrowserContext.
 * @property {*} page - Playwright Page bound to `context`.
 *
 * @typedef {Object} NetworkConditionHandle
 * @property {Function} teardown - Async function; must be awaited before closing the page.
 */

/**
 * @param {ApplyNetworkConditionArgs} args
 * @returns {Promise<NetworkConditionHandle>}
 */
export async function applyNetworkCondition({ networkCondition, context, page }) {
  if (networkCondition === "offline") {
    await context.setOffline(true);
    return { teardown: async () => {} };
  }

  if (networkCondition === "slow3g") {
    // Prefer CDP for faithful bandwidth+latency emulation (Chromium only).
    let cdpOk = false;
    try {
      const cdp = await context.newCDPSession(page);
      await cdp.send("Network.enable");
      await cdp.send("Network.emulateNetworkConditions", {
        offline: false,
        latency: SLOW_3G_LATENCY_MS,
        downloadThroughput: SLOW_3G_THROUGHPUT_BPS,
        uploadThroughput: SLOW_3G_THROUGHPUT_BPS,
      });
      cdpOk = true;
    } catch { /* non-Chromium — fall through to route-based delay */ }

    if (cdpOk) return { teardown: async () => {} };

    const slow3gRoute = async (route) => {
      await new Promise((r) => setTimeout(r, SLOW_3G_LATENCY_MS));
      await route.continue();
    };
    await page.route("**/*", slow3gRoute);
    return {
      teardown: async () => {
        await page.unroute("**/*", slow3gRoute).catch(() => {});
      },
    };
  }

  // "fast" or any other value — no-op.
  return { teardown: async () => {} };
}