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
in all the pages and iframes is controlled by the same clock.
## async method: Clock.installFakeTimers
## async method: Clock.fastForward
* since: v1.45
Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and
reopening it later, after given time.
**Usage**
```js
await page.clock.fastForward(1000);
await page.clock.fastForward('30:00');
```
```python async
await page.clock.fast_forward(1000)
await page.clock.fast_forward("30:00")
```
```python sync
page.clock.fast_forward(1000)
page.clock.fast_forward("30:00")
```
```java
page.clock().fastForward(1000);
page.clock().fastForward("30:00");
```
```csharp
await page.Clock.FastForwardAsync(1000);
await page.Clock.FastForwardAsync("30:00");
```
### param: Clock.fastForward.ticks
* since: v1.45
- `ticks` <[int]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
## async method: Clock.fastForwardTo
* since: v1.45
Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and
reopening it at the specified time.
**Usage**
```js
await page.clock.fastForwardTo(new Date('2020-02-02'));
await page.clock.fastForwardTo('2020-02-02');
```
```python async
await page.clock.fast_forward_to(datetime.datetime(2020, 2, 2))
await page.clock.fast_forward_to("2020-02-02")
```
```python sync
page.clock.fast_forward_to(datetime.datetime(2020, 2, 2))
page.clock.fast_forward_to("2020-02-02")
```
```java
page.clock().fastForwardTo(Instant.parse("2020-02-02"));
page.clock().fastForwardTo("2020-02-02");
```
```csharp
await page.Clock.FastForwardToAsync(DateTime.Parse("2020-02-02"));
await page.Clock.FastForwardToAsync("2020-02-02");
```
### param: Clock.fastForwardTo.time
* since: v1.45
- `time` <[int]|[string]|[Date]>
## async method: Clock.install
* since: v1.45
Install fake implementations for the following time-related functions:
* `Date`
* `setTimeout`
* `clearTimeout`
* `setInterval`
@ -21,41 +98,18 @@ Install fake implementations for the following time-related functions:
* `cancelIdleCallback`
* `performance`
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.skipTime`] for more information.
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information.
### param: Clock.installFakeTimers.time
### option: Clock.install.time
* since: v1.45
- `time` <[int]|[Date]>
Install fake timers with the specified base time.
### option: Clock.installFakeTimers.loopLimit
* since: v1.45
- `loopLimit` <[int]>
The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`.
## async method: Clock.runAllTimers
* since: v1.45
- returns: <[int]>
Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well.
Fake timers must be installed.
Returns fake milliseconds since the unix epoch.
**Details**
This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers.
It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error.
- `time` <[int]|[string]|[Date]>
Time to initialize with, current system time by default.
## async method: Clock.runFor
* since: v1.45
- returns: <[int]>
Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch.
Fake timers must be installed.
Returns fake milliseconds since the unix epoch.
Advance the clock, firing all the time-related callbacks.
**Usage**
@ -66,12 +120,12 @@ await page.clock.runFor('30:00');
```python async
await page.clock.run_for(1000);
await page.clock.run_for('30:00')
await page.clock.run_for("30:00")
```
```python sync
page.clock.run_for(1000);
page.clock.run_for('30:00')
page.clock.run_for("30:00")
```
```java
@ -84,84 +138,104 @@ await page.Clock.RunForAsync(1000);
await page.Clock.RunForAsync("30:00");
```
### param: Clock.runFor.time
### param: Clock.runFor.ticks
* since: v1.45
- `time` <[int]|[string]>
- `ticks` <[int]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
## async method: Clock.runToLastTimer
* since: v1.45
- returns: <[int]>
This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary.
If new timers are added while it is executing they will be run only if they would occur before this time.
This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning.
Fake timers must be installed.
Returns fake milliseconds since the unix epoch.
## async method: Clock.runToNextTimer
* since: v1.45
- returns: <[int]>
Advances the clock to the moment of the first scheduled timer, firing it.
Fake timers must be installed.
Returns fake milliseconds since the unix epoch.
## async method: Clock.setTime
## async method: Clock.pause
* since: v1.45
Set the clock to the specified time.
Pause timers. Once this method is called, no timers are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.fastForwardTo`] or [`method: Clock.resume`] is called.
When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as a browser)
being put to sleep and resumed later, skipping intermediary timers.
### param: Clock.setTime.time
## async method: Clock.resume
* since: v1.45
- `time` <[int]|[Date]>
Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
## async method: Clock.skipTime
## async method: Clock.setFixedTime
* since: v1.45
- returns: <[int]>
Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time.
When fake timers are installed, [`method: Clock.skipTime`] only fires due timers at most once, while [`method: Clock.runFor`] fires all the timers up to the current time.
Returns fake milliseconds since the unix epoch.
Makes `Date.now` and `new Date()` return fixed fake time at all times,
keeps all the timers running.
**Usage**
```js
await page.clock.skipTime(1000);
await page.clock.skipTime('30:00');
await page.clock.setFixedTime(Date.now());
await page.clock.setFixedTime(new Date('2020-02-02'));
await page.clock.setFixedTime('2020-02-02');
```
```python async
await page.clock.skipTime(1000);
await page.clock.skipTime('30:00')
await page.clock.set_fixed_time(datetime.datetime.now())
await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
await page.clock.set_fixed_time("2020-02-02")
```
```python sync
page.clock.skipTime(1000);
page.clock.skipTime('30:00')
page.clock.set_fixed_time(datetime.datetime.now())
page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
page.clock.set_fixed_time("2020-02-02")
```
```java
page.clock().skipTime(1000);
page.clock().skipTime("30:00");
page.clock().setFixedTime(Instant.now());
page.clock().setFixedTime(Instant.parse("2020-02-02"));
page.clock().setFixedTime("2020-02-02");
```
```csharp
await page.Clock.SkipTimeAsync(1000);
await page.Clock.SkipTimeAsync("30:00");
await page.Clock.SetFixedTimeAsync(DateTime.Now);
await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
await page.Clock.SetFixedTimeAsync("2020-02-02");
```
### param: Clock.skipTime.time
### param: Clock.setFixedTime.time
* since: v1.45
- `time` <[int]|[string]>
- `time` <[int]|[string]|[Date]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
Time to be set.
## async method: Clock.setSystemTime
* since: v1.45
Sets current system time but does not trigger any timers, unlike [`method: Clock.fastForwardTo`].
**Usage**
```js
await page.clock.setSystemTime(Date.now());
await page.clock.setSystemTime(new Date('2020-02-02'));
await page.clock.setSystemTime('2020-02-02');
```
```python async
await page.clock.set_system_time(datetime.datetime.now())
await page.clock.set_system_time(datetime.datetime(2020, 2, 2))
await page.clock.set_system_time("2020-02-02")
```
```python sync
page.clock.set_system_time(datetime.datetime.now())
page.clock.set_system_time(datetime.datetime(2020, 2, 2))
page.clock.set_system_time("2020-02-02")
```
```java
page.clock().setSystemTime(Instant.now());
page.clock().setSystemTime(Instant.parse("2020-02-02"));
page.clock().setSystemTime("2020-02-02");
```
```csharp
await page.Clock.SetSystemTimeAsync(DateTime.Now);
await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
await page.Clock.SetSystemTimeAsync("2020-02-02");
```
### param: Clock.setSystemTime.time
* since: v1.45
- `time` <[int]|[string]|[Date]>

View File

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

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 context = BrowserContext.from(response.context);
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
if (!forReuse && !!process.env.PW_FREEZE_TIME)
await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true);
if (!forReuse && !!process.env.PW_FREEZE_TIME) {
await this._wrapApiCall(async () => {
await context.clock.install({ time: 0 });
await context.clock.pause();
}, true);
}
return context;
}

View File

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

View File

@ -963,41 +963,40 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
enabled: tBoolean,
});
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
scheme.BrowserContextClockInstallFakeTimersParams = tObject({
time: tNumber,
loopLimit: tOptional(tNumber),
scheme.BrowserContextClockFastForwardParams = tObject({
ticksNumber: tOptional(tNumber),
ticksString: tOptional(tString),
});
scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({}));
scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({}));
scheme.BrowserContextClockRunAllTimersResult = tObject({
fakeTime: tNumber,
scheme.BrowserContextClockFastForwardResult = tOptional(tObject({}));
scheme.BrowserContextClockFastForwardToParams = tObject({
timeNumber: tOptional(tNumber),
timeString: tOptional(tString),
});
scheme.BrowserContextClockFastForwardToResult = tOptional(tObject({}));
scheme.BrowserContextClockInstallParams = tObject({
timeNumber: tOptional(tNumber),
timeString: tOptional(tString),
});
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
scheme.BrowserContextClockPauseParams = tOptional(tObject({}));
scheme.BrowserContextClockPauseResult = tOptional(tObject({}));
scheme.BrowserContextClockResumeParams = tOptional(tObject({}));
scheme.BrowserContextClockResumeResult = tOptional(tObject({}));
scheme.BrowserContextClockRunForParams = tObject({
ticksNumber: tOptional(tNumber),
ticksString: tOptional(tString),
});
scheme.BrowserContextClockRunForResult = tOptional(tObject({}));
scheme.BrowserContextClockSetFixedTimeParams = tObject({
timeNumber: tOptional(tNumber),
timeString: tOptional(tString),
});
scheme.BrowserContextClockRunForResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({}));
scheme.BrowserContextClockRunToLastTimerResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({}));
scheme.BrowserContextClockRunToNextTimerResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockSetTimeParams = tObject({
time: tNumber,
});
scheme.BrowserContextClockSetTimeResult = tOptional(tObject({}));
scheme.BrowserContextClockSkipTimeParams = tObject({
scheme.BrowserContextClockSetFixedTimeResult = tOptional(tObject({}));
scheme.BrowserContextClockSetSystemTimeParams = tObject({
timeNumber: tOptional(tNumber),
timeString: tOptional(tString),
});
scheme.BrowserContextClockSkipTimeResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockSetSystemTimeResult = tOptional(tObject({}));
scheme.PageInitializer = tObject({
mainFrame: tChannel(['Frame']),
viewportSize: tOptional(tObject({

View File

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

View File

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

View File

@ -26,7 +26,6 @@ export type ClockMethods = {
export type ClockConfig = {
now?: number | Date;
loopLimit?: number;
};
export type InstallConfig = ClockConfig & {
@ -53,26 +52,39 @@ type Timer = {
};
interface Embedder {
postTask(task: () => void): void;
postTaskPeriodically(task: () => void, delay: number): () => void;
dateNow(): number;
performanceNow(): DOMHighResTimeStamp;
setTimeout(task: () => void, timeout?: number): () => void;
setInterval(task: () => void, delay: number): () => void;
}
type Time = {
// ms since Epoch
time: number;
// Ticks since the session began (ala performance.now)
ticks: number;
// Whether fixed time was set.
isFixedTime: boolean;
// Origin time since Epoch when session started.
origin: number;
};
type LogEntryType = 'fastForward' | 'fastForwardTo' | 'install' | 'pause' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime';
export class ClockController {
readonly timeOrigin: number;
private _now: { time: number, ticks: number, timeFrozen: boolean };
private _loopLimit: number;
readonly _now: Time;
private _duringTick = false;
private _timers = new Map<number, Timer>();
private _uniqueTimerId = idCounterStart;
private _embedder: Embedder;
readonly disposables: (() => void)[] = [];
private _log: { type: LogEntryType, time: number, param?: number }[] = [];
private _realTime: { startTicks: number, lastSyncTicks: number } | undefined;
private _currentRealTimeTimer: { callAt: number, dispose: () => void } | undefined;
constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) {
const start = Math.floor(getEpoch(startDate));
this.timeOrigin = start;
this._now = { time: start, ticks: 0, timeFrozen: false };
constructor(embedder: Embedder) {
this._now = { time: 0, isFixedTime: false, ticks: 0, origin: -1 };
this._embedder = embedder;
this._loopLimit = loopLimit;
}
uninstall() {
@ -81,109 +93,147 @@ export class ClockController {
}
now(): number {
this._replayLogOnce();
return this._now.time;
}
setTime(now: Date | number, options: { freeze?: boolean } = {}) {
this._now.time = getEpoch(now);
this._now.timeFrozen = !!options.freeze;
install(time: number) {
this._replayLogOnce();
this._innerSetTime(time);
}
setSystemTime(time: number) {
this._replayLogOnce();
this._innerSetTime(time);
}
setFixedTime(time: number) {
this._replayLogOnce();
this._innerSetFixedTime(time);
}
performanceNow(): DOMHighResTimeStamp {
this._replayLogOnce();
return this._now.ticks;
}
private _innerSetTime(time: number) {
this._now.time = time;
this._now.isFixedTime = false;
if (this._now.origin < 0)
this._now.origin = this._now.time;
}
private _innerSetFixedTime(time: number) {
this._innerSetTime(time);
this._now.isFixedTime = true;
}
private _advanceNow(toTicks: number) {
if (!this._now.timeFrozen)
if (!this._now.isFixedTime)
this._now.time += toTicks - this._now.ticks;
this._now.ticks = toTicks;
}
private async _doTick(msFloat: number): Promise<number> {
if (msFloat < 0)
throw new TypeError('Negative ticks are not supported');
async log(type: LogEntryType, time: number, param?: number) {
this._log.push({ type, time, param });
}
async runFor(ticks: number) {
this._replayLogOnce();
if (ticks < 0)
throw new TypeError('Negative ticks are not supported');
await this._runTo(this._now.ticks + ticks);
}
private async _runTo(tickTo: number) {
if (this._now.ticks > tickTo)
return;
const ms = Math.floor(msFloat);
const tickTo = this._now.ticks + ms;
let tickFrom = this._now.ticks;
let previous = this._now.ticks;
let firstException: Error | undefined;
let timer = this._firstTimerInRange(tickFrom, tickTo);
while (timer && tickFrom <= tickTo) {
tickFrom = timer.callAt;
const error = await this._callTimer(timer).catch(e => e);
firstException = firstException || error;
timer = this._firstTimerInRange(previous, tickTo);
previous = tickFrom;
while (true) {
const result = await this._callFirstTimer(tickTo);
if (!result.timerFound)
break;
firstException = firstException || result.error;
}
this._advanceNow(tickTo);
if (firstException)
throw firstException;
return this._now.ticks;
}
async recordTick(tickValue: string | number) {
const msFloat = parseTime(tickValue);
this._advanceNow(this._now.ticks + msFloat);
pause() {
this._replayLogOnce();
this._innerPause();
}
async tick(tickValue: string | number): Promise<number> {
return await this._doTick(parseTime(tickValue));
private _innerPause() {
this._realTime = undefined;
this._updateRealTimeTimer();
}
async next(): Promise<number> {
const timer = this._firstTimer();
if (!timer)
return this._now.ticks;
await this._callTimer(timer);
return this._now.ticks;
resume() {
this._replayLogOnce();
this._innerResume();
}
async runToFrame(): Promise<number> {
return this.tick(this.getTimeToNextFrame());
private _innerResume() {
const now = this._embedder.performanceNow();
this._realTime = { startTicks: now, lastSyncTicks: now };
this._updateRealTimeTimer();
}
async runAll(): Promise<number> {
for (let i = 0; i < this._loopLimit; i++) {
const numTimers = this._timers.size;
if (numTimers === 0)
return this._now.ticks;
await this.next();
private _updateRealTimeTimer() {
if (!this._realTime) {
this._currentRealTimeTimer?.dispose();
this._currentRealTimeTimer = undefined;
return;
}
const excessJob = this._firstTimer();
if (!excessJob)
return this._now.ticks;
throw this._getInfiniteLoopError(excessJob);
const firstTimer = this._firstTimer();
// Either run the next timer or move time in 100ms chunks.
const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100);
if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt)
return;
if (this._currentRealTimeTimer) {
this._currentRealTimeTimer.dispose();
this._currentRealTimeTimer = undefined;
}
this._currentRealTimeTimer = {
callAt,
dispose: this._embedder.setTimeout(() => {
const now = Math.ceil(this._embedder.performanceNow());
this._currentRealTimeTimer = undefined;
const sinceLastSync = now - this._realTime!.lastSyncTicks;
this._realTime!.lastSyncTicks = now;
// eslint-disable-next-line no-console
this._runTo(this._now.ticks + sinceLastSync).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
}, callAt - this._now.ticks),
};
}
async runToLast(): Promise<number> {
const timer = this._lastTimer();
if (!timer)
return this._now.ticks;
return await this.tick(timer.callAt - this._now.ticks);
}
reset() {
this._timers.clear();
this._now = { time: this.timeOrigin, ticks: 0, timeFrozen: false };
}
async jump(tickValue: string | number): Promise<number> {
const msFloat = parseTime(tickValue);
const ms = Math.floor(msFloat);
async fastForward(ticks: number) {
this._replayLogOnce();
const ms = ticks | 0;
for (const timer of this._timers.values()) {
if (this._now.ticks + ms > timer.callAt)
timer.callAt = this._now.ticks + ms;
}
return await this.tick(ms);
await this.runFor(ms);
}
async fastForwardTo(time: number) {
this._replayLogOnce();
const ticks = time - this._now.time;
await this.fastForward(ticks);
}
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
this._replayLogOnce();
if (options.func === undefined)
throw new Error('Callback must be provided to timer calls');
@ -204,56 +254,56 @@ export class ClockController {
error: new Error(),
};
this._timers.set(timer.id, timer);
if (this._realTime)
this._updateRealTimeTimer();
return timer.id;
}
private _firstTimerInRange(from: number, to: number): Timer | null {
let firstTimer: Timer | null = null;
for (const timer of this._timers.values()) {
const isInRange = inRange(from, to, timer);
if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1))
firstTimer = timer;
}
return firstTimer;
}
countTimers() {
return this._timers.size;
}
private _firstTimer(): Timer | null {
private _firstTimer(beforeTick?: number): Timer | null {
let firstTimer: Timer | null = null;
for (const timer of this._timers.values()) {
if (!firstTimer || compareTimers(firstTimer, timer) === 1)
const isInRange = beforeTick === undefined || timer.callAt <= beforeTick;
if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1))
firstTimer = timer;
}
return firstTimer;
}
private _lastTimer(): Timer | null {
let lastTimer: Timer | null = null;
private _takeFirstTimer(beforeTick?: number): Timer | null {
const timer = this._firstTimer(beforeTick);
if (!timer)
return null;
for (const timer of this._timers.values()) {
if (!lastTimer || compareTimers(lastTimer, timer) === -1)
lastTimer = timer;
}
return lastTimer;
}
private async _callTimer(timer: Timer) {
this._advanceNow(timer.callAt);
if (timer.type === TimerType.Interval)
this._timers.get(timer.id)!.callAt += timer.delay;
else
this._timers.delete(timer.id);
return timer;
}
private async _callFirstTimer(beforeTick: number): Promise<{ timerFound: boolean, error?: Error }> {
const timer = this._takeFirstTimer(beforeTick);
if (!timer)
return { timerFound: false };
this._duringTick = true;
try {
if (typeof timer.func !== 'function') {
(() => { eval(timer.func); })();
return;
let error: Error | undefined;
try {
(() => { eval(timer.func); })();
} catch (e) {
error = e;
}
await new Promise<void>(f => this._embedder.setTimeout(f));
return { timerFound: true, error };
}
let args = timer.args;
@ -262,67 +312,26 @@ export class ClockController {
else if (timer.type === TimerType.IdleCallback)
args = [{ didTimeout: false, timeRemaining: () => 0 }];
timer.func.apply(null, args);
await new Promise<void>(f => this._embedder.postTask(f));
let error: Error | undefined;
try {
timer.func.apply(null, args);
} catch (e) {
error = e;
}
await new Promise<void>(f => this._embedder.setTimeout(f));
return { timerFound: true, error };
} finally {
this._duringTick = false;
}
}
private _getInfiniteLoopError(job: Timer) {
const infiniteLoopError = new Error(
`Aborting after running ${this._loopLimit} timers, assuming an infinite loop!`,
);
if (!job.error)
return infiniteLoopError;
// pattern never matched in Node
const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/;
const clockMethodPattern = new RegExp(
String(Object.keys(this).join('|')),
);
let matchedLineIndex = -1;
job.error.stack!.split('\n').some((line, i) => {
// If we've matched a computed target line (e.g. setTimeout) then we
// don't need to look any further. Return true to stop iterating.
const matchedComputedTarget = line.match(computedTargetPattern);
/* istanbul ignore if */
if (matchedComputedTarget) {
matchedLineIndex = i;
return true;
}
// If we've matched a clock method line, then there may still be
// others further down the trace. Return false to keep iterating.
const matchedClockMethod = line.match(clockMethodPattern);
if (matchedClockMethod) {
matchedLineIndex = i;
return false;
}
// If we haven't matched anything on this line, but we matched
// previously and set the matched line index, then we can stop.
// If we haven't matched previously, then we should keep iterating.
return matchedLineIndex >= 0;
});
const funcName = typeof job.func === 'function' ? job.func.name : 'anonymous';
const stack = `${infiniteLoopError}\n${job.type || 'Microtask'} - ${funcName}\n${job.error.stack!
.split('\n')
.slice(matchedLineIndex + 1)
.join('\n')}`;
infiniteLoopError.stack = stack;
return infiniteLoopError;
}
getTimeToNextFrame() {
return 16 - this._now.ticks % 16;
}
clearTimer(timerId: number, type: TimerType) {
this._replayLogOnce();
if (!timerId) {
// null appears to be allowed in most browsers, and appears to be
// relied upon by some libraries, like Bootstrap carousel
@ -356,64 +365,49 @@ export class ClockController {
}
}
advanceAutomatically(advanceTimeDelta: number = 20): () => void {
return this._embedder.postTaskPeriodically(
() => this._doTick(advanceTimeDelta!),
advanceTimeDelta,
);
private _replayLogOnce() {
if (!this._log.length)
return;
let lastLogTime = -1;
let isPaused = false;
for (const { type, time, param } of this._log) {
if (!isPaused && lastLogTime !== -1)
this._advanceNow(this._now.ticks + time - lastLogTime);
lastLogTime = time;
if (type === 'install') {
this._innerSetTime(param!);
} else if (type === 'fastForward' || type === 'runFor') {
this._advanceNow(this._now.ticks + param!);
} else if (type === 'fastForwardTo') {
this._innerSetTime(param!);
} else if (type === 'pause') {
this._innerPause();
isPaused = true;
} else if (type === 'resume') {
this._innerResume();
isPaused = false;
} else if (type === 'setFixedTime') {
this._innerSetFixedTime(param!);
} else if (type === 'setSystemTime') {
this._innerSetTime(param!);
}
}
if (!isPaused && lastLogTime > 0)
this._advanceNow(this._now.ticks + this._embedder.dateNow() - lastLogTime);
this._log.length = 0;
}
}
function getEpoch(epoch: Date | number | undefined): number {
if (!epoch)
return 0;
if (typeof epoch !== 'number')
return epoch.getTime();
return epoch;
}
function inRange(from: number, to: number, timer: Timer): boolean {
return timer && timer.callAt >= from && timer.callAt <= to;
}
/**
* Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into
* number of milliseconds. This is used to support human-readable strings passed
* to clock.tick()
*/
function parseTime(value: number | string): number {
if (typeof value === 'number')
return value;
if (!value)
return 0;
const str = value;
const strings = str.split(':');
const l = strings.length;
let i = l;
let ms = 0;
let parsed;
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
throw new Error(
`Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`,
);
}
while (i--) {
parsed = parseInt(strings[i], 10);
if (parsed >= 60)
throw new Error(`Invalid time ${str}`);
ms += parsed * Math.pow(60, l - i - 1);
}
return ms * 1000;
}
function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
let prop;
for (prop of Object.keys(source) as (keyof DateConstructor)[])
target[prop] = source[prop];
for (const prop in source) {
if (source.hasOwnProperty(prop))
target[prop] = (source as any)[prop];
}
target.toString = () => source.toString();
target.prototype = source.prototype;
target.parse = source.parse;
@ -485,7 +479,7 @@ function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Int
* All properties of Intl are non-enumerable, so we need
* to do a bit of work to get them out.
*/
for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[])
for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[])
ClockIntl[key] = NativeIntl[key];
ClockIntl.DateTimeFormat = function(...args: any[]) {
@ -644,8 +638,8 @@ function getClearHandler(type: TimerType) {
function fakePerformance(clock: ClockController, performance: Performance): Performance {
const result: any = {
now: () => clock.performanceNow(),
timeOrigin: clock.timeOrigin,
};
result.__defineGetter__('timeOrigin', () => clock._now.origin || 0);
// eslint-disable-next-line no-proto
for (const key of Object.keys((performance as any).__proto__)) {
if (key === 'now' || key === 'timeOrigin')
@ -658,19 +652,22 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf
return result;
}
export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
const originals = platformOriginals(globalObject);
const embedder = {
postTask: (task: () => void) => {
originals.bound.setTimeout(task, 0);
const embedder: Embedder = {
dateNow: () => originals.raw.Date.now(),
performanceNow: () => originals.raw.performance!.now(),
setTimeout: (task: () => void, timeout?: number) => {
const timerId = originals.bound.setTimeout(task, timeout);
return () => originals.bound.clearTimeout(timerId);
},
postTaskPeriodically: (task: () => void, delay: number) => {
const intervalId = globalObject.setInterval(task, delay);
setInterval: (task: () => void, delay: number) => {
const intervalId = originals.bound.setInterval(task, delay);
return () => originals.bound.clearInterval(intervalId);
},
};
const clock = new ClockController(embedder, config.now, config.loopLimit);
const clock = new ClockController(embedder);
const api = createApi(clock, originals.bound);
return { clock, api, originals: originals.raw };
}
@ -682,7 +679,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
throw new TypeError(`Can't install fake timers twice on the same global object.`);
}
const { clock, api, originals } = createClock(globalObject, config);
const { clock, api, originals } = createClock(globalObject);
const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[];
for (const method of toFake) {
@ -706,11 +703,10 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
}
export function inject(globalObject: WindowOrWorkerGlobalScope) {
const { clock: controller } = install(globalObject);
controller.resume();
return {
install: (config: InstallConfig) => {
const { clock } = install(globalObject, config);
return clock;
},
controller,
builtin: platformOriginals(globalObject).bound,
};
}

View File

@ -17247,8 +17247,40 @@ export interface BrowserServer {
* controlled by the same clock.
*/
export interface Clock {
/**
* Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user
* closing the laptop lid for a while and reopening it later, after given time.
*
* **Usage**
*
* ```js
* await page.clock.fastForward(1000);
* await page.clock.fastForward('30:00');
* ```
*
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/
fastForward(ticks: number|string): Promise<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:
* - `Date`
* - `setTimeout`
* - `clearTimeout`
* - `setInterval`
@ -17261,34 +17293,33 @@ export interface Clock {
*
* Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers,
* and control the behavior of time-dependent functions. See
* [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and
* [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) for more information.
* @param time Install fake timers with the specified base time.
* [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for) and
* [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward) for more information.
* @param options
*/
installFakeTimers(time: number|Date, options?: {
install(options?: {
/**
* The maximum number of timers that will be run in
* [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`.
* Time to initialize with, current system time by default.
*/
loopLimit?: number;
time?: number|string|Date;
}): Promise<void>;
/**
* Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be
* run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch.
*
* **Details**
*
* This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use,
* or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite
* loop of timers and throws an error.
* Pause timers. Once this method is called, no timers are fired unless
* [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for),
* [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward),
* [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to) or
* [clock.resume()](https://playwright.dev/docs/api/class-clock#clock-resume) is called.
*/
runAllTimers(): Promise<number>;
pause(): Promise<void>;
/**
* Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must
* be installed. Returns fake milliseconds since the unix epoch.
* Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
*/
resume(): Promise<void>;
/**
* Advance the clock, firing all the time-related callbacks.
*
* **Usage**
*
@ -17297,55 +17328,41 @@ export interface Clock {
* await page.clock.runFor('30:00');
* ```
*
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/
runFor(time: number|string): Promise<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
* 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.
* Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
*
* **Usage**
*
* ```js
* await page.clock.skipTime(1000);
* await page.clock.skipTime('30:00');
* await page.clock.setFixedTime(Date.now());
* await page.clock.setFixedTime(new Date('2020-02-02'));
* await page.clock.setFixedTime('2020-02-02');
* ```
*
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
* @param time Time to be set.
*/
skipTime(time: number|string): Promise<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>;
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallFakeTimersResult>;
clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllTimersResult>;
clockFastForward(params: BrowserContextClockFastForwardParams, metadata?: CallMetadata): Promise<BrowserContextClockFastForwardResult>;
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>;
clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastTimerResult>;
clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToNextTimerResult>;
clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetTimeResult>;
clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSkipTimeResult>;
clockSetFixedTime(params: BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetFixedTimeResult>;
clockSetSystemTime(params: BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetSystemTimeResult>;
}
export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel,
@ -1755,58 +1756,66 @@ export type BrowserContextUpdateSubscriptionOptions = {
};
export type BrowserContextUpdateSubscriptionResult = void;
export type BrowserContextClockInstallFakeTimersParams = {
time: number,
loopLimit?: number,
export type BrowserContextClockFastForwardParams = {
ticksNumber?: number,
ticksString?: string,
};
export type BrowserContextClockInstallFakeTimersOptions = {
loopLimit?: number,
export type BrowserContextClockFastForwardOptions = {
ticksNumber?: number,
ticksString?: string,
};
export type BrowserContextClockInstallFakeTimersResult = void;
export type BrowserContextClockRunAllTimersParams = {};
export type BrowserContextClockRunAllTimersOptions = {};
export type BrowserContextClockRunAllTimersResult = {
fakeTime: number,
};
export type BrowserContextClockRunForParams = {
export type BrowserContextClockFastForwardResult = void;
export type BrowserContextClockFastForwardToParams = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockFastForwardToOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockFastForwardToResult = void;
export type BrowserContextClockInstallParams = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockInstallOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockInstallResult = void;
export type BrowserContextClockPauseParams = {};
export type BrowserContextClockPauseOptions = {};
export type BrowserContextClockPauseResult = void;
export type BrowserContextClockResumeParams = {};
export type BrowserContextClockResumeOptions = {};
export type BrowserContextClockResumeResult = void;
export type BrowserContextClockRunForParams = {
ticksNumber?: number,
ticksString?: string,
};
export type BrowserContextClockRunForOptions = {
ticksNumber?: number,
ticksString?: string,
};
export type BrowserContextClockRunForResult = void;
export type BrowserContextClockSetFixedTimeParams = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockRunForResult = {
fakeTime: number,
};
export type BrowserContextClockRunToLastTimerParams = {};
export type BrowserContextClockRunToLastTimerOptions = {};
export type BrowserContextClockRunToLastTimerResult = {
fakeTime: number,
};
export type BrowserContextClockRunToNextTimerParams = {};
export type BrowserContextClockRunToNextTimerOptions = {};
export type BrowserContextClockRunToNextTimerResult = {
fakeTime: number,
};
export type BrowserContextClockSetTimeParams = {
time: number,
};
export type BrowserContextClockSetTimeOptions = {
};
export type BrowserContextClockSetTimeResult = void;
export type BrowserContextClockSkipTimeParams = {
export type BrowserContextClockSetFixedTimeOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockSkipTimeOptions = {
export type BrowserContextClockSetFixedTimeResult = void;
export type BrowserContextClockSetSystemTimeParams = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockSkipTimeResult = {
fakeTime: number,
export type BrowserContextClockSetSystemTimeOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockSetSystemTimeResult = void;
export interface BrowserContextEvents {
'bindingCall': BrowserContextBindingCallEvent;

View File

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

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('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);
});

View File

@ -35,8 +35,12 @@ const it = test.extend<{ calls: { params: any[] }[] }>({
});
it.describe('runFor', () => {
it.beforeEach(async ({ page }) => {
await page.clock.install();
await page.clock.pause();
});
it('triggers immediately without specified delay', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub);
});
@ -46,7 +50,6 @@ it.describe('runFor', () => {
});
it('does not trigger without sufficient delay', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
});
@ -55,7 +58,6 @@ it.describe('runFor', () => {
});
it('triggers after sufficient delay', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
});
@ -64,7 +66,6 @@ it.describe('runFor', () => {
});
it('triggers simultaneous timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(window.stub, 100);
@ -74,7 +75,6 @@ it.describe('runFor', () => {
});
it('triggers multiple simultaneous timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(window.stub, 100);
@ -86,7 +86,6 @@ it.describe('runFor', () => {
});
it('waits after setTimeout was called', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 150);
});
@ -97,18 +96,16 @@ it.describe('runFor', () => {
});
it('triggers event when some throw', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => { throw new Error(); }, 100);
setTimeout(window.stub, 120);
});
await expect(page.clock.runFor(120)).rejects.toThrow();
expect(calls).toHaveLength(1);
});
it('creates updated Date while ticking', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.clock.setSystemTime(0);
await page.evaluate(async () => {
setInterval(() => {
window.stub(new Date().getTime());
@ -130,7 +127,6 @@ it.describe('runFor', () => {
});
it('passes 8 seconds', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setInterval(window.stub, 4000);
});
@ -140,7 +136,6 @@ it.describe('runFor', () => {
});
it('passes 1 minute', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setInterval(window.stub, 6000);
});
@ -150,7 +145,6 @@ it.describe('runFor', () => {
});
it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setInterval(window.stub, 10000);
});
@ -160,7 +154,6 @@ it.describe('runFor', () => {
});
it('throws for invalid format', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setInterval(window.stub, 10000);
});
@ -169,332 +162,93 @@ it.describe('runFor', () => {
});
it('returns the current now value', async ({ page }) => {
await page.clock.installFakeTimers(0);
await page.clock.setSystemTime(0);
const value = 200;
await page.clock.runFor(value);
expect(await page.evaluate(() => Date.now())).toBe(value);
});
});
it.describe('skipTime', () => {
it.describe('fastForward', () => {
it.beforeEach(async ({ page }) => {
await page.clock.install();
await page.clock.pause();
await page.clock.setSystemTime(0);
});
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
window.stub('should not be logged');
}, 1000);
});
await page.clock.skipTime(500);
await page.clock.fastForward(500);
expect(calls).toEqual([]);
});
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
window.stub(Date.now());
}, 1000);
});
await page.clock.skipTime(2000);
await page.clock.fastForward(2000);
expect(calls).toEqual([{ params: [2000] }]);
});
it('supports string time arguments', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
window.stub(Date.now());
}, 100000); // 100000 = 1:40
});
await page.clock.skipTime('01:50');
await page.clock.fastForward('01:50');
expect(calls).toEqual([{ params: [110000] }]);
});
});
it.describe('runAllTimers', () => {
it('if there are no timers just return', async ({ page }) => {
await page.clock.installFakeTimers(0);
await page.clock.runAllTimers();
it.describe('fastForwardTo', () => {
it.beforeEach(async ({ page }) => {
await page.clock.install();
await page.clock.pause();
await page.clock.setSystemTime(0);
});
it('runs all timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
await page.clock.runAllTimers();
expect(calls.length).toBe(2);
});
it('new timers added while running are also run', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
await page.evaluate(async () => {
setTimeout(() => {
setTimeout(window.stub, 50);
}, 10);
window.stub('should not be logged');
}, 1000);
});
await page.clock.runAllTimers();
expect(calls.length).toBe(1);
await page.clock.fastForwardTo(500);
expect(calls).toEqual([]);
});
it('new timers added in promises while running are also run', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => {
setTimeout(window.stub, 50);
});
}, 10);
window.stub(Date.now());
}, 1000);
});
await page.clock.runAllTimers();
expect(calls.length).toBe(1);
});
it('throws before allowing infinite recursion', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
const recursiveCallback = () => {
window.stub();
setTimeout(recursiveCallback, 10);
};
setTimeout(recursiveCallback, 10);
});
await expect(page.clock.runAllTimers()).rejects.toThrow();
expect(calls).toHaveLength(1000);
});
it('throws before allowing infinite recursion from promises', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
const recursiveCallback = () => {
window.stub();
void Promise.resolve().then(() => {
setTimeout(recursiveCallback, 10);
});
};
setTimeout(recursiveCallback, 10);
});
await expect(page.clock.runAllTimers()).rejects.toThrow();
expect(calls).toHaveLength(1000);
});
it('the loop limit can be set when creating a clock', async ({ page, calls }) => {
await page.clock.installFakeTimers(0, { loopLimit: 1 });
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
await expect(page.clock.runAllTimers()).rejects.toThrow();
expect(calls).toHaveLength(1);
});
it('should settle user-created promises', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => window.stub());
}, 55);
});
await page.clock.runAllTimers();
expect(calls).toHaveLength(1);
});
it('should settle nested user-created promises', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => window.stub());
});
});
}, 55);
});
await page.clock.runAllTimers();
expect(calls).toHaveLength(1);
});
it('should settle local promises before firing timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
void Promise.resolve().then(() => window.stub(1));
setTimeout(() => window.stub(2), 55);
});
await page.clock.runAllTimers();
expect(calls).toEqual([
{ params: [1] },
{ params: [2] },
]);
});
});
it.describe('runToLastTimer', () => {
it('returns current time when there are no timers', async ({ page }) => {
await page.clock.installFakeTimers(0);
const time = await page.clock.runToLastTimer();
expect(time).toBe(0);
});
it('runs all existing timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(2);
});
it('returns time of the last timer', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 50);
});
const time = await page.clock.runToLastTimer();
expect(time).toBe(50);
});
it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 10);
setTimeout(window.stub, 10);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(2);
});
it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
window.stub();
setTimeout(window.stub, 50);
}, 10);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(1);
});
it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(() => {
setTimeout(window.stub, 50);
}, 10);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(2);
});
it('new timers cannot cause an infinite loop', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
const recursiveCallback = () => {
window.stub();
setTimeout(recursiveCallback, 0);
};
setTimeout(recursiveCallback, 0);
setTimeout(window.stub, 100);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(102);
});
it('should support clocks with start time', async ({ page, calls }) => {
await page.clock.installFakeTimers(200);
await page.evaluate(async () => {
setTimeout(function cb() {
window.stub();
setTimeout(cb, 50);
}, 50);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(1);
});
it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
const recursiveCallback = () => {
void Promise.resolve().then(() => {
setTimeout(recursiveCallback, 0);
});
};
setTimeout(recursiveCallback, 0);
setTimeout(window.stub, 100);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(1);
});
it('should settle user-created promises', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => window.stub());
}, 55);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(1);
});
it('should settle nested user-created promises', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => {
void Promise.resolve().then(() => window.stub());
});
});
}, 55);
});
await page.clock.runToLastTimer();
expect(calls.length).toBe(1);
});
it('should settle local promises before firing timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
void Promise.resolve().then(() => window.stub(1));
setTimeout(() => window.stub(2), 55);
});
await page.clock.runToLastTimer();
expect(calls).toEqual([
{ params: [1] },
{ params: [2] },
]);
});
it('should settle user-created promises before firing more timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
void Promise.resolve().then(() => window.stub(1));
}, 55);
setTimeout(() => window.stub(2), 75);
});
await page.clock.runToLastTimer();
expect(calls).toEqual([
{ params: [1] },
{ params: [2] },
]);
await page.clock.fastForwardTo(2000);
expect(calls).toEqual([{ params: [2000] }]);
});
});
it.describe('stubTimers', () => {
it.beforeEach(async ({ page }) => {
await page.clock.install();
await page.clock.pause();
await page.clock.setSystemTime(0);
});
it('sets initial timestamp', async ({ page, calls }) => {
await page.clock.installFakeTimers(1400);
await page.clock.setSystemTime(1400);
expect(await page.evaluate(() => Date.now())).toBe(1400);
});
it('replaces global setTimeout', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 1000);
});
@ -503,13 +257,11 @@ it.describe('stubTimers', () => {
});
it('global fake setTimeout should return id', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
const to = await page.evaluate(() => setTimeout(window.stub, 1000));
expect(typeof to).toBe('number');
});
it('replaces global clearTimeout', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
const to = setTimeout(window.stub, 1000);
clearTimeout(to);
@ -519,7 +271,6 @@ it.describe('stubTimers', () => {
});
it('replaces global setInterval', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setInterval(window.stub, 500);
});
@ -528,7 +279,6 @@ it.describe('stubTimers', () => {
});
it('replaces global clearInterval', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
const to = setInterval(window.stub, 500);
clearInterval(to);
@ -538,7 +288,6 @@ it.describe('stubTimers', () => {
});
it('replaces global performance.now', async ({ page }) => {
await page.clock.installFakeTimers(0);
const promise = page.evaluate(async () => {
const prev = performance.now();
await new Promise(f => setTimeout(f, 1000));
@ -549,30 +298,35 @@ it.describe('stubTimers', () => {
expect(await promise).toEqual({ prev: 0, next: 1000 });
});
it('replaces global performance.timeOrigin', async ({ page }) => {
await page.clock.installFakeTimers(1000);
const promise = page.evaluate(async () => {
const prev = performance.now();
await new Promise(f => setTimeout(f, 1000));
const next = performance.now();
return { prev, next };
});
expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000);
await page.clock.runFor(1000);
expect(await promise).toEqual({ prev: 0, next: 1000 });
});
it('fakes Date constructor', async ({ page }) => {
await page.clock.installFakeTimers(0);
const now = await page.evaluate(() => new Date().getTime());
expect(now).toBe(0);
});
});
it.describe('stubTimers', () => {
it('replaces global performance.timeOrigin', async ({ page }) => {
await page.clock.install({ time: 1000 });
await page.clock.pause();
await page.clock.setSystemTime(1000);
const promise = page.evaluate(async () => {
const prev = performance.now();
await new Promise(f => setTimeout(f, 1000));
const next = performance.now();
return { prev, next };
});
await page.clock.runFor(1000);
expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000);
expect(await promise).toEqual({ prev: 0, next: 1000 });
});
});
it.describe('popup', () => {
it('should tick after popup', async ({ page }) => {
await page.clock.install();
await page.clock.pause();
const now = new Date('2015-09-25');
await page.clock.installFakeTimers(now);
await page.clock.setSystemTime(now);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(() => window.open('about:blank')),
@ -584,11 +338,12 @@ it.describe('popup', () => {
expect(popupTimeAfter).toBe(now.getTime() + 1000);
});
it('should tick before popup', async ({ page, browserName }) => {
it('should tick before popup', async ({ page }) => {
await page.clock.install();
await page.clock.pause();
const now = new Date('2015-09-25');
await page.clock.installFakeTimers(now);
const ticks = await page.clock.runFor(1000);
expect(ticks).toBe(1000);
await page.clock.setSystemTime(now);
await page.clock.runFor(1000);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
@ -597,90 +352,47 @@ it.describe('popup', () => {
const popupTime = await popup.evaluate(() => Date.now());
expect(popupTime).toBe(now.getTime() + 1000);
});
});
it.describe('runToNextTimer', () => {
it('triggers the next timer', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
it('should run time before popup', async ({ page, server }) => {
server.setRoute('/popup.html', async (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<script>window.time = Date.now()</script>`);
});
expect(await page.clock.runToNextTimer()).toBe(100);
expect(calls).toHaveLength(1);
await page.clock.setSystemTime(0);
await page.goto(server.EMPTY_PAGE);
// Wait for 2 second in real life to check that it is past in popup.
await page.waitForTimeout(2000);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'),
]);
const popupTime = await popup.evaluate('time');
expect(popupTime).toBeGreaterThanOrEqual(2000);
});
it('does not trigger simultaneous timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(() => {
setTimeout(() => {
window.stub();
}, 100);
setTimeout(() => {
window.stub();
}, 100);
it('should not run time before popup on pause', async ({ page, server }) => {
server.setRoute('/popup.html', async (req, res) => {
res.setHeader('Content-Type', 'text/html');
res.end(`<script>window.time = Date.now()</script>`);
});
await page.clock.runToNextTimer();
expect(calls).toHaveLength(1);
});
it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
window.stub();
}, 100);
setTimeout(() => {
window.stub();
}, 100);
setTimeout(() => {
window.stub();
}, 99);
setTimeout(() => {
window.stub();
}, 100);
});
await page.clock.runToNextTimer();
expect(calls).toHaveLength(1);
await page.clock.runToNextTimer();
expect(calls).toHaveLength(2);
await page.clock.runToNextTimer();
expect(calls).toHaveLength(3);
await page.clock.runToNextTimer();
expect(calls).toHaveLength(4);
});
it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
window.stub(1);
setTimeout(() => {
setTimeout(() => window.stub(2), 0);
}, 0);
});
await page.clock.runToNextTimer();
expect(calls).toEqual([{ params: [1] }]);
await page.clock.runToNextTimer();
expect(calls).toEqual([{ params: [1] }, { params: [2] }]);
});
it('throws exception thrown by timer', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(() => {
throw new Error();
}, 100);
});
await expect(page.clock.runToNextTimer()).rejects.toThrow();
await page.clock.install();
await page.clock.pause();
await page.clock.setSystemTime(0);
await page.goto(server.EMPTY_PAGE);
// Wait for 2 second in real life to check that it is past in popup.
await page.waitForTimeout(2000);
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'),
]);
const popupTime = await popup.evaluate('time');
expect(popupTime).toBe(0);
});
});
it.describe('setTime', () => {
it.describe('setFixedTime', () => {
it('does not fake methods', async ({ page }) => {
await page.clock.setTime(0);
await page.clock.setFixedTime(0);
// Should not stall.
await page.evaluate(() => {
@ -688,54 +400,145 @@ it.describe('setTime', () => {
});
});
it('allows setting time multiple times', async ({ page, calls }) => {
await page.clock.setTime(100);
it('allows setting time multiple times', async ({ page }) => {
await page.clock.setFixedTime(100);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.setTime(200);
await page.clock.setFixedTime(200);
expect(await page.evaluate(() => Date.now())).toBe(200);
});
it('supports skipTime w/o fake timers', async ({ page }) => {
await page.clock.setTime(100);
it('fixed time is not affected by clock manipulation', async ({ page }) => {
await page.clock.setFixedTime(100);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.fastForward(20);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.skipTime(20);
expect(await page.evaluate(() => Date.now())).toBe(120);
});
it('allows installing fake timers after settings time', async ({ page, calls }) => {
await page.clock.setTime(100);
await page.clock.setFixedTime(100);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.installFakeTimers(200);
await page.clock.setFixedTime(200);
await page.evaluate(async () => {
setTimeout(() => window.stub(Date.now()));
});
await page.clock.runFor(0);
expect(calls).toEqual([{ params: [200] }]);
});
});
it('allows setting time after installing fake timers', async ({ page, calls }) => {
await page.clock.installFakeTimers(200);
await page.evaluate(async () => {
setTimeout(() => window.stub(Date.now()));
});
await page.clock.setTime(220);
expect(calls).toEqual([{ params: [220] }]);
it.describe('while running', () => {
it('should progress time', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.waitForTimeout(1000);
const now = await page.evaluate(() => Date.now());
expect(now).toBeGreaterThanOrEqual(1000);
expect(now).toBeLessThanOrEqual(2000);
});
it('does not allow flowing time backwards', async ({ page, calls }) => {
await page.clock.installFakeTimers(200);
await expect(page.clock.setTime(180)).rejects.toThrow();
it('should runFor', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.clock.runFor(10000);
const now = await page.evaluate(() => Date.now());
expect(now).toBeGreaterThanOrEqual(10000);
expect(now).toBeLessThanOrEqual(11000);
});
it('should turn setTime into jump', async ({ page, calls }) => {
await page.clock.installFakeTimers(0);
await page.evaluate(async () => {
setTimeout(window.stub, 100);
setTimeout(window.stub, 200);
});
await page.clock.setTime(100);
expect(calls).toHaveLength(1);
await page.clock.setTime(200);
expect(calls).toHaveLength(2);
it('should fastForward', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.clock.fastForward(10000);
const now = await page.evaluate(() => Date.now());
expect(now).toBeGreaterThanOrEqual(10000);
expect(now).toBeLessThanOrEqual(11000);
});
it('should fastForwardTo', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.clock.fastForwardTo(10000);
const now = await page.evaluate(() => Date.now());
expect(now).toBeGreaterThanOrEqual(10000);
expect(now).toBeLessThanOrEqual(11000);
});
it('should pause', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.clock.pause();
await page.waitForTimeout(1000);
await page.clock.resume();
const now = await page.evaluate(() => Date.now());
expect(now).toBeGreaterThanOrEqual(0);
expect(now).toBeLessThanOrEqual(1000);
});
it('should pause and fastForwardTo', async ({ page }) => {
await page.clock.install({ time: 0 });
await page.goto('data:text/html,');
await page.clock.pause();
await page.clock.fastForwardTo(1000);
const now = await page.evaluate(() => Date.now());
expect(now).toBe(1000);
});
it('should set system time on pause', async ({ page }) => {
await page.clock.install();
await page.goto('data:text/html,');
await page.clock.pause();
await page.clock.setSystemTime(1000);
const now = await page.evaluate(() => Date.now());
expect(now).toBe(1000);
});
});
it.describe('while on pause', () => {
it('fastForward should not run nested immediate', async ({ page, calls }) => {
await page.clock.install();
await page.goto('data:text/html,');
await page.clock.pause();
await page.evaluate(() => {
setTimeout(() => {
window.stub('outer');
setTimeout(() => window.stub('inner'), 0);
}, 1000);
});
await page.clock.fastForward(1000);
expect(calls).toEqual([{ params: ['outer'] }]);
await page.clock.fastForward(1);
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
});
it('runFor should not run nested immediate', async ({ page, calls }) => {
await page.clock.install();
await page.goto('data:text/html,');
await page.clock.pause();
await page.evaluate(() => {
setTimeout(() => {
window.stub('outer');
setTimeout(() => window.stub('inner'), 0);
}, 1000);
});
await page.clock.runFor(1000);
expect(calls).toEqual([{ params: ['outer'] }]);
await page.clock.runFor(1);
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
});
it('runFor should not run nested immediate from microtask', async ({ page, calls }) => {
await page.clock.install();
await page.goto('data:text/html,');
await page.clock.pause();
await page.evaluate(() => {
setTimeout(() => {
window.stub('outer');
void Promise.resolve().then(() => setTimeout(() => window.stub('inner'), 0));
}, 1000);
});
await page.clock.runFor(1000);
expect(calls).toEqual([{ params: ['outer'] }]);
await page.clock.runFor(1);
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
});
});