chore: clock api review (#31237)

This commit is contained in:
Pavel Feldman 2024-06-11 09:42:15 -07:00 committed by GitHub
parent c08000b967
commit 6399e8de4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1254 additions and 2019 deletions

View file

@ -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 Note that clock is installed for the entire [BrowserContext], so the time
in all the pages and iframes is controlled by the same clock. 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 * since: v1.45
Install fake implementations for the following time-related functions: Install fake implementations for the following time-related functions:
* `Date`
* `setTimeout` * `setTimeout`
* `clearTimeout` * `clearTimeout`
* `setInterval` * `setInterval`
@ -21,41 +98,18 @@ Install fake implementations for the following time-related functions:
* `cancelIdleCallback` * `cancelIdleCallback`
* `performance` * `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 * since: v1.45
- `time` <[int]|[Date]> - `time` <[int]|[string]|[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 to initialize with, current system time by default.
## async method: Clock.runFor ## async method: Clock.runFor
* since: v1.45 * since: v1.45
- returns: <[int]>
Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Advance the clock, firing all the time-related callbacks.
Fake timers must be installed.
Returns fake milliseconds since the unix epoch.
**Usage** **Usage**
@ -66,12 +120,12 @@ await page.clock.runFor('30:00');
```python async ```python async
await page.clock.run_for(1000); await page.clock.run_for(1000);
await page.clock.run_for('30:00') await page.clock.run_for("30:00")
``` ```
```python sync ```python sync
page.clock.run_for(1000); page.clock.run_for(1000);
page.clock.run_for('30:00') page.clock.run_for("30:00")
``` ```
```java ```java
@ -84,84 +138,104 @@ await page.Clock.RunForAsync(1000);
await page.Clock.RunForAsync("30:00"); await page.Clock.RunForAsync("30:00");
``` ```
### param: Clock.runFor.time ### param: Clock.runFor.ticks
* since: v1.45 * 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. 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 ## async method: Clock.pause
* 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
* since: v1.45 * 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) ## async method: Clock.resume
being put to sleep and resumed later, skipping intermediary timers.
### param: Clock.setTime.time
* since: v1.45 * 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 * since: v1.45
- returns: <[int]>
Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time. Makes `Date.now` and `new Date()` return fixed fake time at all times,
keeps all the timers running.
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.
**Usage** **Usage**
```js ```js
await page.clock.skipTime(1000); await page.clock.setFixedTime(Date.now());
await page.clock.skipTime('30:00'); await page.clock.setFixedTime(new Date('2020-02-02'));
await page.clock.setFixedTime('2020-02-02');
``` ```
```python async ```python async
await page.clock.skipTime(1000); await page.clock.set_fixed_time(datetime.datetime.now())
await page.clock.skipTime('30:00') await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
await page.clock.set_fixed_time("2020-02-02")
``` ```
```python sync ```python sync
page.clock.skipTime(1000); page.clock.set_fixed_time(datetime.datetime.now())
page.clock.skipTime('30:00') page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
page.clock.set_fixed_time("2020-02-02")
``` ```
```java ```java
page.clock().skipTime(1000); page.clock().setFixedTime(Instant.now());
page.clock().skipTime("30:00"); page.clock().setFixedTime(Instant.parse("2020-02-02"));
page.clock().setFixedTime("2020-02-02");
``` ```
```csharp ```csharp
await page.Clock.SkipTimeAsync(1000); await page.Clock.SetFixedTimeAsync(DateTime.Now);
await page.Clock.SkipTimeAsync("30:00"); 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 * 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]>

View file

@ -17,228 +17,324 @@ Accurately simulating time-dependent behavior is essential for verifying the cor
- `cancelAnimationFrame` - `cancelAnimationFrame`
- `requestIdleCallback` - `requestIdleCallback`
- `cancelIdleCallback` - `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 Often you only need to fake `Date.now` while keeping the timers going.
await page.clock.setTime(new Date('2020-02-02')); That way the time flows naturally, but `Date.now` always returns a fixed value.
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.
```html ```html
<div id="current-time" data-testid="current-time"></div> <div id="current-time" data-testid="current-time"></div>
<script> <script>
const renderTime = () => { const renderTime = () => {
document.getElementById('current-time').textContent = document.getElementById('current-time').textContent =
new Date() = time.toLocalTimeString(); new Date().toLocaleTimeString();
}; };
setInterval(renderTime, 1000); setInterval(renderTime, 1000);
</script> </script>
``` ```
```js ```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 page.goto('http://localhost:3333');
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); 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
<div id="current-time" data-testid="current-time"></div>
<script>
const renderTime = () => {
document.getElementById('current-time').textContent =
new Date().toLocaleTimeString();
};
setInterval(renderTime, 1000);
</script>
```
```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'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
``` ```
```python async ```python async
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) # Initialize clock with some time before the test time and let the page load
await page.goto('http://localhost:3333') # naturally. `Date.now` will progress as the timers fire.
locator = page.get_by_test_id('current-time') await page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') await page.goto("http://localhost:3333")
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) # Take control over time flow.
await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') 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 ```python sync
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)) # Initialize clock with some time before the test time and let the page load
page.goto('http://localhost:3333') # naturally. `Date.now` will progress as the timers fire.
locator = page.get_by_test_id('current-time') page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
expect(locator).to_have_text('2/2/2024, 10:00:00 AM') page.goto("http://localhost:3333")
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) # Take control over time flow.
expect(locator).to_have_text('2/2/2024, 10:30:00 AM') 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 ```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"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time"); 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"); 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"); assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
``` ```
```csharp ```csharp
// Initialize clock with a specific time, only fake Date.now. // Initialize clock with some time before the test time and let the page load naturally.
await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)); // `Date.now` will progress as the timers fire.
await page.GotoAsync("http://localhost:3333"); await Page.Clock.InstallAsync(new
var locator = page.GetByTestId("current-time"); {
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); 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)); // Take control over time flow.
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); 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. Inactivity monitoring is a common feature in web applications that logs out users after a period of inactivity.
In cases like this you need to ensure that `Date.now` and timers are consistent. Testing this feature can be tricky because you need to wait for a long time to see the effect.
You can achieve this by installing the fake timers. With the help of the clock, you can speed up time and test this feature quickly.
```html
<div id="current-time" data-testid="current-time"></div>
<script>
const renderTime = () => {
document.getElementById('current-time').textContent =
new Date() = time.toLocalTimeString();
};
setInterval(renderTime, 1000);
</script>
```
```js ```js
// Initialize clock with a specific time, take full control over time. // Initial time does not matter for the test, so we can pick current time.
await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); await page.clock.install();
await page.goto('http://localhost:3333'); 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 // Fast forward time 5 minutes as if the user did not do anything.
// closed and opened the lid of the laptop. // Fast forward is like closing the laptop lid and opening it after 5 minutes.
await page.clock.skipTime('30:00'); // All the timers due will fire once immediately, as in the real browser.
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); 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 ```python async
# Initialize clock with a specific time, take full control over time. # Initial time does not matter for the test, so we can pick current time.
await page.clock.install_fake_timers( await page.clock.install()
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) await page.goto("http://localhost:3333")
) # Interact with the page
await page.goto('http://localhost:3333') await page.get_by_role("button").click()
locator = page.get_by_test_id('current-time')
await expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
# Fast forward time 30 minutes without firing intermediate timers, as if the user # Fast forward time 5 minutes as if the user did not do anything.
# closed and opened the lid of the laptop. # Fast forward is like closing the laptop lid and opening it after 5 minutes.
await page.clock.skip_time('30:00') # All the timers due will fire once immediately, as in the real browser.
await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') 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 ```python sync
# Initialize clock with a specific time, take full control over time. # Initial time does not matter for the test, so we can pick current time.
page.clock.install_fake_timers( page.clock.install()
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst) page.goto("http://localhost:3333")
) # Interact with the page
page.goto('http://localhost:3333') page.get_by_role("button").click()
locator = page.get_by_test_id('current-time')
expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
# Fast forward time 30 minutes without firing intermediate timers, as if the user # Fast forward time 5 minutes as if the user did not do anything.
# closed and opened the lid of the laptop. # Fast forward is like closing the laptop lid and opening it after 5 minutes.
page.clock.skip_time('30:00') # All the timers due will fire once immediately, as in the real browser.
expect(locator).to_have_text('2/2/2024, 10:30:00 AM') 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 ```java
// Initialize clock with a specific time, take full control over time. // Initial time does not matter for the test, so we can pick current time.
page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); page.clock().install();
page.navigate("http://localhost:3333"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time"); Locator locator = page.getByRole("button");
assertThat(locator).hasText("2/2/2024, 10:00:00 AM")
// Fast forward time 30 minutes without firing intermediate timers, as if the user // Interact with the page
// closed and opened the lid of the laptop. locator.click();
page.clock().skipTime("30:00");
assertThat(locator).hasText("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().fastForward("5:00");
// Check that the user was logged out automatically.
assertThat(page.getByText("You have been logged out due to inactivity.")).isVisible();
``` ```
```csharp ```csharp
// Initialize clock with a specific time, take full control over time. // Initial time does not matter for the test, so we can pick current time.
await page.Clock.InstallFakeTimersAsync( await Page.Clock.InstallAsync();
new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)
);
await page.GotoAsync("http://localhost:3333"); 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 // Interact with the page
// closed and opened the lid of the laptop. await page.GetByRole("button").ClickAsync();
await page.Clock.SkipTimeAsync("30:00");
await Expect(locator).ToHaveTextAsync("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.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 In rare cases, you may want to tick through time manually, firing all timers and
control over the passage of time. animation frames in the process to achieve a fine-grained control over the passage of time.
```html ```html
<div id="current-time" data-testid="current-time"></div> <div id="current-time" data-testid="current-time"></div>
<script> <script>
const renderTime = () => { const renderTime = () => {
document.getElementById('current-time').textContent = document.getElementById('current-time').textContent =
new Date() = time.toLocalTimeString(); new Date().toLocaleTimeString();
}; };
setInterval(renderTime, 1000); setInterval(renderTime, 1000);
</script> </script>
``` ```
```js ```js
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, let the page load naturally.
await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); await page.clock.install({ time: new Date('2024-02-02T08:00:00') });
await page.goto('http://localhost:3333'); 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. // Tick through time manually, firing all timers in the process.
// In this case, time will be updated in the screen 2 times. // In this case, time will be updated in the screen 2 times.
await page.clock.runFor(2000); 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 ```python async
# Initialize clock with a specific time, take full control over time. # Initialize clock with a specific time, let the page load naturally.
await page.clock.install_fake_timers( await page.clock.install(time=
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst),
) )
await page.goto('http://localhost:3333') await page.goto("http://localhost:3333")
locator = page.get_by_test_id('current-time') 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. # Tick through time manually, firing all timers in the process.
# In this case, time will be updated in the screen 2 times. # In this case, time will be updated in the screen 2 times.
await page.clock.run_for(2000) 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 ```python sync
# Initialize clock with a specific time, take full control over time. # Initialize clock with a specific time, let the page load naturally.
page.clock.install_fake_timers( page.clock.install(
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst), time=datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst),
) )
page.goto('http://localhost:3333') page.goto("http://localhost:3333")
locator = page.get_by_test_id('current-time') 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. # Tick through time manually, firing all timers in the process.
# In this case, time will be updated in the screen 2 times. # In this case, time will be updated in the screen 2 times.
page.clock.run_for(2000) 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 ```java
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, let the page load naturally.
page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); page.clock().install(new Clock.InstallOptions()
.setTime(Instant.parse("2024-02-02T08:00:00")));
page.navigate("http://localhost:3333"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time"); 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. // Tick through time manually, firing all timers in the process.
// In this case, time will be updated in the screen 2 times. // In this case, time will be updated in the screen 2 times.
page.clock().runFor(2000); page.clock().runFor(2000);
@ -246,15 +342,22 @@ assertThat(locator).hasText("2/2/2024, 10:00:02 AM");
``` ```
```csharp ```csharp
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, let the page load naturally.
await page.Clock.InstallFakeTimersAsync( await Page.Clock.InstallAsync(new
new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) {
); Time = new DateTime(2024, 2, 2, 8, 0, 0, DateTimeKind.Pst)
});
await page.GotoAsync("http://localhost:3333"); await page.GotoAsync("http://localhost:3333");
var locator = page.GetByTestId("current-time"); 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. // Tick through time manually, firing all timers in the process.
// In this case, time will be updated in the screen 2 times. // 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"); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM");
``` ```

View file

@ -85,8 +85,12 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
const context = BrowserContext.from(response.context); const context = BrowserContext.from(response.context);
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
if (!forReuse && !!process.env.PW_FREEZE_TIME) if (!forReuse && !!process.env.PW_FREEZE_TIME) {
await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true); await this._wrapApiCall(async () => {
await context.clock.install({ time: 0 });
await context.clock.pause();
}, true);
}
return context; return context;
} }

View file

@ -24,44 +24,50 @@ export class Clock implements api.Clock {
this._browserContext = browserContext; this._browserContext = browserContext;
} }
async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) { async install(options: { time?: number | string | Date } = { }) {
const timeMs = time instanceof Date ? time.getTime() : time; await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit });
} }
async runAllTimers(): Promise<number> { async fastForward(ticks: number | string) {
const result = await this._browserContext._channel.clockRunAllTimers(); await this._browserContext._channel.clockFastForward(parseTicks(ticks));
return result.fakeTime;
} }
async runFor(time: number | string): Promise<number> { async fastForwardTo(time: number | string | Date) {
const result = await this._browserContext._channel.clockRunFor({ await this._browserContext._channel.clockFastForwardTo(parseTime(time));
timeNumber: typeof time === 'number' ? time : undefined,
timeString: typeof time === 'string' ? time : undefined
});
return result.fakeTime;
} }
async runToLastTimer(): Promise<number> { async pause() {
const result = await this._browserContext._channel.clockRunToLastTimer(); await this._browserContext._channel.clockPause({});
return result.fakeTime;
} }
async runToNextTimer(): Promise<number> { async resume() {
const result = await this._browserContext._channel.clockRunToNextTimer(); await this._browserContext._channel.clockResume({});
return result.fakeTime;
} }
async setTime(time: number | Date) { async runFor(ticks: number | string) {
const timeMs = time instanceof Date ? time.getTime() : time; await this._browserContext._channel.clockRunFor(parseTicks(ticks));
await this._browserContext._channel.clockSetTime({ time: timeMs });
} }
async skipTime(time: number | string) { async setFixedTime(time: string | number | Date) {
const result = await this._browserContext._channel.clockSkipTime({ await this._browserContext._channel.clockSetFixedTime(parseTime(time));
timeNumber: typeof time === 'number' ? time : undefined, }
timeString: typeof time === 'string' ? time : undefined
}); async setSystemTime(time: string | number | Date) {
return result.fakeTime; 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
};
}

View file

@ -963,41 +963,40 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
enabled: tBoolean, enabled: tBoolean,
}); });
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({})); scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
scheme.BrowserContextClockInstallFakeTimersParams = tObject({ scheme.BrowserContextClockFastForwardParams = tObject({
time: tNumber, ticksNumber: tOptional(tNumber),
loopLimit: tOptional(tNumber), ticksString: tOptional(tString),
}); });
scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({})); scheme.BrowserContextClockFastForwardResult = tOptional(tObject({}));
scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({})); scheme.BrowserContextClockFastForwardToParams = tObject({
scheme.BrowserContextClockRunAllTimersResult = tObject({ timeNumber: tOptional(tNumber),
fakeTime: 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({ scheme.BrowserContextClockRunForParams = tObject({
ticksNumber: tOptional(tNumber),
ticksString: tOptional(tString),
});
scheme.BrowserContextClockRunForResult = tOptional(tObject({}));
scheme.BrowserContextClockSetFixedTimeParams = tObject({
timeNumber: tOptional(tNumber), timeNumber: tOptional(tNumber),
timeString: tOptional(tString), timeString: tOptional(tString),
}); });
scheme.BrowserContextClockRunForResult = tObject({ scheme.BrowserContextClockSetFixedTimeResult = tOptional(tObject({}));
fakeTime: tNumber, scheme.BrowserContextClockSetSystemTimeParams = tObject({
});
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({
timeNumber: tOptional(tNumber), timeNumber: tOptional(tNumber),
timeString: tOptional(tString), timeString: tOptional(tString),
}); });
scheme.BrowserContextClockSkipTimeResult = tObject({ scheme.BrowserContextClockSetSystemTimeResult = tOptional(tObject({}));
fakeTime: tNumber,
});
scheme.PageInitializer = tObject({ scheme.PageInitializer = tObject({
mainFrame: tChannel(['Frame']), mainFrame: tChannel(['Frame']),
viewportSize: tOptional(tObject({ viewportSize: tOptional(tObject({

View file

@ -16,81 +16,74 @@
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import * as clockSource from '../generated/clockSource'; import * as clockSource from '../generated/clockSource';
import { isJavaScriptErrorInEvaluate } from './javascript';
export class Clock { export class Clock {
private _browserContext: BrowserContext; private _browserContext: BrowserContext;
private _scriptInjected = false; private _scriptInstalled = false;
private _clockInstalled = false;
private _now = 0;
constructor(browserContext: BrowserContext) { constructor(browserContext: BrowserContext) {
this._browserContext = browserContext; this._browserContext = browserContext;
} }
async installFakeTimers(time: number, loopLimit: number | undefined) { async fastForward(ticks: number | string) {
await this._injectScriptIfNeeded(); await this._installIfNeeded();
await this._addAndEvaluate(`(() => { const ticksMillis = parseTicks(ticks);
globalThis.__pwClock.clock?.uninstall(); await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`);
globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })}); await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`);
})();`);
this._now = time;
this._clockInstalled = true;
} }
async runToNextTimer(): Promise<number> { async fastForwardTo(ticks: number | string) {
this._assertInstalled(); await this._installIfNeeded();
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.next()`); const timeMillis = parseTime(ticks);
return this._now; await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForwardTo', ${Date.now()}, ${timeMillis})`);
await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForwardTo(${timeMillis})`);
} }
async runAllTimers(): Promise<number> { async install(time: number | string | undefined) {
this._assertInstalled(); await this._installIfNeeded();
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAll()`); const timeMillis = time !== undefined ? parseTime(time) : Date.now();
return this._now; await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`);
await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`);
} }
async runToLastTimer(): Promise<number> { async pause() {
this._assertInstalled(); await this._installIfNeeded();
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLast()`); await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pause', ${Date.now()})`);
return this._now; await this._evaluateInFrames(`globalThis.__pwClock.controller.pause()`);
} }
async setTime(time: number) { async resume() {
if (this._clockInstalled) { await this._installIfNeeded();
const jump = time - this._now; await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('resume', ${Date.now()})`);
if (jump < 0) await this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`);
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 skipTime(time: number | string) { async setFixedTime(time: string | number) {
const delta = parseTime(time); await this._installIfNeeded();
await this.setTime(this._now + delta); const timeMillis = parseTime(time);
return this._now; 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<number> { async setSystemTime(time: string | number) {
this._assertInstalled(); await this._installIfNeeded();
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.recordTick(${JSON.stringify(time)})`); const timeMillis = parseTime(time);
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`);
return this._now; await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`);
} }
private async _injectScriptIfNeeded() { async runFor(ticks: number | string) {
if (this._scriptInjected) 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; return;
this._scriptInjected = true; this._scriptInstalled = true;
const script = `(() => { const script = `(() => {
const module = {}; const module = {};
${clockSource.source} ${clockSource.source}
@ -106,37 +99,56 @@ export class Clock {
private async _evaluateInFrames(script: string) { private async _evaluateInFrames(script: string) {
const frames = this._browserContext.pages().map(page => page.frames()).flat(); 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]; 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 { * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into
if (typeof time === 'number') * number of milliseconds. This is used to support human-readable strings passed
return time; * to clock.tick()
if (!time) */
function parseTicks(value: number | string): number {
if (typeof value === 'number')
return value;
if (!value)
return 0; return 0;
const str = value;
const strings = time.split(':'); const strings = str.split(':');
const l = strings.length; const l = strings.length;
let i = l; let i = l;
let ms = 0; let ms = 0;
let parsed; let parsed;
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time)) if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`); throw new Error(
`Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`,
);
}
while (i--) { while (i--) {
parsed = parseInt(strings[i], 10); parsed = parseInt(strings[i], 10);
if (parsed >= 60) if (parsed >= 60)
throw new Error(`Invalid time ${time}`); throw new Error(`Invalid time ${str}`);
ms += parsed * Math.pow(60, l - i - 1); ms += parsed * Math.pow(60, l - i - 1);
} }
return ms * 1000; 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();
}

View file

@ -312,32 +312,36 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return { artifact: ArtifactDispatcher.from(this, artifact) }; return { artifact: ArtifactDispatcher.from(this, artifact) };
} }
async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallFakeTimersResult> { async clockFastForward(params: channels.BrowserContextClockFastForwardParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockFastForwardResult> {
await this._context.clock.installFakeTimers(params.time, params.loopLimit); await this._context.clock.fastForward(params.ticksString ?? params.ticksNumber ?? 0);
} }
async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllTimersResult> { async clockFastForwardTo(params: channels.BrowserContextClockFastForwardToParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockFastForwardToResult> {
return { fakeTime: await this._context.clock.runAllTimers() }; await this._context.clock.fastForwardTo(params.timeString ?? params.timeNumber ?? 0);
} }
async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToLastTimerResult> { async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> {
return { fakeTime: await this._context.clock.runToLastTimer() }; await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined);
} }
async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToNextTimerResult> { async clockPause(params: channels.BrowserContextClockPauseParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockPauseResult> {
return { fakeTime: await this._context.clock.runToNextTimer() }; await this._context.clock.pause();
} }
async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSetTimeResult> { async clockResume(params: channels.BrowserContextClockResumeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockResumeResult> {
await this._context.clock.setTime(params.time); await this._context.clock.resume();
}
async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSkipTimeResult> {
return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) };
} }
async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunForResult> { async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunForResult> {
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<channels.BrowserContextClockSetFixedTimeResult> {
await this._context.clock.setFixedTime(params.timeString ?? params.timeNumber ?? 0);
}
async clockSetSystemTime(params: channels.BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSetSystemTimeResult> {
await this._context.clock.setSystemTime(params.timeString ?? params.timeNumber ?? 0);
} }
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> { async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {

View file

@ -26,7 +26,6 @@ export type ClockMethods = {
export type ClockConfig = { export type ClockConfig = {
now?: number | Date; now?: number | Date;
loopLimit?: number;
}; };
export type InstallConfig = ClockConfig & { export type InstallConfig = ClockConfig & {
@ -53,26 +52,39 @@ type Timer = {
}; };
interface Embedder { interface Embedder {
postTask(task: () => void): void; dateNow(): number;
postTaskPeriodically(task: () => void, delay: number): () => void; 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 { export class ClockController {
readonly timeOrigin: number; readonly _now: Time;
private _now: { time: number, ticks: number, timeFrozen: boolean };
private _loopLimit: number;
private _duringTick = false; private _duringTick = false;
private _timers = new Map<number, Timer>(); private _timers = new Map<number, Timer>();
private _uniqueTimerId = idCounterStart; private _uniqueTimerId = idCounterStart;
private _embedder: Embedder; private _embedder: Embedder;
readonly disposables: (() => void)[] = []; 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) { constructor(embedder: Embedder) {
const start = Math.floor(getEpoch(startDate)); this._now = { time: 0, isFixedTime: false, ticks: 0, origin: -1 };
this.timeOrigin = start;
this._now = { time: start, ticks: 0, timeFrozen: false };
this._embedder = embedder; this._embedder = embedder;
this._loopLimit = loopLimit;
} }
uninstall() { uninstall() {
@ -81,109 +93,147 @@ export class ClockController {
} }
now(): number { now(): number {
this._replayLogOnce();
return this._now.time; return this._now.time;
} }
setTime(now: Date | number, options: { freeze?: boolean } = {}) { install(time: number) {
this._now.time = getEpoch(now); this._replayLogOnce();
this._now.timeFrozen = !!options.freeze; this._innerSetTime(time);
}
setSystemTime(time: number) {
this._replayLogOnce();
this._innerSetTime(time);
}
setFixedTime(time: number) {
this._replayLogOnce();
this._innerSetFixedTime(time);
} }
performanceNow(): DOMHighResTimeStamp { performanceNow(): DOMHighResTimeStamp {
this._replayLogOnce();
return this._now.ticks; 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) { private _advanceNow(toTicks: number) {
if (!this._now.timeFrozen) if (!this._now.isFixedTime)
this._now.time += toTicks - this._now.ticks; this._now.time += toTicks - this._now.ticks;
this._now.ticks = toTicks; this._now.ticks = toTicks;
} }
private async _doTick(msFloat: number): Promise<number> { async log(type: LogEntryType, time: number, param?: number) {
if (msFloat < 0) this._log.push({ type, time, param });
throw new TypeError('Negative ticks are not supported'); }
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 firstException: Error | undefined;
let timer = this._firstTimerInRange(tickFrom, tickTo); while (true) {
while (timer && tickFrom <= tickTo) { const result = await this._callFirstTimer(tickTo);
tickFrom = timer.callAt; if (!result.timerFound)
const error = await this._callTimer(timer).catch(e => e); break;
firstException = firstException || error; firstException = firstException || result.error;
timer = this._firstTimerInRange(previous, tickTo);
previous = tickFrom;
} }
this._advanceNow(tickTo); this._advanceNow(tickTo);
if (firstException) if (firstException)
throw firstException; throw firstException;
return this._now.ticks;
} }
async recordTick(tickValue: string | number) { pause() {
const msFloat = parseTime(tickValue); this._replayLogOnce();
this._advanceNow(this._now.ticks + msFloat); this._innerPause();
} }
async tick(tickValue: string | number): Promise<number> { private _innerPause() {
return await this._doTick(parseTime(tickValue)); this._realTime = undefined;
this._updateRealTimeTimer();
} }
async next(): Promise<number> { resume() {
const timer = this._firstTimer(); this._replayLogOnce();
if (!timer) this._innerResume();
return this._now.ticks;
await this._callTimer(timer);
return this._now.ticks;
} }
async runToFrame(): Promise<number> { private _innerResume() {
return this.tick(this.getTimeToNextFrame()); const now = this._embedder.performanceNow();
this._realTime = { startTicks: now, lastSyncTicks: now };
this._updateRealTimeTimer();
} }
async runAll(): Promise<number> { private _updateRealTimeTimer() {
for (let i = 0; i < this._loopLimit; i++) { if (!this._realTime) {
const numTimers = this._timers.size; this._currentRealTimeTimer?.dispose();
if (numTimers === 0) this._currentRealTimeTimer = undefined;
return this._now.ticks; return;
await this.next();
} }
const excessJob = this._firstTimer(); const firstTimer = this._firstTimer();
if (!excessJob)
return this._now.ticks; // Either run the next timer or move time in 100ms chunks.
throw this._getInfiniteLoopError(excessJob); 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<number> { async fastForward(ticks: number) {
const timer = this._lastTimer(); this._replayLogOnce();
if (!timer) const ms = ticks | 0;
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<number> {
const msFloat = parseTime(tickValue);
const ms = Math.floor(msFloat);
for (const timer of this._timers.values()) { for (const timer of this._timers.values()) {
if (this._now.ticks + ms > timer.callAt) if (this._now.ticks + ms > timer.callAt)
timer.callAt = this._now.ticks + ms; 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 { addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
this._replayLogOnce();
if (options.func === undefined) if (options.func === undefined)
throw new Error('Callback must be provided to timer calls'); throw new Error('Callback must be provided to timer calls');
@ -204,56 +254,56 @@ export class ClockController {
error: new Error(), error: new Error(),
}; };
this._timers.set(timer.id, timer); this._timers.set(timer.id, timer);
if (this._realTime)
this._updateRealTimeTimer();
return timer.id; 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() { countTimers() {
return this._timers.size; return this._timers.size;
} }
private _firstTimer(): Timer | null { private _firstTimer(beforeTick?: number): Timer | null {
let firstTimer: Timer | null = null; let firstTimer: Timer | null = null;
for (const timer of this._timers.values()) { 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; firstTimer = timer;
} }
return firstTimer; return firstTimer;
} }
private _lastTimer(): Timer | null { private _takeFirstTimer(beforeTick?: number): Timer | null {
let lastTimer: Timer | null = 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); this._advanceNow(timer.callAt);
if (timer.type === TimerType.Interval) if (timer.type === TimerType.Interval)
this._timers.get(timer.id)!.callAt += timer.delay; this._timers.get(timer.id)!.callAt += timer.delay;
else else
this._timers.delete(timer.id); 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; this._duringTick = true;
try { try {
if (typeof timer.func !== 'function') { if (typeof timer.func !== 'function') {
(() => { eval(timer.func); })(); let error: Error | undefined;
return; try {
(() => { eval(timer.func); })();
} catch (e) {
error = e;
}
await new Promise<void>(f => this._embedder.setTimeout(f));
return { timerFound: true, error };
} }
let args = timer.args; let args = timer.args;
@ -262,67 +312,26 @@ export class ClockController {
else if (timer.type === TimerType.IdleCallback) else if (timer.type === TimerType.IdleCallback)
args = [{ didTimeout: false, timeRemaining: () => 0 }]; args = [{ didTimeout: false, timeRemaining: () => 0 }];
timer.func.apply(null, args); let error: Error | undefined;
await new Promise<void>(f => this._embedder.postTask(f)); try {
timer.func.apply(null, args);
} catch (e) {
error = e;
}
await new Promise<void>(f => this._embedder.setTimeout(f));
return { timerFound: true, error };
} finally { } finally {
this._duringTick = false; 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() { getTimeToNextFrame() {
return 16 - this._now.ticks % 16; return 16 - this._now.ticks % 16;
} }
clearTimer(timerId: number, type: TimerType) { clearTimer(timerId: number, type: TimerType) {
this._replayLogOnce();
if (!timerId) { if (!timerId) {
// null appears to be allowed in most browsers, and appears to be // null appears to be allowed in most browsers, and appears to be
// relied upon by some libraries, like Bootstrap carousel // relied upon by some libraries, like Bootstrap carousel
@ -356,64 +365,49 @@ export class ClockController {
} }
} }
advanceAutomatically(advanceTimeDelta: number = 20): () => void { private _replayLogOnce() {
return this._embedder.postTaskPeriodically( if (!this._log.length)
() => this._doTick(advanceTimeDelta!), return;
advanceTimeDelta,
); 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 { function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
let prop; for (const prop in source) {
for (prop of Object.keys(source) as (keyof DateConstructor)[]) if (source.hasOwnProperty(prop))
target[prop] = source[prop]; target[prop] = (source as any)[prop];
}
target.toString = () => source.toString(); target.toString = () => source.toString();
target.prototype = source.prototype; target.prototype = source.prototype;
target.parse = source.parse; 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 * All properties of Intl are non-enumerable, so we need
* to do a bit of work to get them out. * 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[key] = NativeIntl[key];
ClockIntl.DateTimeFormat = function(...args: any[]) { ClockIntl.DateTimeFormat = function(...args: any[]) {
@ -644,8 +638,8 @@ function getClearHandler(type: TimerType) {
function fakePerformance(clock: ClockController, performance: Performance): Performance { function fakePerformance(clock: ClockController, performance: Performance): Performance {
const result: any = { const result: any = {
now: () => clock.performanceNow(), now: () => clock.performanceNow(),
timeOrigin: clock.timeOrigin,
}; };
result.__defineGetter__('timeOrigin', () => clock._now.origin || 0);
// eslint-disable-next-line no-proto // eslint-disable-next-line no-proto
for (const key of Object.keys((performance as any).__proto__)) { for (const key of Object.keys((performance as any).__proto__)) {
if (key === 'now' || key === 'timeOrigin') if (key === 'now' || key === 'timeOrigin')
@ -658,19 +652,22 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf
return result; 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 originals = platformOriginals(globalObject);
const embedder = { const embedder: Embedder = {
postTask: (task: () => void) => { dateNow: () => originals.raw.Date.now(),
originals.bound.setTimeout(task, 0); 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) => { setInterval: (task: () => void, delay: number) => {
const intervalId = globalObject.setInterval(task, delay); const intervalId = originals.bound.setInterval(task, delay);
return () => originals.bound.clearInterval(intervalId); 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); const api = createApi(clock, originals.bound);
return { clock, api, originals: originals.raw }; 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.`); 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)[]; const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[];
for (const method of toFake) { for (const method of toFake) {
@ -706,11 +703,10 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
} }
export function inject(globalObject: WindowOrWorkerGlobalScope) { export function inject(globalObject: WindowOrWorkerGlobalScope) {
const { clock: controller } = install(globalObject);
controller.resume();
return { return {
install: (config: InstallConfig) => { controller,
const { clock } = install(globalObject, config);
return clock;
},
builtin: platformOriginals(globalObject).bound, builtin: platformOriginals(globalObject).bound,
}; };
} }

View file

@ -17247,8 +17247,40 @@ export interface BrowserServer {
* controlled by the same clock. * controlled by the same clock.
*/ */
export interface 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<void>;
/**
* 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<void>;
/** /**
* Install fake implementations for the following time-related functions: * Install fake implementations for the following time-related functions:
* - `Date`
* - `setTimeout` * - `setTimeout`
* - `clearTimeout` * - `clearTimeout`
* - `setInterval` * - `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, * 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 * and control the behavior of time-dependent functions. See
* [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and * [clock.runFor(ticks)](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. * [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward) for more information.
* @param time Install fake timers with the specified base time.
* @param options * @param options
*/ */
installFakeTimers(time: number|Date, options?: { install(options?: {
/** /**
* The maximum number of timers that will be run in * Time to initialize with, current system time by default.
* [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`.
*/ */
loopLimit?: number; time?: number|string|Date;
}): Promise<void>; }): Promise<void>;
/** /**
* Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be * Pause timers. Once this method is called, no timers are fired unless
* run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch. * [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),
* **Details** * [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.
* 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.
*/ */
runAllTimers(): Promise<number>; pause(): Promise<void>;
/** /**
* Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must * Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
* be installed. Returns fake milliseconds since the unix epoch. */
resume(): Promise<void>;
/**
* Advance the clock, firing all the time-related callbacks.
* *
* **Usage** * **Usage**
* *
@ -17297,55 +17328,41 @@ export interface Clock {
* await page.clock.runFor('30:00'); * 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. * "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<number>; runFor(ticks: number|string): Promise<void>;
/** /**
* This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as * Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
* 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<number>;
/**
* 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<number>;
/**
* 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<void>;
/**
* 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.
* *
* **Usage** * **Usage**
* *
* ```js * ```js
* await page.clock.skipTime(1000); * await page.clock.setFixedTime(Date.now());
* await page.clock.skipTime('30:00'); * 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 * @param time Time to be set.
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/ */
skipTime(time: number|string): Promise<number>; setFixedTime(time: number|string|Date): Promise<void>;
/**
* 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<void>;
} }
/** /**

View file

@ -1460,13 +1460,14 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>; harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallFakeTimersResult>; clockFastForward(params: BrowserContextClockFastForwardParams, metadata?: CallMetadata): Promise<BrowserContextClockFastForwardResult>;
clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllTimersResult>; clockFastForwardTo(params: BrowserContextClockFastForwardToParams, metadata?: CallMetadata): Promise<BrowserContextClockFastForwardToResult>;
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>;
clockPause(params?: BrowserContextClockPauseParams, metadata?: CallMetadata): Promise<BrowserContextClockPauseResult>;
clockResume(params?: BrowserContextClockResumeParams, metadata?: CallMetadata): Promise<BrowserContextClockResumeResult>;
clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise<BrowserContextClockRunForResult>; clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise<BrowserContextClockRunForResult>;
clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastTimerResult>; clockSetFixedTime(params: BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetFixedTimeResult>;
clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToNextTimerResult>; clockSetSystemTime(params: BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetSystemTimeResult>;
clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetTimeResult>;
clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSkipTimeResult>;
} }
export type BrowserContextBindingCallEvent = { export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel, binding: BindingCallChannel,
@ -1755,58 +1756,66 @@ export type BrowserContextUpdateSubscriptionOptions = {
}; };
export type BrowserContextUpdateSubscriptionResult = void; export type BrowserContextUpdateSubscriptionResult = void;
export type BrowserContextClockInstallFakeTimersParams = { export type BrowserContextClockFastForwardParams = {
time: number, ticksNumber?: number,
loopLimit?: number, ticksString?: string,
}; };
export type BrowserContextClockInstallFakeTimersOptions = { export type BrowserContextClockFastForwardOptions = {
loopLimit?: number, ticksNumber?: number,
ticksString?: string,
}; };
export type BrowserContextClockInstallFakeTimersResult = void; export type BrowserContextClockFastForwardResult = void;
export type BrowserContextClockRunAllTimersParams = {}; export type BrowserContextClockFastForwardToParams = {
export type BrowserContextClockRunAllTimersOptions = {};
export type BrowserContextClockRunAllTimersResult = {
fakeTime: number,
};
export type BrowserContextClockRunForParams = {
timeNumber?: number, timeNumber?: number,
timeString?: string, 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 = { export type BrowserContextClockRunForOptions = {
ticksNumber?: number,
ticksString?: string,
};
export type BrowserContextClockRunForResult = void;
export type BrowserContextClockSetFixedTimeParams = {
timeNumber?: number, timeNumber?: number,
timeString?: string, timeString?: string,
}; };
export type BrowserContextClockRunForResult = { export type BrowserContextClockSetFixedTimeOptions = {
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 = {
timeNumber?: number, timeNumber?: number,
timeString?: string, timeString?: string,
}; };
export type BrowserContextClockSkipTimeOptions = { export type BrowserContextClockSetFixedTimeResult = void;
export type BrowserContextClockSetSystemTimeParams = {
timeNumber?: number, timeNumber?: number,
timeString?: string, timeString?: string,
}; };
export type BrowserContextClockSkipTimeResult = { export type BrowserContextClockSetSystemTimeOptions = {
fakeTime: number, timeNumber?: number,
timeString?: string,
}; };
export type BrowserContextClockSetSystemTimeResult = void;
export interface BrowserContextEvents { export interface BrowserContextEvents {
'bindingCall': BrowserContextBindingCallEvent; 'bindingCall': BrowserContextBindingCallEvent;

View file

@ -1204,40 +1204,39 @@ BrowserContext:
- requestFailed - requestFailed
enabled: boolean enabled: boolean
clockInstallFakeTimers: clockFastForward:
parameters: parameters:
time: number ticksNumber: number?
loopLimit: number? ticksString: string?
clockRunAllTimers: clockFastForwardTo:
returns: parameters:
fakeTime: number timeNumber: number?
timeString: string?
clockInstall:
parameters:
timeNumber: number?
timeString: string?
clockPause:
clockResume:
clockRunFor: clockRunFor:
parameters: parameters:
timeNumber: number? ticksNumber: number?
timeString: string? ticksString: string?
returns:
fakeTime: number
clockRunToLastTimer: clockSetFixedTime:
returns: parameters:
fakeTime: number timeNumber: number?
timeString: string?
clockRunToNextTimer:
returns: clockSetSystemTime:
fakeTime: number
clockSetTime:
parameters:
time: number
clockSkipTime:
parameters: parameters:
timeNumber: number? timeNumber: number?
timeString: string? timeString: string?
returns:
fakeTime: number
events: events:

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@ import { test as it, expect } from './pageTest';
it.skip(!process.env.PW_FREEZE_TIME); 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); expect(await page.evaluate('Date.now()')).toBe(0);
}); });

View file

@ -35,8 +35,12 @@ const it = test.extend<{ calls: { params: any[] }[] }>({
}); });
it.describe('runFor', () => { it.describe('runFor', () => {
it.beforeEach(async ({ page }) => {
await page.clock.install();
await page.clock.pause();
});
it('triggers immediately without specified delay', async ({ page, calls }) => { it('triggers immediately without specified delay', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub); setTimeout(window.stub);
}); });
@ -46,7 +50,6 @@ it.describe('runFor', () => {
}); });
it('does not trigger without sufficient delay', async ({ page, calls }) => { it('does not trigger without sufficient delay', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
@ -55,7 +58,6 @@ it.describe('runFor', () => {
}); });
it('triggers after sufficient delay', async ({ page, calls }) => { it('triggers after sufficient delay', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
@ -64,7 +66,6 @@ it.describe('runFor', () => {
}); });
it('triggers simultaneous timers', async ({ page, calls }) => { it('triggers simultaneous timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
@ -74,7 +75,6 @@ it.describe('runFor', () => {
}); });
it('triggers multiple simultaneous timers', async ({ page, calls }) => { it('triggers multiple simultaneous timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
@ -86,7 +86,6 @@ it.describe('runFor', () => {
}); });
it('waits after setTimeout was called', async ({ page, calls }) => { it('waits after setTimeout was called', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 150); setTimeout(window.stub, 150);
}); });
@ -97,18 +96,16 @@ it.describe('runFor', () => {
}); });
it('triggers event when some throw', async ({ page, calls }) => { it('triggers event when some throw', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { throw new Error(); }, 100); setTimeout(() => { throw new Error(); }, 100);
setTimeout(window.stub, 120); setTimeout(window.stub, 120);
}); });
await expect(page.clock.runFor(120)).rejects.toThrow(); await expect(page.clock.runFor(120)).rejects.toThrow();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('creates updated Date while ticking', async ({ page, calls }) => { it('creates updated Date while ticking', async ({ page, calls }) => {
await page.clock.installFakeTimers(0); await page.clock.setSystemTime(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(() => { setInterval(() => {
window.stub(new Date().getTime()); window.stub(new Date().getTime());
@ -130,7 +127,6 @@ it.describe('runFor', () => {
}); });
it('passes 8 seconds', async ({ page, calls }) => { it('passes 8 seconds', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 4000); setInterval(window.stub, 4000);
}); });
@ -140,7 +136,6 @@ it.describe('runFor', () => {
}); });
it('passes 1 minute', async ({ page, calls }) => { it('passes 1 minute', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 6000); setInterval(window.stub, 6000);
}); });
@ -150,7 +145,6 @@ it.describe('runFor', () => {
}); });
it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => { it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 10000); setInterval(window.stub, 10000);
}); });
@ -160,7 +154,6 @@ it.describe('runFor', () => {
}); });
it('throws for invalid format', async ({ page, calls }) => { it('throws for invalid format', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 10000); setInterval(window.stub, 10000);
}); });
@ -169,332 +162,93 @@ it.describe('runFor', () => {
}); });
it('returns the current now value', async ({ page }) => { it('returns the current now value', async ({ page }) => {
await page.clock.installFakeTimers(0); await page.clock.setSystemTime(0);
const value = 200; const value = 200;
await page.clock.runFor(value); await page.clock.runFor(value);
expect(await page.evaluate(() => Date.now())).toBe(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 }) => { it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub('should not be logged'); window.stub('should not be logged');
}, 1000); }, 1000);
}); });
await page.clock.skipTime(500); await page.clock.fastForward(500);
expect(calls).toEqual([]); expect(calls).toEqual([]);
}); });
it('pushes back execution time for skipped timers', async ({ page, calls }) => { it('pushes back execution time for skipped timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub(Date.now()); window.stub(Date.now());
}, 1000); }, 1000);
}); });
await page.clock.skipTime(2000); await page.clock.fastForward(2000);
expect(calls).toEqual([{ params: [2000] }]); expect(calls).toEqual([{ params: [2000] }]);
}); });
it('supports string time arguments', async ({ page, calls }) => { it('supports string time arguments', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub(Date.now()); window.stub(Date.now());
}, 100000); // 100000 = 1:40 }, 100000); // 100000 = 1:40
}); });
await page.clock.skipTime('01:50'); await page.clock.fastForward('01:50');
expect(calls).toEqual([{ params: [110000] }]); expect(calls).toEqual([{ params: [110000] }]);
}); });
}); });
it.describe('runAllTimers', () => { it.describe('fastForwardTo', () => {
it('if there are no timers just return', async ({ page }) => { it.beforeEach(async ({ page }) => {
await page.clock.installFakeTimers(0); await page.clock.install();
await page.clock.runAllTimers(); await page.clock.pause();
await page.clock.setSystemTime(0);
}); });
it('runs all timers', async ({ page, calls }) => { it(`ignores timers which wouldn't be run`, 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);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
setTimeout(window.stub, 50); window.stub('should not be logged');
}, 10); }, 1000);
}); });
await page.clock.runAllTimers(); await page.clock.fastForwardTo(500);
expect(calls.length).toBe(1); expect(calls).toEqual([]);
}); });
it('new timers added in promises while running are also run', async ({ page, calls }) => { it('pushes back execution time for skipped timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => { window.stub(Date.now());
setTimeout(window.stub, 50); }, 1000);
});
}, 10);
}); });
await page.clock.runAllTimers();
expect(calls.length).toBe(1);
});
it('throws before allowing infinite recursion', async ({ page, calls }) => { await page.clock.fastForwardTo(2000);
await page.clock.installFakeTimers(0); expect(calls).toEqual([{ params: [2000] }]);
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] },
]);
}); });
}); });
it.describe('stubTimers', () => { 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 }) => { 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); expect(await page.evaluate(() => Date.now())).toBe(1400);
}); });
it('replaces global setTimeout', async ({ page, calls }) => { it('replaces global setTimeout', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 1000); setTimeout(window.stub, 1000);
}); });
@ -503,13 +257,11 @@ it.describe('stubTimers', () => {
}); });
it('global fake setTimeout should return id', async ({ page, calls }) => { it('global fake setTimeout should return id', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
const to = await page.evaluate(() => setTimeout(window.stub, 1000)); const to = await page.evaluate(() => setTimeout(window.stub, 1000));
expect(typeof to).toBe('number'); expect(typeof to).toBe('number');
}); });
it('replaces global clearTimeout', async ({ page, calls }) => { it('replaces global clearTimeout', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const to = setTimeout(window.stub, 1000); const to = setTimeout(window.stub, 1000);
clearTimeout(to); clearTimeout(to);
@ -519,7 +271,6 @@ it.describe('stubTimers', () => {
}); });
it('replaces global setInterval', async ({ page, calls }) => { it('replaces global setInterval', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 500); setInterval(window.stub, 500);
}); });
@ -528,7 +279,6 @@ it.describe('stubTimers', () => {
}); });
it('replaces global clearInterval', async ({ page, calls }) => { it('replaces global clearInterval', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const to = setInterval(window.stub, 500); const to = setInterval(window.stub, 500);
clearInterval(to); clearInterval(to);
@ -538,7 +288,6 @@ it.describe('stubTimers', () => {
}); });
it('replaces global performance.now', async ({ page }) => { it('replaces global performance.now', async ({ page }) => {
await page.clock.installFakeTimers(0);
const promise = page.evaluate(async () => { const promise = page.evaluate(async () => {
const prev = performance.now(); const prev = performance.now();
await new Promise(f => setTimeout(f, 1000)); await new Promise(f => setTimeout(f, 1000));
@ -549,30 +298,35 @@ it.describe('stubTimers', () => {
expect(await promise).toEqual({ prev: 0, next: 1000 }); 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 }) => { it('fakes Date constructor', async ({ page }) => {
await page.clock.installFakeTimers(0);
const now = await page.evaluate(() => new Date().getTime()); const now = await page.evaluate(() => new Date().getTime());
expect(now).toBe(0); 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.describe('popup', () => {
it('should tick after popup', async ({ page }) => { it('should tick after popup', async ({ page }) => {
await page.clock.install();
await page.clock.pause();
const now = new Date('2015-09-25'); const now = new Date('2015-09-25');
await page.clock.installFakeTimers(now); await page.clock.setSystemTime(now);
const [popup] = await Promise.all([ const [popup] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.evaluate(() => window.open('about:blank')), page.evaluate(() => window.open('about:blank')),
@ -584,11 +338,12 @@ it.describe('popup', () => {
expect(popupTimeAfter).toBe(now.getTime() + 1000); 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'); const now = new Date('2015-09-25');
await page.clock.installFakeTimers(now); await page.clock.setSystemTime(now);
const ticks = await page.clock.runFor(1000); await page.clock.runFor(1000);
expect(ticks).toBe(1000);
const [popup] = await Promise.all([ const [popup] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
@ -597,90 +352,47 @@ it.describe('popup', () => {
const popupTime = await popup.evaluate(() => Date.now()); const popupTime = await popup.evaluate(() => Date.now());
expect(popupTime).toBe(now.getTime() + 1000); expect(popupTime).toBe(now.getTime() + 1000);
}); });
});
it.describe('runToNextTimer', () => { it('should run time before popup', async ({ page, server }) => {
it('triggers the next timer', async ({ page, calls }) => { server.setRoute('/popup.html', async (req, res) => {
await page.clock.installFakeTimers(0); res.setHeader('Content-Type', 'text/html');
await page.evaluate(async () => { res.end(`<script>window.time = Date.now()</script>`);
setTimeout(window.stub, 100);
}); });
expect(await page.clock.runToNextTimer()).toBe(100); await page.clock.setSystemTime(0);
expect(calls).toHaveLength(1); 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 }) => { it('should not run time before popup on pause', async ({ page, server }) => {
await page.clock.installFakeTimers(0); server.setRoute('/popup.html', async (req, res) => {
await page.evaluate(() => { res.setHeader('Content-Type', 'text/html');
setTimeout(() => { res.end(`<script>window.time = Date.now()</script>`);
window.stub();
}, 100);
setTimeout(() => {
window.stub();
}, 100);
}); });
await page.clock.install();
await page.clock.runToNextTimer(); await page.clock.pause();
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.
it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { await page.waitForTimeout(2000);
await page.clock.installFakeTimers(0); const [popup] = await Promise.all([
await page.evaluate(async () => { page.waitForEvent('popup'),
setTimeout(() => { page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'),
window.stub(); ]);
}, 100); const popupTime = await popup.evaluate('time');
setTimeout(() => { expect(popupTime).toBe(0);
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();
}); });
}); });
it.describe('setTime', () => { it.describe('setFixedTime', () => {
it('does not fake methods', async ({ page }) => { it('does not fake methods', async ({ page }) => {
await page.clock.setTime(0); await page.clock.setFixedTime(0);
// Should not stall. // Should not stall.
await page.evaluate(() => { await page.evaluate(() => {
@ -688,54 +400,145 @@ it.describe('setTime', () => {
}); });
}); });
it('allows setting time multiple times', async ({ page, calls }) => { it('allows setting time multiple times', async ({ page }) => {
await page.clock.setTime(100); await page.clock.setFixedTime(100);
expect(await page.evaluate(() => Date.now())).toBe(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); expect(await page.evaluate(() => Date.now())).toBe(200);
}); });
it('supports skipTime w/o fake timers', async ({ page }) => { it('fixed time is not affected by clock manipulation', async ({ page }) => {
await page.clock.setTime(100); 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); 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 }) => { 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); expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.installFakeTimers(200); await page.clock.setFixedTime(200);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => window.stub(Date.now())); setTimeout(() => window.stub(Date.now()));
}); });
await page.clock.runFor(0); await page.clock.runFor(0);
expect(calls).toEqual([{ params: [200] }]); expect(calls).toEqual([{ params: [200] }]);
}); });
});
it('allows setting time after installing fake timers', async ({ page, calls }) => { it.describe('while running', () => {
await page.clock.installFakeTimers(200); it('should progress time', async ({ page }) => {
await page.evaluate(async () => { await page.clock.install({ time: 0 });
setTimeout(() => window.stub(Date.now())); await page.goto('data:text/html,');
}); await page.waitForTimeout(1000);
await page.clock.setTime(220); const now = await page.evaluate(() => Date.now());
expect(calls).toEqual([{ params: [220] }]); expect(now).toBeGreaterThanOrEqual(1000);
expect(now).toBeLessThanOrEqual(2000);
}); });
it('does not allow flowing time backwards', async ({ page, calls }) => { it('should runFor', async ({ page }) => {
await page.clock.installFakeTimers(200); await page.clock.install({ time: 0 });
await expect(page.clock.setTime(180)).rejects.toThrow(); 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 }) => { it('should fastForward', async ({ page }) => {
await page.clock.installFakeTimers(0); await page.clock.install({ time: 0 });
await page.evaluate(async () => { await page.goto('data:text/html,');
setTimeout(window.stub, 100); await page.clock.fastForward(10000);
setTimeout(window.stub, 200); const now = await page.evaluate(() => Date.now());
}); expect(now).toBeGreaterThanOrEqual(10000);
await page.clock.setTime(100); expect(now).toBeLessThanOrEqual(11000);
expect(calls).toHaveLength(1); });
await page.clock.setTime(200);
expect(calls).toHaveLength(2); 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'] }]);
}); });
}); });