Skip to content

[Bug]: clock.fastForward() and runFor() never return while the page has a zero-delay setInterval() #43947

Closed

Description

@dennev

Version

1.65.0

Steps to reproduce

  1. In an empty folder, run npm i -D @playwright/test@1.65.0 and npx playwright install chromium.
  2. Save this test as tests/clock-interval.spec.ts:
import { test } from '@playwright/test';

test('fastForward with an interval that has no delay', async ({ page }) => {
  test.setTimeout(10_000);
  await page.clock.install({ time: 0 });
  await page.clock.pauseAt(1000);
  await page.evaluate(() => {
    setInterval(() => {});
  });
  await page.clock.fastForward(1000);
});
  1. Run npx playwright test.

Expected behavior

page.clock.fastForward(1000) calls the interval callback and returns, as it does with setInterval(() => {}, 1).

Actual behavior

page.clock.fastForward(1000) does not return. The test fails with Test timeout of 10000ms exceeded.

The result is the same:

  • with setInterval(fn, 0),
  • with page.clock.runFor(10) instead of fastForward(1000),
  • with a running clock (page.clock.install() without pauseAt()).

With a running clock, other timers stop too: a setTimeout(fn, 10) that the page sets together with the interval has not fired after page.waitForTimeout(1000).

With setInterval(fn, 1), all of these work.

I see this on 1.65.0 with Chromium, and on main (58d69dc) with Chromium, Firefox and WebKit.

Additional context

In packages/injected/src/clock.ts, _takeFirstTimer() moves an interval forward by its delay (lines 355-356):

if (timer.type === TimerType.Interval)
  timer.callAt = shiftTicks(timer.callAt, timer.delay);

With a delay of 0, the interval is due again at the same tick, so the loop in _runTo() always finds a due timer and does not end. With a running clock, the real-time timer runs this loop, so later timers do not fire, and fastForward() and runFor() wait for the loop to end when they pause the clock.

Browsers and Node.js give such an interval a minimum delay: in the HTML timer initialization steps, a timeout below 4 becomes 4 when the nesting level is greater than 5, and Node.js sets a delay below 1 to 1.

I found this with Toast.ProgressFill of @kobalte/core, which calls setInterval() without a delay while a toast is shown.

I would like to work on this. I have a one-line fix and a regression test. The fix makes _takeFirstTimer() move an interval forward by at least 1 ms (Math.max(timer.delay, 1)). This is the same 1 ms step that addTimer() already uses for a zero-delay timer that is set during a timer callback. The first call is not delayed, as with setTimeout(fn). The regression test times out without the fix and passes with it. With the fix, tests/library/unit/clock.spec.ts and tests/library/page-clock.spec.ts pass on Chromium, Firefox and WebKit. I will wait for the issue to be assigned before I open a pull request.

Environment

System:
  OS: Windows 11 10.0.26300
  CPU: (12) x64 12th Gen Intel(R) Core(TM) i5-12400F
Binaries:
  Node: 26.12.1
  npm: 12.0.2
npmPackages:
  @playwright/test: 1.65.0 => 1.65.0

Activity

  1. github-actions commented on Nov 6, 2026

    @github-actions
  2. microsoft-playwright-automation commented on Nov 6, 2026

    @microsoft-playwright-automation
    Contributor

    Hi, I'm the Playwright bot and I reproduced this on Linux.

    Reproduced on main (58d69dcc392af3a6994eab0323dc2e883f05f396, 1.66.0-next) in Chromium, Firefox and WebKit. A zero-delay interval keeps its next execution at the same tick, so the clock never finishes processing it. The proposed fix advances repeat calls by 1 ms, without delaying the first call or changing positive delays.

    Reproduction and checks

    The existing CK-022 replaces global setInterval test now checks an interval without a delay. With the clock installed at time 0 and paused at 1000, its added case is:

    await page.evaluate(() => setInterval(window.stub));
    await page.clock.fastForward(0);    // One immediate callback.
    await page.clock.fastForward(1000); // One more callback, not every skipped tick.
    await page.clock.runFor(10);        // Ten more callbacks.

    Before the fix, the test exceeded its 10-second timeout in all three browsers while callbacks continued to run. After the fix, all 657 tests in tests/library/page-clock.spec.ts and tests/library/unit/clock.spec.ts passed across Chromium 158.0.8127.18, Firefox 159.0 and WebKit 27.2, headless on Linux.

    Commands:

    npm run test -- tests/library/page-clock.spec.ts --project=chromium-library --project=firefox-library --project=webkit-library --grep 'CK-022' --timeout=10000 --retries=0 --workers=3 --reporter=line
    npm run build
    npm run test -- tests/library/page-clock.spec.ts tests/library/unit/clock.spec.ts --project=chromium-library --project=firefox-library --project=webkit-library --retries=0 --workers=3 --reporter=dot
    npm run flint

    The build and all lint/type checks passed. The reported published version and Windows were not tested.

    Draft PR: #43948

    Triaged on Linux by the Playwright bot - agent run

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions