Skip to content

[Bug]: waitForLoadState('networkidle') never resolves after a request followed by an iframe detach on an already-idle page #43879

Closed

Description

@kenwongkaien

Version

1.64.1 (the same logic is on main in packages/playwright-core/src/server/frames.ts)

Steps to reproduce

Save this as repro.mts in a folder with @playwright/test installed, then run node repro.mts:

import { createServer } from "node:http";
import { chromium } from "@playwright/test";

const server = createServer((request, response) => {
  if (request.url === "/data") return void setTimeout(() => response.end("ok"), 50);
  response.setHeader("content-type", "text/html");
  response.end(`<iframe srcdoc="<p>preview</p>"></iframe>`);
});
await new Promise<void>((resolve) => server.listen(0, resolve));
const { port } = server.address() as { port: number };

const attempt = async (removeIframe: boolean) => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(`http://localhost:${port}/`);
  await page.waitForLoadState("networkidle");
  await page.evaluate(() => fetch("/data").then((r) => r.text()));
  await page.waitForTimeout(700);
  if (removeIframe) await page.evaluate(() => document.querySelector("iframe")!.remove());
  const started = Date.now();
  const outcome = await page
    .waitForLoadState("networkidle", { timeout: 5_000 })
    .then(() => `resolved in ${Date.now() - started} ms`)
    .catch(() => "TIMED OUT after 5 s");
  await browser.close();
  return outcome;
};

console.log("fetch, no iframe change:    ", await attempt(false));
console.log("fetch, then iframe removed: ", await attempt(true));
server.close();

Expected behavior

Both waits resolve. The page has had no request in flight for well over 500 ms.

Actual behavior

fetch, no iframe change:     resolved in 0 ms
fetch, then iframe removed:  TIMED OUT after 5 s

After the iframe is removed, networkidle never fires again unless the page makes another request. In our test suite, this made waitForLoadState("networkidle") wait until the test timed out. It happened intermittently on a client-side route change that unmounted a srcdoc preview iframe.

Additional context

This is how we read the code in Frame (frames.ts):

  1. The frame goes idle. networkidle is added to _firedLifecycleEvents, and _firedNetworkIdleSelf is true.
  2. The fetch calls _stopNetworkIdleTimer, which sets _firedNetworkIdleSelf = false.
  3. When the fetch finishes, _startNetworkIdleTimer returns early because _firedLifecycleEvents still has networkidle. So _firedNetworkIdleSelf stays false.
  4. Removing the iframe triggers _recalculateNetworkIdle. It sees _firedNetworkIdleSelf === false and deletes networkidle from _firedLifecycleEvents.
  5. No idle timer is running, so nothing adds networkidle back.

Any lifecycle recalculation would have the same effect after step 3, not just an iframe detach.

Waits straight after goto / reload aren't affected, because a new document resets the state. We've worked around it by waiting on goto / reload only, and by counting in-flight requests ourselves elsewhere.

Environment

OS: macOS 26.6.2 (arm64)
Node.js: 24.22.2
@playwright/test: 1.64.1
Browser: Chromium (bundled)

Activity

  1. dibas-np commented on Nov 3, 2026

    @dibas-np

    This looks like the same root cause as #42598. That issue had a different trigger: domcontentloaded removed the event there, instead of an iframe detach. _startNetworkIdleTimer returns early while networkidle is still recorded, so once anything recalculates lifecycle state and removes it, no timer is left to bring it back.

    Two PRs proposed fixes for that one (#42621, #42599). Both were closed, and the maintainers' answer on #42598 was:

    network idle is a regret - we are only keeping it for simple static demo scripts where they can be deterministic. We marked it as discouraged in our docs since for a long list of reasons, including the one you are reporting.

    So a fix here seems unlikely. Your workaround is what the docs point to: wait for the specific response (page.waitForResponse) or for the UI state you care about (web-first assertions such as expect(locator).toBeVisible()), not networkidle.

  2. pavelfeldman commented on Nov 3, 2026

    @pavelfeldman
    Member

    We can't reason about networkidle on a non-deteministic app, its usage is documented as discouraged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions