From 6399e8de4e35730a54421e420283ff58d4180963 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 11 Jun 2024 09:42:15 -0700 Subject: [PATCH] chore: clock api review (#31237) --- docs/src/api/class-clock.md | 232 ++-- docs/src/clock.md | 347 ++++-- .../playwright-core/src/client/browser.ts | 8 +- packages/playwright-core/src/client/clock.ts | 60 +- .../playwright-core/src/protocol/validator.ts | 51 +- packages/playwright-core/src/server/clock.ts | 146 +-- .../dispatchers/browserContextDispatcher.ts | 34 +- .../src/server/injected/clock.ts | 426 ++++--- packages/playwright-core/types/types.d.ts | 129 +- packages/protocol/src/channels.ts | 93 +- packages/protocol/src/protocol.yml | 49 +- tests/library/clock.spec.ts | 1068 +++-------------- tests/page/page-clock.frozen.spec.ts | 3 +- tests/page/page-clock.spec.ts | 627 ++++------ 14 files changed, 1254 insertions(+), 2019 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 31115db41b..bf8b9ac59a 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -6,11 +6,88 @@ Accurately simulating time-dependent behavior is essential for verifying the cor Note that clock is installed for the entire [BrowserContext], so the time in all the pages and iframes is controlled by the same clock. -## async method: Clock.installFakeTimers +## async method: Clock.fastForward +* since: v1.45 + +Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and +reopening it later, after given time. + +**Usage** + +```js +await page.clock.fastForward(1000); +await page.clock.fastForward('30:00'); +``` + +```python async +await page.clock.fast_forward(1000) +await page.clock.fast_forward("30:00") +``` + +```python sync +page.clock.fast_forward(1000) +page.clock.fast_forward("30:00") +``` + +```java +page.clock().fastForward(1000); +page.clock().fastForward("30:00"); +``` + +```csharp +await page.Clock.FastForwardAsync(1000); +await page.Clock.FastForwardAsync("30:00"); +``` + +### param: Clock.fastForward.ticks +* since: v1.45 +- `ticks` <[int]|[string]> + +Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + +## async method: Clock.fastForwardTo +* since: v1.45 + +Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and +reopening it at the specified time. + +**Usage** + +```js +await page.clock.fastForwardTo(new Date('2020-02-02')); +await page.clock.fastForwardTo('2020-02-02'); +``` + +```python async +await page.clock.fast_forward_to(datetime.datetime(2020, 2, 2)) +await page.clock.fast_forward_to("2020-02-02") +``` + +```python sync +page.clock.fast_forward_to(datetime.datetime(2020, 2, 2)) +page.clock.fast_forward_to("2020-02-02") +``` + +```java +page.clock().fastForwardTo(Instant.parse("2020-02-02")); +page.clock().fastForwardTo("2020-02-02"); +``` + +```csharp +await page.Clock.FastForwardToAsync(DateTime.Parse("2020-02-02")); +await page.Clock.FastForwardToAsync("2020-02-02"); +``` + +### param: Clock.fastForwardTo.time +* since: v1.45 +- `time` <[int]|[string]|[Date]> + +## async method: Clock.install * since: v1.45 Install fake implementations for the following time-related functions: +* `Date` * `setTimeout` * `clearTimeout` * `setInterval` @@ -21,41 +98,18 @@ Install fake implementations for the following time-related functions: * `cancelIdleCallback` * `performance` -Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.skipTime`] for more information. +Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information. -### param: Clock.installFakeTimers.time +### option: Clock.install.time * since: v1.45 -- `time` <[int]|[Date]> - -Install fake timers with the specified base time. - -### option: Clock.installFakeTimers.loopLimit -* since: v1.45 -- `loopLimit` <[int]> - -The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`. - -## async method: Clock.runAllTimers -* since: v1.45 -- returns: <[int]> - -Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. - -**Details** - -This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers. -It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error. +- `time` <[int]|[string]|[Date]> +Time to initialize with, current system time by default. ## async method: Clock.runFor * since: v1.45 -- returns: <[int]> -Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. +Advance the clock, firing all the time-related callbacks. **Usage** @@ -66,12 +120,12 @@ await page.clock.runFor('30:00'); ```python async await page.clock.run_for(1000); -await page.clock.run_for('30:00') +await page.clock.run_for("30:00") ``` ```python sync page.clock.run_for(1000); -page.clock.run_for('30:00') +page.clock.run_for("30:00") ``` ```java @@ -84,84 +138,104 @@ await page.Clock.RunForAsync(1000); await page.Clock.RunForAsync("30:00"); ``` -### param: Clock.runFor.time +### param: Clock.runFor.ticks * since: v1.45 -- `time` <[int]|[string]> +- `ticks` <[int]|[string]> Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. -## async method: Clock.runToLastTimer -* since: v1.45 -- returns: <[int]> - -This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary. -If new timers are added while it is executing they will be run only if they would occur before this time. -This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. - - -## async method: Clock.runToNextTimer -* since: v1.45 -- returns: <[int]> - -Advances the clock to the moment of the first scheduled timer, firing it. -Fake timers must be installed. -Returns fake milliseconds since the unix epoch. - - -## async method: Clock.setTime +## async method: Clock.pause * since: v1.45 -Set the clock to the specified time. +Pause timers. Once this method is called, no timers are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.fastForwardTo`] or [`method: Clock.resume`] is called. -When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as a browser) -being put to sleep and resumed later, skipping intermediary timers. - -### param: Clock.setTime.time +## async method: Clock.resume * since: v1.45 -- `time` <[int]|[Date]> +Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. -## async method: Clock.skipTime +## async method: Clock.setFixedTime * since: v1.45 -- returns: <[int]> -Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time. - -When fake timers are installed, [`method: Clock.skipTime`] only fires due timers at most once, while [`method: Clock.runFor`] fires all the timers up to the current time. -Returns fake milliseconds since the unix epoch. +Makes `Date.now` and `new Date()` return fixed fake time at all times, +keeps all the timers running. **Usage** ```js -await page.clock.skipTime(1000); -await page.clock.skipTime('30:00'); +await page.clock.setFixedTime(Date.now()); +await page.clock.setFixedTime(new Date('2020-02-02')); +await page.clock.setFixedTime('2020-02-02'); ``` ```python async -await page.clock.skipTime(1000); -await page.clock.skipTime('30:00') +await page.clock.set_fixed_time(datetime.datetime.now()) +await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2)) +await page.clock.set_fixed_time("2020-02-02") ``` ```python sync -page.clock.skipTime(1000); -page.clock.skipTime('30:00') +page.clock.set_fixed_time(datetime.datetime.now()) +page.clock.set_fixed_time(datetime.datetime(2020, 2, 2)) +page.clock.set_fixed_time("2020-02-02") ``` ```java -page.clock().skipTime(1000); -page.clock().skipTime("30:00"); +page.clock().setFixedTime(Instant.now()); +page.clock().setFixedTime(Instant.parse("2020-02-02")); +page.clock().setFixedTime("2020-02-02"); ``` ```csharp -await page.Clock.SkipTimeAsync(1000); -await page.Clock.SkipTimeAsync("30:00"); +await page.Clock.SetFixedTimeAsync(DateTime.Now); +await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2)); +await page.Clock.SetFixedTimeAsync("2020-02-02"); ``` -### param: Clock.skipTime.time +### param: Clock.setFixedTime.time * since: v1.45 -- `time` <[int]|[string]> +- `time` <[int]|[string]|[Date]> -Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. +Time to be set. + +## async method: Clock.setSystemTime +* since: v1.45 + +Sets current system time but does not trigger any timers, unlike [`method: Clock.fastForwardTo`]. + +**Usage** + +```js +await page.clock.setSystemTime(Date.now()); +await page.clock.setSystemTime(new Date('2020-02-02')); +await page.clock.setSystemTime('2020-02-02'); +``` + +```python async +await page.clock.set_system_time(datetime.datetime.now()) +await page.clock.set_system_time(datetime.datetime(2020, 2, 2)) +await page.clock.set_system_time("2020-02-02") +``` + +```python sync +page.clock.set_system_time(datetime.datetime.now()) +page.clock.set_system_time(datetime.datetime(2020, 2, 2)) +page.clock.set_system_time("2020-02-02") +``` + +```java +page.clock().setSystemTime(Instant.now()); +page.clock().setSystemTime(Instant.parse("2020-02-02")); +page.clock().setSystemTime("2020-02-02"); +``` + +```csharp +await page.Clock.SetSystemTimeAsync(DateTime.Now); +await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2)); +await page.Clock.SetSystemTimeAsync("2020-02-02"); +``` + +### param: Clock.setSystemTime.time +* since: v1.45 +- `time` <[int]|[string]|[Date]> diff --git a/docs/src/clock.md b/docs/src/clock.md index 5299145916..82fba434ec 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -17,228 +17,324 @@ Accurately simulating time-dependent behavior is essential for verifying the cor - `cancelAnimationFrame` - `requestIdleCallback` - `cancelIdleCallback` + - `performance` -By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option. +## Test with predefined time -```js -await page.clock.setTime(new Date('2020-02-02')); -await page.clock.installFakeTimers(new Date('2020-02-02')); -``` - -## Mock Date.now - -Most of the time, you only need to fake `Date.now` and no other time-related functions. -That way the time flows naturally, but `Date.now` returns a fixed value. +Often you only need to fake `Date.now` while keeping the timers going. +That way the time flows naturally, but `Date.now` always returns a fixed value. ```html
``` ```js -await page.clock.setTime(new Date('2024-02-02T10:00:00')); +await page.clock.setFixedTime(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); -await page.clock.setTime(new Date('2024-02-02T10:30:00')); +await page.clock.setFixedTime(new Date('2024-02-02T10:30:00')); +// We know that the page has a timer that updates the time every second. +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); +``` + +## Consistent time and timers + +Sometimes your timers depend on `Date.now` and are confused when the `Date.now` value does not change over time. +In this case, you can install the clock and fast forward to the time of interest when testing. + +```html +
+ +``` + +```js +// Initialize clock with some time before the test time and let the page load +// naturally. `Date.now` will progress as the timers fire. +await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); +await page.goto('http://localhost:3333'); + +// Take control over time flow. +await page.clock.pause(); +// Pretend that the user closed the laptop lid and opened it again at 10am. +await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00')); + +// Assert the page state. +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); + +// Close the laptop lid again and open it at 10:30am. +await page.clock.fastForward('30:00'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); ``` ```python async -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) -await page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# Initialize clock with some time before the test time and let the page load +# naturally. `Date.now` will progress as the timers fire. +await page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0)) +await page.goto("http://localhost:3333") -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) -await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# Take control over time flow. +await page.clock.pause() +# Pretend that the user closed the laptop lid and opened it again at 10am. +await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) + +# Assert the page state. +await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") + +# Close the laptop lid again and open it at 10:30am. +await page.clock.fast_forward("30:00") +await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM") ``` ```python sync -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) -page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# Initialize clock with some time before the test time and let the page load +# naturally. `Date.now` will progress as the timers fire. +page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0)) +page.goto("http://localhost:3333") -page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) -expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# Take control over time flow. +page.clock.pause() +# Pretend that the user closed the laptop lid and opened it again at 10am. +page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) + +# Assert the page state. +expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM") + +# Close the laptop lid again and open it at 10:30am. +page.clock.fast_forward("30:00") +expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM") ``` ```java -page.clock().setTime(Instant.parse("2024-02-02T10:00:00")); +// Initialize clock with some time before the test time and let the page load +// naturally. `Date.now` will progress as the timers fire. +page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00"))); page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); + +// Take control over time flow. +page.clock().pause(); +// Pretend that the user closed the laptop lid and opened it again at 10am. +page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00")); + +// Assert the page state. assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); -page.clock().setTime(Instant.parse("2024-02-02T10:30:00")); +// Close the laptop lid again and open it at 10:30am. +page.clock().fastForward("30:00"); assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); ``` ```csharp -// Initialize clock with a specific time, only fake Date.now. -await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)); -await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("current-time"); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); +// Initialize clock with some time before the test time and let the page load naturally. +// `Date.now` will progress as the timers fire. +await Page.Clock.InstallAsync(new +{ + Time = new DateTime(2024, 2, 2, 8, 0, 0) +}); +await Page.GotoAsync("http://localhost:3333"); -await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 30, 0, DateTimeKind.Pst)); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); +// Take control over time flow. +await Page.Clock.PauseAsync(); +// Pretend that the user closed the laptop lid and opened it again at 10am. +await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0)); + +// Assert the page state. +await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM"); + +// Close the laptop lid again and open it at 10:30am. +await Page.Clock.FastForwardAsync("30:00"); +await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM"); ``` -## Mock Date.now consistent with the timers +## Test inactivity monitoring -Sometimes your timers depend on `Date.now` and are confused when the time stands still. -In cases like this you need to ensure that `Date.now` and timers are consistent. -You can achieve this by installing the fake timers. - -```html -
- -``` +Inactivity monitoring is a common feature in web applications that logs out users after a period of inactivity. +Testing this feature can be tricky because you need to wait for a long time to see the effect. +With the help of the clock, you can speed up time and test this feature quickly. ```js -// Initialize clock with a specific time, take full control over time. -await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); +// Initial time does not matter for the test, so we can pick current time. +await page.clock.install(); await page.goto('http://localhost:3333'); -await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); +// Interact with the page +await page.getByRole('button').click(); -// Fast forward time 30 minutes without firing intermediate timers, as if the user -// closed and opened the lid of the laptop. -await page.clock.skipTime('30:00'); -await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); +// Fast forward time 5 minutes as if the user did not do anything. +// Fast forward is like closing the laptop lid and opening it after 5 minutes. +// All the timers due will fire once immediately, as in the real browser. +await page.clock.fastForward('5:00'); + +// Check that the user was logged out automatically. +await expect(page.getByText('You have been logged out due to inactivity.')).toBeVisible(); ``` ```python async -# Initialize clock with a specific time, take full control over time. -await page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) -) -await page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# Initial time does not matter for the test, so we can pick current time. +await page.clock.install() +await page.goto("http://localhost:3333") +# Interact with the page +await page.get_by_role("button").click() -# Fast forward time 30 minutes without firing intermediate timers, as if the user -# closed and opened the lid of the laptop. -await page.clock.skip_time('30:00') -await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# Fast forward time 5 minutes as if the user did not do anything. +# Fast forward is like closing the laptop lid and opening it after 5 minutes. +# All the timers due will fire once immediately, as in the real browser. +await page.clock.fast_forward("5:00") + +# Check that the user was logged out automatically. +await expect(page.getByText("You have been logged out due to inactivity.")).toBeVisible() ``` ```python sync -# Initialize clock with a specific time, take full control over time. -page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) -) -page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') -expect(locator).to_have_text('2/2/2024, 10:00:00 AM') +# Initial time does not matter for the test, so we can pick current time. +page.clock.install() +page.goto("http://localhost:3333") +# Interact with the page +page.get_by_role("button").click() -# Fast forward time 30 minutes without firing intermediate timers, as if the user -# closed and opened the lid of the laptop. -page.clock.skip_time('30:00') -expect(locator).to_have_text('2/2/2024, 10:30:00 AM') +# Fast forward time 5 minutes as if the user did not do anything. +# Fast forward is like closing the laptop lid and opening it after 5 minutes. +# All the timers due will fire once immediately, as in the real browser. +page.clock.fast_forward("5:00") + +# Check that the user was logged out automatically. +expect(page.get_by_text("You have been logged out due to inactivity.")).to_be_visible() ``` ```java -// Initialize clock with a specific time, take full control over time. -page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); +// Initial time does not matter for the test, so we can pick current time. +page.clock().install(); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("current-time"); -assertThat(locator).hasText("2/2/2024, 10:00:00 AM") +Locator locator = page.getByRole("button"); -// Fast forward time 30 minutes without firing intermediate timers, as if the user -// closed and opened the lid of the laptop. -page.clock().skipTime("30:00"); -assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); +// Interact with the page +locator.click(); + +// Fast forward time 5 minutes as if the user did not do anything. +// Fast forward is like closing the laptop lid and opening it after 5 minutes. +// All the timers due will fire once immediately, as in the real browser. +page.clock().fastForward("5:00"); + +// Check that the user was logged out automatically. +assertThat(page.getByText("You have been logged out due to inactivity.")).isVisible(); ``` ```csharp -// Initialize clock with a specific time, take full control over time. -await page.Clock.InstallFakeTimersAsync( - new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) -); +// Initial time does not matter for the test, so we can pick current time. +await Page.Clock.InstallAsync(); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("current-time"); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); -// Fast forward time 30 minutes without firing intermediate timers, as if the user -// closed and opened the lid of the laptop. -await page.Clock.SkipTimeAsync("30:00"); -await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); +// Interact with the page +await page.GetByRole("button").ClickAsync(); + +// Fast forward time 5 minutes as if the user did not do anything. +// Fast forward is like closing the laptop lid and opening it after 5 minutes. +// All the timers due will fire once immediately, as in the real browser. +await Page.Clock.FastForwardAsync("5:00"); + +// Check that the user was logged out automatically. +await Expect(Page.GetByText("You have been logged out due to inactivity.")).ToBeVisibleAsync(); ``` -## Tick through time manually +## Tick through time manually, firing all the timers consistently -In rare cases, you may want to tick through time manually, firing all timers and animation frames in the process to achieve a fine-grained -control over the passage of time. +In rare cases, you may want to tick through time manually, firing all timers and +animation frames in the process to achieve a fine-grained control over the passage of time. ```html
``` ```js -// Initialize clock with a specific time, take full control over time. -await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); +// Initialize clock with a specific time, let the page load naturally. +await page.clock.install({ time: new Date('2024-02-02T08:00:00') }); await page.goto('http://localhost:3333'); +// Pause the time flow, stop the timers, you now have manual control +// over the page time. +await page.clock.pause(); +await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00')); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); + // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. await page.clock.runFor(2000); -await expect(locator).to_have_text('2/2/2024, 10:00:02 AM'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:02 AM'); ``` ```python async -# Initialize clock with a specific time, take full control over time. -await page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), +# Initialize clock with a specific time, let the page load naturally. +await page.clock.install(time= + datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst), ) -await page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') +await page.goto("http://localhost:3333") +locator = page.get_by_test_id("current-time") + +# Pause the time flow, stop the timers, you now have manual control +# over the page time. +await page.clock.pause() +await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +await expect(locator).to_have_text("2/2/2024, 10:00:00 AM") # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. await page.clock.run_for(2000) -await expect(locator).to_have_text('2/2/2024, 10:00:02 AM') +await expect(locator).to_have_text("2/2/2024, 10:00:02 AM") ``` ```python sync -# Initialize clock with a specific time, take full control over time. -page.clock.install_fake_timers( - datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), +# Initialize clock with a specific time, let the page load naturally. +page.clock.install( + time=datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst), ) -page.goto('http://localhost:3333') -locator = page.get_by_test_id('current-time') +page.goto("http://localhost:3333") +locator = page.get_by_test_id("current-time") + +# Pause the time flow, stop the timers, you now have manual control +# over the page time. +page.clock.pause() +page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0)) +expect(locator).to_have_text("2/2/2024, 10:00:00 AM") # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. page.clock.run_for(2000) -expect(locator).to_have_text('2/2/2024, 10:00:02 AM') +expect(locator).to_have_text("2/2/2024, 10:00:02 AM") ``` ```java -// Initialize clock with a specific time, take full control over time. -page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); +// Initialize clock with a specific time, let the page load naturally. +page.clock().install(new Clock.InstallOptions() + .setTime(Instant.parse("2024-02-02T08:00:00"))); page.navigate("http://localhost:3333"); Locator locator = page.getByTestId("current-time"); +// Pause the time flow, stop the timers, you now have manual control +// over the page time. +page.clock().pause(); +page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00")); +assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); + // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. page.clock().runFor(2000); @@ -246,15 +342,22 @@ assertThat(locator).hasText("2/2/2024, 10:00:02 AM"); ``` ```csharp -// Initialize clock with a specific time, take full control over time. -await page.Clock.InstallFakeTimersAsync( - new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) -); +// Initialize clock with a specific time, let the page load naturally. +await Page.Clock.InstallAsync(new +{ + Time = new DateTime(2024, 2, 2, 8, 0, 0, DateTimeKind.Pst) +}); await page.GotoAsync("http://localhost:3333"); var locator = page.GetByTestId("current-time"); +// Pause the time flow, stop the timers, you now have manual control +// over the page time. +await Page.Clock.PauseAsync(); +await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0)); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); + // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -await page.Clock.RunForAsync(2000); +await Page.Clock.RunForAsync(2000); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM"); ``` diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index ed903b674e..c1a9b0259d 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -85,8 +85,12 @@ export class Browser extends ChannelOwner implements ap const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); - if (!forReuse && !!process.env.PW_FREEZE_TIME) - await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true); + if (!forReuse && !!process.env.PW_FREEZE_TIME) { + await this._wrapApiCall(async () => { + await context.clock.install({ time: 0 }); + await context.clock.pause(); + }, true); + } return context; } diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 792e36a135..f4d2133b40 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -24,44 +24,50 @@ export class Clock implements api.Clock { this._browserContext = browserContext; } - async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) { - const timeMs = time instanceof Date ? time.getTime() : time; - await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit }); + async install(options: { time?: number | string | Date } = { }) { + await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {}); } - async runAllTimers(): Promise { - const result = await this._browserContext._channel.clockRunAllTimers(); - return result.fakeTime; + async fastForward(ticks: number | string) { + await this._browserContext._channel.clockFastForward(parseTicks(ticks)); } - async runFor(time: number | string): Promise { - const result = await this._browserContext._channel.clockRunFor({ - timeNumber: typeof time === 'number' ? time : undefined, - timeString: typeof time === 'string' ? time : undefined - }); - return result.fakeTime; + async fastForwardTo(time: number | string | Date) { + await this._browserContext._channel.clockFastForwardTo(parseTime(time)); } - async runToLastTimer(): Promise { - const result = await this._browserContext._channel.clockRunToLastTimer(); - return result.fakeTime; + async pause() { + await this._browserContext._channel.clockPause({}); } - async runToNextTimer(): Promise { - const result = await this._browserContext._channel.clockRunToNextTimer(); - return result.fakeTime; + async resume() { + await this._browserContext._channel.clockResume({}); } - async setTime(time: number | Date) { - const timeMs = time instanceof Date ? time.getTime() : time; - await this._browserContext._channel.clockSetTime({ time: timeMs }); + async runFor(ticks: number | string) { + await this._browserContext._channel.clockRunFor(parseTicks(ticks)); } - async skipTime(time: number | string) { - const result = await this._browserContext._channel.clockSkipTime({ - timeNumber: typeof time === 'number' ? time : undefined, - timeString: typeof time === 'string' ? time : undefined - }); - return result.fakeTime; + async setFixedTime(time: string | number | Date) { + await this._browserContext._channel.clockSetFixedTime(parseTime(time)); + } + + async setSystemTime(time: string | number | Date) { + await this._browserContext._channel.clockSetSystemTime(parseTime(time)); } } + +function parseTime(time: string | number | Date): { timeNumber?: number, timeString?: string } { + if (typeof time === 'number') + return { timeNumber: time }; + if (typeof time === 'string') + return { timeString: time }; + return { timeNumber: time.getTime() }; +} + +function parseTicks(ticks: string | number): { ticksNumber?: number, ticksString?: string } { + return { + ticksNumber: typeof ticks === 'number' ? ticks : undefined, + ticksString: typeof ticks === 'string' ? ticks : undefined + }; +} diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 0a60a92ebb..169b7574df 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -963,41 +963,40 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({ enabled: tBoolean, }); scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({})); -scheme.BrowserContextClockInstallFakeTimersParams = tObject({ - time: tNumber, - loopLimit: tOptional(tNumber), +scheme.BrowserContextClockFastForwardParams = tObject({ + ticksNumber: tOptional(tNumber), + ticksString: tOptional(tString), }); -scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({})); -scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({})); -scheme.BrowserContextClockRunAllTimersResult = tObject({ - fakeTime: tNumber, +scheme.BrowserContextClockFastForwardResult = tOptional(tObject({})); +scheme.BrowserContextClockFastForwardToParams = tObject({ + timeNumber: tOptional(tNumber), + timeString: tOptional(tString), }); +scheme.BrowserContextClockFastForwardToResult = tOptional(tObject({})); +scheme.BrowserContextClockInstallParams = tObject({ + timeNumber: tOptional(tNumber), + timeString: tOptional(tString), +}); +scheme.BrowserContextClockInstallResult = tOptional(tObject({})); +scheme.BrowserContextClockPauseParams = tOptional(tObject({})); +scheme.BrowserContextClockPauseResult = tOptional(tObject({})); +scheme.BrowserContextClockResumeParams = tOptional(tObject({})); +scheme.BrowserContextClockResumeResult = tOptional(tObject({})); scheme.BrowserContextClockRunForParams = tObject({ + ticksNumber: tOptional(tNumber), + ticksString: tOptional(tString), +}); +scheme.BrowserContextClockRunForResult = tOptional(tObject({})); +scheme.BrowserContextClockSetFixedTimeParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockRunForResult = tObject({ - fakeTime: tNumber, -}); -scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({})); -scheme.BrowserContextClockRunToLastTimerResult = tObject({ - fakeTime: tNumber, -}); -scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({})); -scheme.BrowserContextClockRunToNextTimerResult = tObject({ - fakeTime: tNumber, -}); -scheme.BrowserContextClockSetTimeParams = tObject({ - time: tNumber, -}); -scheme.BrowserContextClockSetTimeResult = tOptional(tObject({})); -scheme.BrowserContextClockSkipTimeParams = tObject({ +scheme.BrowserContextClockSetFixedTimeResult = tOptional(tObject({})); +scheme.BrowserContextClockSetSystemTimeParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockSkipTimeResult = tObject({ - fakeTime: tNumber, -}); +scheme.BrowserContextClockSetSystemTimeResult = tOptional(tObject({})); scheme.PageInitializer = tObject({ mainFrame: tChannel(['Frame']), viewportSize: tOptional(tObject({ diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index dd7f194a1d..3cbb0b1b0a 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -16,81 +16,74 @@ import type { BrowserContext } from './browserContext'; import * as clockSource from '../generated/clockSource'; +import { isJavaScriptErrorInEvaluate } from './javascript'; export class Clock { private _browserContext: BrowserContext; - private _scriptInjected = false; - private _clockInstalled = false; - private _now = 0; + private _scriptInstalled = false; constructor(browserContext: BrowserContext) { this._browserContext = browserContext; } - async installFakeTimers(time: number, loopLimit: number | undefined) { - await this._injectScriptIfNeeded(); - await this._addAndEvaluate(`(() => { - globalThis.__pwClock.clock?.uninstall(); - globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })}); - })();`); - this._now = time; - this._clockInstalled = true; + async fastForward(ticks: number | string) { + await this._installIfNeeded(); + const ticksMillis = parseTicks(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`); } - async runToNextTimer(): Promise { - this._assertInstalled(); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.next()`); - return this._now; + async fastForwardTo(ticks: number | string) { + await this._installIfNeeded(); + const timeMillis = parseTime(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForwardTo', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForwardTo(${timeMillis})`); } - async runAllTimers(): Promise { - this._assertInstalled(); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAll()`); - return this._now; + async install(time: number | string | undefined) { + await this._installIfNeeded(); + const timeMillis = time !== undefined ? parseTime(time) : Date.now(); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`); } - async runToLastTimer(): Promise { - this._assertInstalled(); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLast()`); - return this._now; + async pause() { + await this._installIfNeeded(); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pause', ${Date.now()})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.pause()`); } - async setTime(time: number) { - if (this._clockInstalled) { - const jump = time - this._now; - if (jump < 0) - throw new Error('Unable to set time into the past when fake timers are installed'); - await this._addAndEvaluate(`globalThis.__pwClock.clock.jump(${jump})`); - this._now = time; - return this._now; - } - - await this._injectScriptIfNeeded(); - await this._addAndEvaluate(`(() => { - globalThis.__pwClock.clock?.uninstall(); - globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); - })();`); - this._now = time; - return this._now; + async resume() { + await this._installIfNeeded(); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('resume', ${Date.now()})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`); } - async skipTime(time: number | string) { - const delta = parseTime(time); - await this.setTime(this._now + delta); - return this._now; + async setFixedTime(time: string | number) { + await this._installIfNeeded(); + const timeMillis = parseTime(time); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setFixedTime', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.setFixedTime(${timeMillis})`); } - async runFor(time: number | string): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwClock.clock.recordTick(${JSON.stringify(time)})`); - this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); - return this._now; + async setSystemTime(time: string | number) { + await this._installIfNeeded(); + const timeMillis = parseTime(time); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`); } - private async _injectScriptIfNeeded() { - if (this._scriptInjected) + async runFor(ticks: number | string) { + await this._installIfNeeded(); + const ticksMillis = parseTicks(ticks); + await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`); + await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`); + } + + private async _installIfNeeded() { + if (this._scriptInstalled) return; - this._scriptInjected = true; + this._scriptInstalled = true; const script = `(() => { const module = {}; ${clockSource.source} @@ -106,37 +99,56 @@ export class Clock { private async _evaluateInFrames(script: string) { const frames = this._browserContext.pages().map(page => page.frames()).flat(); - const results = await Promise.all(frames.map(frame => frame.evaluateExpression(script))); + const results = await Promise.all(frames.map(async frame => { + try { + await frame.nonStallingEvaluateInExistingContext(script, false, 'main'); + } catch (e) { + if (isJavaScriptErrorInEvaluate(e)) + throw e; + } + })); return results[0]; } - - private _assertInstalled() { - if (!this._clockInstalled) - throw new Error('Clock is not installed'); - } } -// Taken from sinonjs/fake-timerss-src. -function parseTime(time: string | number): number { - if (typeof time === 'number') - return time; - if (!time) +/** + * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into + * number of milliseconds. This is used to support human-readable strings passed + * to clock.tick() + */ +function parseTicks(value: number | string): number { + if (typeof value === 'number') + return value; + if (!value) return 0; + const str = value; - const strings = time.split(':'); + const strings = str.split(':'); const l = strings.length; let i = l; let ms = 0; let parsed; - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time)) - throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`); + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { + throw new Error( + `Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`, + ); + } while (i--) { parsed = parseInt(strings[i], 10); if (parsed >= 60) - throw new Error(`Invalid time ${time}`); + throw new Error(`Invalid time ${str}`); ms += parsed * Math.pow(60, l - i - 1); } + return ms * 1000; } + +function parseTime(epoch: string | number | undefined): number { + if (!epoch) + return 0; + if (typeof epoch === 'number') + return epoch; + return new Date(epoch).getTime(); +} diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index dd1f61f57b..86b9a5576c 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -312,32 +312,36 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.installFakeTimers(params.time, params.loopLimit); + async clockFastForward(params: channels.BrowserContextClockFastForwardParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.fastForward(params.ticksString ?? params.ticksNumber ?? 0); } - async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runAllTimers() }; + async clockFastForwardTo(params: channels.BrowserContextClockFastForwardToParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.fastForwardTo(params.timeString ?? params.timeNumber ?? 0); } - async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runToLastTimer() }; + async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined); } - async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runToNextTimer() }; + async clockPause(params: channels.BrowserContextClockPauseParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.pause(); } - async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise { - await this._context.clock.setTime(params.time); - } - - async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) }; + async clockResume(params: channels.BrowserContextClockResumeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.resume(); } async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runFor(params.timeString || params.timeNumber || 0) }; + await this._context.clock.runFor(params.ticksString ?? params.ticksNumber ?? 0); + } + + async clockSetFixedTime(params: channels.BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.setFixedTime(params.timeString ?? params.timeNumber ?? 0); + } + + async clockSetSystemTime(params: channels.BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.setSystemTime(params.timeString ?? params.timeNumber ?? 0); } async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise { diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 508d7a59e5..5f6d4cf613 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -26,7 +26,6 @@ export type ClockMethods = { export type ClockConfig = { now?: number | Date; - loopLimit?: number; }; export type InstallConfig = ClockConfig & { @@ -53,26 +52,39 @@ type Timer = { }; interface Embedder { - postTask(task: () => void): void; - postTaskPeriodically(task: () => void, delay: number): () => void; + dateNow(): number; + performanceNow(): DOMHighResTimeStamp; + setTimeout(task: () => void, timeout?: number): () => void; + setInterval(task: () => void, delay: number): () => void; } +type Time = { + // ms since Epoch + time: number; + // Ticks since the session began (ala performance.now) + ticks: number; + // Whether fixed time was set. + isFixedTime: boolean; + // Origin time since Epoch when session started. + origin: number; +}; + +type LogEntryType = 'fastForward' | 'fastForwardTo' | 'install' | 'pause' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime'; + export class ClockController { - readonly timeOrigin: number; - private _now: { time: number, ticks: number, timeFrozen: boolean }; - private _loopLimit: number; + readonly _now: Time; private _duringTick = false; private _timers = new Map(); private _uniqueTimerId = idCounterStart; private _embedder: Embedder; readonly disposables: (() => void)[] = []; + private _log: { type: LogEntryType, time: number, param?: number }[] = []; + private _realTime: { startTicks: number, lastSyncTicks: number } | undefined; + private _currentRealTimeTimer: { callAt: number, dispose: () => void } | undefined; - constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) { - const start = Math.floor(getEpoch(startDate)); - this.timeOrigin = start; - this._now = { time: start, ticks: 0, timeFrozen: false }; + constructor(embedder: Embedder) { + this._now = { time: 0, isFixedTime: false, ticks: 0, origin: -1 }; this._embedder = embedder; - this._loopLimit = loopLimit; } uninstall() { @@ -81,109 +93,147 @@ export class ClockController { } now(): number { + this._replayLogOnce(); return this._now.time; } - setTime(now: Date | number, options: { freeze?: boolean } = {}) { - this._now.time = getEpoch(now); - this._now.timeFrozen = !!options.freeze; + install(time: number) { + this._replayLogOnce(); + this._innerSetTime(time); + } + + setSystemTime(time: number) { + this._replayLogOnce(); + this._innerSetTime(time); + } + + setFixedTime(time: number) { + this._replayLogOnce(); + this._innerSetFixedTime(time); } performanceNow(): DOMHighResTimeStamp { + this._replayLogOnce(); return this._now.ticks; } + private _innerSetTime(time: number) { + this._now.time = time; + this._now.isFixedTime = false; + if (this._now.origin < 0) + this._now.origin = this._now.time; + } + + private _innerSetFixedTime(time: number) { + this._innerSetTime(time); + this._now.isFixedTime = true; + } + private _advanceNow(toTicks: number) { - if (!this._now.timeFrozen) + if (!this._now.isFixedTime) this._now.time += toTicks - this._now.ticks; this._now.ticks = toTicks; } - private async _doTick(msFloat: number): Promise { - if (msFloat < 0) - throw new TypeError('Negative ticks are not supported'); + async log(type: LogEntryType, time: number, param?: number) { + this._log.push({ type, time, param }); + } + + async runFor(ticks: number) { + this._replayLogOnce(); + if (ticks < 0) + throw new TypeError('Negative ticks are not supported'); + await this._runTo(this._now.ticks + ticks); + } + + private async _runTo(tickTo: number) { + if (this._now.ticks > tickTo) + return; - const ms = Math.floor(msFloat); - const tickTo = this._now.ticks + ms; - let tickFrom = this._now.ticks; - let previous = this._now.ticks; let firstException: Error | undefined; - let timer = this._firstTimerInRange(tickFrom, tickTo); - while (timer && tickFrom <= tickTo) { - tickFrom = timer.callAt; - const error = await this._callTimer(timer).catch(e => e); - firstException = firstException || error; - timer = this._firstTimerInRange(previous, tickTo); - previous = tickFrom; + while (true) { + const result = await this._callFirstTimer(tickTo); + if (!result.timerFound) + break; + firstException = firstException || result.error; } this._advanceNow(tickTo); if (firstException) throw firstException; - - return this._now.ticks; } - async recordTick(tickValue: string | number) { - const msFloat = parseTime(tickValue); - this._advanceNow(this._now.ticks + msFloat); + pause() { + this._replayLogOnce(); + this._innerPause(); } - async tick(tickValue: string | number): Promise { - return await this._doTick(parseTime(tickValue)); + private _innerPause() { + this._realTime = undefined; + this._updateRealTimeTimer(); } - async next(): Promise { - const timer = this._firstTimer(); - if (!timer) - return this._now.ticks; - await this._callTimer(timer); - return this._now.ticks; + resume() { + this._replayLogOnce(); + this._innerResume(); } - async runToFrame(): Promise { - return this.tick(this.getTimeToNextFrame()); + private _innerResume() { + const now = this._embedder.performanceNow(); + this._realTime = { startTicks: now, lastSyncTicks: now }; + this._updateRealTimeTimer(); } - async runAll(): Promise { - for (let i = 0; i < this._loopLimit; i++) { - const numTimers = this._timers.size; - if (numTimers === 0) - return this._now.ticks; - - await this.next(); + private _updateRealTimeTimer() { + if (!this._realTime) { + this._currentRealTimeTimer?.dispose(); + this._currentRealTimeTimer = undefined; + return; } - const excessJob = this._firstTimer(); - if (!excessJob) - return this._now.ticks; - throw this._getInfiniteLoopError(excessJob); + const firstTimer = this._firstTimer(); + + // Either run the next timer or move time in 100ms chunks. + const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100); + if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt) + return; + + if (this._currentRealTimeTimer) { + this._currentRealTimeTimer.dispose(); + this._currentRealTimeTimer = undefined; + } + + this._currentRealTimeTimer = { + callAt, + dispose: this._embedder.setTimeout(() => { + const now = Math.ceil(this._embedder.performanceNow()); + this._currentRealTimeTimer = undefined; + const sinceLastSync = now - this._realTime!.lastSyncTicks; + this._realTime!.lastSyncTicks = now; + // eslint-disable-next-line no-console + this._runTo(this._now.ticks + sinceLastSync).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); + }, callAt - this._now.ticks), + }; } - async runToLast(): Promise { - const timer = this._lastTimer(); - if (!timer) - return this._now.ticks; - return await this.tick(timer.callAt - this._now.ticks); - } - - reset() { - this._timers.clear(); - this._now = { time: this.timeOrigin, ticks: 0, timeFrozen: false }; - } - - async jump(tickValue: string | number): Promise { - const msFloat = parseTime(tickValue); - const ms = Math.floor(msFloat); - + async fastForward(ticks: number) { + this._replayLogOnce(); + const ms = ticks | 0; for (const timer of this._timers.values()) { if (this._now.ticks + ms > timer.callAt) timer.callAt = this._now.ticks + ms; } - return await this.tick(ms); + await this.runFor(ms); + } + + async fastForwardTo(time: number) { + this._replayLogOnce(); + const ticks = time - this._now.time; + await this.fastForward(ticks); } addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { + this._replayLogOnce(); if (options.func === undefined) throw new Error('Callback must be provided to timer calls'); @@ -204,56 +254,56 @@ export class ClockController { error: new Error(), }; this._timers.set(timer.id, timer); + if (this._realTime) + this._updateRealTimeTimer(); return timer.id; } - private _firstTimerInRange(from: number, to: number): Timer | null { - let firstTimer: Timer | null = null; - for (const timer of this._timers.values()) { - const isInRange = inRange(from, to, timer); - if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1)) - firstTimer = timer; - } - return firstTimer; - } - countTimers() { return this._timers.size; } - private _firstTimer(): Timer | null { + private _firstTimer(beforeTick?: number): Timer | null { let firstTimer: Timer | null = null; for (const timer of this._timers.values()) { - if (!firstTimer || compareTimers(firstTimer, timer) === 1) + const isInRange = beforeTick === undefined || timer.callAt <= beforeTick; + if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1)) firstTimer = timer; } return firstTimer; } - private _lastTimer(): Timer | null { - let lastTimer: Timer | null = null; + private _takeFirstTimer(beforeTick?: number): Timer | null { + const timer = this._firstTimer(beforeTick); + if (!timer) + return null; - for (const timer of this._timers.values()) { - if (!lastTimer || compareTimers(lastTimer, timer) === -1) - lastTimer = timer; - } - return lastTimer; - } - - private async _callTimer(timer: Timer) { this._advanceNow(timer.callAt); if (timer.type === TimerType.Interval) this._timers.get(timer.id)!.callAt += timer.delay; else this._timers.delete(timer.id); + return timer; + } + + private async _callFirstTimer(beforeTick: number): Promise<{ timerFound: boolean, error?: Error }> { + const timer = this._takeFirstTimer(beforeTick); + if (!timer) + return { timerFound: false }; this._duringTick = true; try { if (typeof timer.func !== 'function') { - (() => { eval(timer.func); })(); - return; + let error: Error | undefined; + try { + (() => { eval(timer.func); })(); + } catch (e) { + error = e; + } + await new Promise(f => this._embedder.setTimeout(f)); + return { timerFound: true, error }; } let args = timer.args; @@ -262,67 +312,26 @@ export class ClockController { else if (timer.type === TimerType.IdleCallback) args = [{ didTimeout: false, timeRemaining: () => 0 }]; - timer.func.apply(null, args); - await new Promise(f => this._embedder.postTask(f)); + let error: Error | undefined; + try { + timer.func.apply(null, args); + } catch (e) { + error = e; + } + await new Promise(f => this._embedder.setTimeout(f)); + return { timerFound: true, error }; } finally { this._duringTick = false; } } - private _getInfiniteLoopError(job: Timer) { - const infiniteLoopError = new Error( - `Aborting after running ${this._loopLimit} timers, assuming an infinite loop!`, - ); - - if (!job.error) - return infiniteLoopError; - - // pattern never matched in Node - const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; - const clockMethodPattern = new RegExp( - String(Object.keys(this).join('|')), - ); - - let matchedLineIndex = -1; - job.error.stack!.split('\n').some((line, i) => { - // If we've matched a computed target line (e.g. setTimeout) then we - // don't need to look any further. Return true to stop iterating. - const matchedComputedTarget = line.match(computedTargetPattern); - /* istanbul ignore if */ - if (matchedComputedTarget) { - matchedLineIndex = i; - return true; - } - - // If we've matched a clock method line, then there may still be - // others further down the trace. Return false to keep iterating. - const matchedClockMethod = line.match(clockMethodPattern); - if (matchedClockMethod) { - matchedLineIndex = i; - return false; - } - - // If we haven't matched anything on this line, but we matched - // previously and set the matched line index, then we can stop. - // If we haven't matched previously, then we should keep iterating. - return matchedLineIndex >= 0; - }); - - const funcName = typeof job.func === 'function' ? job.func.name : 'anonymous'; - const stack = `${infiniteLoopError}\n${job.type || 'Microtask'} - ${funcName}\n${job.error.stack! - .split('\n') - .slice(matchedLineIndex + 1) - .join('\n')}`; - - infiniteLoopError.stack = stack; - return infiniteLoopError; - } - getTimeToNextFrame() { return 16 - this._now.ticks % 16; } clearTimer(timerId: number, type: TimerType) { + this._replayLogOnce(); + if (!timerId) { // null appears to be allowed in most browsers, and appears to be // relied upon by some libraries, like Bootstrap carousel @@ -356,64 +365,49 @@ export class ClockController { } } - advanceAutomatically(advanceTimeDelta: number = 20): () => void { - return this._embedder.postTaskPeriodically( - () => this._doTick(advanceTimeDelta!), - advanceTimeDelta, - ); + private _replayLogOnce() { + if (!this._log.length) + return; + + let lastLogTime = -1; + let isPaused = false; + + for (const { type, time, param } of this._log) { + if (!isPaused && lastLogTime !== -1) + this._advanceNow(this._now.ticks + time - lastLogTime); + lastLogTime = time; + + if (type === 'install') { + this._innerSetTime(param!); + } else if (type === 'fastForward' || type === 'runFor') { + this._advanceNow(this._now.ticks + param!); + } else if (type === 'fastForwardTo') { + this._innerSetTime(param!); + } else if (type === 'pause') { + this._innerPause(); + isPaused = true; + } else if (type === 'resume') { + this._innerResume(); + isPaused = false; + } else if (type === 'setFixedTime') { + this._innerSetFixedTime(param!); + } else if (type === 'setSystemTime') { + this._innerSetTime(param!); + } + } + + if (!isPaused && lastLogTime > 0) + this._advanceNow(this._now.ticks + this._embedder.dateNow() - lastLogTime); + + this._log.length = 0; } } -function getEpoch(epoch: Date | number | undefined): number { - if (!epoch) - return 0; - if (typeof epoch !== 'number') - return epoch.getTime(); - return epoch; -} - -function inRange(from: number, to: number, timer: Timer): boolean { - return timer && timer.callAt >= from && timer.callAt <= to; -} - -/** - * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into - * number of milliseconds. This is used to support human-readable strings passed - * to clock.tick() - */ -function parseTime(value: number | string): number { - if (typeof value === 'number') - return value; - if (!value) - return 0; - const str = value; - - const strings = str.split(':'); - const l = strings.length; - let i = l; - let ms = 0; - let parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error( - `Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`, - ); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - if (parsed >= 60) - throw new Error(`Invalid time ${str}`); - ms += parsed * Math.pow(60, l - i - 1); - } - - return ms * 1000; -} - function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date { - let prop; - for (prop of Object.keys(source) as (keyof DateConstructor)[]) - target[prop] = source[prop]; + for (const prop in source) { + if (source.hasOwnProperty(prop)) + target[prop] = (source as any)[prop]; + } target.toString = () => source.toString(); target.prototype = source.prototype; target.parse = source.parse; @@ -485,7 +479,7 @@ function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Int * All properties of Intl are non-enumerable, so we need * to do a bit of work to get them out. */ - for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) + for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[]) ClockIntl[key] = NativeIntl[key]; ClockIntl.DateTimeFormat = function(...args: any[]) { @@ -644,8 +638,8 @@ function getClearHandler(type: TimerType) { function fakePerformance(clock: ClockController, performance: Performance): Performance { const result: any = { now: () => clock.performanceNow(), - timeOrigin: clock.timeOrigin, }; + result.__defineGetter__('timeOrigin', () => clock._now.origin || 0); // eslint-disable-next-line no-proto for (const key of Object.keys((performance as any).__proto__)) { if (key === 'now' || key === 'timeOrigin') @@ -658,19 +652,22 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf return result; } -export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { +export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { const originals = platformOriginals(globalObject); - const embedder = { - postTask: (task: () => void) => { - originals.bound.setTimeout(task, 0); + const embedder: Embedder = { + dateNow: () => originals.raw.Date.now(), + performanceNow: () => originals.raw.performance!.now(), + setTimeout: (task: () => void, timeout?: number) => { + const timerId = originals.bound.setTimeout(task, timeout); + return () => originals.bound.clearTimeout(timerId); }, - postTaskPeriodically: (task: () => void, delay: number) => { - const intervalId = globalObject.setInterval(task, delay); + setInterval: (task: () => void, delay: number) => { + const intervalId = originals.bound.setInterval(task, delay); return () => originals.bound.clearInterval(intervalId); }, }; - const clock = new ClockController(embedder, config.now, config.loopLimit); + const clock = new ClockController(embedder); const api = createApi(clock, originals.bound); return { clock, api, originals: originals.raw }; } @@ -682,7 +679,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install throw new TypeError(`Can't install fake timers twice on the same global object.`); } - const { clock, api, originals } = createClock(globalObject, config); + const { clock, api, originals } = createClock(globalObject); const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[]; for (const method of toFake) { @@ -706,11 +703,10 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install } export function inject(globalObject: WindowOrWorkerGlobalScope) { + const { clock: controller } = install(globalObject); + controller.resume(); return { - install: (config: InstallConfig) => { - const { clock } = install(globalObject, config); - return clock; - }, + controller, builtin: platformOriginals(globalObject).bound, }; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 10dd00f198..7fe30e2ef2 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17247,8 +17247,40 @@ export interface BrowserServer { * controlled by the same clock. */ export interface Clock { + /** + * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user + * closing the laptop lid for a while and reopening it later, after given time. + * + * **Usage** + * + * ```js + * await page.clock.fastForward(1000); + * await page.clock.fastForward('30:00'); + * ``` + * + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are + * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + */ + fastForward(ticks: number|string): Promise; + + /** + * Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user + * closing the laptop lid for a while and reopening it at the specified time. + * + * **Usage** + * + * ```js + * await page.clock.fastForwardTo(new Date('2020-02-02')); + * await page.clock.fastForwardTo('2020-02-02'); + * ``` + * + * @param time + */ + fastForwardTo(time: number|string|Date): Promise; + /** * Install fake implementations for the following time-related functions: + * - `Date` * - `setTimeout` * - `clearTimeout` * - `setInterval` @@ -17261,34 +17293,33 @@ export interface Clock { * * Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, * and control the behavior of time-dependent functions. See - * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and - * [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) for more information. - * @param time Install fake timers with the specified base time. + * [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for) and + * [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward) for more information. * @param options */ - installFakeTimers(time: number|Date, options?: { + install(options?: { /** - * The maximum number of timers that will be run in - * [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`. + * Time to initialize with, current system time by default. */ - loopLimit?: number; + time?: number|string|Date; }): Promise; /** - * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be - * run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch. - * - * **Details** - * - * This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, - * or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite - * loop of timers and throws an error. + * Pause timers. Once this method is called, no timers are fired unless + * [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for), + * [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward), + * [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to) or + * [clock.resume()](https://playwright.dev/docs/api/class-clock#clock-resume) is called. */ - runAllTimers(): Promise; + pause(): Promise; /** - * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must - * be installed. Returns fake milliseconds since the unix epoch. + * Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual. + */ + resume(): Promise; + + /** + * Advance the clock, firing all the time-related callbacks. * * **Usage** * @@ -17297,55 +17328,41 @@ export interface Clock { * await page.clock.runFor('30:00'); * ``` * - * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are + * @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. */ - runFor(time: number|string): Promise; + runFor(ticks: number|string): Promise; /** - * This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as - * necessary. If new timers are added while it is executing they will be run only if they would occur before this - * time. This is useful when you want to run a test to completion, but the test recursively sets timers that would - * cause runAll to trigger an infinite loop warning. Fake timers must be installed. Returns fake milliseconds since - * the unix epoch. - */ - runToLastTimer(): Promise; - - /** - * Advances the clock to the moment of the first scheduled timer, firing it. Fake timers must be installed. Returns - * fake milliseconds since the unix epoch. - */ - runToNextTimer(): Promise; - - /** - * Set the clock to the specified time. - * - * When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as - * a browser) being put to sleep and resumed later, skipping intermediary timers. - * @param time - */ - setTime(time: number|Date): Promise; - - /** - * Advance the clock by jumping forward in time, equivalent to running - * [clock.setTime(time)](https://playwright.dev/docs/api/class-clock#clock-set-time) with the new target time. - * - * When fake timers are installed, [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) - * only fires due timers at most once, while - * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) fires all the timers up to the - * current time. Returns fake milliseconds since the unix epoch. + * Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running. * * **Usage** * * ```js - * await page.clock.skipTime(1000); - * await page.clock.skipTime('30:00'); + * await page.clock.setFixedTime(Date.now()); + * await page.clock.setFixedTime(new Date('2020-02-02')); + * await page.clock.setFixedTime('2020-02-02'); * ``` * - * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are - * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + * @param time Time to be set. */ - skipTime(time: number|string): Promise; + setFixedTime(time: number|string|Date): Promise; + + /** + * Sets current system time but does not trigger any timers, unlike + * [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to). + * + * **Usage** + * + * ```js + * await page.clock.setSystemTime(Date.now()); + * await page.clock.setSystemTime(new Date('2020-02-02')); + * await page.clock.setSystemTime('2020-02-02'); + * ``` + * + * @param time + */ + setSystemTime(time: number|string|Date): Promise; } /** diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index c8523e8de5..85bd3a8ffa 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1460,13 +1460,14 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise; - clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise; - clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise; + clockFastForward(params: BrowserContextClockFastForwardParams, metadata?: CallMetadata): Promise; + clockFastForwardTo(params: BrowserContextClockFastForwardToParams, metadata?: CallMetadata): Promise; + clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise; + clockPause(params?: BrowserContextClockPauseParams, metadata?: CallMetadata): Promise; + clockResume(params?: BrowserContextClockResumeParams, metadata?: CallMetadata): Promise; clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise; - clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise; - clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise; - clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise; - clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise; + clockSetFixedTime(params: BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata): Promise; + clockSetSystemTime(params: BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata): Promise; } export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, @@ -1755,58 +1756,66 @@ export type BrowserContextUpdateSubscriptionOptions = { }; export type BrowserContextUpdateSubscriptionResult = void; -export type BrowserContextClockInstallFakeTimersParams = { - time: number, - loopLimit?: number, +export type BrowserContextClockFastForwardParams = { + ticksNumber?: number, + ticksString?: string, }; -export type BrowserContextClockInstallFakeTimersOptions = { - loopLimit?: number, +export type BrowserContextClockFastForwardOptions = { + ticksNumber?: number, + ticksString?: string, }; -export type BrowserContextClockInstallFakeTimersResult = void; -export type BrowserContextClockRunAllTimersParams = {}; -export type BrowserContextClockRunAllTimersOptions = {}; -export type BrowserContextClockRunAllTimersResult = { - fakeTime: number, -}; -export type BrowserContextClockRunForParams = { +export type BrowserContextClockFastForwardResult = void; +export type BrowserContextClockFastForwardToParams = { timeNumber?: number, timeString?: string, }; +export type BrowserContextClockFastForwardToOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockFastForwardToResult = void; +export type BrowserContextClockInstallParams = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockInstallOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockInstallResult = void; +export type BrowserContextClockPauseParams = {}; +export type BrowserContextClockPauseOptions = {}; +export type BrowserContextClockPauseResult = void; +export type BrowserContextClockResumeParams = {}; +export type BrowserContextClockResumeOptions = {}; +export type BrowserContextClockResumeResult = void; +export type BrowserContextClockRunForParams = { + ticksNumber?: number, + ticksString?: string, +}; export type BrowserContextClockRunForOptions = { + ticksNumber?: number, + ticksString?: string, +}; +export type BrowserContextClockRunForResult = void; +export type BrowserContextClockSetFixedTimeParams = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockRunForResult = { - fakeTime: number, -}; -export type BrowserContextClockRunToLastTimerParams = {}; -export type BrowserContextClockRunToLastTimerOptions = {}; -export type BrowserContextClockRunToLastTimerResult = { - fakeTime: number, -}; -export type BrowserContextClockRunToNextTimerParams = {}; -export type BrowserContextClockRunToNextTimerOptions = {}; -export type BrowserContextClockRunToNextTimerResult = { - fakeTime: number, -}; -export type BrowserContextClockSetTimeParams = { - time: number, -}; -export type BrowserContextClockSetTimeOptions = { - -}; -export type BrowserContextClockSetTimeResult = void; -export type BrowserContextClockSkipTimeParams = { +export type BrowserContextClockSetFixedTimeOptions = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockSkipTimeOptions = { +export type BrowserContextClockSetFixedTimeResult = void; +export type BrowserContextClockSetSystemTimeParams = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockSkipTimeResult = { - fakeTime: number, +export type BrowserContextClockSetSystemTimeOptions = { + timeNumber?: number, + timeString?: string, }; +export type BrowserContextClockSetSystemTimeResult = void; export interface BrowserContextEvents { 'bindingCall': BrowserContextBindingCallEvent; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 7f6bd3bfe1..65fc2db4de 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1204,40 +1204,39 @@ BrowserContext: - requestFailed enabled: boolean - clockInstallFakeTimers: + clockFastForward: parameters: - time: number - loopLimit: number? + ticksNumber: number? + ticksString: string? - clockRunAllTimers: - returns: - fakeTime: number + clockFastForwardTo: + parameters: + timeNumber: number? + timeString: string? + + clockInstall: + parameters: + timeNumber: number? + timeString: string? + + clockPause: + + clockResume: clockRunFor: parameters: - timeNumber: number? - timeString: string? - returns: - fakeTime: number + ticksNumber: number? + ticksString: string? - clockRunToLastTimer: - returns: - fakeTime: number - - clockRunToNextTimer: - returns: - fakeTime: number - - clockSetTime: - parameters: - time: number - - clockSkipTime: + clockSetFixedTime: + parameters: + timeNumber: number? + timeString: string? + + clockSetSystemTime: parameters: timeNumber: number? timeString: string? - returns: - fakeTime: number events: diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index 554b49d1a7..a77356c172 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -18,8 +18,9 @@ import { test, expect } from '@playwright/test'; import { createClock as rawCreateClock, install as rawInstall } from '../../packages/playwright-core/src/server/injected/clock'; import type { InstallConfig, ClockController, ClockMethods } from '../../packages/playwright-core/src/server/injected/clock'; -const createClock = (now?: Date | number, loopLimit?: number): ClockController & ClockMethods => { - const { clock, api } = rawCreateClock(globalThis, { now, loopLimit }); +const createClock = (now?: number): ClockController & ClockMethods => { + const { clock, api } = rawCreateClock(globalThis); + clock.setSystemTime(now || 0); for (const key of Object.keys(api)) clock[key] = api[key]; return clock as ClockController & ClockMethods; @@ -27,26 +28,25 @@ const createClock = (now?: Date | number, loopLimit?: number): ClockController & type ClockFixtures = { clock: ClockController & ClockMethods; - now: Date | number | undefined; - loopLimit: number | undefined; - install: (config?: InstallConfig) => ClockController & ClockMethods; + now: number | undefined; + install: (now?: number) => ClockController & ClockMethods; installEx: (config?: InstallConfig) => { clock: ClockController, api: ClockMethods, originals: ClockMethods }; }; const it = test.extend({ - clock: async ({ now, loopLimit }, use) => { - const clock = createClock(now, loopLimit); + clock: async ({ now }, use) => { + const clock = createClock(now); await use(clock); }, now: undefined, - loopLimit: undefined, - install: async ({}, use) => { let clockObject: ClockController & ClockMethods; - const install = (config?: InstallConfig) => { - const { clock, api } = rawInstall(globalThis, config); + const install = (now?: number) => { + const { clock, api } = rawInstall(globalThis); + if (now) + clock.setSystemTime(now); for (const key of Object.keys(api)) clock[key] = api[key]; clockObject = clock as ClockController & ClockMethods; @@ -98,7 +98,7 @@ it.describe('setTimeout', () => { clock1.setTimeout(stubs[0], 100); clock2.setTimeout(stubs[1], 100); - await clock2.tick(200); + await clock2.runFor(200); expect(stubs[0].called).toBeFalsy(); expect(stubs[1].called).toBeTruthy(); @@ -110,7 +110,7 @@ it.describe('setTimeout', () => { evalCalled = true; // @ts-expect-error }, '10'); - await clock.tick(10); + await clock.runFor(10); expect(evalCalled).toBeTruthy(); }); @@ -120,7 +120,7 @@ it.describe('setTimeout', () => { evalCalled = true; // @ts-expect-error }, 'string'); - await clock.tick(10); + await clock.runFor(10); expect(evalCalled).toBeTruthy(); }); @@ -128,67 +128,52 @@ it.describe('setTimeout', () => { it('passes setTimeout parameters', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 2, 'the first', 'the second'); - await clock.tick(3); + await clock.runFor(3); expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); }); it('calls correct timeout on recursive tick', async ({ clock }) => { const stub = createStub(); const recurseCallback = () => { - void clock.tick(100); + void clock.runFor(100); }; clock.setTimeout(recurseCallback, 50); clock.setTimeout(stub, 100); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeTruthy(); }); it('does not depend on this', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - await clock.tick(100); + await clock.runFor(100); expect(stub.called).toBeTruthy(); }); it('is not influenced by forward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 5000); - await clock.tick(1000); - clock.setTime(new clock.Date().getTime() + 1000); - await clock.tick(3990); + await clock.runFor(1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); + await clock.runFor(3990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by backward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 5000); - await clock.tick(1000); - clock.setTime(new clock.Date().getTime() - 1000); - await clock.tick(3990); + await clock.runFor(1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); + await clock.runFor(3990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); - it('handles Infinity and negative Infinity correctly', async ({ clock }) => { - const calls = []; - clock.setTimeout(() => { - calls.push('NaN'); - }, NaN); - clock.setTimeout(() => { - calls.push('Infinity'); - }, Number.POSITIVE_INFINITY); - clock.setTimeout(() => { - calls.push('-Infinity'); - }, Number.NEGATIVE_INFINITY); - await clock.runAll(); - expect(calls).toEqual(['NaN', 'Infinity', '-Infinity']); - }); - it.describe('use of eval when not in node', () => { it.beforeEach(() => { globalThis.evalCalled = false; @@ -200,7 +185,7 @@ it.describe('setTimeout', () => { it('evals non-function callbacks', async ({ clock }) => { clock.setTimeout('globalThis.evalCalled = true', 10); - await clock.tick(10); + await clock.runFor(10); expect(globalThis.evalCalled).toBeTruthy(); }); @@ -209,7 +194,7 @@ it.describe('setTimeout', () => { const x = 15; try { clock.setTimeout('x', x); - await clock.tick(x); + await clock.runFor(x); expect(true).toBeFalsy(); } catch (e) { expect(e).toBeInstanceOf(ReferenceError); @@ -218,25 +203,25 @@ it.describe('setTimeout', () => { }); }); -it.describe('tick', () => { +it.describe('runFor', () => { it('triggers immediately without specified delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub); - await clock.tick(0); + await clock.runFor(0); expect(stub.called).toBeTruthy(); }); it('does not trigger without sufficient delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - await clock.tick(10); + await clock.runFor(10); expect(stub.called).toBeFalsy(); }); it('triggers after sufficient delay', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 100); - await clock.tick(100); + await clock.runFor(100); expect(stub.called).toBeTruthy(); }); @@ -244,7 +229,7 @@ it.describe('tick', () => { const spies = [createStub(), createStub()]; clock.setTimeout(spies[0], 100); clock.setTimeout(spies[1], 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); }); @@ -255,7 +240,7 @@ it.describe('tick', () => { clock.setTimeout(spies[1], 100); clock.setTimeout(spies[2], 99); clock.setTimeout(spies[3], 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); @@ -274,19 +259,19 @@ it.describe('tick', () => { // First spy calls another setTimeout with delay=0 clock.setTimeout(spies[0], 0); clock.setTimeout(spies[2], 10); - await clock.tick(10); + await clock.runFor(10); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); expect(spies[2].called).toBeTruthy(); }); it('waits after setTimeout was called', async ({ clock }) => { - await clock.tick(100); + await clock.runFor(100); const stub = createStub(); clock.setTimeout(stub, 150); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); - await clock.tick(100); + await clock.runFor(100); expect(stub.called).toBeTruthy(); }); @@ -294,19 +279,19 @@ it.describe('tick', () => { const stubs = [createStub(), createStub(), createStub()]; clock.setTimeout(stubs[0], 100); clock.setTimeout(stubs[1], 120); - await clock.tick(10); - await clock.tick(89); + await clock.runFor(10); + await clock.runFor(89); expect(stubs[0].called).toBeFalsy(); expect(stubs[1].called).toBeFalsy(); clock.setTimeout(stubs[2], 20); - await clock.tick(1); + await clock.runFor(1); expect(stubs[0].called).toBeTruthy(); expect(stubs[1].called).toBeFalsy(); expect(stubs[2].called).toBeFalsy(); - await clock.tick(19); + await clock.runFor(19); expect(stubs[1].called).toBeFalsy(); expect(stubs[2].called).toBeTruthy(); - await clock.tick(1); + await clock.runFor(1); expect(stubs[1].called).toBeTruthy(); }); @@ -316,7 +301,7 @@ it.describe('tick', () => { clock.setTimeout(stubs[0], 100); clock.setTimeout(stubs[1], 120); - await expect(clock.tick(120)).rejects.toThrow(); + await expect(clock.runFor(120)).rejects.toThrow(); expect(stubs[0].called).toBeTruthy(); expect(stubs[1].called).toBeTruthy(); @@ -325,7 +310,7 @@ it.describe('tick', () => { it('calls function with global object or null (strict mode) as this', async ({ clock }) => { const stub = createStub().throws(); clock.setTimeout(stub, 100); - await expect(clock.tick(100)).rejects.toThrow(); + await expect(clock.runFor(100)).rejects.toThrow(); expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); }); @@ -334,7 +319,7 @@ it.describe('tick', () => { clock.setTimeout(spies[0], 13); clock.setTimeout(spies[1], 11); - await clock.tick(15); + await clock.runFor(15); expect(spies[1].calledBefore(spies[0])).toBeTruthy(); }); @@ -346,7 +331,7 @@ it.describe('tick', () => { spy(new clock.Date().getTime()); }, 10); - await clock.tick(100); + await clock.runFor(100); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -364,7 +349,7 @@ it.describe('tick', () => { it('fires timer in intervals of 13', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 13); - await clock.tick(500); + await clock.runFor(500); expect(spy.callCount).toBe(38); }); @@ -372,7 +357,7 @@ it.describe('tick', () => { const spy = createStub(); // @ts-expect-error clock.setInterval(spy, '13'); - await clock.tick(500); + await clock.runFor(500); expect(spy.callCount).toBe(38); }); @@ -388,7 +373,7 @@ it.describe('tick', () => { spy10(new clock.Date().getTime()); }, 10); - await clock.tick(500); + await clock.runFor(500); expect(spy13.callCount).toBe(38); expect(spy10.callCount).toBe(50); @@ -402,7 +387,7 @@ it.describe('tick', () => { clock.setInterval(spies[0], 10); clock.setTimeout(spies[1], 50); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); expect(spies[0].callCount).toBe(10); @@ -417,62 +402,18 @@ it.describe('tick', () => { clock.clearInterval(id); }); id = clock.setInterval(callback, 10); - await clock.tick(100); + await clock.runFor(100); expect(callback.callCount).toBe(3); }); - it('passes 8 seconds', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 4000); - await clock.tick('08'); - expect(spy.callCount).toBe(2); - }); - - it('passes 1 minute', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 6000); - await clock.tick('01:00'); - expect(spy.callCount).toBe(10); - }); - - it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - await clock.tick('02:34:10'); - expect(spy.callCount).toBe(925); - }); - - it('throws for invalid format', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - - await expect(clock.tick('12:02:34:10')).rejects.toThrow(); - - expect(spy.callCount).toBe(0); - }); - - it('throws for invalid minutes', async ({ clock }) => { - const spy = createStub(); - clock.setInterval(spy, 10000); - await expect(clock.tick('67:10')).rejects.toThrow(); - expect(spy.callCount).toBe(0); - }); - it('throws for negative minutes', async ({ clock }) => { const spy = createStub(); clock.setInterval(spy, 10000); - await expect(clock.tick('-7:10')).rejects.toThrow(); + await expect(clock.runFor(-7)).rejects.toThrow(); expect(spy.callCount).toBe(0); }); - it('treats missing argument as 0', async ({ clock }) => { - // @ts-expect-error - await clock.tick(); - - expect(clock.now()).toBe(0); - }); - it('fires nested setTimeout calls properly', async ({ clock }) => { let i = 0; const callback = () => { @@ -483,7 +424,7 @@ it.describe('tick', () => { }; callback(); - await clock.tick(1000); + await clock.runFor(1000); expect(i).toBe(11); }); @@ -492,74 +433,69 @@ it.describe('tick', () => { throw new Error('oh no!'); }; clock.setTimeout(callback, 1000); - await expect(clock.tick(1000)).rejects.toThrow(); - }); - - it('returns the current now value', async ({ clock }) => { - const value = await clock.tick(200); - expect(clock.now()).toBe(value); + await expect(clock.runFor(1000)).rejects.toThrow(); }); it('is not influenced by forward system clock changes', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() + 1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tick(1990); + await clock.runFor(1990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by forward system clock changes 2', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() - 1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tick(1990); + await clock.runFor(1990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() + 1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); throw new Error(); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await expect(clock.tick(1990)).rejects.toThrow(); + await expect(clock.runFor(1990)).rejects.toThrow(); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('is not influenced by forward system clock changes when an error is thrown 2', async ({ clock }) => { const callback = () => { - clock.setTime(new clock.Date().getTime() - 1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); throw new Error(); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await expect(clock.tick(1990)).rejects.toThrow(); + await expect(clock.runFor(1990)).rejects.toThrow(); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); it('throws on negative ticks', async ({ clock }) => { - await expect(clock.tick(-500)).rejects.toThrow('Negative ticks are not supported'); + await expect(clock.runFor(-500)).rejects.toThrow('Negative ticks are not supported'); }); it('creates updated Date while ticking promises', async ({ clock }) => { @@ -571,7 +507,7 @@ it.describe('tick', () => { }); }, 10); - await clock.tick(100); + await clock.runFor(100); expect(spy.callCount).toBe(10); expect(spy.calledWith(10)).toBeTruthy(); @@ -602,7 +538,7 @@ it.describe('tick', () => { }); }, 10); - await clock.tick(500); + await clock.runFor(500); expect(spy13.callCount).toBe(38); expect(spy10.callCount).toBe(50); @@ -624,7 +560,7 @@ it.describe('tick', () => { }); id = clock.setInterval(callback, 10); - await clock.tick(100); + await clock.runFor(100); expect(callback.callCount).toBe(3); }); @@ -646,22 +582,22 @@ it.describe('tick', () => { // Clock API is async. await new Promise(setImmediate); - await clock.tick(1000); + await clock.runFor(1000); expect(i).toBe(11); }); it('is not influenced by forward system clock changes in promises', async ({ clock }) => { const callback = () => { void Promise.resolve().then(() => { - clock.setTime(new clock.Date().getTime() + 1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); }); }; const stub = createStub(); clock.setTimeout(callback, 1000); clock.setTimeout(stub, 2000); - await clock.tick(1990); + await clock.runFor(1990); expect(stub.callCount).toBe(0); - await clock.tick(20); + await clock.runFor(20); expect(stub.callCount).toBe(1); }); @@ -672,7 +608,7 @@ it.describe('tick', () => { void Promise.resolve().then(spy); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spy.called).toBeTruthy(); }); @@ -687,7 +623,7 @@ it.describe('tick', () => { .then(spies[2]); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -703,7 +639,7 @@ it.describe('tick', () => { void Promise.resolve().then(spies[2]); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].called).toBeTruthy(); expect(spies[1].called).toBeTruthy(); @@ -721,7 +657,7 @@ it.describe('tick', () => { }); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spy.called).toBeTruthy(); }); @@ -734,7 +670,7 @@ it.describe('tick', () => { void Promise.resolve().then(spies[2]).catch(spies[3]); }, 100); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].callCount).toBe(0); expect(spies[1].called).toBeTruthy(); @@ -751,7 +687,7 @@ it.describe('tick', () => { clock.setTimeout(spies[1], 200); - await clock.tick(200); + await clock.runFor(200); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -764,7 +700,7 @@ it.describe('tick', () => { // Clock API is async. await new Promise(setImmediate); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); @@ -781,516 +717,18 @@ it.describe('tick', () => { // Clock API is async. await new Promise(setImmediate); - await clock.tick(100); + await clock.runFor(100); expect(spies[0].calledBefore(spies[1])).toBeTruthy(); }); }); -it.describe('next', () => { - it('triggers the next timer', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100); - - await clock.next(); - - expect(stub.called).toBeTruthy(); - }); - - it('does not trigger simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - - await clock.next(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - }); - - it('subsequent calls trigger simultaneous timers', async ({ clock }) => { - const spies = [createStub(), createStub(), createStub(), createStub()]; - clock.setTimeout(spies[0], 100); - clock.setTimeout(spies[1], 100); - clock.setTimeout(spies[2], 99); - clock.setTimeout(spies[3], 100); - - await clock.next(); - - expect(spies[2].called).toBeTruthy(); - expect(spies[0].called).toBeFalsy(); - expect(spies[1].called).toBeFalsy(); - expect(spies[3].called).toBeFalsy(); - - await clock.next(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - expect(spies[3].called).toBeFalsy(); - - await clock.next(); - - expect(spies[1].called).toBeTruthy(); - expect(spies[3].called).toBeFalsy(); - - await clock.next(); - - expect(spies[3].called).toBeTruthy(); - }); - - it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 0); - }), - createStub(), - createStub(), - ]; - - // First spy calls another setTimeout with delay=0 - clock.setTimeout(spies[0], 0); - clock.setTimeout(spies[2], 10); - - await clock.next(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - - await clock.next(); - - expect(spies[1].called).toBeTruthy(); - - await clock.next(); - - expect(spies[2].called).toBeTruthy(); - }); - - it('throws exception thrown by timer', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - await expect(clock.next()).rejects.toThrow(); - expect(stub.called).toBeTruthy(); - }); - - it('calls function with global object or null (strict mode) as this', async ({ clock }) => { - const stub = createStub().throws(); - clock.setTimeout(stub, 100); - await expect(clock.next()).rejects.toThrow(); - expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); - }); - - it('subsequent calls trigger in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 13); - clock.setTimeout(spies[1], 11); - - await clock.next(); - await clock.next(); - - expect(spies[1].calledBefore(spies[0])).toBeTruthy(); - }); - - it('creates updated Date while ticking', async ({ clock }) => { - const spy = createStub(); - - clock.setInterval(() => { - spy(new clock.Date().getTime()); - }, 10); - - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(spy.callCount).toBe(10); - expect(spy.calledWith(10)).toBeTruthy(); - expect(spy.calledWith(20)).toBeTruthy(); - expect(spy.calledWith(30)).toBeTruthy(); - expect(spy.calledWith(40)).toBeTruthy(); - expect(spy.calledWith(50)).toBeTruthy(); - expect(spy.calledWith(60)).toBeTruthy(); - expect(spy.calledWith(70)).toBeTruthy(); - expect(spy.calledWith(80)).toBeTruthy(); - expect(spy.calledWith(90)).toBeTruthy(); - expect(spy.calledWith(100)).toBeTruthy(); - }); - - it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setInterval(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - expect(spies[0].callCount).toBe(5); - expect(spies[1].callCount).toBe(1); - }); - - it('subsequent calls do not fire canceled intervals', async ({ clock }) => { - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let id; - const callback = createStub(() => { - if (callback.callCount === 3) - clock.clearInterval(id); - }); - - id = clock.setInterval(callback, 10); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(callback.callCount).toBe(3); - }); - - it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { - clock.setTimeout(createStub(), 55); - await clock.next(); - - expect(clock.now()).toBe(55); - }); - - it('returns the current now value', async ({ clock }) => { - clock.setTimeout(createStub(), 55); - const value = await clock.next(); - - expect(clock.now()).toBe(value); - }); - - it('does not fire intervals canceled in promises', async ({ clock }) => { - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let id; - const callback = createStub(() => { - if (callback.callCount === 3) { - void Promise.resolve().then(() => { - clock.clearInterval(id); - }); - } - }); - - id = clock.setInterval(callback, 10); - await clock.next(); - await clock.next(); - await clock.next(); - await clock.next(); - - expect(callback.callCount).toBe(3); - }); - - it('should settle user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(spy); - }, 55); - - await clock.next(); - - expect(spy.called).toBeTruthy(); - }); - - it('should settle nested user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(spy); - }); - }); - }, 55); - - await clock.next(); - - expect(spy.called).toBeTruthy(); - }); - - it('should settle local promises before firing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - - // Clock API is async. - await new Promise(setImmediate); - await clock.next(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); -}); - -it.describe('runAll', () => { - it('if there are no timers just return', async ({ clock }) => { - await clock.runAll(); - }); - - it('runs all timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - - await clock.runAll(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added while running are also run', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 50); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - - await clock.runAll(); - - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('throws before allowing infinite recursion', async ({ clock }) => { - const recursiveCallback = () => { - clock.setTimeout(recursiveCallback, 10); - }; - recursiveCallback(); - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('the loop limit can be set when creating a clock', async ({}) => { - const clock = createClock(0, 1); - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('the loop limit can be set when installing a clock', async ({ install }) => { - const clock = install({ loopLimit: 1 }); - const spies = [createStub(), createStub()]; - setTimeout(spies[0], 10); - setTimeout(spies[1], 50); - - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('throws before allowing infinite recursion from promises', async ({ clock }) => { - const recursiveCallback = () => { - void Promise.resolve().then(() => { - clock.setTimeout(recursiveCallback, 10); - }); - }; - recursiveCallback(); - - // Clock API is async. - await new Promise(setImmediate); - await expect(clock.runAll()).rejects.toThrow(); - }); - - it('should settle user-created promises', async ({ clock }) => { - const spy = createStub(); - clock.setTimeout(() => { - void Promise.resolve().then(spy); - }, 55); - await clock.runAll(); - expect(spy.called).toBeTruthy(); - }); - - it('should settle nested user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(spy); - }); - }); - }, 55); - - await clock.runAll(); - - expect(spy.called).toBeTruthy(); - }); - - it('should settle local promises before firing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - - // Clock API is async. - await new Promise(setImmediate); - await clock.runAll(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); - - it('should settle user-created promises before firing more timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(() => { - void Promise.resolve().then(spies[0]); - }, 55); - clock.setTimeout(spies[1], 75); - await clock.runAll(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); -}); - -it.describe('runToLast', () => { - it('returns current time when there are no timers', async ({ clock }) => { - const time = await clock.runToLast(); - expect(time).toBe(0); - }); - - it('runs all existing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('returns time of the last timer', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 50); - const time = await clock.runToLast(); - expect(time).toBe(50); - }); - - it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(spies[0], 10); - clock.setTimeout(spies[1], 10); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - }); - - it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { - const spies = [ - createStub(() => { - clock.setTimeout(spies[1], 50); - }), - createStub(), - ]; - - // Spy calls another setTimeout - clock.setTimeout(spies[0], 10); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeFalsy(); - }); - - it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { - const spies = [ - createStub(), - createStub(() => { - clock.setTimeout(spies[2], 50); - }), - createStub(), - ]; - - clock.setTimeout(spies[0], 100); - // Spy calls another setTimeout - clock.setTimeout(spies[1], 10); - await clock.runToLast(); - expect(spies[0].called).toBeTruthy(); - expect(spies[1].called).toBeTruthy(); - expect(spies[2].called).toBeTruthy(); - }); - - it('new timers cannot cause an infinite loop', async ({ clock }) => { - const spy = createStub(); - const recursiveCallback = () => { - clock.setTimeout(recursiveCallback, 0); - }; - - clock.setTimeout(recursiveCallback, 0); - clock.setTimeout(spy, 100); - await clock.runToLast(); - expect(spy.called).toBeTruthy(); - }); - - it('should support clocks with start time', async ({ clock }) => { - let invocations = 0; - - clock.setTimeout(function cb() { - invocations++; - clock.setTimeout(cb, 50); - }, 50); - - await clock.runToLast(); - - expect(invocations).toBe(1); - }); - - it('should settle user-created promises', async ({ clock }) => { - const spy = createStub(); - clock.setTimeout(() => { - void Promise.resolve().then(spy); - }, 55); - await clock.runToLast(); - expect(spy.called).toBeTruthy(); - }); - - it('should settle nested user-created promises', async ({ clock }) => { - const spy = createStub(); - - clock.setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(spy); - }); - }); - }, 55); - - await clock.runToLast(); - expect(spy.called).toBeTruthy(); - }); - - it('should settle local promises before firing timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - void Promise.resolve().then(spies[0]); - clock.setTimeout(spies[1], 55); - - // Clock API is async. - await new Promise(setImmediate); - await clock.runToLast(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); - - it('should settle user-created promises before firing more timers', async ({ clock }) => { - const spies = [createStub(), createStub()]; - clock.setTimeout(() => { - void Promise.resolve().then(spies[0]); - }, 55); - clock.setTimeout(spies[1], 75); - await clock.runToLast(); - expect(spies[0].calledBefore(spies[1])).toBeTruthy(); - }); -}); - it.describe('clearTimeout', () => { it('removes timeout', async ({ clock }) => { const stub = createStub(); const id = clock.setTimeout(stub, 50); clock.clearTimeout(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1298,7 +736,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setInterval(stub, 50); clock.clearTimeout(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1306,7 +744,7 @@ it.describe('clearTimeout', () => { const stub = createStub(); const id = clock.setInterval(stub); clock.clearTimeout(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1315,21 +753,6 @@ it.describe('clearTimeout', () => { }); }); -it.describe('reset', () => { - it('resets to the time install with - issue #183', async ({ clock }) => { - await clock.tick(100); - clock.reset(); - expect(clock.now()).toBe(0); - }); - - it('resets hrTime - issue #206', async ({ clock }) => { - await clock.tick(100); - expect(clock.performance.now()).toEqual(100); - clock.reset(); - expect(clock.performance.now()).toEqual(0); - }); -}); - it.describe('setInterval', () => { it('throws if no arguments', async ({ clock }) => { expect(() => { @@ -1353,7 +776,7 @@ it.describe('setInterval', () => { it('schedules recurring timeout', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - await clock.tick(99); + await clock.runFor(99); expect(stub.callCount).toBe(9); }); @@ -1361,23 +784,23 @@ it.describe('setInterval', () => { it('is not influenced by forward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - await clock.tick(11); + await clock.runFor(11); expect(stub.callCount).toBe(1); - clock.setTime(new clock.Date().getTime() + 1000); - await clock.tick(8); + clock.setSystemTime(new clock.Date().getTime() + 1000); + await clock.runFor(8); expect(stub.callCount).toBe(1); - await clock.tick(3); + await clock.runFor(3); expect(stub.callCount).toBe(2); }); it('is not influenced by backward system clock changes', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 10); - await clock.tick(5); - clock.setTime(new clock.Date().getTime() - 1000); - await clock.tick(6); + await clock.runFor(5); + clock.setSystemTime(new clock.Date().getTime() - 1000); + await clock.runFor(6); expect(stub.callCount).toBe(1); - await clock.tick(10); + await clock.runFor(10); expect(stub.callCount).toBe(2); }); @@ -1388,7 +811,7 @@ it.describe('setInterval', () => { }); const id = clock.setInterval(stub, 10); - await clock.tick(100); + await clock.runFor(100); expect(stub.callCount).toBe(3); }); @@ -1396,7 +819,7 @@ it.describe('setInterval', () => { it('passes setTimeout parameters', async ({ clock }) => { const stub = createStub(); clock.setInterval(stub, 2, 'the first', 'the second'); - await clock.tick(3); + await clock.runFor(3); expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); }); }); @@ -1406,7 +829,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setInterval(stub, 50); clock.clearInterval(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1414,7 +837,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setInterval(stub); clock.clearInterval(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1422,7 +845,7 @@ it.describe('clearInterval', () => { const stub = createStub(); const id = clock.setTimeout(stub, 50); clock.clearInterval(id); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeFalsy(); }); @@ -1458,14 +881,14 @@ it.describe('date', () => { it('listens to ticking clock', async ({ clock }) => { const date1 = new clock.Date(); - await clock.tick(3); + await clock.runFor(3); const date2 = new clock.Date(); expect(date2.getTime() - date1.getTime()).toBe(3); }); it('listens to system clock changes', async ({ clock }) => { const date1 = new clock.Date(); - clock.setTime(date1.getTime() + 1000); + clock.setSystemTime(date1.getTime() + 1000); const date2 = new clock.Date(); expect(date2.getTime() - date1.getTime()).toBe(1000); }); @@ -1568,16 +991,16 @@ it.describe('stubTimers', () => { it('returns clock object', async ({ install }) => { const clock = install(); expect(clock).toEqual(expect.any(Object)); - expect(clock.tick).toEqual(expect.any(Function)); + expect(clock.runFor).toEqual(expect.any(Function)); }); it('takes an object parameter', async ({ install }) => { - const clock = install({}); + const clock = install(); expect(clock).toEqual(expect.any(Object)); }); it('sets initial timestamp', async ({ install }) => { - const clock = install({ now: 1400 }); + const clock = install(1400); expect(clock.now()).toBe(1400); }); @@ -1586,7 +1009,7 @@ it.describe('stubTimers', () => { const stub = createStub(); setTimeout(stub, 1000); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeTruthy(); }); @@ -1603,7 +1026,7 @@ it.describe('stubTimers', () => { const stub = createStub(); clearTimeout(setTimeout(stub, 1000)); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeFalsy(); }); @@ -1613,7 +1036,7 @@ it.describe('stubTimers', () => { const stub = createStub(); setInterval(stub, 500); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.callCount).toBe(2); }); @@ -1623,7 +1046,7 @@ it.describe('stubTimers', () => { const stub = createStub(); clearInterval(setInterval(stub, 500)); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeFalsy(); }); @@ -1631,7 +1054,7 @@ it.describe('stubTimers', () => { it('replaces global performance.now', async ({ install }) => { const clock = install(); const prev = performance.now(); - await clock.tick(1000); + await clock.runFor(1000); const next = performance.now(); expect(next).toBe(1000); expect(prev).toBe(0); @@ -1733,12 +1156,6 @@ it.describe('stubTimers', () => { expect(Date.prototype).toEqual(clock.Date.prototype); }); - it('decide on Date.now support at call-time when supported', async ({ install }) => { - (Date.now as any) = () => {}; - install({ now: 0 }); - expect(Date.now).toEqual(expect.any(Function)); - }); - it('mirrors custom Date properties', async ({ install }) => { const f = () => { return ''; @@ -1761,7 +1178,7 @@ it.describe('stubTimers', () => { expect(Date).not.toBe(originals.Date); }); - it('resets faked methods', async ({ install }) => { + it('resets faked methods', async ({ }) => { const { clock, originals } = rawInstall(globalThis, { now: 0, toFake: ['setTimeout', 'Date'], @@ -1784,60 +1201,6 @@ it.describe('stubTimers', () => { }); }); -it.describe('shouldAdvanceTime', () => { - it('should create an auto advancing timer', async () => { - const testDelay = 29; - const date = new Date('2015-09-25'); - const clock = createClock(date); - clock.advanceAutomatically(); - expect(clock.Date.now()).toBe(1443139200000); - const timeoutStarted = clock.Date.now(); - - let callback: (r: number) => void; - const promise = new Promise(r => callback = r); - - clock.setTimeout(() => { - const timeDifference = clock.Date.now() - timeoutStarted; - callback(timeDifference); - }, testDelay); - expect(await promise).toBe(testDelay); - - }); - - it('should test setInterval', async () => { - const interval = 20; - let intervalsTriggered = 0; - const cyclesToTrigger = 3; - const date = new Date('2015-09-25'); - const clock = createClock(date); - clock.advanceAutomatically(); - expect(clock.Date.now()).toBe(1443139200000); - const timeoutStarted = clock.Date.now(); - - let callback: (r: number) => void; - const promise = new Promise(r => callback = r); - - const intervalId = clock.setInterval(() => { - if (++intervalsTriggered === cyclesToTrigger) { - clock.clearInterval(intervalId); - const timeDifference = clock.Date.now() - timeoutStarted; - callback(timeDifference); - } - }, interval); - - expect(await promise).toBe(interval * cyclesToTrigger); - }); - - it('should not depend on having to stub setInterval or clearInterval to work', async ({ install }) => { - const origSetInterval = globalThis.setInterval; - const origClearInterval = globalThis.clearInterval; - - install({ toFake: ['setTimeout'] }); - expect(globalThis.setInterval).toBe(origSetInterval); - expect(globalThis.clearInterval).toBe(origClearInterval); - }); -}); - it.describe('requestAnimationFrame', () => { it('throws if no arguments', async ({ clock }) => { expect(() => { @@ -1860,30 +1223,30 @@ it.describe('requestAnimationFrame', () => { it('should run every 16ms', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(15); + await clock.runFor(15); expect(stub.callCount).toBe(0); - await clock.tick(1); + await clock.runFor(1); expect(stub.callCount).toBe(1); }); it('should be called with performance.now() when available', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(20); + await clock.runFor(20); expect(stub.calledWith(16)).toBeTruthy(); }); it('should be called with performance.now() even when performance unavailable', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(20); + await clock.runFor(20); expect(stub.calledWith(16)).toBeTruthy(); }); it('should call callback once', async ({ clock }) => { const stub = createStub(); clock.requestAnimationFrame(stub); - await clock.tick(32); + await clock.runFor(32); expect(stub.callCount).toBe(1); }); @@ -1891,9 +1254,9 @@ it.describe('requestAnimationFrame', () => { const stub1 = createStub(); const stub2 = createStub(); clock.requestAnimationFrame(stub1); - await clock.tick(5); + await clock.runFor(5); clock.requestAnimationFrame(stub2); - await clock.tick(11); + await clock.runFor(11); expect(stub1.calledWith(16)).toBeTruthy(); expect(stub2.calledWith(16)).toBeTruthy(); }); @@ -1902,18 +1265,18 @@ it.describe('requestAnimationFrame', () => { const stub1 = createStub(); const stub2 = createStub(); clock.requestAnimationFrame(stub1); - await clock.tick(57); + await clock.runFor(57); clock.requestAnimationFrame(stub2); - await clock.tick(10); + await clock.runFor(10); expect(stub1.calledWith(16)).toBeTruthy(); expect(stub2.calledWith(64)).toBeTruthy(); }); it('should schedule for next frame if on current frame', async ({ clock }) => { const stub = createStub(); - await clock.tick(16); + await clock.runFor(16); clock.requestAnimationFrame(stub); - await clock.tick(16); + await clock.runFor(16); expect(stub.calledWith(32)).toBeTruthy(); }); }); @@ -1923,7 +1286,7 @@ it.describe('cancelAnimationFrame', () => { const stub = createStub(); const id = clock.requestAnimationFrame(stub); clock.cancelAnimationFrame(id); - await clock.tick(16); + await clock.runFor(16); expect(stub.called).toBeFalsy(); }); @@ -1933,7 +1296,7 @@ it.describe('cancelAnimationFrame', () => { expect(() => { clock.cancelAnimationFrame(id); }).toThrow(); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeTruthy(); }); @@ -1943,7 +1306,7 @@ it.describe('cancelAnimationFrame', () => { expect(() => { clock.cancelAnimationFrame(id); }).toThrow(); - await clock.tick(50); + await clock.runFor(50); expect(stub.called).toBeTruthy(); }); @@ -1952,21 +1315,11 @@ it.describe('cancelAnimationFrame', () => { }); }); -it.describe('runToFrame', () => { - it('should tick next frame', async ({ clock }) => { - await clock.runToFrame(); - expect(clock.now()).toBe(16); - await clock.tick(3); - await clock.runToFrame(); - expect(clock.now()).toBe(32); - }); -}); - -it.describe('jump', () => { +it.describe('fastForward', () => { it('ignores timers which wouldn\'t be run', async ({ clock }) => { const stub = createStub(); clock.setTimeout(stub, 1000); - await clock.jump(500); + await clock.fastForward(500); expect(stub.called).toBeFalsy(); }); @@ -1975,7 +1328,7 @@ it.describe('jump', () => { clock.setTimeout(() => { stub(clock.Date.now()); }, 1000); - await clock.jump(2000); + await clock.fastForward(2000); expect(stub.callCount).toBe(1); expect(stub.calledWith(2000)).toBeTruthy(); }); @@ -1988,19 +1341,12 @@ it.describe('jump', () => { clock.setTimeout(shortTimers[0], 250); clock.setInterval(shortTimers[1], 100); clock.requestAnimationFrame(shortTimers[2]); - await clock.jump(1500); + await clock.fastForward(1500); for (const stub of longTimers) expect(stub.called).toBeFalsy(); for (const stub of shortTimers) expect(stub.callCount).toBe(1); }); - - it('supports string time arguments', async ({ clock }) => { - const stub = createStub(); - clock.setTimeout(stub, 100000); // 100000 = 1:40 - await clock.jump('01:50'); - expect(stub.callCount).toBe(1); - }); }); it.describe('performance.now()', () => { @@ -2010,7 +1356,7 @@ it.describe('performance.now()', () => { }); it('should run along with clock.tick', async ({ clock }) => { - await clock.tick(5000); + await clock.runFor(5000); const result = clock.performance.now(); expect(result).toBe(5000); }); @@ -2019,7 +1365,7 @@ it.describe('performance.now()', () => { for (let i = 0; i < 10; i++) { const next = clock.performance.now(); expect(next).toBe(1000 * i); - await clock.tick(1000); + await clock.runFor(1000); } }); @@ -2028,7 +1374,7 @@ it.describe('performance.now()', () => { const result = clock.performance.now(); expect(result).toBe(2500); }, 2500); - await clock.tick(5000); + await clock.runFor(5000); }); }); @@ -2054,7 +1400,7 @@ it.describe('requestIdleCallback', () => { it('runs after all timers', async ({ clock }) => { const stub = createStub(); clock.requestIdleCallback(stub); - await clock.tick(1000); + await clock.runFor(1000); expect(stub.called).toBeTruthy(); const idleCallbackArg = stub.firstCall.args[0]; expect(idleCallbackArg.didTimeout).toBeFalsy(); @@ -2066,7 +1412,7 @@ it.describe('requestIdleCallback', () => { clock.setTimeout(() => {}, 10); clock.setTimeout(() => {}, 30); clock.requestIdleCallback(stub, { timeout: 20 }); - await clock.tick(20); + await clock.runFor(20); expect(stub.called).toBeTruthy(); }); @@ -2074,7 +1420,7 @@ it.describe('requestIdleCallback', () => { const stub = createStub(); clock.setTimeout(() => {}, 30); clock.requestIdleCallback(stub); - await clock.tick(35); + await clock.runFor(35); expect(stub.called).toBeFalsy(); }); }); @@ -2084,149 +1430,11 @@ it.describe('cancelIdleCallback', () => { const stub = createStub(); const callbackId = clock.requestIdleCallback(stub, { timeout: 0 }); clock.cancelIdleCallback(callbackId); - await clock.tick(0); + await clock.runFor(0); expect(stub.called).toBeFalsy(); }); }); -it.describe('loop limit stack trace', () => { - const expectedMessage = - 'Aborting after running 5 timers, assuming an infinite loop!'; - it.use({ loopLimit: 5 }); - - it.describe('setTimeout', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.setTimeout(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.setTimeout(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); - - it.describe('requestIdleCallback', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); - - it.describe('setInterval', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.setInterval(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.setInterval(recursiveCreateTimer, 10); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); - - it.describe('requestAnimationFrame', () => { - it('provides a stack trace for running all async', async ({ clock }) => { - const catchSpy = createStub(); - const recursiveCreateTimer = () => { - clock.requestAnimationFrame(recursiveCreateTimer); - }; - - recursiveCreateTimer(); - await clock.runAll().catch(catchSpy); - expect(catchSpy.callCount).toBe(1); - const err = catchSpy.firstCall.args[0]; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); - }); - - it('provides a stack trace for running all sync', async ({ clock }) => { - let caughtError = false; - const recursiveCreateTimer = () => { - clock.requestAnimationFrame(recursiveCreateTimer); - }; - - recursiveCreateTimer(); - try { - await clock.runAll(); - } catch (err) { - caughtError = true; - expect(err.message).toBe(expectedMessage); - expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); - } - expect(caughtError).toBeTruthy(); - }); - }); -}); - it.describe('Intl API', () => { function isFirstOfMonth(ianaTimeZone, timestamp?: number) { return ( @@ -2301,13 +1509,13 @@ it.describe('Intl API', () => { it('formatToParts via isFirstOfMonth -> Returns true when passed no timestamp and system time is first of the month', async ({ install }) => { // June 1 04:00 UTC - Toronto is June 1 00:00 - install({ now: Date.UTC(2022, 5, 1, 4) }); + install(Date.UTC(2022, 5, 1, 4)); expect(isFirstOfMonth('America/Toronto')).toBeTruthy(); }); it('formatToParts via isFirstOfMonth -> Returns false when passed no timestamp and system time is not first of the month', async ({ install }) => { // June 1 00:00 UTC - Toronto is May 31 20:00 - install({ now: Date.UTC(2022, 5, 1) }); + install(Date.UTC(2022, 5, 1)); expect(isFirstOfMonth('America/Toronto')).toBeFalsy(); }); diff --git a/tests/page/page-clock.frozen.spec.ts b/tests/page/page-clock.frozen.spec.ts index 3d763a2cc3..bddb794da1 100644 --- a/tests/page/page-clock.frozen.spec.ts +++ b/tests/page/page-clock.frozen.spec.ts @@ -18,6 +18,7 @@ import { test as it, expect } from './pageTest'; it.skip(!process.env.PW_FREEZE_TIME); -it('cock should be frozen', async ({ page }) => { +it('clock should be frozen', async ({ page }) => { + await page.clock.setSystemTime(0); expect(await page.evaluate('Date.now()')).toBe(0); }); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 4a6ca00896..fdc01c5570 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -35,8 +35,12 @@ const it = test.extend<{ calls: { params: any[] }[] }>({ }); it.describe('runFor', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + }); + it('triggers immediately without specified delay', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub); }); @@ -46,7 +50,6 @@ it.describe('runFor', () => { }); it('does not trigger without sufficient delay', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); @@ -55,7 +58,6 @@ it.describe('runFor', () => { }); it('triggers after sufficient delay', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); @@ -64,7 +66,6 @@ it.describe('runFor', () => { }); it('triggers simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); @@ -74,7 +75,6 @@ it.describe('runFor', () => { }); it('triggers multiple simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); @@ -86,7 +86,6 @@ it.describe('runFor', () => { }); it('waits after setTimeout was called', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 150); }); @@ -97,18 +96,16 @@ it.describe('runFor', () => { }); it('triggers event when some throw', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { throw new Error(); }, 100); setTimeout(window.stub, 120); }); - await expect(page.clock.runFor(120)).rejects.toThrow(); expect(calls).toHaveLength(1); }); it('creates updated Date while ticking', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); + await page.clock.setSystemTime(0); await page.evaluate(async () => { setInterval(() => { window.stub(new Date().getTime()); @@ -130,7 +127,6 @@ it.describe('runFor', () => { }); it('passes 8 seconds', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 4000); }); @@ -140,7 +136,6 @@ it.describe('runFor', () => { }); it('passes 1 minute', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 6000); }); @@ -150,7 +145,6 @@ it.describe('runFor', () => { }); it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); @@ -160,7 +154,6 @@ it.describe('runFor', () => { }); it('throws for invalid format', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); @@ -169,332 +162,93 @@ it.describe('runFor', () => { }); it('returns the current now value', async ({ page }) => { - await page.clock.installFakeTimers(0); + await page.clock.setSystemTime(0); const value = 200; await page.clock.runFor(value); expect(await page.evaluate(() => Date.now())).toBe(value); }); }); -it.describe('skipTime', () => { +it.describe('fastForward', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); + }); + it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub('should not be logged'); }, 1000); }); - await page.clock.skipTime(500); + await page.clock.fastForward(500); expect(calls).toEqual([]); }); it('pushes back execution time for skipped timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 1000); }); - await page.clock.skipTime(2000); + await page.clock.fastForward(2000); expect(calls).toEqual([{ params: [2000] }]); }); it('supports string time arguments', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 100000); // 100000 = 1:40 }); - await page.clock.skipTime('01:50'); + await page.clock.fastForward('01:50'); expect(calls).toEqual([{ params: [110000] }]); }); }); -it.describe('runAllTimers', () => { - it('if there are no timers just return', async ({ page }) => { - await page.clock.installFakeTimers(0); - await page.clock.runAllTimers(); +it.describe('fastForwardTo', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); }); - it('runs all timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - await page.clock.runAllTimers(); - expect(calls.length).toBe(2); - }); - - it('new timers added while running are also run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); + it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { await page.evaluate(async () => { setTimeout(() => { - setTimeout(window.stub, 50); - }, 10); + window.stub('should not be logged'); + }, 1000); }); - await page.clock.runAllTimers(); - expect(calls.length).toBe(1); + await page.clock.fastForwardTo(500); + expect(calls).toEqual([]); }); - it('new timers added in promises while running are also run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); + it('pushes back execution time for skipped timers', async ({ page, calls }) => { await page.evaluate(async () => { setTimeout(() => { - void Promise.resolve().then(() => { - setTimeout(window.stub, 50); - }); - }, 10); + window.stub(Date.now()); + }, 1000); }); - await page.clock.runAllTimers(); - expect(calls.length).toBe(1); - }); - it('throws before allowing infinite recursion', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - window.stub(); - setTimeout(recursiveCallback, 10); - }; - setTimeout(recursiveCallback, 10); - }); - await expect(page.clock.runAllTimers()).rejects.toThrow(); - expect(calls).toHaveLength(1000); - }); - - it('throws before allowing infinite recursion from promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - window.stub(); - void Promise.resolve().then(() => { - setTimeout(recursiveCallback, 10); - }); - }; - setTimeout(recursiveCallback, 10); - }); - await expect(page.clock.runAllTimers()).rejects.toThrow(); - expect(calls).toHaveLength(1000); - }); - - it('the loop limit can be set when creating a clock', async ({ page, calls }) => { - await page.clock.installFakeTimers(0, { loopLimit: 1 }); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - await expect(page.clock.runAllTimers()).rejects.toThrow(); - expect(calls).toHaveLength(1); - }); - - it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => window.stub()); - }, 55); - }); - await page.clock.runAllTimers(); - expect(calls).toHaveLength(1); - }); - - it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => window.stub()); - }); - }); - }, 55); - }); - await page.clock.runAllTimers(); - expect(calls).toHaveLength(1); - }); - - it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - void Promise.resolve().then(() => window.stub(1)); - setTimeout(() => window.stub(2), 55); - }); - await page.clock.runAllTimers(); - expect(calls).toEqual([ - { params: [1] }, - { params: [2] }, - ]); - }); -}); - -it.describe('runToLastTimer', () => { - it('returns current time when there are no timers', async ({ page }) => { - await page.clock.installFakeTimers(0); - const time = await page.clock.runToLastTimer(); - expect(time).toBe(0); - }); - - it('runs all existing timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(2); - }); - - it('returns time of the last timer', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 50); - }); - const time = await page.clock.runToLastTimer(); - expect(time).toBe(50); - }); - - it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 10); - setTimeout(window.stub, 10); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(2); - }); - - it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - window.stub(); - setTimeout(window.stub, 50); - }, 10); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 100); - setTimeout(() => { - setTimeout(window.stub, 50); - }, 10); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(2); - }); - - it('new timers cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - window.stub(); - setTimeout(recursiveCallback, 0); - }; - setTimeout(recursiveCallback, 0); - setTimeout(window.stub, 100); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(102); - }); - - it('should support clocks with start time', async ({ page, calls }) => { - await page.clock.installFakeTimers(200); - await page.evaluate(async () => { - setTimeout(function cb() { - window.stub(); - setTimeout(cb, 50); - }, 50); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - const recursiveCallback = () => { - void Promise.resolve().then(() => { - setTimeout(recursiveCallback, 0); - }); - }; - setTimeout(recursiveCallback, 0); - setTimeout(window.stub, 100); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => window.stub()); - }, 55); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => { - void Promise.resolve().then(() => window.stub()); - }); - }); - }, 55); - }); - await page.clock.runToLastTimer(); - expect(calls.length).toBe(1); - }); - - it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - void Promise.resolve().then(() => window.stub(1)); - setTimeout(() => window.stub(2), 55); - }); - await page.clock.runToLastTimer(); - expect(calls).toEqual([ - { params: [1] }, - { params: [2] }, - ]); - }); - - it('should settle user-created promises before firing more timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - void Promise.resolve().then(() => window.stub(1)); - }, 55); - setTimeout(() => window.stub(2), 75); - }); - await page.clock.runToLastTimer(); - expect(calls).toEqual([ - { params: [1] }, - { params: [2] }, - ]); + await page.clock.fastForwardTo(2000); + expect(calls).toEqual([{ params: [2000] }]); }); }); it.describe('stubTimers', () => { + it.beforeEach(async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); + }); it('sets initial timestamp', async ({ page, calls }) => { - await page.clock.installFakeTimers(1400); + await page.clock.setSystemTime(1400); expect(await page.evaluate(() => Date.now())).toBe(1400); }); it('replaces global setTimeout', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 1000); }); @@ -503,13 +257,11 @@ it.describe('stubTimers', () => { }); it('global fake setTimeout should return id', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); const to = await page.evaluate(() => setTimeout(window.stub, 1000)); expect(typeof to).toBe('number'); }); it('replaces global clearTimeout', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setTimeout(window.stub, 1000); clearTimeout(to); @@ -519,7 +271,6 @@ it.describe('stubTimers', () => { }); it('replaces global setInterval', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 500); }); @@ -528,7 +279,6 @@ it.describe('stubTimers', () => { }); it('replaces global clearInterval', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setInterval(window.stub, 500); clearInterval(to); @@ -538,7 +288,6 @@ it.describe('stubTimers', () => { }); it('replaces global performance.now', async ({ page }) => { - await page.clock.installFakeTimers(0); const promise = page.evaluate(async () => { const prev = performance.now(); await new Promise(f => setTimeout(f, 1000)); @@ -549,30 +298,35 @@ it.describe('stubTimers', () => { expect(await promise).toEqual({ prev: 0, next: 1000 }); }); - it('replaces global performance.timeOrigin', async ({ page }) => { - await page.clock.installFakeTimers(1000); - const promise = page.evaluate(async () => { - const prev = performance.now(); - await new Promise(f => setTimeout(f, 1000)); - const next = performance.now(); - return { prev, next }; - }); - expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); - await page.clock.runFor(1000); - expect(await promise).toEqual({ prev: 0, next: 1000 }); - }); - it('fakes Date constructor', async ({ page }) => { - await page.clock.installFakeTimers(0); const now = await page.evaluate(() => new Date().getTime()); expect(now).toBe(0); }); }); +it.describe('stubTimers', () => { + it('replaces global performance.timeOrigin', async ({ page }) => { + await page.clock.install({ time: 1000 }); + await page.clock.pause(); + await page.clock.setSystemTime(1000); + const promise = page.evaluate(async () => { + const prev = performance.now(); + await new Promise(f => setTimeout(f, 1000)); + const next = performance.now(); + return { prev, next }; + }); + await page.clock.runFor(1000); + expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); + expect(await promise).toEqual({ prev: 0, next: 1000 }); + }); +}); + it.describe('popup', () => { it('should tick after popup', async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); const now = new Date('2015-09-25'); - await page.clock.installFakeTimers(now); + await page.clock.setSystemTime(now); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.evaluate(() => window.open('about:blank')), @@ -584,11 +338,12 @@ it.describe('popup', () => { expect(popupTimeAfter).toBe(now.getTime() + 1000); }); - it('should tick before popup', async ({ page, browserName }) => { + it('should tick before popup', async ({ page }) => { + await page.clock.install(); + await page.clock.pause(); const now = new Date('2015-09-25'); - await page.clock.installFakeTimers(now); - const ticks = await page.clock.runFor(1000); - expect(ticks).toBe(1000); + await page.clock.setSystemTime(now); + await page.clock.runFor(1000); const [popup] = await Promise.all([ page.waitForEvent('popup'), @@ -597,90 +352,47 @@ it.describe('popup', () => { const popupTime = await popup.evaluate(() => Date.now()); expect(popupTime).toBe(now.getTime() + 1000); }); -}); -it.describe('runToNextTimer', () => { - it('triggers the next timer', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 100); + it('should run time before popup', async ({ page, server }) => { + server.setRoute('/popup.html', async (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); }); - expect(await page.clock.runToNextTimer()).toBe(100); - expect(calls).toHaveLength(1); + await page.clock.setSystemTime(0); + await page.goto(server.EMPTY_PAGE); + // Wait for 2 second in real life to check that it is past in popup. + await page.waitForTimeout(2000); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), + ]); + const popupTime = await popup.evaluate('time'); + expect(popupTime).toBeGreaterThanOrEqual(2000); }); - it('does not trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(() => { - setTimeout(() => { - window.stub(); - }, 100); - setTimeout(() => { - window.stub(); - }, 100); + it('should not run time before popup on pause', async ({ page, server }) => { + server.setRoute('/popup.html', async (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); }); - - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(1); - }); - - it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - window.stub(); - }, 100); - setTimeout(() => { - window.stub(); - }, 100); - setTimeout(() => { - window.stub(); - }, 99); - setTimeout(() => { - window.stub(); - }, 100); - }); - - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(1); - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(2); - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(3); - await page.clock.runToNextTimer(); - expect(calls).toHaveLength(4); - }); - - it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - window.stub(1); - setTimeout(() => { - setTimeout(() => window.stub(2), 0); - }, 0); - }); - - await page.clock.runToNextTimer(); - expect(calls).toEqual([{ params: [1] }]); - await page.clock.runToNextTimer(); - expect(calls).toEqual([{ params: [1] }, { params: [2] }]); - }); - - it('throws exception thrown by timer', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(() => { - throw new Error(); - }, 100); - }); - - await expect(page.clock.runToNextTimer()).rejects.toThrow(); + await page.clock.install(); + await page.clock.pause(); + await page.clock.setSystemTime(0); + await page.goto(server.EMPTY_PAGE); + // Wait for 2 second in real life to check that it is past in popup. + await page.waitForTimeout(2000); + const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'), + ]); + const popupTime = await popup.evaluate('time'); + expect(popupTime).toBe(0); }); }); -it.describe('setTime', () => { +it.describe('setFixedTime', () => { it('does not fake methods', async ({ page }) => { - await page.clock.setTime(0); + await page.clock.setFixedTime(0); // Should not stall. await page.evaluate(() => { @@ -688,54 +400,145 @@ it.describe('setTime', () => { }); }); - it('allows setting time multiple times', async ({ page, calls }) => { - await page.clock.setTime(100); + it('allows setting time multiple times', async ({ page }) => { + await page.clock.setFixedTime(100); expect(await page.evaluate(() => Date.now())).toBe(100); - await page.clock.setTime(200); + await page.clock.setFixedTime(200); expect(await page.evaluate(() => Date.now())).toBe(200); }); - it('supports skipTime w/o fake timers', async ({ page }) => { - await page.clock.setTime(100); + it('fixed time is not affected by clock manipulation', async ({ page }) => { + await page.clock.setFixedTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.fastForward(20); expect(await page.evaluate(() => Date.now())).toBe(100); - await page.clock.skipTime(20); - expect(await page.evaluate(() => Date.now())).toBe(120); }); it('allows installing fake timers after settings time', async ({ page, calls }) => { - await page.clock.setTime(100); + await page.clock.setFixedTime(100); expect(await page.evaluate(() => Date.now())).toBe(100); - await page.clock.installFakeTimers(200); + await page.clock.setFixedTime(200); await page.evaluate(async () => { setTimeout(() => window.stub(Date.now())); }); await page.clock.runFor(0); expect(calls).toEqual([{ params: [200] }]); }); +}); - it('allows setting time after installing fake timers', async ({ page, calls }) => { - await page.clock.installFakeTimers(200); - await page.evaluate(async () => { - setTimeout(() => window.stub(Date.now())); - }); - await page.clock.setTime(220); - expect(calls).toEqual([{ params: [220] }]); +it.describe('while running', () => { + it('should progress time', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.waitForTimeout(1000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(1000); + expect(now).toBeLessThanOrEqual(2000); }); - it('does not allow flowing time backwards', async ({ page, calls }) => { - await page.clock.installFakeTimers(200); - await expect(page.clock.setTime(180)).rejects.toThrow(); + it('should runFor', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.runFor(10000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(10000); + expect(now).toBeLessThanOrEqual(11000); }); - it('should turn setTime into jump', async ({ page, calls }) => { - await page.clock.installFakeTimers(0); - await page.evaluate(async () => { - setTimeout(window.stub, 100); - setTimeout(window.stub, 200); - }); - await page.clock.setTime(100); - expect(calls).toHaveLength(1); - await page.clock.setTime(200); - expect(calls).toHaveLength(2); + it('should fastForward', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.fastForward(10000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(10000); + expect(now).toBeLessThanOrEqual(11000); + }); + + it('should fastForwardTo', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.fastForwardTo(10000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(10000); + expect(now).toBeLessThanOrEqual(11000); + }); + + it('should pause', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.waitForTimeout(1000); + await page.clock.resume(); + const now = await page.evaluate(() => Date.now()); + expect(now).toBeGreaterThanOrEqual(0); + expect(now).toBeLessThanOrEqual(1000); + }); + + it('should pause and fastForwardTo', async ({ page }) => { + await page.clock.install({ time: 0 }); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.clock.fastForwardTo(1000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBe(1000); + }); + + it('should set system time on pause', async ({ page }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.clock.setSystemTime(1000); + const now = await page.evaluate(() => Date.now()); + expect(now).toBe(1000); + }); +}); + +it.describe('while on pause', () => { + it('fastForward should not run nested immediate', async ({ page, calls }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.evaluate(() => { + setTimeout(() => { + window.stub('outer'); + setTimeout(() => window.stub('inner'), 0); + }, 1000); + }); + await page.clock.fastForward(1000); + expect(calls).toEqual([{ params: ['outer'] }]); + await page.clock.fastForward(1); + expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]); + }); + + it('runFor should not run nested immediate', async ({ page, calls }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.evaluate(() => { + setTimeout(() => { + window.stub('outer'); + setTimeout(() => window.stub('inner'), 0); + }, 1000); + }); + await page.clock.runFor(1000); + expect(calls).toEqual([{ params: ['outer'] }]); + await page.clock.runFor(1); + expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]); + }); + + it('runFor should not run nested immediate from microtask', async ({ page, calls }) => { + await page.clock.install(); + await page.goto('data:text/html,'); + await page.clock.pause(); + await page.evaluate(() => { + setTimeout(() => { + window.stub('outer'); + void Promise.resolve().then(() => setTimeout(() => window.stub('inner'), 0)); + }, 1000); + }); + await page.clock.runFor(1000); + expect(calls).toEqual([{ params: ['outer'] }]); + await page.clock.runFor(1); + expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]); }); });