api(clock): rework api based on the review (#31137)

This commit is contained in:
Pavel Feldman 2024-06-04 06:51:35 -07:00 committed by GitHub
parent 727b2189e4
commit c516ba0ec8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 667 additions and 662 deletions

View file

@ -6,211 +6,161 @@ Accurately simulating time-dependent behavior is essential for verifying the cor
Note that clock is installed for the entire [BrowserContext], so the time Note that clock is installed for the entire [BrowserContext], so the time
in all the pages and iframes is controlled by the same clock. in all the pages and iframes is controlled by the same clock.
## async method: Clock.install ## async method: Clock.installFakeTimers
* since: v1.45 * since: v1.45
Creates a clock and installs it globally. Install fake implementations for the following time-related functions:
**Usage** * `setTimeout`
* `clearTimeout`
* `setInterval`
* `clearInterval`
* `requestAnimationFrame`
* `cancelAnimationFrame`
* `requestIdleCallback`
* `cancelIdleCallback`
* `performance`
```js 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.
await page.clock.install();
await page.clock.install({ now });
await page.clock.install({ now, toFake: ['Date'] });
```
```python async ### param: Clock.installFakeTimers.time
await page.clock.install()
await page.clock.install(now=now)
await page.clock.install(now=now, toFake=['Date'])
```
```python sync
page.clock.install()
page.clock.install(now=now)
page.clock.install(now=now, toFake=['Date'])
```
```java
page.clock().install();
page.clock().install(
new Clock.InstallOptions()
.setNow(now));
page.clock().install(
new Clock.InstallOptions()
.setNow(now)
.setToFake(new String[]{"Date"}));
```
```csharp
await page.Clock.InstallAsync();
await page.Clock.InstallAsync(
new ClockInstallOptions { Now = now });
await page.Clock.InstallAsync(
new ClockInstallOptions
{
Now = now,
ToFake = new[] { "Date" }
});
```
### option: Clock.install.now
* since: v1.45 * since: v1.45
- `now` <[int]|[Date]> - `time` <[int]|[Date]>
Install fake timers with the specified unix epoch (default: 0). Install fake timers with the specified base time.
### option: Clock.install.toFake ### option: Clock.installFakeTimers.loopLimit
* since: v1.45
- `toFake` <[Array]<[FakeMethod]<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">>>
An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: ['setTimeout'] })` will fake only `setTimeout()`.
By default, all the methods are faked.
### option: Clock.install.loopLimit
* since: v1.45 * since: v1.45
- `loopLimit` <[int]> - `loopLimit` <[int]>
The maximum number of timers that will be run when calling [`method: Clock.runAll`]. Defaults to `1000`. The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`.
### option: Clock.install.shouldAdvanceTime ## async method: Clock.runAllTimers
* since: v1.45
- `shouldAdvanceTime` <[boolean]>
Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the mocked time will be incremented by
20ms for every 20ms change in the real system time). Defaults to `false`.
### option: Clock.install.advanceTimeDelta
* since: v1.45
- `advanceTimeDelta` <[int]>
Relevant only when using with [`option: shouldAdvanceTime`]. Increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change
in the real system time (default: 20).
## async method: Clock.jump
* since: v1.45
Advance the clock by jumping forward in time, firing callbacks 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.
**Usage**
```js
await page.clock.jump(1000);
await page.clock.jump('30:00');
```
```python async
await page.clock.jump(1000);
await page.clock.jump('30:00')
```
```python sync
page.clock.jump(1000);
page.clock.jump('30:00')
```
```java
page.clock().jump(1000);
page.clock().jump("30:00");
```
```csharp
await page.Clock.JumpAsync(1000);
await page.Clock.JumpAsync("30:00");
```
### param: Clock.jump.time
* since: v1.45
- `time` <[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.next
* since: v1.45 * since: v1.45
- returns: <[int]> - returns: <[int]>
Advances the clock to the the moment of the first scheduled timer, firing it. 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. Returns fake milliseconds since the unix epoch.
**Usage**
```js
await page.clock.next();
```
```python async
await page.clock.next()
```
```python sync
page.clock.next()
```
```java
page.clock().next();
```
```csharp
await page.Clock.NextAsync();
```
## async method: Clock.runAll
* 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. Returns fake milliseconds since the unix epoch.
**Details** **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. 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. It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error.
## async method: Clock.runToLast ## 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.
**Usage**
```js
await page.clock.runFor(1000);
await page.clock.runFor('30:00');
```
```python async
await page.clock.run_for(1000);
await page.clock.run_for('30:00')
```
```python sync
page.clock.run_for(1000);
page.clock.run_for('30:00')
```
```java
page.clock().runFor(1000);
page.clock().runFor("30:00");
```
```csharp
await page.Clock.RunForAsync(1000);
await page.Clock.RunForAsync("30:00");
```
### param: Clock.runFor.time
* since: v1.45
- `time` <[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 * since: v1.45
- returns: <[int]> - 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. 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. 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. 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. Returns fake milliseconds since the unix epoch.
## async method: Clock.tick ## async method: Clock.runToNextTimer
* since: v1.45 * since: v1.45
- returns: <[int]> - returns: <[int]>
Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake milliseconds since the unix epoch. Advances the clock to the the moment of the first scheduled timer, firing it.
Fake timers must be installed.
Returns fake milliseconds since the unix epoch.
## async method: Clock.setTime
* since: v1.45
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: Clock.setTime.time
* since: v1.45
- `time` <[int]|[Date]>
## async method: Clock.skipTime
* 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.
**Usage** **Usage**
```js ```js
await page.clock.tick(1000); await page.clock.skipTime(1000);
await page.clock.tick('30:00'); await page.clock.skipTime('30:00');
``` ```
```python async ```python async
await page.clock.tick(1000); await page.clock.skipTime(1000);
await page.clock.tick('30:00') await page.clock.skipTime('30:00')
``` ```
```python sync ```python sync
page.clock.tick(1000); page.clock.skipTime(1000);
page.clock.tick('30:00') page.clock.skipTime('30:00')
``` ```
```java ```java
page.clock().tick(1000); page.clock().skipTime(1000);
page.clock().tick("30:00"); page.clock().skipTime("30:00");
``` ```
```csharp ```csharp
await page.Clock.TickAsync(1000); await page.Clock.SkipTimeAsync(1000);
await page.Clock.TickAsync("30:00"); await page.Clock.SkipTimeAsync("30:00");
``` ```
### param: Clock.tick.time ### param: Clock.skipTime.time
* since: v1.45 * since: v1.45
- `time` <[int]|[string]> - `time` <[int]|[string]>

View file

@ -21,254 +21,240 @@ Accurately simulating time-dependent behavior is essential for verifying the cor
By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option. By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option.
```js ```js
await page.clock.install(); await page.clock.setTime(new Date('2020-02-02'));
await page.clock.install({ now: new Date('2020-02-02') }); await page.clock.installFakeTimers(new Date('2020-02-02'));
``` ```
## Freeze Date.now ## Mock Date.now
Sometimes you only need to fake `Date.now` and no other time-related functions. 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. That way the time flows naturally, but `Date.now` returns a fixed value.
```html ```html
<input type="datetime-local" id="my-time" data-testid="my-time"> <div id="current-time" data-testid="current-time"></div>
<script> <script>
const renderTime = () => { const renderTime = () => {
const time = new Date(); document.getElementById('current-time').textContent =
document.getElementById('my-time').value = time.toISOString().slice(0, 16); new Date() = time.toLocalTimeString();
setTimeout(renderTime, 1000);
}; };
renderTime(); setInterval(renderTime, 1000);
</script> </script>
``` ```
```js ```js
// Initialize clock with a specific time, only fake Date.now. await page.clock.setTime(new Date('2024-02-02T10:00:00'));
await page.clock.install({
now: new Date('2024-01-01T10:00:00Z'),
toFake: ['Date'],
});
await page.goto('http://localhost:3333'); await page.goto('http://localhost:3333');
await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); 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 expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
``` ```
```python async ```python async
# Initialize clock with a specific time, only fake Date.now. page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst))
await page.clock.install(
now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
toFake=['Date'],
)
await page.goto('http://localhost:3333') await page.goto('http://localhost:3333')
locator = page.get_by_test_id('my-time') locator = page.get_by_test_id('current-time')
await expect(locator).to_have_value('2024-01-01T10:00') await expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
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')
``` ```
```python sync ```python sync
# Initialize clock with a specific time, only fake Date.now. page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst))
page.clock.install(
now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
to_fake=['Date'],
)
page.goto('http://localhost:3333') page.goto('http://localhost:3333')
locator = page.get_by_test_id('my-time') locator = page.get_by_test_id('current-time')
expect(locator).to_have_value('2024-01-01T10:00') expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
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')
``` ```
```java ```java
// Initialize clock with a specific time, only fake Date.now. page.clock().setTime(Instant.parse("2024-02-02T10:00:00"));
page.clock().install(
new Clock.InstallOptions()
.setNow(Instant.parse("2024-01-01T10:00:00Z"))
.setToFake(new String[]{"Date"})
);
page.navigate("http://localhost:3333"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("my-time"); Locator locator = page.getByTestId("current-time");
assertThat(locator).hasValue("2024-01-01T10:00"); assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
page.clock().setTime(Instant.parse("2024-02-02T10:30:00"));
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
``` ```
```csharp ```csharp
// Initialize clock with a specific time, only fake Date.now. // Initialize clock with a specific time, only fake Date.now.
await page.Clock.InstallAsync( await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst));
new ClockInstallOptions
{
Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc),
ToFake = new[] { "Date" }
});
await page.GotoAsync("http://localhost:3333"); await page.GotoAsync("http://localhost:3333");
var locator = page.GetByTestId("my-time"); var locator = page.GetByTestId("current-time");
await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
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");
``` ```
## Assert page at different points in time ## Mock Date.now consistent with the timers
More often you need to simulate the passage of time to test time-dependent behavior. Sometimes your timers depend on `Date.now` and are confused when the time stands still.
You can jump the clock forward in time to simulate the passage of time without waiting for real-time to pass. 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 ```html
<input type="datetime-local" id="my-time" data-testid="my-time"> <div id="current-time" data-testid="current-time"></div>
<script> <script>
const renderTime = () => { const renderTime = () => {
const time = new Date(); document.getElementById('current-time').textContent =
document.getElementById('my-time').value = time.toISOString().slice(0, 16); new Date() = time.toLocalTimeString();
setTimeout(renderTime, 1000);
}; };
renderTime(); setInterval(renderTime, 1000);
</script> </script>
``` ```
```js ```js
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, take full control over time.
await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00'));
await page.goto('http://localhost:3333'); await page.goto('http://localhost:3333');
await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM');
// Fast forward time 30 minutes without firing intermediate timers, as if the user // Fast forward time 30 minutes without firing intermediate timers, as if the user
// closed and opened the lid of the laptop. // closed and opened the lid of the laptop.
await page.clock.jump('30:00'); await page.clock.skipTime('30:00');
await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:30'); await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
``` ```
```python async ```python async
# Initialize clock with a specific time, take full control over time. # Initialize clock with a specific time, take full control over time.
await page.clock.install( await page.clock.install_fake_timers(
now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)
) )
await page.goto('http://localhost:3333') await page.goto('http://localhost:3333')
locator = page.get_by_test_id('my-time') locator = page.get_by_test_id('current-time')
await expect(locator).to_have_value('2024-01-01T10:00') await expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
# Fast forward time 30 minutes without firing intermediate timers, as if the user # Fast forward time 30 minutes without firing intermediate timers, as if the user
# closed and opened the lid of the laptop. # closed and opened the lid of the laptop.
await page.clock.jump('30:00') await page.clock.skip_time('30:00')
await expect(locator).to_have_value('2024-01-01T10:30') await expect(locator).to_have_text('2/2/2024, 10:30:00 AM')
``` ```
```python sync ```python sync
# Initialize clock with a specific time, take full control over time. # Initialize clock with a specific time, take full control over time.
page.clock.install( page.clock.install_fake_timers(
now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)
) )
page.goto('http://localhost:3333') page.goto('http://localhost:3333')
locator = page.get_by_test_id('my-time') locator = page.get_by_test_id('current-time')
expect(locator).to_have_value('2024-01-01T10:00') expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
# Fast forward time 30 minutes without firing intermediate timers, as if the user # Fast forward time 30 minutes without firing intermediate timers, as if the user
# closed and opened the lid of the laptop. # closed and opened the lid of the laptop.
page.clock.jump('30:00') page.clock.skip_time('30:00')
expect(locator).to_have_value('2024-01-01T10:30') expect(locator).to_have_text('2/2/2024, 10:30:00 AM')
``` ```
```java ```java
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, take full control over time.
page.clock().install( page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00"));
new Clock.InstallOptions()
.setNow(Instant.parse("2024-01-01T10:00:00Z"))
);
page.navigate("http://localhost:3333"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("my-time"); Locator locator = page.getByTestId("current-time");
assertThat(locator).hasValue("2024-01-01T10:00"); assertThat(locator).hasText("2/2/2024, 10:00:00 AM")
// Fast forward time 30 minutes without firing intermediate timers, as if the user // Fast forward time 30 minutes without firing intermediate timers, as if the user
// closed and opened the lid of the laptop. // closed and opened the lid of the laptop.
page.clock().jump("30:00"); page.clock().skipTime("30:00");
assertThat(locator).hasValue("2024-01-01T10:30"); assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
``` ```
```csharp ```csharp
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, take full control over time.
await page.Clock.InstallAsync( await page.Clock.InstallFakeTimersAsync(
new ClockInstallOptions new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)
{ );
Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc),
});
await page.GotoAsync("http://localhost:3333"); await page.GotoAsync("http://localhost:3333");
var locator = page.GetByTestId("my-time"); var locator = page.GetByTestId("current-time");
await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
// Fast forward time 30 minutes without firing intermediate timers, as if the user // Fast forward time 30 minutes without firing intermediate timers, as if the user
// closed and opened the lid of the laptop. // closed and opened the lid of the laptop.
await page.Clock.JumpAsync("30:00"); await page.Clock.SkipTimeAsync("30:00");
await Expect(locator).ToHaveValueAsync("2024-01-01T10:30"); await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
``` ```
## Tick through time manually ## Tick through time manually
In some cases, you may want to tick through time manually, firing all timers in the process. 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
This can be useful when you want to simulate the passage of time in a controlled manner. control over the passage of time.
```html ```html
<input type="datetime-local" id="my-time" data-testid="my-time"> <div id="current-time" data-testid="current-time"></div>
<script> <script>
const renderTime = () => { const renderTime = () => {
const time = new Date(); document.getElementById('current-time').textContent =
document.getElementById('my-time').value = time.toISOString().slice(0, 16); new Date() = time.toLocalTimeString();
setTimeout(renderTime, 1000);
}; };
renderTime(); setInterval(renderTime, 1000);
</script> </script>
``` ```
```js ```js
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, take full control over time.
await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00'));
await page.goto('http://localhost:3333'); await page.goto('http://localhost:3333');
// Tick through time manually, firing all timers in the process. // Tick through time manually, firing all timers in the process.
// In this case, time will be updated in the screen 2 times. // In this case, time will be updated in the screen 2 times.
await page.clock.tick(2000); await page.clock.runFor(2000);
await expect(locator).to_have_text('2/2/2024, 10:00:02 AM')
``` ```
```python async ```python async
# Initialize clock with a specific time, take full control over time. # Initialize clock with a specific time, take full control over time.
await page.clock.install( await page.clock.install_fake_timers(
now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst),
) )
await page.goto('http://localhost:3333') await page.goto('http://localhost:3333')
locator = page.get_by_test_id('my-time') locator = page.get_by_test_id('current-time')
# Tick through time manually, firing all timers in the process. # Tick through time manually, firing all timers in the process.
# In this case, time will be updated in the screen 2 times. # In this case, time will be updated in the screen 2 times.
await page.clock.tick(2000) await page.clock.run_for(2000)
await expect(locator).to_have_text('2/2/2024, 10:00:02 AM')
``` ```
```python sync ```python sync
# Initialize clock with a specific time, take full control over time. # Initialize clock with a specific time, take full control over time.
page.clock.install( page.clock.install_fake_timers(
now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst),
) )
page.goto('http://localhost:3333') page.goto('http://localhost:3333')
locator = page.get_by_test_id('my-time') locator = page.get_by_test_id('current-time')
# Tick through time manually, firing all timers in the process. # Tick through time manually, firing all timers in the process.
# In this case, time will be updated in the screen 2 times. # In this case, time will be updated in the screen 2 times.
page.clock.tick(2000) page.clock.run_for(2000)
expect(locator).to_have_text('2/2/2024, 10:00:02 AM')
``` ```
```java ```java
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, take full control over time.
page.clock().install( page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00"));
new Clock.InstallOptions()
.setNow(Instant.parse("2024-01-01T10:00:00Z"))
);
page.navigate("http://localhost:3333"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("my-time"); Locator locator = page.getByTestId("current-time");
// Tick through time manually, firing all timers in the process. // Tick through time manually, firing all timers in the process.
// In this case, time will be updated in the screen 2 times. // In this case, time will be updated in the screen 2 times.
page.clock().tick(2000); page.clock().runFor(2000);
assertThat(locator).hasText("2/2/2024, 10:00:02 AM");
``` ```
```csharp ```csharp
// Initialize clock with a specific time, take full control over time. // Initialize clock with a specific time, take full control over time.
await page.Clock.InstallAsync( await page.Clock.InstallFakeTimersAsync(
new ClockInstallOptions new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)
{ );
Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc),
});
await page.GotoAsync("http://localhost:3333"); await page.GotoAsync("http://localhost:3333");
var locator = page.GetByTestId("my-time"); var locator = page.GetByTestId("current-time");
// Tick through time manually, firing all timers in the process. // Tick through time manually, firing all timers in the process.
// In this case, time will be updated in the screen 2 times. // In this case, time will be updated in the screen 2 times.
await page.Clock.TickAsync(2000); await page.Clock.RunForAsync(2000);
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM");
``` ```

View file

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

View file

@ -15,7 +15,6 @@
*/ */
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
export class Clock implements api.Clock { export class Clock implements api.Clock {
@ -25,35 +24,41 @@ export class Clock implements api.Clock {
this._browserContext = browserContext; this._browserContext = browserContext;
} }
async install(options?: Omit<channels.BrowserContextClockInstallOptions, 'now'> & { now?: number | Date }) { async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) {
const now = options && options.now ? (options.now instanceof Date ? options.now.getTime() : options.now) : undefined; const timeMs = time instanceof Date ? time.getTime() : time;
await this._browserContext._channel.clockInstall({ ...options, now }); await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit });
} }
async jump(time: number | string) { async runAllTimers(): Promise<number> {
await this._browserContext._channel.clockJump({ const result = await this._browserContext._channel.clockRunAllTimers();
return result.fakeTime;
}
async runFor(time: number | string): Promise<number> {
const result = await this._browserContext._channel.clockRunFor({
timeNumber: typeof time === 'number' ? time : undefined, timeNumber: typeof time === 'number' ? time : undefined,
timeString: typeof time === 'string' ? time : undefined timeString: typeof time === 'string' ? time : undefined
}); });
}
async next(): Promise<number> {
const result = await this._browserContext._channel.clockNext();
return result.fakeTime; return result.fakeTime;
} }
async runAll(): Promise<number> { async runToLastTimer(): Promise<number> {
const result = await this._browserContext._channel.clockRunAll(); const result = await this._browserContext._channel.clockRunToLastTimer();
return result.fakeTime; return result.fakeTime;
} }
async runToLast(): Promise<number> { async runToNextTimer(): Promise<number> {
const result = await this._browserContext._channel.clockRunToLast(); const result = await this._browserContext._channel.clockRunToNextTimer();
return result.fakeTime; return result.fakeTime;
} }
async tick(time: number | string): Promise<number> { async setTime(time: number | Date) {
const result = await this._browserContext._channel.clockTick({ const timeMs = time instanceof Date ? time.getTime() : time;
await this._browserContext._channel.clockSetTime({ time: timeMs });
}
async skipTime(time: number | string) {
const result = await this._browserContext._channel.clockSkipTime({
timeNumber: typeof time === 'number' ? time : undefined, timeNumber: typeof time === 'number' ? time : undefined,
timeString: typeof time === 'string' ? time : undefined timeString: typeof time === 'string' ? time : undefined
}); });

View file

@ -963,36 +963,39 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
enabled: tBoolean, enabled: tBoolean,
}); });
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({})); scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
scheme.BrowserContextClockInstallParams = tObject({ scheme.BrowserContextClockInstallFakeTimersParams = tObject({
now: tOptional(tNumber), time: tNumber,
toFake: tOptional(tArray(tString)),
loopLimit: tOptional(tNumber), loopLimit: tOptional(tNumber),
shouldAdvanceTime: tOptional(tBoolean),
advanceTimeDelta: tOptional(tNumber),
}); });
scheme.BrowserContextClockInstallResult = tOptional(tObject({})); scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({}));
scheme.BrowserContextClockJumpParams = tObject({ scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({}));
scheme.BrowserContextClockRunAllTimersResult = tObject({
fakeTime: tNumber,
});
scheme.BrowserContextClockRunForParams = tObject({
timeNumber: tOptional(tNumber), timeNumber: tOptional(tNumber),
timeString: tOptional(tString), timeString: tOptional(tString),
}); });
scheme.BrowserContextClockJumpResult = tOptional(tObject({})); scheme.BrowserContextClockRunForResult = tObject({
scheme.BrowserContextClockNextParams = tOptional(tObject({}));
scheme.BrowserContextClockNextResult = tObject({
fakeTime: tNumber, fakeTime: tNumber,
}); });
scheme.BrowserContextClockRunAllParams = tOptional(tObject({})); scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({}));
scheme.BrowserContextClockRunAllResult = tObject({ scheme.BrowserContextClockRunToLastTimerResult = tObject({
fakeTime: tNumber, fakeTime: tNumber,
}); });
scheme.BrowserContextClockRunToLastParams = tOptional(tObject({})); scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({}));
scheme.BrowserContextClockRunToLastResult = tObject({ scheme.BrowserContextClockRunToNextTimerResult = tObject({
fakeTime: tNumber, fakeTime: tNumber,
}); });
scheme.BrowserContextClockTickParams = tObject({ scheme.BrowserContextClockSetTimeParams = tObject({
time: tNumber,
});
scheme.BrowserContextClockSetTimeResult = tOptional(tObject({}));
scheme.BrowserContextClockSkipTimeParams = tObject({
timeNumber: tOptional(tNumber), timeNumber: tOptional(tNumber),
timeString: tOptional(tString), timeString: tOptional(tString),
}); });
scheme.BrowserContextClockTickResult = tObject({ scheme.BrowserContextClockSkipTimeResult = tObject({
fakeTime: tNumber, fakeTime: tNumber,
}); });
scheme.PageInitializer = tObject({ scheme.PageInitializer = tObject({

View file

@ -14,59 +14,94 @@
* limitations under the License. * limitations under the License.
*/ */
import type * as channels from '@protocol/channels';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import * as fakeTimersSource from '../generated/fakeTimersSource'; import * as fakeTimersSource from '../generated/fakeTimersSource';
export class Clock { export class Clock {
private _browserContext: BrowserContext; private _browserContext: BrowserContext;
private _installed = false; private _scriptInjected = false;
private _fakeTimersInstalled = false;
private _now = 0;
constructor(browserContext: BrowserContext) { constructor(browserContext: BrowserContext) {
this._browserContext = browserContext; this._browserContext = browserContext;
} }
async install(params: channels.BrowserContextClockInstallOptions) { async installFakeTimers(time: number, loopLimit: number | undefined) {
if (this._installed) await this._injectScriptIfNeeded();
throw new Error('Cannot install more than one clock per context'); await this._addAndEvaluate(`(() => {
this._installed = true; globalThis.__pwFakeTimers.clock?.uninstall();
globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, loopLimit })});
})();`);
this._now = time;
this._fakeTimersInstalled = true;
}
async runToNextTimer(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`);
return this._now;
}
async runAllTimers(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runAll()`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runAllAsync()`);
return this._now;
}
async runToLastTimer(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runToLast()`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runToLastAsync()`);
return this._now;
}
async setTime(time: number) {
if (this._fakeTimersInstalled) {
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.__pwFakeTimers.clock.jump(${jump})`);
this._now = time;
return this._now;
}
await this._injectScriptIfNeeded();
await this._addAndEvaluate(`(() => {
globalThis.__pwFakeTimers.clock?.uninstall();
globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, toFake: ['Date'] })});
})();`);
this._now = time;
return this._now;
}
async skipTime(time: number | string) {
const delta = parseTime(time);
await this.setTime(this._now + delta);
return this._now;
}
async runFor(time: number | string): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.tick(${JSON.stringify(time)})`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.tickAsync(${JSON.stringify(time)})`);
return this._now;
}
private async _injectScriptIfNeeded() {
if (this._scriptInjected)
return;
this._scriptInjected = true;
const script = `(() => { const script = `(() => {
const module = {}; const module = {};
${fakeTimersSource.source} ${fakeTimersSource.source}
globalThis.__pwFakeTimers = (module.exports.install())(${JSON.stringify(params)}); globalThis.__pwFakeTimers = (module.exports.inject())();
})();`; })();`;
await this._addAndEvaluate(script); await this._addAndEvaluate(script);
} }
async jump(time: number | string) {
this._assertInstalled();
await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)})`);
}
async next(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.next()`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.nextAsync()`);
}
async runAll(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runAll()`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runAllAsync()`);
}
async runToLast(): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runToLast()`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runToLastAsync()`);
}
async tick(time: number | string): Promise<number> {
this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.tick(${JSON.stringify(time)})`);
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.tickAsync(${JSON.stringify(time)})`);
}
private async _addAndEvaluate(script: string) { private async _addAndEvaluate(script: string) {
await this._browserContext.addInitScript(script); await this._browserContext.addInitScript(script);
return await this._evaluateInFrames(script); return await this._evaluateInFrames(script);
@ -79,7 +114,32 @@ export class Clock {
} }
private _assertInstalled() { private _assertInstalled() {
if (!this._installed) if (!this._fakeTimersInstalled)
throw new Error('Clock is not installed'); 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)
return 0;
const strings = time.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`);
while (i--) {
parsed = parseInt(strings[i], 10);
if (parsed >= 60)
throw new Error(`Invalid time ${time}`);
ms += parsed * Math.pow(60, l - i - 1);
}
return ms * 1000;
}

View file

@ -312,28 +312,32 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return { artifact: ArtifactDispatcher.from(this, artifact) }; return { artifact: ArtifactDispatcher.from(this, artifact) };
} }
async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> { async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallFakeTimersResult> {
await this._context.clock.install(params); await this._context.clock.installFakeTimers(params.time, params.loopLimit);
} }
async clockJump(params: channels.BrowserContextClockJumpParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockJumpResult> { async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllTimersResult> {
await this._context.clock.jump(params.timeString || params.timeNumber || 0); return { fakeTime: await this._context.clock.runAllTimers() };
} }
async clockNext(params: channels.BrowserContextClockNextParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockNextResult> { async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToLastTimerResult> {
return { fakeTime: await this._context.clock.next() }; return { fakeTime: await this._context.clock.runToLastTimer() };
} }
async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllResult> { async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToNextTimerResult> {
return { fakeTime: await this._context.clock.runAll() }; return { fakeTime: await this._context.clock.runToNextTimer() };
} }
async clockRunToLast(params: channels.BrowserContextClockRunToLastParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToLastResult> { async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSetTimeResult> {
return { fakeTime: await this._context.clock.runToLast() }; await this._context.clock.setTime(params.time);
} }
async clockTick(params: channels.BrowserContextClockTickParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockTickResult> { async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSkipTimeResult> {
return { fakeTime: await this._context.clock.tick(params.timeString || params.timeNumber || 0) }; return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) };
}
async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunForResult> {
return { fakeTime: await this._context.clock.runFor(params.timeString || params.timeNumber || 0) };
} }
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> { async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {

View file

@ -16,9 +16,8 @@
// @ts-ignore // @ts-ignore
import SinonFakeTimers from '../../third_party/fake-timers-src'; import SinonFakeTimers from '../../third_party/fake-timers-src';
import type * as channels from '@protocol/channels';
export function install(params: channels.BrowserContextClockInstallOptions) { export function inject() {
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
const window = globalThis; const window = globalThis;
const builtin = { const builtin = {
@ -34,7 +33,7 @@ export function install(params: channels.BrowserContextClockInstallOptions) {
Intl: window.Intl, Intl: window.Intl,
Date: window.Date, Date: window.Date,
}; };
const result = SinonFakeTimers.install(params); const result = SinonFakeTimers;
result.builtin = builtin; result.builtin = builtin;
return result; return result;
} }

View file

@ -1654,6 +1654,7 @@ function withGlobal(_global) {
* @returns {Clock} * @returns {Clock}
*/ */
function install(config) { function install(config) {
console.log('INSTALL', config);
if ( if (
arguments.length > 1 || arguments.length > 1 ||
config instanceof Date || config instanceof Date ||

View file

@ -17247,81 +17247,35 @@ export interface BrowserServer {
*/ */
export interface Clock { export interface Clock {
/** /**
* Creates a clock and installs it globally. * Install fake implementations for the following time-related functions:
* * - `setTimeout`
* **Usage** * - `clearTimeout`
* * - `setInterval`
* ```js * - `clearInterval`
* await page.clock.install(); * - `requestAnimationFrame`
* await page.clock.install({ now }); * - `cancelAnimationFrame`
* await page.clock.install({ now, toFake: ['Date'] }); * - `requestIdleCallback`
* ``` * - `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
* [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.
* @param options * @param options
*/ */
install(options?: { installFakeTimers(time: number|Date, options?: {
/** /**
* Relevant only when using with `shouldAdvanceTime`. Increment mocked time by advanceTimeDelta ms every * The maximum number of timers that will be run in
* advanceTimeDelta ms change in the real system time (default: 20). * [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`.
*/
advanceTimeDelta?: number;
/**
* The maximum number of timers that will be run when calling
* [clock.runAll()](https://playwright.dev/docs/api/class-clock#clock-run-all). Defaults to `1000`.
*/ */
loopLimit?: number; loopLimit?: number;
/**
* Install fake timers with the specified unix epoch (default: 0).
*/
now?: number|Date;
/**
* Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the
* mocked time will be incremented by 20ms for every 20ms change in the real system time). Defaults to `false`.
*/
shouldAdvanceTime?: boolean;
/**
* An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake:
* ['setTimeout'] })` will fake only `setTimeout()`. By default, all the methods are faked.
*/
toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">;
}): Promise<void>; }): Promise<void>;
/**
* Advance the clock by jumping forward in time, firing callbacks 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.
*
* **Usage**
*
* ```js
* await page.clock.jump(1000);
* await page.clock.jump('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
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/
jump(time: number|string): Promise<void>;
/**
* Advances the clock to the the moment of the first scheduled timer, firing it. Returns fake milliseconds since the
* unix epoch.
*
* **Usage**
*
* ```js
* await page.clock.next();
* ```
*
*/
next(): Promise<number>;
/** /**
* Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be
* run as well. Returns fake milliseconds since the unix epoch. * run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch.
* *
* **Details** * **Details**
* *
@ -17329,31 +17283,68 @@ export interface Clock {
* or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite * 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. * loop of timers and throws an error.
*/ */
runAll(): Promise<number>; runAllTimers(): Promise<number>;
/** /**
* This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must
* necessary. If new timers are added while it is executing they will be run only if they would occur before this * be installed. Returns fake milliseconds since the unix epoch.
* 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. Returns fake milliseconds since the unix epoch.
*/
runToLast(): Promise<number>;
/**
* Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake
* milliseconds since the unix epoch.
* *
* **Usage** * **Usage**
* *
* ```js * ```js
* await page.clock.tick(1000); * await page.clock.runFor(1000);
* await page.clock.tick('30:00'); * await page.clock.runFor('30:00');
* ``` * ```
* *
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are * @param 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. * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/ */
tick(time: number|string): Promise<number>; runFor(time: number|string): Promise<number>;
/**
* 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 the moment of the first scheduled timer, firing it. Fake timers must be installed.
* Returns fake milliseconds since the unix epoch.
*/
runToNextTimer(): Promise<number>;
/**
* Set the clock to the specified time.
*
* When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as
* a browser) being put to sleep and resumed later, skipping intermediary timers.
* @param time
*/
setTime(time: number|Date): Promise<void>;
/**
* Advance the clock by jumping forward in time, equivalent to running
* [clock.setTime(time)](https://playwright.dev/docs/api/class-clock#clock-set-time) with the new target time.
*
* When fake timers are installed, [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time)
* only fires due timers at most once, while
* [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) fires all the timers up to the
* current time. Returns fake milliseconds since the unix epoch.
*
* **Usage**
*
* ```js
* await page.clock.skipTime(1000);
* await page.clock.skipTime('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
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
*/
skipTime(time: number|string): Promise<number>;
} }
/** /**

View file

@ -1460,12 +1460,13 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>; harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>; clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallFakeTimersResult>;
clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise<BrowserContextClockJumpResult>; clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllTimersResult>;
clockNext(params?: BrowserContextClockNextParams, metadata?: CallMetadata): Promise<BrowserContextClockNextResult>; clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise<BrowserContextClockRunForResult>;
clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllResult>; clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastTimerResult>;
clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastResult>; clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToNextTimerResult>;
clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise<BrowserContextClockTickResult>; clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetTimeResult>;
clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSkipTimeResult>;
} }
export type BrowserContextBindingCallEvent = { export type BrowserContextBindingCallEvent = {
binding: BindingCallChannel, binding: BindingCallChannel,
@ -1754,54 +1755,56 @@ export type BrowserContextUpdateSubscriptionOptions = {
}; };
export type BrowserContextUpdateSubscriptionResult = void; export type BrowserContextUpdateSubscriptionResult = void;
export type BrowserContextClockInstallParams = { export type BrowserContextClockInstallFakeTimersParams = {
now?: number, time: number,
toFake?: string[],
loopLimit?: number, loopLimit?: number,
shouldAdvanceTime?: boolean,
advanceTimeDelta?: number,
}; };
export type BrowserContextClockInstallOptions = { export type BrowserContextClockInstallFakeTimersOptions = {
now?: number,
toFake?: string[],
loopLimit?: number, loopLimit?: number,
shouldAdvanceTime?: boolean,
advanceTimeDelta?: number,
}; };
export type BrowserContextClockInstallResult = void; export type BrowserContextClockInstallFakeTimersResult = void;
export type BrowserContextClockJumpParams = { export type BrowserContextClockRunAllTimersParams = {};
timeNumber?: number, export type BrowserContextClockRunAllTimersOptions = {};
timeString?: string, export type BrowserContextClockRunAllTimersResult = {
};
export type BrowserContextClockJumpOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockJumpResult = void;
export type BrowserContextClockNextParams = {};
export type BrowserContextClockNextOptions = {};
export type BrowserContextClockNextResult = {
fakeTime: number, fakeTime: number,
}; };
export type BrowserContextClockRunAllParams = {}; export type BrowserContextClockRunForParams = {
export type BrowserContextClockRunAllOptions = {};
export type BrowserContextClockRunAllResult = {
fakeTime: number,
};
export type BrowserContextClockRunToLastParams = {};
export type BrowserContextClockRunToLastOptions = {};
export type BrowserContextClockRunToLastResult = {
fakeTime: number,
};
export type BrowserContextClockTickParams = {
timeNumber?: number, timeNumber?: number,
timeString?: string, timeString?: string,
}; };
export type BrowserContextClockTickOptions = { export type BrowserContextClockRunForOptions = {
timeNumber?: number, timeNumber?: number,
timeString?: string, timeString?: string,
}; };
export type BrowserContextClockTickResult = { 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 = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockSkipTimeOptions = {
timeNumber?: number,
timeString?: string,
};
export type BrowserContextClockSkipTimeResult = {
fakeTime: number, fakeTime: number,
}; };

View file

@ -1204,34 +1204,35 @@ BrowserContext:
- requestFailed - requestFailed
enabled: boolean enabled: boolean
clockInstall: clockInstallFakeTimers:
parameters: parameters:
now: number? time: number
toFake:
type: array?
items: string
loopLimit: number? loopLimit: number?
shouldAdvanceTime: boolean?
advanceTimeDelta: number?
clockJump: clockRunAllTimers:
returns:
fakeTime: number
clockRunFor:
parameters: parameters:
timeNumber: number? timeNumber: number?
timeString: string? timeString: string?
clockNext:
returns: returns:
fakeTime: number fakeTime: number
clockRunAll: clockRunToLastTimer:
returns: returns:
fakeTime: number fakeTime: number
clockRunToLast: clockRunToNextTimer:
returns: returns:
fakeTime: number fakeTime: number
clockTick: clockSetTime:
parameters:
time: number
clockSkipTime:
parameters: parameters:
timeNumber: number? timeNumber: number?
timeString: string? timeString: string?

View file

@ -34,87 +34,87 @@ const it = test.extend<{ calls: { params: any[] }[] }>({
} }
}); });
it.describe('tick', () => { it.describe('runFor', () => {
it('triggers immediately without specified delay', async ({ page, calls }) => { it('triggers immediately without specified delay', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub); setTimeout(window.stub);
}); });
await page.clock.tick(0); await page.clock.runFor(0);
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('does not trigger without sufficient delay', async ({ page, calls }) => { it('does not trigger without sufficient delay', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
await page.clock.tick(10); await page.clock.runFor(10);
expect(calls).toEqual([]); expect(calls).toEqual([]);
}); });
it('triggers after sufficient delay', async ({ page, calls }) => { it('triggers after sufficient delay', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
await page.clock.tick(100); await page.clock.runFor(100);
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('triggers simultaneous timers', async ({ page, calls }) => { it('triggers simultaneous timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
await page.clock.tick(100); await page.clock.runFor(100);
expect(calls).toHaveLength(2); expect(calls).toHaveLength(2);
}); });
it('triggers multiple simultaneous timers', async ({ page, calls }) => { it('triggers multiple simultaneous timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
setTimeout(window.stub, 99); setTimeout(window.stub, 99);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
await page.clock.tick(100); await page.clock.runFor(100);
expect(calls.length).toBe(4); expect(calls.length).toBe(4);
}); });
it('waits after setTimeout was called', async ({ page, calls }) => { it('waits after setTimeout was called', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 150); setTimeout(window.stub, 150);
}); });
await page.clock.tick(50); await page.clock.runFor(50);
expect(calls).toEqual([]); expect(calls).toEqual([]);
await page.clock.tick(100); await page.clock.runFor(100);
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('triggers event when some throw', async ({ page, calls }) => { it('triggers event when some throw', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { throw new Error(); }, 100); setTimeout(() => { throw new Error(); }, 100);
setTimeout(window.stub, 120); setTimeout(window.stub, 120);
}); });
await expect(page.clock.tick(120)).rejects.toThrow(); await expect(page.clock.runFor(120)).rejects.toThrow();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('creates updated Date while ticking', async ({ page, calls }) => { it('creates updated Date while ticking', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(() => { setInterval(() => {
window.stub(new Date().getTime()); window.stub(new Date().getTime());
}, 10); }, 10);
}); });
await page.clock.tick(100); await page.clock.runFor(100);
expect(calls).toEqual([ expect(calls).toEqual([
{ params: [10] }, { params: [10] },
{ params: [20] }, { params: [20] },
@ -130,117 +130,117 @@ it.describe('tick', () => {
}); });
it('passes 8 seconds', async ({ page, calls }) => { it('passes 8 seconds', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 4000); setInterval(window.stub, 4000);
}); });
await page.clock.tick('08'); await page.clock.runFor('08');
expect(calls.length).toBe(2); expect(calls.length).toBe(2);
}); });
it('passes 1 minute', async ({ page, calls }) => { it('passes 1 minute', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 6000); setInterval(window.stub, 6000);
}); });
await page.clock.tick('01:00'); await page.clock.runFor('01:00');
expect(calls.length).toBe(10); expect(calls.length).toBe(10);
}); });
it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => { it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 10000); setInterval(window.stub, 10000);
}); });
await page.clock.tick('02:34:10'); await page.clock.runFor('02:34:10');
expect(calls.length).toBe(925); expect(calls.length).toBe(925);
}); });
it('throws for invalid format', async ({ page, calls }) => { it('throws for invalid format', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 10000); setInterval(window.stub, 10000);
}); });
await expect(page.clock.tick('12:02:34:10')).rejects.toThrow(); await expect(page.clock.runFor('12:02:34:10')).rejects.toThrow();
expect(calls).toEqual([]); expect(calls).toEqual([]);
}); });
it('returns the current now value', async ({ page }) => { it('returns the current now value', async ({ page }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
const value = 200; const value = 200;
await page.clock.tick(value); await page.clock.runFor(value);
expect(await page.evaluate(() => Date.now())).toBe(value); expect(await page.evaluate(() => Date.now())).toBe(value);
}); });
}); });
it.describe('jump', () => { it.describe('skipTime', () => {
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub('should not be logged'); window.stub('should not be logged');
}, 1000); }, 1000);
}); });
await page.clock.jump(500); await page.clock.skipTime(500);
expect(calls).toEqual([]); expect(calls).toEqual([]);
}); });
it('pushes back execution time for skipped timers', async ({ page, calls }) => { it('pushes back execution time for skipped timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub(Date.now()); window.stub(Date.now());
}, 1000); }, 1000);
}); });
await page.clock.jump(2000); await page.clock.skipTime(2000);
expect(calls).toEqual([{ params: [2000] }]); expect(calls).toEqual([{ params: [2000] }]);
}); });
it('supports string time arguments', async ({ page, calls }) => { it('supports string time arguments', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub(Date.now()); window.stub(Date.now());
}, 100000); // 100000 = 1:40 }, 100000); // 100000 = 1:40
}); });
await page.clock.jump('01:50'); await page.clock.skipTime('01:50');
expect(calls).toEqual([{ params: [110000] }]); expect(calls).toEqual([{ params: [110000] }]);
}); });
}); });
it.describe('runAll', () => { it.describe('runAllTimers', () => {
it('if there are no timers just return', async ({ page }) => { it('if there are no timers just return', async ({ page }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.clock.runAll(); await page.clock.runAllTimers();
}); });
it('runs all timers', async ({ page, calls }) => { it('runs all timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 10); setTimeout(window.stub, 10);
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}); });
await page.clock.runAll(); await page.clock.runAllTimers();
expect(calls.length).toBe(2); expect(calls.length).toBe(2);
}); });
it('new timers added while running are also run', async ({ page, calls }) => { it('new timers added while running are also run', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}, 10); }, 10);
}); });
await page.clock.runAll(); await page.clock.runAllTimers();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('new timers added in promises while running are also run', async ({ page, calls }) => { it('new timers added in promises while running are also run', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => { void Promise.resolve().then(() => {
@ -248,12 +248,12 @@ it.describe('runAll', () => {
}); });
}, 10); }, 10);
}); });
await page.clock.runAll(); await page.clock.runAllTimers();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('throws before allowing infinite recursion', async ({ page, calls }) => { it('throws before allowing infinite recursion', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const recursiveCallback = () => { const recursiveCallback = () => {
window.stub(); window.stub();
@ -261,12 +261,12 @@ it.describe('runAll', () => {
}; };
setTimeout(recursiveCallback, 10); setTimeout(recursiveCallback, 10);
}); });
await expect(page.clock.runAll()).rejects.toThrow(); await expect(page.clock.runAllTimers()).rejects.toThrow();
expect(calls).toHaveLength(1000); expect(calls).toHaveLength(1000);
}); });
it('throws before allowing infinite recursion from promises', async ({ page, calls }) => { it('throws before allowing infinite recursion from promises', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const recursiveCallback = () => { const recursiveCallback = () => {
window.stub(); window.stub();
@ -276,33 +276,33 @@ it.describe('runAll', () => {
}; };
setTimeout(recursiveCallback, 10); setTimeout(recursiveCallback, 10);
}); });
await expect(page.clock.runAll()).rejects.toThrow(); await expect(page.clock.runAllTimers()).rejects.toThrow();
expect(calls).toHaveLength(1000); expect(calls).toHaveLength(1000);
}); });
it('the loop limit can be set when creating a clock', async ({ page, calls }) => { it('the loop limit can be set when creating a clock', async ({ page, calls }) => {
await page.clock.install({ loopLimit: 1 }); await page.clock.installFakeTimers(0, { loopLimit: 1 });
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 10); setTimeout(window.stub, 10);
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}); });
await expect(page.clock.runAll()).rejects.toThrow(); await expect(page.clock.runAllTimers()).rejects.toThrow();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('should settle user-created promises', async ({ page, calls }) => { it('should settle user-created promises', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => window.stub()); void Promise.resolve().then(() => window.stub());
}, 55); }, 55);
}); });
await page.clock.runAll(); await page.clock.runAllTimers();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('should settle nested user-created promises', async ({ page, calls }) => { it('should settle nested user-created promises', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => { void Promise.resolve().then(() => {
@ -312,17 +312,17 @@ it.describe('runAll', () => {
}); });
}, 55); }, 55);
}); });
await page.clock.runAll(); await page.clock.runAllTimers();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('should settle local promises before firing timers', async ({ page, calls }) => { it('should settle local promises before firing timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
void Promise.resolve().then(() => window.stub(1)); void Promise.resolve().then(() => window.stub(1));
setTimeout(() => window.stub(2), 55); setTimeout(() => window.stub(2), 55);
}); });
await page.clock.runAll(); await page.clock.runAllTimers();
expect(calls).toEqual([ expect(calls).toEqual([
{ params: [1] }, { params: [1] },
{ params: [2] }, { params: [2] },
@ -330,69 +330,69 @@ it.describe('runAll', () => {
}); });
}); });
it.describe('runToLast', () => { it.describe('runToLastTimer', () => {
it('returns current time when there are no timers', async ({ page }) => { it('returns current time when there are no timers', async ({ page }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
const time = await page.clock.runToLast(); const time = await page.clock.runToLastTimer();
expect(time).toBe(0); expect(time).toBe(0);
}); });
it('runs all existing timers', async ({ page, calls }) => { it('runs all existing timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 10); setTimeout(window.stub, 10);
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(2); expect(calls.length).toBe(2);
}); });
it('returns time of the last timer', async ({ page, calls }) => { it('returns time of the last timer', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 10); setTimeout(window.stub, 10);
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}); });
const time = await page.clock.runToLast(); const time = await page.clock.runToLastTimer();
expect(time).toBe(50); expect(time).toBe(50);
}); });
it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => { it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 10); setTimeout(window.stub, 10);
setTimeout(window.stub, 10); setTimeout(window.stub, 10);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(2); 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 }) => { it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub(); window.stub();
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}, 10); }, 10);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => { it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
setTimeout(() => { setTimeout(() => {
setTimeout(window.stub, 50); setTimeout(window.stub, 50);
}, 10); }, 10);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(2); expect(calls.length).toBe(2);
}); });
it('new timers cannot cause an infinite loop', async ({ page, calls }) => { it('new timers cannot cause an infinite loop', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const recursiveCallback = () => { const recursiveCallback = () => {
window.stub(); window.stub();
@ -401,24 +401,24 @@ it.describe('runToLast', () => {
setTimeout(recursiveCallback, 0); setTimeout(recursiveCallback, 0);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(102); expect(calls.length).toBe(102);
}); });
it('should support clocks with start time', async ({ page, calls }) => { it('should support clocks with start time', async ({ page, calls }) => {
await page.clock.install({ now: 200 }); await page.clock.installFakeTimers(200);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(function cb() { setTimeout(function cb() {
window.stub(); window.stub();
setTimeout(cb, 50); setTimeout(cb, 50);
}, 50); }, 50);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => { it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const recursiveCallback = () => { const recursiveCallback = () => {
void Promise.resolve().then(() => { void Promise.resolve().then(() => {
@ -428,23 +428,23 @@ it.describe('runToLast', () => {
setTimeout(recursiveCallback, 0); setTimeout(recursiveCallback, 0);
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('should settle user-created promises', async ({ page, calls }) => { it('should settle user-created promises', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => window.stub()); void Promise.resolve().then(() => window.stub());
}, 55); }, 55);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('should settle nested user-created promises', async ({ page, calls }) => { it('should settle nested user-created promises', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => { void Promise.resolve().then(() => {
@ -454,17 +454,17 @@ it.describe('runToLast', () => {
}); });
}, 55); }, 55);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('should settle local promises before firing timers', async ({ page, calls }) => { it('should settle local promises before firing timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
void Promise.resolve().then(() => window.stub(1)); void Promise.resolve().then(() => window.stub(1));
setTimeout(() => window.stub(2), 55); setTimeout(() => window.stub(2), 55);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls).toEqual([ expect(calls).toEqual([
{ params: [1] }, { params: [1] },
{ params: [2] }, { params: [2] },
@ -472,14 +472,14 @@ it.describe('runToLast', () => {
}); });
it('should settle user-created promises before firing more timers', async ({ page, calls }) => { it('should settle user-created promises before firing more timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
void Promise.resolve().then(() => window.stub(1)); void Promise.resolve().then(() => window.stub(1));
}, 55); }, 55);
setTimeout(() => window.stub(2), 75); setTimeout(() => window.stub(2), 75);
}); });
await page.clock.runToLast(); await page.clock.runToLastTimer();
expect(calls).toEqual([ expect(calls).toEqual([
{ params: [1] }, { params: [1] },
{ params: [2] }, { params: [2] },
@ -489,152 +489,92 @@ it.describe('runToLast', () => {
it.describe('stubTimers', () => { it.describe('stubTimers', () => {
it('sets initial timestamp', async ({ page, calls }) => { it('sets initial timestamp', async ({ page, calls }) => {
await page.clock.install({ now: 1400 }); await page.clock.installFakeTimers(1400);
expect(await page.evaluate(() => Date.now())).toBe(1400); expect(await page.evaluate(() => Date.now())).toBe(1400);
}); });
it('replaces global setTimeout', async ({ page, calls }) => { it('replaces global setTimeout', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 1000); setTimeout(window.stub, 1000);
}); });
await page.clock.tick(1000); await page.clock.runFor(1000);
expect(calls.length).toBe(1); expect(calls.length).toBe(1);
}); });
it('global fake setTimeout should return id', async ({ page, calls }) => { it('global fake setTimeout should return id', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
const to = await page.evaluate(() => setTimeout(window.stub, 1000)); const to = await page.evaluate(() => setTimeout(window.stub, 1000));
expect(typeof to).toBe('number'); expect(typeof to).toBe('number');
}); });
it('replaces global clearTimeout', async ({ page, calls }) => { it('replaces global clearTimeout', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const to = setTimeout(window.stub, 1000); const to = setTimeout(window.stub, 1000);
clearTimeout(to); clearTimeout(to);
}); });
await page.clock.tick(1000); await page.clock.runFor(1000);
expect(calls).toEqual([]); expect(calls).toEqual([]);
}); });
it('replaces global setInterval', async ({ page, calls }) => { it('replaces global setInterval', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setInterval(window.stub, 500); setInterval(window.stub, 500);
}); });
await page.clock.tick(1000); await page.clock.runFor(1000);
expect(calls.length).toBe(2); expect(calls.length).toBe(2);
}); });
it('replaces global clearInterval', async ({ page, calls }) => { it('replaces global clearInterval', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
const to = setInterval(window.stub, 500); const to = setInterval(window.stub, 500);
clearInterval(to); clearInterval(to);
}); });
await page.clock.tick(1000); await page.clock.runFor(1000);
expect(calls).toEqual([]); expect(calls).toEqual([]);
}); });
it('replaces global performance.now', async ({ page }) => { it('replaces global performance.now', async ({ page }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
const promise = page.evaluate(async () => { const promise = page.evaluate(async () => {
const prev = performance.now(); const prev = performance.now();
await new Promise(f => setTimeout(f, 1000)); await new Promise(f => setTimeout(f, 1000));
const next = performance.now(); const next = performance.now();
return { prev, next }; return { prev, next };
}); });
await page.clock.tick(1000); await page.clock.runFor(1000);
expect(await promise).toEqual({ prev: 0, next: 1000 }); expect(await promise).toEqual({ prev: 0, next: 1000 });
}); });
it('fakes Date constructor', async ({ page }) => { it('fakes Date constructor', async ({ page }) => {
await page.clock.install({ now: 0 }); await page.clock.installFakeTimers(0);
const now = await page.evaluate(() => new Date().getTime()); const now = await page.evaluate(() => new Date().getTime());
expect(now).toBe(0); expect(now).toBe(0);
}); });
it('does not fake methods not provided', async ({ page }) => {
await page.clock.install({
now: 0,
toFake: ['Date'],
});
// Should not stall.
await page.evaluate(() => {
return new Promise(f => setTimeout(f, 1));
});
});
});
it.describe('shouldAdvanceTime', () => {
it('should create an auto advancing timer', async ({ page, calls }) => {
const testDelay = 29;
const now = new Date('2015-09-25');
await page.clock.install({ now, shouldAdvanceTime: true });
const pageNow = await page.evaluate(() => Date.now());
expect(pageNow).toBe(1443139200000);
await page.evaluate(async testDelay => {
return new Promise<void>(f => {
const timeoutStarted = Date.now();
setTimeout(() => {
window.stub(Date.now() - timeoutStarted);
f();
}, testDelay);
});
}, testDelay);
expect(calls).toEqual([
{ params: [testDelay] }
]);
});
it('should test setInterval', async ({ page, calls }) => {
const now = new Date('2015-09-25');
await page.clock.install({ now, shouldAdvanceTime: true });
const timeDifference = await page.evaluate(async () => {
return new Promise(f => {
const interval = 20;
const cyclesToTrigger = 3;
const timeoutStarted = Date.now();
let intervalsTriggered = 0;
const intervalId = setInterval(() => {
if (++intervalsTriggered === cyclesToTrigger) {
clearInterval(intervalId);
const timeDifference = Date.now() - timeoutStarted;
f(timeDifference - interval * cyclesToTrigger);
}
}, interval);
});
});
expect(timeDifference).toBe(0);
});
}); });
it.describe('popup', () => { it.describe('popup', () => {
it('should tick after popup', async ({ page }) => { it('should tick after popup', async ({ page }) => {
const now = new Date('2015-09-25'); const now = new Date('2015-09-25');
await page.clock.install({ now }); await page.clock.installFakeTimers(now);
const [popup] = await Promise.all([ const [popup] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.evaluate(() => window.open('about:blank')), page.evaluate(() => window.open('about:blank')),
]); ]);
const popupTime = await popup.evaluate(() => Date.now()); const popupTime = await popup.evaluate(() => Date.now());
expect(popupTime).toBe(now.getTime()); expect(popupTime).toBe(now.getTime());
await page.clock.tick(1000); await page.clock.runFor(1000);
const popupTimeAfter = await popup.evaluate(() => Date.now()); const popupTimeAfter = await popup.evaluate(() => Date.now());
expect(popupTimeAfter).toBe(now.getTime() + 1000); expect(popupTimeAfter).toBe(now.getTime() + 1000);
}); });
it('should tick before popup', async ({ page, browserName }) => { it('should tick before popup', async ({ page, browserName }) => {
it.skip(browserName === 'chromium');
const now = new Date('2015-09-25'); const now = new Date('2015-09-25');
await page.clock.install({ now }); await page.clock.installFakeTimers(now);
const newNow = await page.clock.tick(1000); const newNow = await page.clock.runFor(1000);
expect(newNow).toBe(now.getTime() + 1000); expect(newNow).toBe(now.getTime() + 1000);
const [popup] = await Promise.all([ const [popup] = await Promise.all([
@ -646,18 +586,18 @@ it.describe('popup', () => {
}); });
}); });
it.describe('next', () => { it.describe('runToNextTimer', () => {
it('triggers the next timer', async ({ page, calls }) => { it('triggers the next timer', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 100); setTimeout(window.stub, 100);
}); });
expect(await page.clock.next()).toBe(100); expect(await page.clock.runToNextTimer()).toBe(100);
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('does not trigger simultaneous timers', async ({ page, calls }) => { it('does not trigger simultaneous timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(() => { await page.evaluate(() => {
setTimeout(() => { setTimeout(() => {
window.stub(); window.stub();
@ -667,12 +607,12 @@ it.describe('next', () => {
}, 100); }, 100);
}); });
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
}); });
it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
window.stub(); window.stub();
@ -688,18 +628,18 @@ it.describe('next', () => {
}, 100); }, 100);
}); });
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toHaveLength(1); expect(calls).toHaveLength(1);
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toHaveLength(2); expect(calls).toHaveLength(2);
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toHaveLength(3); expect(calls).toHaveLength(3);
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toHaveLength(4); expect(calls).toHaveLength(4);
}); });
it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => { it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
window.stub(1); window.stub(1);
setTimeout(() => { setTimeout(() => {
@ -707,20 +647,82 @@ it.describe('next', () => {
}, 0); }, 0);
}); });
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toEqual([{ params: [1] }]); expect(calls).toEqual([{ params: [1] }]);
await page.clock.next(); await page.clock.runToNextTimer();
expect(calls).toEqual([{ params: [1] }, { params: [2] }]); expect(calls).toEqual([{ params: [1] }, { params: [2] }]);
}); });
it('throws exception thrown by timer', async ({ page, calls }) => { it('throws exception thrown by timer', async ({ page, calls }) => {
await page.clock.install(); await page.clock.installFakeTimers(0);
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(() => { setTimeout(() => {
throw new Error(); throw new Error();
}, 100); }, 100);
}); });
await expect(page.clock.next()).rejects.toThrow(); await expect(page.clock.runToNextTimer()).rejects.toThrow();
});
});
it.describe('setTime', () => {
it('does not fake methods', async ({ page }) => {
await page.clock.setTime(0);
// Should not stall.
await page.evaluate(() => {
return new Promise(f => setTimeout(f, 1));
});
});
it('allows setting time multiple times', async ({ page, calls }) => {
await page.clock.setTime(100);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.setTime(200);
expect(await page.evaluate(() => Date.now())).toBe(200);
});
it('supports skipTime w/o fake timers', async ({ page }) => {
await page.clock.setTime(100);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.skipTime(20);
expect(await page.evaluate(() => Date.now())).toBe(120);
});
it('allows installing fake timers after settings time', async ({ page, calls }) => {
await page.clock.setTime(100);
expect(await page.evaluate(() => Date.now())).toBe(100);
await page.clock.installFakeTimers(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('does not allow flowing time backwards', async ({ page, calls }) => {
await page.clock.installFakeTimers(200);
await expect(page.clock.setTime(180)).rejects.toThrow();
});
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);
}); });
}); });