chore: clock api review (#31237)
This commit is contained in:
parent
c08000b967
commit
6399e8de4e
|
|
@ -6,11 +6,88 @@ Accurately simulating time-dependent behavior is essential for verifying the cor
|
||||||
Note that clock is installed for the entire [BrowserContext], so the time
|
Note that clock is installed for the entire [BrowserContext], so the time
|
||||||
in all the pages and iframes is controlled by the same clock.
|
in all the pages and iframes is controlled by the same clock.
|
||||||
|
|
||||||
## async method: Clock.installFakeTimers
|
## async method: Clock.fastForward
|
||||||
|
* since: v1.45
|
||||||
|
|
||||||
|
Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and
|
||||||
|
reopening it later, after given time.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.clock.fastForward(1000);
|
||||||
|
await page.clock.fastForward('30:00');
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
await page.clock.fast_forward(1000)
|
||||||
|
await page.clock.fast_forward("30:00")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
page.clock.fast_forward(1000)
|
||||||
|
page.clock.fast_forward("30:00")
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
page.clock().fastForward(1000);
|
||||||
|
page.clock().fastForward("30:00");
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await page.Clock.FastForwardAsync(1000);
|
||||||
|
await page.Clock.FastForwardAsync("30:00");
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Clock.fastForward.ticks
|
||||||
|
* since: v1.45
|
||||||
|
- `ticks` <[int]|[string]>
|
||||||
|
|
||||||
|
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||||
|
|
||||||
|
## async method: Clock.fastForwardTo
|
||||||
|
* since: v1.45
|
||||||
|
|
||||||
|
Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user closing the laptop lid for a while and
|
||||||
|
reopening it at the specified time.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.clock.fastForwardTo(new Date('2020-02-02'));
|
||||||
|
await page.clock.fastForwardTo('2020-02-02');
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
await page.clock.fast_forward_to(datetime.datetime(2020, 2, 2))
|
||||||
|
await page.clock.fast_forward_to("2020-02-02")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
page.clock.fast_forward_to(datetime.datetime(2020, 2, 2))
|
||||||
|
page.clock.fast_forward_to("2020-02-02")
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
page.clock().fastForwardTo(Instant.parse("2020-02-02"));
|
||||||
|
page.clock().fastForwardTo("2020-02-02");
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await page.Clock.FastForwardToAsync(DateTime.Parse("2020-02-02"));
|
||||||
|
await page.Clock.FastForwardToAsync("2020-02-02");
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Clock.fastForwardTo.time
|
||||||
|
* since: v1.45
|
||||||
|
- `time` <[int]|[string]|[Date]>
|
||||||
|
|
||||||
|
## async method: Clock.install
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
|
|
||||||
Install fake implementations for the following time-related functions:
|
Install fake implementations for the following time-related functions:
|
||||||
|
|
||||||
|
* `Date`
|
||||||
* `setTimeout`
|
* `setTimeout`
|
||||||
* `clearTimeout`
|
* `clearTimeout`
|
||||||
* `setInterval`
|
* `setInterval`
|
||||||
|
|
@ -21,41 +98,18 @@ Install fake implementations for the following time-related functions:
|
||||||
* `cancelIdleCallback`
|
* `cancelIdleCallback`
|
||||||
* `performance`
|
* `performance`
|
||||||
|
|
||||||
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.skipTime`] for more information.
|
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information.
|
||||||
|
|
||||||
### param: Clock.installFakeTimers.time
|
### option: Clock.install.time
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
- `time` <[int]|[Date]>
|
- `time` <[int]|[string]|[Date]>
|
||||||
|
|
||||||
Install fake timers with the specified base time.
|
|
||||||
|
|
||||||
### option: Clock.installFakeTimers.loopLimit
|
|
||||||
* since: v1.45
|
|
||||||
- `loopLimit` <[int]>
|
|
||||||
|
|
||||||
The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`.
|
|
||||||
|
|
||||||
## async method: Clock.runAllTimers
|
|
||||||
* since: v1.45
|
|
||||||
- returns: <[int]>
|
|
||||||
|
|
||||||
Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well.
|
|
||||||
Fake timers must be installed.
|
|
||||||
Returns fake milliseconds since the unix epoch.
|
|
||||||
|
|
||||||
**Details**
|
|
||||||
|
|
||||||
This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use, or the delays in those timers.
|
|
||||||
It runs a maximum of [`option: loopLimit`] times after which it assumes there is an infinite loop of timers and throws an error.
|
|
||||||
|
|
||||||
|
Time to initialize with, current system time by default.
|
||||||
|
|
||||||
## async method: Clock.runFor
|
## async method: Clock.runFor
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
- returns: <[int]>
|
|
||||||
|
|
||||||
Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch.
|
Advance the clock, firing all the time-related callbacks.
|
||||||
Fake timers must be installed.
|
|
||||||
Returns fake milliseconds since the unix epoch.
|
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
|
@ -66,12 +120,12 @@ await page.clock.runFor('30:00');
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
await page.clock.run_for(1000);
|
await page.clock.run_for(1000);
|
||||||
await page.clock.run_for('30:00')
|
await page.clock.run_for("30:00")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
page.clock.run_for(1000);
|
page.clock.run_for(1000);
|
||||||
page.clock.run_for('30:00')
|
page.clock.run_for("30:00")
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -84,84 +138,104 @@ await page.Clock.RunForAsync(1000);
|
||||||
await page.Clock.RunForAsync("30:00");
|
await page.Clock.RunForAsync("30:00");
|
||||||
```
|
```
|
||||||
|
|
||||||
### param: Clock.runFor.time
|
### param: Clock.runFor.ticks
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
- `time` <[int]|[string]>
|
- `ticks` <[int]|[string]>
|
||||||
|
|
||||||
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||||
|
|
||||||
|
|
||||||
## async method: Clock.runToLastTimer
|
## async method: Clock.pause
|
||||||
* since: v1.45
|
|
||||||
- returns: <[int]>
|
|
||||||
|
|
||||||
This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as necessary.
|
|
||||||
If new timers are added while it is executing they will be run only if they would occur before this time.
|
|
||||||
This is useful when you want to run a test to completion, but the test recursively sets timers that would cause runAll to trigger an infinite loop warning.
|
|
||||||
Fake timers must be installed.
|
|
||||||
Returns fake milliseconds since the unix epoch.
|
|
||||||
|
|
||||||
|
|
||||||
## async method: Clock.runToNextTimer
|
|
||||||
* since: v1.45
|
|
||||||
- returns: <[int]>
|
|
||||||
|
|
||||||
Advances the clock to the moment of the first scheduled timer, firing it.
|
|
||||||
Fake timers must be installed.
|
|
||||||
Returns fake milliseconds since the unix epoch.
|
|
||||||
|
|
||||||
|
|
||||||
## async method: Clock.setTime
|
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
|
|
||||||
Set the clock to the specified time.
|
Pause timers. Once this method is called, no timers are fired unless [`method: Clock.runFor`], [`method: Clock.fastForward`], [`method: Clock.fastForwardTo`] or [`method: Clock.resume`] is called.
|
||||||
|
|
||||||
When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as a browser)
|
## async method: Clock.resume
|
||||||
being put to sleep and resumed later, skipping intermediary timers.
|
|
||||||
|
|
||||||
### param: Clock.setTime.time
|
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
- `time` <[int]|[Date]>
|
|
||||||
|
|
||||||
|
Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
|
||||||
|
|
||||||
## async method: Clock.skipTime
|
## async method: Clock.setFixedTime
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
- returns: <[int]>
|
|
||||||
|
|
||||||
Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time.
|
Makes `Date.now` and `new Date()` return fixed fake time at all times,
|
||||||
|
keeps all the timers running.
|
||||||
When fake timers are installed, [`method: Clock.skipTime`] only fires due timers at most once, while [`method: Clock.runFor`] fires all the timers up to the current time.
|
|
||||||
Returns fake milliseconds since the unix epoch.
|
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await page.clock.skipTime(1000);
|
await page.clock.setFixedTime(Date.now());
|
||||||
await page.clock.skipTime('30:00');
|
await page.clock.setFixedTime(new Date('2020-02-02'));
|
||||||
|
await page.clock.setFixedTime('2020-02-02');
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
await page.clock.skipTime(1000);
|
await page.clock.set_fixed_time(datetime.datetime.now())
|
||||||
await page.clock.skipTime('30:00')
|
await page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
|
||||||
|
await page.clock.set_fixed_time("2020-02-02")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
page.clock.skipTime(1000);
|
page.clock.set_fixed_time(datetime.datetime.now())
|
||||||
page.clock.skipTime('30:00')
|
page.clock.set_fixed_time(datetime.datetime(2020, 2, 2))
|
||||||
|
page.clock.set_fixed_time("2020-02-02")
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.clock().skipTime(1000);
|
page.clock().setFixedTime(Instant.now());
|
||||||
page.clock().skipTime("30:00");
|
page.clock().setFixedTime(Instant.parse("2020-02-02"));
|
||||||
|
page.clock().setFixedTime("2020-02-02");
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await page.Clock.SkipTimeAsync(1000);
|
await page.Clock.SetFixedTimeAsync(DateTime.Now);
|
||||||
await page.Clock.SkipTimeAsync("30:00");
|
await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
|
||||||
|
await page.Clock.SetFixedTimeAsync("2020-02-02");
|
||||||
```
|
```
|
||||||
|
|
||||||
### param: Clock.skipTime.time
|
### param: Clock.setFixedTime.time
|
||||||
* since: v1.45
|
* since: v1.45
|
||||||
- `time` <[int]|[string]>
|
- `time` <[int]|[string]|[Date]>
|
||||||
|
|
||||||
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
Time to be set.
|
||||||
|
|
||||||
|
## async method: Clock.setSystemTime
|
||||||
|
* since: v1.45
|
||||||
|
|
||||||
|
Sets current system time but does not trigger any timers, unlike [`method: Clock.fastForwardTo`].
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
```js
|
||||||
|
await page.clock.setSystemTime(Date.now());
|
||||||
|
await page.clock.setSystemTime(new Date('2020-02-02'));
|
||||||
|
await page.clock.setSystemTime('2020-02-02');
|
||||||
|
```
|
||||||
|
|
||||||
|
```python async
|
||||||
|
await page.clock.set_system_time(datetime.datetime.now())
|
||||||
|
await page.clock.set_system_time(datetime.datetime(2020, 2, 2))
|
||||||
|
await page.clock.set_system_time("2020-02-02")
|
||||||
|
```
|
||||||
|
|
||||||
|
```python sync
|
||||||
|
page.clock.set_system_time(datetime.datetime.now())
|
||||||
|
page.clock.set_system_time(datetime.datetime(2020, 2, 2))
|
||||||
|
page.clock.set_system_time("2020-02-02")
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
page.clock().setSystemTime(Instant.now());
|
||||||
|
page.clock().setSystemTime(Instant.parse("2020-02-02"));
|
||||||
|
page.clock().setSystemTime("2020-02-02");
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await page.Clock.SetSystemTimeAsync(DateTime.Now);
|
||||||
|
await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
|
||||||
|
await page.Clock.SetSystemTimeAsync("2020-02-02");
|
||||||
|
```
|
||||||
|
|
||||||
|
### param: Clock.setSystemTime.time
|
||||||
|
* since: v1.45
|
||||||
|
- `time` <[int]|[string]|[Date]>
|
||||||
|
|
|
||||||
|
|
@ -17,228 +17,324 @@ Accurately simulating time-dependent behavior is essential for verifying the cor
|
||||||
- `cancelAnimationFrame`
|
- `cancelAnimationFrame`
|
||||||
- `requestIdleCallback`
|
- `requestIdleCallback`
|
||||||
- `cancelIdleCallback`
|
- `cancelIdleCallback`
|
||||||
|
- `performance`
|
||||||
|
|
||||||
By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option.
|
## Test with predefined time
|
||||||
|
|
||||||
```js
|
Often you only need to fake `Date.now` while keeping the timers going.
|
||||||
await page.clock.setTime(new Date('2020-02-02'));
|
That way the time flows naturally, but `Date.now` always returns a fixed value.
|
||||||
await page.clock.installFakeTimers(new Date('2020-02-02'));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mock Date.now
|
|
||||||
|
|
||||||
Most of the time, you only need to fake `Date.now` and no other time-related functions.
|
|
||||||
That way the time flows naturally, but `Date.now` returns a fixed value.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div id="current-time" data-testid="current-time"></div>
|
<div id="current-time" data-testid="current-time"></div>
|
||||||
<script>
|
<script>
|
||||||
const renderTime = () => {
|
const renderTime = () => {
|
||||||
document.getElementById('current-time').textContent =
|
document.getElementById('current-time').textContent =
|
||||||
new Date() = time.toLocalTimeString();
|
new Date().toLocaleTimeString();
|
||||||
};
|
};
|
||||||
setInterval(renderTime, 1000);
|
setInterval(renderTime, 1000);
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
await page.clock.setTime(new Date('2024-02-02T10:00:00'));
|
await page.clock.setFixedTime(new Date('2024-02-02T10:00:00'));
|
||||||
await page.goto('http://localhost:3333');
|
await page.goto('http://localhost:3333');
|
||||||
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM');
|
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM');
|
||||||
|
|
||||||
await page.clock.setTime(new Date('2024-02-02T10:30:00'));
|
await page.clock.setFixedTime(new Date('2024-02-02T10:30:00'));
|
||||||
|
// We know that the page has a timer that updates the time every second.
|
||||||
|
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consistent time and timers
|
||||||
|
|
||||||
|
Sometimes your timers depend on `Date.now` and are confused when the `Date.now` value does not change over time.
|
||||||
|
In this case, you can install the clock and fast forward to the time of interest when testing.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div id="current-time" data-testid="current-time"></div>
|
||||||
|
<script>
|
||||||
|
const renderTime = () => {
|
||||||
|
document.getElementById('current-time').textContent =
|
||||||
|
new Date().toLocaleTimeString();
|
||||||
|
};
|
||||||
|
setInterval(renderTime, 1000);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Initialize clock with some time before the test time and let the page load
|
||||||
|
// naturally. `Date.now` will progress as the timers fire.
|
||||||
|
await page.clock.install({ time: new Date('2024-02-02T08:00:00') });
|
||||||
|
await page.goto('http://localhost:3333');
|
||||||
|
|
||||||
|
// Take control over time flow.
|
||||||
|
await page.clock.pause();
|
||||||
|
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||||
|
await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00'));
|
||||||
|
|
||||||
|
// Assert the page state.
|
||||||
|
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM');
|
||||||
|
|
||||||
|
// Close the laptop lid again and open it at 10:30am.
|
||||||
|
await page.clock.fastForward('30:00');
|
||||||
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
|
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst))
|
# Initialize clock with some time before the test time and let the page load
|
||||||
await page.goto('http://localhost:3333')
|
# naturally. `Date.now` will progress as the timers fire.
|
||||||
locator = page.get_by_test_id('current-time')
|
await page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
|
||||||
await expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
|
await page.goto("http://localhost:3333")
|
||||||
|
|
||||||
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst))
|
# Take control over time flow.
|
||||||
await expect(locator).to_have_text('2/2/2024, 10:30:00 AM')
|
await page.clock.pause()
|
||||||
|
# Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||||
|
await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0))
|
||||||
|
|
||||||
|
# Assert the page state.
|
||||||
|
await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
|
||||||
|
|
||||||
|
# Close the laptop lid again and open it at 10:30am.
|
||||||
|
await page.clock.fast_forward("30:00")
|
||||||
|
await expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst))
|
# Initialize clock with some time before the test time and let the page load
|
||||||
page.goto('http://localhost:3333')
|
# naturally. `Date.now` will progress as the timers fire.
|
||||||
locator = page.get_by_test_id('current-time')
|
page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
|
||||||
expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
|
page.goto("http://localhost:3333")
|
||||||
|
|
||||||
page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst))
|
# Take control over time flow.
|
||||||
expect(locator).to_have_text('2/2/2024, 10:30:00 AM')
|
page.clock.pause()
|
||||||
|
# Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||||
|
page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0))
|
||||||
|
|
||||||
|
# Assert the page state.
|
||||||
|
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
|
||||||
|
|
||||||
|
# Close the laptop lid again and open it at 10:30am.
|
||||||
|
page.clock.fast_forward("30:00")
|
||||||
|
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.clock().setTime(Instant.parse("2024-02-02T10:00:00"));
|
// Initialize clock with some time before the test time and let the page load
|
||||||
|
// naturally. `Date.now` will progress as the timers fire.
|
||||||
|
page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00")));
|
||||||
page.navigate("http://localhost:3333");
|
page.navigate("http://localhost:3333");
|
||||||
Locator locator = page.getByTestId("current-time");
|
Locator locator = page.getByTestId("current-time");
|
||||||
|
|
||||||
|
// Take control over time flow.
|
||||||
|
page.clock().pause();
|
||||||
|
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||||
|
page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00"));
|
||||||
|
|
||||||
|
// Assert the page state.
|
||||||
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
||||||
|
|
||||||
page.clock().setTime(Instant.parse("2024-02-02T10:30:00"));
|
// Close the laptop lid again and open it at 10:30am.
|
||||||
|
page.clock().fastForward("30:00");
|
||||||
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
|
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Initialize clock with a specific time, only fake Date.now.
|
// Initialize clock with some time before the test time and let the page load naturally.
|
||||||
await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst));
|
// `Date.now` will progress as the timers fire.
|
||||||
await page.GotoAsync("http://localhost:3333");
|
await Page.Clock.InstallAsync(new
|
||||||
var locator = page.GetByTestId("current-time");
|
{
|
||||||
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
|
Time = new DateTime(2024, 2, 2, 8, 0, 0)
|
||||||
|
});
|
||||||
|
await Page.GotoAsync("http://localhost:3333");
|
||||||
|
|
||||||
await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 30, 0, DateTimeKind.Pst));
|
// Take control over time flow.
|
||||||
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
|
await Page.Clock.PauseAsync();
|
||||||
|
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||||
|
await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0));
|
||||||
|
|
||||||
|
// Assert the page state.
|
||||||
|
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM");
|
||||||
|
|
||||||
|
// Close the laptop lid again and open it at 10:30am.
|
||||||
|
await Page.Clock.FastForwardAsync("30:00");
|
||||||
|
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM");
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mock Date.now consistent with the timers
|
## Test inactivity monitoring
|
||||||
|
|
||||||
Sometimes your timers depend on `Date.now` and are confused when the time stands still.
|
Inactivity monitoring is a common feature in web applications that logs out users after a period of inactivity.
|
||||||
In cases like this you need to ensure that `Date.now` and timers are consistent.
|
Testing this feature can be tricky because you need to wait for a long time to see the effect.
|
||||||
You can achieve this by installing the fake timers.
|
With the help of the clock, you can speed up time and test this feature quickly.
|
||||||
|
|
||||||
```html
|
|
||||||
<div id="current-time" data-testid="current-time"></div>
|
|
||||||
<script>
|
|
||||||
const renderTime = () => {
|
|
||||||
document.getElementById('current-time').textContent =
|
|
||||||
new Date() = time.toLocalTimeString();
|
|
||||||
};
|
|
||||||
setInterval(renderTime, 1000);
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Initialize clock with a specific time, take full control over time.
|
// Initial time does not matter for the test, so we can pick current time.
|
||||||
await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00'));
|
await page.clock.install();
|
||||||
await page.goto('http://localhost:3333');
|
await page.goto('http://localhost:3333');
|
||||||
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM');
|
// Interact with the page
|
||||||
|
await page.getByRole('button').click();
|
||||||
|
|
||||||
// Fast forward time 30 minutes without firing intermediate timers, as if the user
|
// Fast forward time 5 minutes as if the user did not do anything.
|
||||||
// closed and opened the lid of the laptop.
|
// Fast forward is like closing the laptop lid and opening it after 5 minutes.
|
||||||
await page.clock.skipTime('30:00');
|
// All the timers due will fire once immediately, as in the real browser.
|
||||||
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM');
|
await page.clock.fastForward('5:00');
|
||||||
|
|
||||||
|
// Check that the user was logged out automatically.
|
||||||
|
await expect(page.getByText('You have been logged out due to inactivity.')).toBeVisible();
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
# Initialize clock with a specific time, take full control over time.
|
# Initial time does not matter for the test, so we can pick current time.
|
||||||
await page.clock.install_fake_timers(
|
await page.clock.install()
|
||||||
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)
|
await page.goto("http://localhost:3333")
|
||||||
)
|
# Interact with the page
|
||||||
await page.goto('http://localhost:3333')
|
await page.get_by_role("button").click()
|
||||||
locator = page.get_by_test_id('current-time')
|
|
||||||
await expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
|
|
||||||
|
|
||||||
# Fast forward time 30 minutes without firing intermediate timers, as if the user
|
# Fast forward time 5 minutes as if the user did not do anything.
|
||||||
# closed and opened the lid of the laptop.
|
# Fast forward is like closing the laptop lid and opening it after 5 minutes.
|
||||||
await page.clock.skip_time('30:00')
|
# All the timers due will fire once immediately, as in the real browser.
|
||||||
await expect(locator).to_have_text('2/2/2024, 10:30:00 AM')
|
await page.clock.fast_forward("5:00")
|
||||||
|
|
||||||
|
# Check that the user was logged out automatically.
|
||||||
|
await expect(page.getByText("You have been logged out due to inactivity.")).toBeVisible()
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
# Initialize clock with a specific time, take full control over time.
|
# Initial time does not matter for the test, so we can pick current time.
|
||||||
page.clock.install_fake_timers(
|
page.clock.install()
|
||||||
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst)
|
page.goto("http://localhost:3333")
|
||||||
)
|
# Interact with the page
|
||||||
page.goto('http://localhost:3333')
|
page.get_by_role("button").click()
|
||||||
locator = page.get_by_test_id('current-time')
|
|
||||||
expect(locator).to_have_text('2/2/2024, 10:00:00 AM')
|
|
||||||
|
|
||||||
# Fast forward time 30 minutes without firing intermediate timers, as if the user
|
# Fast forward time 5 minutes as if the user did not do anything.
|
||||||
# closed and opened the lid of the laptop.
|
# Fast forward is like closing the laptop lid and opening it after 5 minutes.
|
||||||
page.clock.skip_time('30:00')
|
# All the timers due will fire once immediately, as in the real browser.
|
||||||
expect(locator).to_have_text('2/2/2024, 10:30:00 AM')
|
page.clock.fast_forward("5:00")
|
||||||
|
|
||||||
|
# Check that the user was logged out automatically.
|
||||||
|
expect(page.get_by_text("You have been logged out due to inactivity.")).to_be_visible()
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// Initialize clock with a specific time, take full control over time.
|
// Initial time does not matter for the test, so we can pick current time.
|
||||||
page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00"));
|
page.clock().install();
|
||||||
page.navigate("http://localhost:3333");
|
page.navigate("http://localhost:3333");
|
||||||
Locator locator = page.getByTestId("current-time");
|
Locator locator = page.getByRole("button");
|
||||||
assertThat(locator).hasText("2/2/2024, 10:00:00 AM")
|
|
||||||
|
|
||||||
// Fast forward time 30 minutes without firing intermediate timers, as if the user
|
// Interact with the page
|
||||||
// closed and opened the lid of the laptop.
|
locator.click();
|
||||||
page.clock().skipTime("30:00");
|
|
||||||
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
|
// Fast forward time 5 minutes as if the user did not do anything.
|
||||||
|
// Fast forward is like closing the laptop lid and opening it after 5 minutes.
|
||||||
|
// All the timers due will fire once immediately, as in the real browser.
|
||||||
|
page.clock().fastForward("5:00");
|
||||||
|
|
||||||
|
// Check that the user was logged out automatically.
|
||||||
|
assertThat(page.getByText("You have been logged out due to inactivity.")).isVisible();
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Initialize clock with a specific time, take full control over time.
|
// Initial time does not matter for the test, so we can pick current time.
|
||||||
await page.Clock.InstallFakeTimersAsync(
|
await Page.Clock.InstallAsync();
|
||||||
new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)
|
|
||||||
);
|
|
||||||
await page.GotoAsync("http://localhost:3333");
|
await page.GotoAsync("http://localhost:3333");
|
||||||
var locator = page.GetByTestId("current-time");
|
|
||||||
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
|
|
||||||
|
|
||||||
// Fast forward time 30 minutes without firing intermediate timers, as if the user
|
// Interact with the page
|
||||||
// closed and opened the lid of the laptop.
|
await page.GetByRole("button").ClickAsync();
|
||||||
await page.Clock.SkipTimeAsync("30:00");
|
|
||||||
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
|
// Fast forward time 5 minutes as if the user did not do anything.
|
||||||
|
// Fast forward is like closing the laptop lid and opening it after 5 minutes.
|
||||||
|
// All the timers due will fire once immediately, as in the real browser.
|
||||||
|
await Page.Clock.FastForwardAsync("5:00");
|
||||||
|
|
||||||
|
// Check that the user was logged out automatically.
|
||||||
|
await Expect(Page.GetByText("You have been logged out due to inactivity.")).ToBeVisibleAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tick through time manually
|
## Tick through time manually, firing all the timers consistently
|
||||||
|
|
||||||
In rare cases, you may want to tick through time manually, firing all timers and animation frames in the process to achieve a fine-grained
|
In rare cases, you may want to tick through time manually, firing all timers and
|
||||||
control over the passage of time.
|
animation frames in the process to achieve a fine-grained control over the passage of time.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div id="current-time" data-testid="current-time"></div>
|
<div id="current-time" data-testid="current-time"></div>
|
||||||
<script>
|
<script>
|
||||||
const renderTime = () => {
|
const renderTime = () => {
|
||||||
document.getElementById('current-time').textContent =
|
document.getElementById('current-time').textContent =
|
||||||
new Date() = time.toLocalTimeString();
|
new Date().toLocaleTimeString();
|
||||||
};
|
};
|
||||||
setInterval(renderTime, 1000);
|
setInterval(renderTime, 1000);
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// Initialize clock with a specific time, take full control over time.
|
// Initialize clock with a specific time, let the page load naturally.
|
||||||
await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00'));
|
await page.clock.install({ time: new Date('2024-02-02T08:00:00') });
|
||||||
await page.goto('http://localhost:3333');
|
await page.goto('http://localhost:3333');
|
||||||
|
|
||||||
|
// Pause the time flow, stop the timers, you now have manual control
|
||||||
|
// over the page time.
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.clock.fastForwardTo(new Date('2024-02-02T10:00:00'));
|
||||||
|
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM');
|
||||||
|
|
||||||
// Tick through time manually, firing all timers in the process.
|
// Tick through time manually, firing all timers in the process.
|
||||||
// In this case, time will be updated in the screen 2 times.
|
// In this case, time will be updated in the screen 2 times.
|
||||||
await page.clock.runFor(2000);
|
await page.clock.runFor(2000);
|
||||||
await expect(locator).to_have_text('2/2/2024, 10:00:02 AM');
|
await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:02 AM');
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
# Initialize clock with a specific time, take full control over time.
|
# Initialize clock with a specific time, let the page load naturally.
|
||||||
await page.clock.install_fake_timers(
|
await page.clock.install(time=
|
||||||
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst),
|
datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst),
|
||||||
)
|
)
|
||||||
await page.goto('http://localhost:3333')
|
await page.goto("http://localhost:3333")
|
||||||
locator = page.get_by_test_id('current-time')
|
locator = page.get_by_test_id("current-time")
|
||||||
|
|
||||||
|
# Pause the time flow, stop the timers, you now have manual control
|
||||||
|
# over the page time.
|
||||||
|
await page.clock.pause()
|
||||||
|
await page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0))
|
||||||
|
await expect(locator).to_have_text("2/2/2024, 10:00:00 AM")
|
||||||
|
|
||||||
# Tick through time manually, firing all timers in the process.
|
# Tick through time manually, firing all timers in the process.
|
||||||
# In this case, time will be updated in the screen 2 times.
|
# In this case, time will be updated in the screen 2 times.
|
||||||
await page.clock.run_for(2000)
|
await page.clock.run_for(2000)
|
||||||
await expect(locator).to_have_text('2/2/2024, 10:00:02 AM')
|
await expect(locator).to_have_text("2/2/2024, 10:00:02 AM")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
# Initialize clock with a specific time, take full control over time.
|
# Initialize clock with a specific time, let the page load naturally.
|
||||||
page.clock.install_fake_timers(
|
page.clock.install(
|
||||||
datetime.datetime(2024, 2, 2, 10, 0, 0, tzinfo=datetime.timezone.pst),
|
time=datetime.datetime(2024, 2, 2, 8, 0, 0, tzinfo=datetime.timezone.pst),
|
||||||
)
|
)
|
||||||
page.goto('http://localhost:3333')
|
page.goto("http://localhost:3333")
|
||||||
locator = page.get_by_test_id('current-time')
|
locator = page.get_by_test_id("current-time")
|
||||||
|
|
||||||
|
# Pause the time flow, stop the timers, you now have manual control
|
||||||
|
# over the page time.
|
||||||
|
page.clock.pause()
|
||||||
|
page.clock.fast_forward_to(datetime.datetime(2024, 2, 2, 10, 0, 0))
|
||||||
|
expect(locator).to_have_text("2/2/2024, 10:00:00 AM")
|
||||||
|
|
||||||
# Tick through time manually, firing all timers in the process.
|
# Tick through time manually, firing all timers in the process.
|
||||||
# In this case, time will be updated in the screen 2 times.
|
# In this case, time will be updated in the screen 2 times.
|
||||||
page.clock.run_for(2000)
|
page.clock.run_for(2000)
|
||||||
expect(locator).to_have_text('2/2/2024, 10:00:02 AM')
|
expect(locator).to_have_text("2/2/2024, 10:00:02 AM")
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// Initialize clock with a specific time, take full control over time.
|
// Initialize clock with a specific time, let the page load naturally.
|
||||||
page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00"));
|
page.clock().install(new Clock.InstallOptions()
|
||||||
|
.setTime(Instant.parse("2024-02-02T08:00:00")));
|
||||||
page.navigate("http://localhost:3333");
|
page.navigate("http://localhost:3333");
|
||||||
Locator locator = page.getByTestId("current-time");
|
Locator locator = page.getByTestId("current-time");
|
||||||
|
|
||||||
|
// Pause the time flow, stop the timers, you now have manual control
|
||||||
|
// over the page time.
|
||||||
|
page.clock().pause();
|
||||||
|
page.clock().fastForwardTo(Instant.parse("2024-02-02T10:00:00"));
|
||||||
|
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
||||||
|
|
||||||
// Tick through time manually, firing all timers in the process.
|
// Tick through time manually, firing all timers in the process.
|
||||||
// In this case, time will be updated in the screen 2 times.
|
// In this case, time will be updated in the screen 2 times.
|
||||||
page.clock().runFor(2000);
|
page.clock().runFor(2000);
|
||||||
|
|
@ -246,15 +342,22 @@ assertThat(locator).hasText("2/2/2024, 10:00:02 AM");
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Initialize clock with a specific time, take full control over time.
|
// Initialize clock with a specific time, let the page load naturally.
|
||||||
await page.Clock.InstallFakeTimersAsync(
|
await Page.Clock.InstallAsync(new
|
||||||
new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)
|
{
|
||||||
);
|
Time = new DateTime(2024, 2, 2, 8, 0, 0, DateTimeKind.Pst)
|
||||||
|
});
|
||||||
await page.GotoAsync("http://localhost:3333");
|
await page.GotoAsync("http://localhost:3333");
|
||||||
var locator = page.GetByTestId("current-time");
|
var locator = page.GetByTestId("current-time");
|
||||||
|
|
||||||
|
// Pause the time flow, stop the timers, you now have manual control
|
||||||
|
// over the page time.
|
||||||
|
await Page.Clock.PauseAsync();
|
||||||
|
await Page.Clock.FastForwardToAsync(new DateTime(2024, 2, 2, 10, 0, 0));
|
||||||
|
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
|
||||||
|
|
||||||
// Tick through time manually, firing all timers in the process.
|
// Tick through time manually, firing all timers in the process.
|
||||||
// In this case, time will be updated in the screen 2 times.
|
// In this case, time will be updated in the screen 2 times.
|
||||||
await page.Clock.RunForAsync(2000);
|
await Page.Clock.RunForAsync(2000);
|
||||||
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM");
|
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM");
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,12 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
||||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||||
const context = BrowserContext.from(response.context);
|
const context = BrowserContext.from(response.context);
|
||||||
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
||||||
if (!forReuse && !!process.env.PW_FREEZE_TIME)
|
if (!forReuse && !!process.env.PW_FREEZE_TIME) {
|
||||||
await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true);
|
await this._wrapApiCall(async () => {
|
||||||
|
await context.clock.install({ time: 0 });
|
||||||
|
await context.clock.pause();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,44 +24,50 @@ export class Clock implements api.Clock {
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) {
|
async install(options: { time?: number | string | Date } = { }) {
|
||||||
const timeMs = time instanceof Date ? time.getTime() : time;
|
await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
|
||||||
await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runAllTimers(): Promise<number> {
|
async fastForward(ticks: number | string) {
|
||||||
const result = await this._browserContext._channel.clockRunAllTimers();
|
await this._browserContext._channel.clockFastForward(parseTicks(ticks));
|
||||||
return result.fakeTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runFor(time: number | string): Promise<number> {
|
async fastForwardTo(time: number | string | Date) {
|
||||||
const result = await this._browserContext._channel.clockRunFor({
|
await this._browserContext._channel.clockFastForwardTo(parseTime(time));
|
||||||
timeNumber: typeof time === 'number' ? time : undefined,
|
|
||||||
timeString: typeof time === 'string' ? time : undefined
|
|
||||||
});
|
|
||||||
return result.fakeTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runToLastTimer(): Promise<number> {
|
async pause() {
|
||||||
const result = await this._browserContext._channel.clockRunToLastTimer();
|
await this._browserContext._channel.clockPause({});
|
||||||
return result.fakeTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runToNextTimer(): Promise<number> {
|
async resume() {
|
||||||
const result = await this._browserContext._channel.clockRunToNextTimer();
|
await this._browserContext._channel.clockResume({});
|
||||||
return result.fakeTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTime(time: number | Date) {
|
async runFor(ticks: number | string) {
|
||||||
const timeMs = time instanceof Date ? time.getTime() : time;
|
await this._browserContext._channel.clockRunFor(parseTicks(ticks));
|
||||||
await this._browserContext._channel.clockSetTime({ time: timeMs });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async skipTime(time: number | string) {
|
async setFixedTime(time: string | number | Date) {
|
||||||
const result = await this._browserContext._channel.clockSkipTime({
|
await this._browserContext._channel.clockSetFixedTime(parseTime(time));
|
||||||
timeNumber: typeof time === 'number' ? time : undefined,
|
}
|
||||||
timeString: typeof time === 'string' ? time : undefined
|
|
||||||
});
|
async setSystemTime(time: string | number | Date) {
|
||||||
return result.fakeTime;
|
await this._browserContext._channel.clockSetSystemTime(parseTime(time));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTime(time: string | number | Date): { timeNumber?: number, timeString?: string } {
|
||||||
|
if (typeof time === 'number')
|
||||||
|
return { timeNumber: time };
|
||||||
|
if (typeof time === 'string')
|
||||||
|
return { timeString: time };
|
||||||
|
return { timeNumber: time.getTime() };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTicks(ticks: string | number): { ticksNumber?: number, ticksString?: string } {
|
||||||
|
return {
|
||||||
|
ticksNumber: typeof ticks === 'number' ? ticks : undefined,
|
||||||
|
ticksString: typeof ticks === 'string' ? ticks : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -963,41 +963,40 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
|
||||||
enabled: tBoolean,
|
enabled: tBoolean,
|
||||||
});
|
});
|
||||||
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
|
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextClockInstallFakeTimersParams = tObject({
|
scheme.BrowserContextClockFastForwardParams = tObject({
|
||||||
time: tNumber,
|
ticksNumber: tOptional(tNumber),
|
||||||
loopLimit: tOptional(tNumber),
|
ticksString: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({}));
|
scheme.BrowserContextClockFastForwardResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({}));
|
scheme.BrowserContextClockFastForwardToParams = tObject({
|
||||||
scheme.BrowserContextClockRunAllTimersResult = tObject({
|
timeNumber: tOptional(tNumber),
|
||||||
fakeTime: tNumber,
|
timeString: tOptional(tString),
|
||||||
});
|
});
|
||||||
|
scheme.BrowserContextClockFastForwardToResult = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextClockInstallParams = tObject({
|
||||||
|
timeNumber: tOptional(tNumber),
|
||||||
|
timeString: tOptional(tString),
|
||||||
|
});
|
||||||
|
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextClockPauseParams = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextClockPauseResult = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextClockResumeParams = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextClockResumeResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextClockRunForParams = tObject({
|
scheme.BrowserContextClockRunForParams = tObject({
|
||||||
|
ticksNumber: tOptional(tNumber),
|
||||||
|
ticksString: tOptional(tString),
|
||||||
|
});
|
||||||
|
scheme.BrowserContextClockRunForResult = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextClockSetFixedTimeParams = tObject({
|
||||||
timeNumber: tOptional(tNumber),
|
timeNumber: tOptional(tNumber),
|
||||||
timeString: tOptional(tString),
|
timeString: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextClockRunForResult = tObject({
|
scheme.BrowserContextClockSetFixedTimeResult = tOptional(tObject({}));
|
||||||
fakeTime: tNumber,
|
scheme.BrowserContextClockSetSystemTimeParams = tObject({
|
||||||
});
|
|
||||||
scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({}));
|
|
||||||
scheme.BrowserContextClockRunToLastTimerResult = tObject({
|
|
||||||
fakeTime: tNumber,
|
|
||||||
});
|
|
||||||
scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({}));
|
|
||||||
scheme.BrowserContextClockRunToNextTimerResult = tObject({
|
|
||||||
fakeTime: tNumber,
|
|
||||||
});
|
|
||||||
scheme.BrowserContextClockSetTimeParams = tObject({
|
|
||||||
time: tNumber,
|
|
||||||
});
|
|
||||||
scheme.BrowserContextClockSetTimeResult = tOptional(tObject({}));
|
|
||||||
scheme.BrowserContextClockSkipTimeParams = tObject({
|
|
||||||
timeNumber: tOptional(tNumber),
|
timeNumber: tOptional(tNumber),
|
||||||
timeString: tOptional(tString),
|
timeString: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextClockSkipTimeResult = tObject({
|
scheme.BrowserContextClockSetSystemTimeResult = tOptional(tObject({}));
|
||||||
fakeTime: tNumber,
|
|
||||||
});
|
|
||||||
scheme.PageInitializer = tObject({
|
scheme.PageInitializer = tObject({
|
||||||
mainFrame: tChannel(['Frame']),
|
mainFrame: tChannel(['Frame']),
|
||||||
viewportSize: tOptional(tObject({
|
viewportSize: tOptional(tObject({
|
||||||
|
|
|
||||||
|
|
@ -16,81 +16,74 @@
|
||||||
|
|
||||||
import type { BrowserContext } from './browserContext';
|
import type { BrowserContext } from './browserContext';
|
||||||
import * as clockSource from '../generated/clockSource';
|
import * as clockSource from '../generated/clockSource';
|
||||||
|
import { isJavaScriptErrorInEvaluate } from './javascript';
|
||||||
|
|
||||||
export class Clock {
|
export class Clock {
|
||||||
private _browserContext: BrowserContext;
|
private _browserContext: BrowserContext;
|
||||||
private _scriptInjected = false;
|
private _scriptInstalled = false;
|
||||||
private _clockInstalled = false;
|
|
||||||
private _now = 0;
|
|
||||||
|
|
||||||
constructor(browserContext: BrowserContext) {
|
constructor(browserContext: BrowserContext) {
|
||||||
this._browserContext = browserContext;
|
this._browserContext = browserContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
async installFakeTimers(time: number, loopLimit: number | undefined) {
|
async fastForward(ticks: number | string) {
|
||||||
await this._injectScriptIfNeeded();
|
await this._installIfNeeded();
|
||||||
await this._addAndEvaluate(`(() => {
|
const ticksMillis = parseTicks(ticks);
|
||||||
globalThis.__pwClock.clock?.uninstall();
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`);
|
||||||
globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })});
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`);
|
||||||
})();`);
|
|
||||||
this._now = time;
|
|
||||||
this._clockInstalled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runToNextTimer(): Promise<number> {
|
async fastForwardTo(ticks: number | string) {
|
||||||
this._assertInstalled();
|
await this._installIfNeeded();
|
||||||
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.next()`);
|
const timeMillis = parseTime(ticks);
|
||||||
return this._now;
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForwardTo', ${Date.now()}, ${timeMillis})`);
|
||||||
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForwardTo(${timeMillis})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runAllTimers(): Promise<number> {
|
async install(time: number | string | undefined) {
|
||||||
this._assertInstalled();
|
await this._installIfNeeded();
|
||||||
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAll()`);
|
const timeMillis = time !== undefined ? parseTime(time) : Date.now();
|
||||||
return this._now;
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`);
|
||||||
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runToLastTimer(): Promise<number> {
|
async pause() {
|
||||||
this._assertInstalled();
|
await this._installIfNeeded();
|
||||||
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLast()`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pause', ${Date.now()})`);
|
||||||
return this._now;
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.pause()`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTime(time: number) {
|
async resume() {
|
||||||
if (this._clockInstalled) {
|
await this._installIfNeeded();
|
||||||
const jump = time - this._now;
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('resume', ${Date.now()})`);
|
||||||
if (jump < 0)
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`);
|
||||||
throw new Error('Unable to set time into the past when fake timers are installed');
|
|
||||||
await this._addAndEvaluate(`globalThis.__pwClock.clock.jump(${jump})`);
|
|
||||||
this._now = time;
|
|
||||||
return this._now;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this._injectScriptIfNeeded();
|
|
||||||
await this._addAndEvaluate(`(() => {
|
|
||||||
globalThis.__pwClock.clock?.uninstall();
|
|
||||||
globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, toFake: ['Date'] })});
|
|
||||||
})();`);
|
|
||||||
this._now = time;
|
|
||||||
return this._now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async skipTime(time: number | string) {
|
async setFixedTime(time: string | number) {
|
||||||
const delta = parseTime(time);
|
await this._installIfNeeded();
|
||||||
await this.setTime(this._now + delta);
|
const timeMillis = parseTime(time);
|
||||||
return this._now;
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setFixedTime', ${Date.now()}, ${timeMillis})`);
|
||||||
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.setFixedTime(${timeMillis})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runFor(time: number | string): Promise<number> {
|
async setSystemTime(time: string | number) {
|
||||||
this._assertInstalled();
|
await this._installIfNeeded();
|
||||||
await this._browserContext.addInitScript(`globalThis.__pwClock.clock.recordTick(${JSON.stringify(time)})`);
|
const timeMillis = parseTime(time);
|
||||||
this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`);
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`);
|
||||||
return this._now;
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _injectScriptIfNeeded() {
|
async runFor(ticks: number | string) {
|
||||||
if (this._scriptInjected)
|
await this._installIfNeeded();
|
||||||
|
const ticksMillis = parseTicks(ticks);
|
||||||
|
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`);
|
||||||
|
await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _installIfNeeded() {
|
||||||
|
if (this._scriptInstalled)
|
||||||
return;
|
return;
|
||||||
this._scriptInjected = true;
|
this._scriptInstalled = true;
|
||||||
const script = `(() => {
|
const script = `(() => {
|
||||||
const module = {};
|
const module = {};
|
||||||
${clockSource.source}
|
${clockSource.source}
|
||||||
|
|
@ -106,37 +99,56 @@ export class Clock {
|
||||||
|
|
||||||
private async _evaluateInFrames(script: string) {
|
private async _evaluateInFrames(script: string) {
|
||||||
const frames = this._browserContext.pages().map(page => page.frames()).flat();
|
const frames = this._browserContext.pages().map(page => page.frames()).flat();
|
||||||
const results = await Promise.all(frames.map(frame => frame.evaluateExpression(script)));
|
const results = await Promise.all(frames.map(async frame => {
|
||||||
|
try {
|
||||||
|
await frame.nonStallingEvaluateInExistingContext(script, false, 'main');
|
||||||
|
} catch (e) {
|
||||||
|
if (isJavaScriptErrorInEvaluate(e))
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}));
|
||||||
return results[0];
|
return results[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _assertInstalled() {
|
|
||||||
if (!this._clockInstalled)
|
|
||||||
throw new Error('Clock is not installed');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taken from sinonjs/fake-timerss-src.
|
/**
|
||||||
function parseTime(time: string | number): number {
|
* Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into
|
||||||
if (typeof time === 'number')
|
* number of milliseconds. This is used to support human-readable strings passed
|
||||||
return time;
|
* to clock.tick()
|
||||||
if (!time)
|
*/
|
||||||
|
function parseTicks(value: number | string): number {
|
||||||
|
if (typeof value === 'number')
|
||||||
|
return value;
|
||||||
|
if (!value)
|
||||||
return 0;
|
return 0;
|
||||||
|
const str = value;
|
||||||
|
|
||||||
const strings = time.split(':');
|
const strings = str.split(':');
|
||||||
const l = strings.length;
|
const l = strings.length;
|
||||||
let i = l;
|
let i = l;
|
||||||
let ms = 0;
|
let ms = 0;
|
||||||
let parsed;
|
let parsed;
|
||||||
|
|
||||||
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time))
|
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
|
||||||
throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`);
|
throw new Error(
|
||||||
|
`Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
while (i--) {
|
while (i--) {
|
||||||
parsed = parseInt(strings[i], 10);
|
parsed = parseInt(strings[i], 10);
|
||||||
if (parsed >= 60)
|
if (parsed >= 60)
|
||||||
throw new Error(`Invalid time ${time}`);
|
throw new Error(`Invalid time ${str}`);
|
||||||
ms += parsed * Math.pow(60, l - i - 1);
|
ms += parsed * Math.pow(60, l - i - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ms * 1000;
|
return ms * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTime(epoch: string | number | undefined): number {
|
||||||
|
if (!epoch)
|
||||||
|
return 0;
|
||||||
|
if (typeof epoch === 'number')
|
||||||
|
return epoch;
|
||||||
|
return new Date(epoch).getTime();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -312,32 +312,36 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
return { artifact: ArtifactDispatcher.from(this, artifact) };
|
return { artifact: ArtifactDispatcher.from(this, artifact) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallFakeTimersResult> {
|
async clockFastForward(params: channels.BrowserContextClockFastForwardParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockFastForwardResult> {
|
||||||
await this._context.clock.installFakeTimers(params.time, params.loopLimit);
|
await this._context.clock.fastForward(params.ticksString ?? params.ticksNumber ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllTimersResult> {
|
async clockFastForwardTo(params: channels.BrowserContextClockFastForwardToParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockFastForwardToResult> {
|
||||||
return { fakeTime: await this._context.clock.runAllTimers() };
|
await this._context.clock.fastForwardTo(params.timeString ?? params.timeNumber ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToLastTimerResult> {
|
async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> {
|
||||||
return { fakeTime: await this._context.clock.runToLastTimer() };
|
await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToNextTimerResult> {
|
async clockPause(params: channels.BrowserContextClockPauseParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockPauseResult> {
|
||||||
return { fakeTime: await this._context.clock.runToNextTimer() };
|
await this._context.clock.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSetTimeResult> {
|
async clockResume(params: channels.BrowserContextClockResumeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockResumeResult> {
|
||||||
await this._context.clock.setTime(params.time);
|
await this._context.clock.resume();
|
||||||
}
|
|
||||||
|
|
||||||
async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSkipTimeResult> {
|
|
||||||
return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunForResult> {
|
async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunForResult> {
|
||||||
return { fakeTime: await this._context.clock.runFor(params.timeString || params.timeNumber || 0) };
|
await this._context.clock.runFor(params.ticksString ?? params.ticksNumber ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clockSetFixedTime(params: channels.BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSetFixedTimeResult> {
|
||||||
|
await this._context.clock.setFixedTime(params.timeString ?? params.timeNumber ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clockSetSystemTime(params: channels.BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockSetSystemTimeResult> {
|
||||||
|
await this._context.clock.setSystemTime(params.timeString ?? params.timeNumber ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {
|
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export type ClockMethods = {
|
||||||
|
|
||||||
export type ClockConfig = {
|
export type ClockConfig = {
|
||||||
now?: number | Date;
|
now?: number | Date;
|
||||||
loopLimit?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InstallConfig = ClockConfig & {
|
export type InstallConfig = ClockConfig & {
|
||||||
|
|
@ -53,26 +52,39 @@ type Timer = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Embedder {
|
interface Embedder {
|
||||||
postTask(task: () => void): void;
|
dateNow(): number;
|
||||||
postTaskPeriodically(task: () => void, delay: number): () => void;
|
performanceNow(): DOMHighResTimeStamp;
|
||||||
|
setTimeout(task: () => void, timeout?: number): () => void;
|
||||||
|
setInterval(task: () => void, delay: number): () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Time = {
|
||||||
|
// ms since Epoch
|
||||||
|
time: number;
|
||||||
|
// Ticks since the session began (ala performance.now)
|
||||||
|
ticks: number;
|
||||||
|
// Whether fixed time was set.
|
||||||
|
isFixedTime: boolean;
|
||||||
|
// Origin time since Epoch when session started.
|
||||||
|
origin: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogEntryType = 'fastForward' | 'fastForwardTo' | 'install' | 'pause' | 'resume' | 'runFor' | 'setFixedTime' | 'setSystemTime';
|
||||||
|
|
||||||
export class ClockController {
|
export class ClockController {
|
||||||
readonly timeOrigin: number;
|
readonly _now: Time;
|
||||||
private _now: { time: number, ticks: number, timeFrozen: boolean };
|
|
||||||
private _loopLimit: number;
|
|
||||||
private _duringTick = false;
|
private _duringTick = false;
|
||||||
private _timers = new Map<number, Timer>();
|
private _timers = new Map<number, Timer>();
|
||||||
private _uniqueTimerId = idCounterStart;
|
private _uniqueTimerId = idCounterStart;
|
||||||
private _embedder: Embedder;
|
private _embedder: Embedder;
|
||||||
readonly disposables: (() => void)[] = [];
|
readonly disposables: (() => void)[] = [];
|
||||||
|
private _log: { type: LogEntryType, time: number, param?: number }[] = [];
|
||||||
|
private _realTime: { startTicks: number, lastSyncTicks: number } | undefined;
|
||||||
|
private _currentRealTimeTimer: { callAt: number, dispose: () => void } | undefined;
|
||||||
|
|
||||||
constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) {
|
constructor(embedder: Embedder) {
|
||||||
const start = Math.floor(getEpoch(startDate));
|
this._now = { time: 0, isFixedTime: false, ticks: 0, origin: -1 };
|
||||||
this.timeOrigin = start;
|
|
||||||
this._now = { time: start, ticks: 0, timeFrozen: false };
|
|
||||||
this._embedder = embedder;
|
this._embedder = embedder;
|
||||||
this._loopLimit = loopLimit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uninstall() {
|
uninstall() {
|
||||||
|
|
@ -81,109 +93,147 @@ export class ClockController {
|
||||||
}
|
}
|
||||||
|
|
||||||
now(): number {
|
now(): number {
|
||||||
|
this._replayLogOnce();
|
||||||
return this._now.time;
|
return this._now.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTime(now: Date | number, options: { freeze?: boolean } = {}) {
|
install(time: number) {
|
||||||
this._now.time = getEpoch(now);
|
this._replayLogOnce();
|
||||||
this._now.timeFrozen = !!options.freeze;
|
this._innerSetTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSystemTime(time: number) {
|
||||||
|
this._replayLogOnce();
|
||||||
|
this._innerSetTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFixedTime(time: number) {
|
||||||
|
this._replayLogOnce();
|
||||||
|
this._innerSetFixedTime(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
performanceNow(): DOMHighResTimeStamp {
|
performanceNow(): DOMHighResTimeStamp {
|
||||||
|
this._replayLogOnce();
|
||||||
return this._now.ticks;
|
return this._now.ticks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _innerSetTime(time: number) {
|
||||||
|
this._now.time = time;
|
||||||
|
this._now.isFixedTime = false;
|
||||||
|
if (this._now.origin < 0)
|
||||||
|
this._now.origin = this._now.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _innerSetFixedTime(time: number) {
|
||||||
|
this._innerSetTime(time);
|
||||||
|
this._now.isFixedTime = true;
|
||||||
|
}
|
||||||
|
|
||||||
private _advanceNow(toTicks: number) {
|
private _advanceNow(toTicks: number) {
|
||||||
if (!this._now.timeFrozen)
|
if (!this._now.isFixedTime)
|
||||||
this._now.time += toTicks - this._now.ticks;
|
this._now.time += toTicks - this._now.ticks;
|
||||||
this._now.ticks = toTicks;
|
this._now.ticks = toTicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _doTick(msFloat: number): Promise<number> {
|
async log(type: LogEntryType, time: number, param?: number) {
|
||||||
if (msFloat < 0)
|
this._log.push({ type, time, param });
|
||||||
throw new TypeError('Negative ticks are not supported');
|
}
|
||||||
|
|
||||||
|
async runFor(ticks: number) {
|
||||||
|
this._replayLogOnce();
|
||||||
|
if (ticks < 0)
|
||||||
|
throw new TypeError('Negative ticks are not supported');
|
||||||
|
await this._runTo(this._now.ticks + ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _runTo(tickTo: number) {
|
||||||
|
if (this._now.ticks > tickTo)
|
||||||
|
return;
|
||||||
|
|
||||||
const ms = Math.floor(msFloat);
|
|
||||||
const tickTo = this._now.ticks + ms;
|
|
||||||
let tickFrom = this._now.ticks;
|
|
||||||
let previous = this._now.ticks;
|
|
||||||
let firstException: Error | undefined;
|
let firstException: Error | undefined;
|
||||||
let timer = this._firstTimerInRange(tickFrom, tickTo);
|
while (true) {
|
||||||
while (timer && tickFrom <= tickTo) {
|
const result = await this._callFirstTimer(tickTo);
|
||||||
tickFrom = timer.callAt;
|
if (!result.timerFound)
|
||||||
const error = await this._callTimer(timer).catch(e => e);
|
break;
|
||||||
firstException = firstException || error;
|
firstException = firstException || result.error;
|
||||||
timer = this._firstTimerInRange(previous, tickTo);
|
|
||||||
previous = tickFrom;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._advanceNow(tickTo);
|
this._advanceNow(tickTo);
|
||||||
if (firstException)
|
if (firstException)
|
||||||
throw firstException;
|
throw firstException;
|
||||||
|
|
||||||
return this._now.ticks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async recordTick(tickValue: string | number) {
|
pause() {
|
||||||
const msFloat = parseTime(tickValue);
|
this._replayLogOnce();
|
||||||
this._advanceNow(this._now.ticks + msFloat);
|
this._innerPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
async tick(tickValue: string | number): Promise<number> {
|
private _innerPause() {
|
||||||
return await this._doTick(parseTime(tickValue));
|
this._realTime = undefined;
|
||||||
|
this._updateRealTimeTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async next(): Promise<number> {
|
resume() {
|
||||||
const timer = this._firstTimer();
|
this._replayLogOnce();
|
||||||
if (!timer)
|
this._innerResume();
|
||||||
return this._now.ticks;
|
|
||||||
await this._callTimer(timer);
|
|
||||||
return this._now.ticks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async runToFrame(): Promise<number> {
|
private _innerResume() {
|
||||||
return this.tick(this.getTimeToNextFrame());
|
const now = this._embedder.performanceNow();
|
||||||
|
this._realTime = { startTicks: now, lastSyncTicks: now };
|
||||||
|
this._updateRealTimeTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async runAll(): Promise<number> {
|
private _updateRealTimeTimer() {
|
||||||
for (let i = 0; i < this._loopLimit; i++) {
|
if (!this._realTime) {
|
||||||
const numTimers = this._timers.size;
|
this._currentRealTimeTimer?.dispose();
|
||||||
if (numTimers === 0)
|
this._currentRealTimeTimer = undefined;
|
||||||
return this._now.ticks;
|
return;
|
||||||
|
|
||||||
await this.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const excessJob = this._firstTimer();
|
const firstTimer = this._firstTimer();
|
||||||
if (!excessJob)
|
|
||||||
return this._now.ticks;
|
// Either run the next timer or move time in 100ms chunks.
|
||||||
throw this._getInfiniteLoopError(excessJob);
|
const callAt = Math.min(firstTimer ? firstTimer.callAt : this._now.ticks + maxTimeout, this._now.ticks + 100);
|
||||||
|
if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this._currentRealTimeTimer) {
|
||||||
|
this._currentRealTimeTimer.dispose();
|
||||||
|
this._currentRealTimeTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currentRealTimeTimer = {
|
||||||
|
callAt,
|
||||||
|
dispose: this._embedder.setTimeout(() => {
|
||||||
|
const now = Math.ceil(this._embedder.performanceNow());
|
||||||
|
this._currentRealTimeTimer = undefined;
|
||||||
|
const sinceLastSync = now - this._realTime!.lastSyncTicks;
|
||||||
|
this._realTime!.lastSyncTicks = now;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
this._runTo(this._now.ticks + sinceLastSync).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
|
||||||
|
}, callAt - this._now.ticks),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async runToLast(): Promise<number> {
|
async fastForward(ticks: number) {
|
||||||
const timer = this._lastTimer();
|
this._replayLogOnce();
|
||||||
if (!timer)
|
const ms = ticks | 0;
|
||||||
return this._now.ticks;
|
|
||||||
return await this.tick(timer.callAt - this._now.ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this._timers.clear();
|
|
||||||
this._now = { time: this.timeOrigin, ticks: 0, timeFrozen: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
async jump(tickValue: string | number): Promise<number> {
|
|
||||||
const msFloat = parseTime(tickValue);
|
|
||||||
const ms = Math.floor(msFloat);
|
|
||||||
|
|
||||||
for (const timer of this._timers.values()) {
|
for (const timer of this._timers.values()) {
|
||||||
if (this._now.ticks + ms > timer.callAt)
|
if (this._now.ticks + ms > timer.callAt)
|
||||||
timer.callAt = this._now.ticks + ms;
|
timer.callAt = this._now.ticks + ms;
|
||||||
}
|
}
|
||||||
return await this.tick(ms);
|
await this.runFor(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fastForwardTo(time: number) {
|
||||||
|
this._replayLogOnce();
|
||||||
|
const ticks = time - this._now.time;
|
||||||
|
await this.fastForward(ticks);
|
||||||
}
|
}
|
||||||
|
|
||||||
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
||||||
|
this._replayLogOnce();
|
||||||
if (options.func === undefined)
|
if (options.func === undefined)
|
||||||
throw new Error('Callback must be provided to timer calls');
|
throw new Error('Callback must be provided to timer calls');
|
||||||
|
|
||||||
|
|
@ -204,56 +254,56 @@ export class ClockController {
|
||||||
error: new Error(),
|
error: new Error(),
|
||||||
};
|
};
|
||||||
this._timers.set(timer.id, timer);
|
this._timers.set(timer.id, timer);
|
||||||
|
if (this._realTime)
|
||||||
|
this._updateRealTimeTimer();
|
||||||
return timer.id;
|
return timer.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _firstTimerInRange(from: number, to: number): Timer | null {
|
|
||||||
let firstTimer: Timer | null = null;
|
|
||||||
for (const timer of this._timers.values()) {
|
|
||||||
const isInRange = inRange(from, to, timer);
|
|
||||||
if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1))
|
|
||||||
firstTimer = timer;
|
|
||||||
}
|
|
||||||
return firstTimer;
|
|
||||||
}
|
|
||||||
|
|
||||||
countTimers() {
|
countTimers() {
|
||||||
return this._timers.size;
|
return this._timers.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _firstTimer(): Timer | null {
|
private _firstTimer(beforeTick?: number): Timer | null {
|
||||||
let firstTimer: Timer | null = null;
|
let firstTimer: Timer | null = null;
|
||||||
|
|
||||||
for (const timer of this._timers.values()) {
|
for (const timer of this._timers.values()) {
|
||||||
if (!firstTimer || compareTimers(firstTimer, timer) === 1)
|
const isInRange = beforeTick === undefined || timer.callAt <= beforeTick;
|
||||||
|
if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1))
|
||||||
firstTimer = timer;
|
firstTimer = timer;
|
||||||
}
|
}
|
||||||
return firstTimer;
|
return firstTimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _lastTimer(): Timer | null {
|
private _takeFirstTimer(beforeTick?: number): Timer | null {
|
||||||
let lastTimer: Timer | null = null;
|
const timer = this._firstTimer(beforeTick);
|
||||||
|
if (!timer)
|
||||||
|
return null;
|
||||||
|
|
||||||
for (const timer of this._timers.values()) {
|
|
||||||
if (!lastTimer || compareTimers(lastTimer, timer) === -1)
|
|
||||||
lastTimer = timer;
|
|
||||||
}
|
|
||||||
return lastTimer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _callTimer(timer: Timer) {
|
|
||||||
this._advanceNow(timer.callAt);
|
this._advanceNow(timer.callAt);
|
||||||
|
|
||||||
if (timer.type === TimerType.Interval)
|
if (timer.type === TimerType.Interval)
|
||||||
this._timers.get(timer.id)!.callAt += timer.delay;
|
this._timers.get(timer.id)!.callAt += timer.delay;
|
||||||
else
|
else
|
||||||
this._timers.delete(timer.id);
|
this._timers.delete(timer.id);
|
||||||
|
return timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _callFirstTimer(beforeTick: number): Promise<{ timerFound: boolean, error?: Error }> {
|
||||||
|
const timer = this._takeFirstTimer(beforeTick);
|
||||||
|
if (!timer)
|
||||||
|
return { timerFound: false };
|
||||||
|
|
||||||
this._duringTick = true;
|
this._duringTick = true;
|
||||||
try {
|
try {
|
||||||
if (typeof timer.func !== 'function') {
|
if (typeof timer.func !== 'function') {
|
||||||
(() => { eval(timer.func); })();
|
let error: Error | undefined;
|
||||||
return;
|
try {
|
||||||
|
(() => { eval(timer.func); })();
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
await new Promise<void>(f => this._embedder.setTimeout(f));
|
||||||
|
return { timerFound: true, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = timer.args;
|
let args = timer.args;
|
||||||
|
|
@ -262,67 +312,26 @@ export class ClockController {
|
||||||
else if (timer.type === TimerType.IdleCallback)
|
else if (timer.type === TimerType.IdleCallback)
|
||||||
args = [{ didTimeout: false, timeRemaining: () => 0 }];
|
args = [{ didTimeout: false, timeRemaining: () => 0 }];
|
||||||
|
|
||||||
timer.func.apply(null, args);
|
let error: Error | undefined;
|
||||||
await new Promise<void>(f => this._embedder.postTask(f));
|
try {
|
||||||
|
timer.func.apply(null, args);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
await new Promise<void>(f => this._embedder.setTimeout(f));
|
||||||
|
return { timerFound: true, error };
|
||||||
} finally {
|
} finally {
|
||||||
this._duringTick = false;
|
this._duringTick = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getInfiniteLoopError(job: Timer) {
|
|
||||||
const infiniteLoopError = new Error(
|
|
||||||
`Aborting after running ${this._loopLimit} timers, assuming an infinite loop!`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!job.error)
|
|
||||||
return infiniteLoopError;
|
|
||||||
|
|
||||||
// pattern never matched in Node
|
|
||||||
const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/;
|
|
||||||
const clockMethodPattern = new RegExp(
|
|
||||||
String(Object.keys(this).join('|')),
|
|
||||||
);
|
|
||||||
|
|
||||||
let matchedLineIndex = -1;
|
|
||||||
job.error.stack!.split('\n').some((line, i) => {
|
|
||||||
// If we've matched a computed target line (e.g. setTimeout) then we
|
|
||||||
// don't need to look any further. Return true to stop iterating.
|
|
||||||
const matchedComputedTarget = line.match(computedTargetPattern);
|
|
||||||
/* istanbul ignore if */
|
|
||||||
if (matchedComputedTarget) {
|
|
||||||
matchedLineIndex = i;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've matched a clock method line, then there may still be
|
|
||||||
// others further down the trace. Return false to keep iterating.
|
|
||||||
const matchedClockMethod = line.match(clockMethodPattern);
|
|
||||||
if (matchedClockMethod) {
|
|
||||||
matchedLineIndex = i;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we haven't matched anything on this line, but we matched
|
|
||||||
// previously and set the matched line index, then we can stop.
|
|
||||||
// If we haven't matched previously, then we should keep iterating.
|
|
||||||
return matchedLineIndex >= 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const funcName = typeof job.func === 'function' ? job.func.name : 'anonymous';
|
|
||||||
const stack = `${infiniteLoopError}\n${job.type || 'Microtask'} - ${funcName}\n${job.error.stack!
|
|
||||||
.split('\n')
|
|
||||||
.slice(matchedLineIndex + 1)
|
|
||||||
.join('\n')}`;
|
|
||||||
|
|
||||||
infiniteLoopError.stack = stack;
|
|
||||||
return infiniteLoopError;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeToNextFrame() {
|
getTimeToNextFrame() {
|
||||||
return 16 - this._now.ticks % 16;
|
return 16 - this._now.ticks % 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimer(timerId: number, type: TimerType) {
|
clearTimer(timerId: number, type: TimerType) {
|
||||||
|
this._replayLogOnce();
|
||||||
|
|
||||||
if (!timerId) {
|
if (!timerId) {
|
||||||
// null appears to be allowed in most browsers, and appears to be
|
// null appears to be allowed in most browsers, and appears to be
|
||||||
// relied upon by some libraries, like Bootstrap carousel
|
// relied upon by some libraries, like Bootstrap carousel
|
||||||
|
|
@ -356,64 +365,49 @@ export class ClockController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
advanceAutomatically(advanceTimeDelta: number = 20): () => void {
|
private _replayLogOnce() {
|
||||||
return this._embedder.postTaskPeriodically(
|
if (!this._log.length)
|
||||||
() => this._doTick(advanceTimeDelta!),
|
return;
|
||||||
advanceTimeDelta,
|
|
||||||
);
|
let lastLogTime = -1;
|
||||||
|
let isPaused = false;
|
||||||
|
|
||||||
|
for (const { type, time, param } of this._log) {
|
||||||
|
if (!isPaused && lastLogTime !== -1)
|
||||||
|
this._advanceNow(this._now.ticks + time - lastLogTime);
|
||||||
|
lastLogTime = time;
|
||||||
|
|
||||||
|
if (type === 'install') {
|
||||||
|
this._innerSetTime(param!);
|
||||||
|
} else if (type === 'fastForward' || type === 'runFor') {
|
||||||
|
this._advanceNow(this._now.ticks + param!);
|
||||||
|
} else if (type === 'fastForwardTo') {
|
||||||
|
this._innerSetTime(param!);
|
||||||
|
} else if (type === 'pause') {
|
||||||
|
this._innerPause();
|
||||||
|
isPaused = true;
|
||||||
|
} else if (type === 'resume') {
|
||||||
|
this._innerResume();
|
||||||
|
isPaused = false;
|
||||||
|
} else if (type === 'setFixedTime') {
|
||||||
|
this._innerSetFixedTime(param!);
|
||||||
|
} else if (type === 'setSystemTime') {
|
||||||
|
this._innerSetTime(param!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPaused && lastLogTime > 0)
|
||||||
|
this._advanceNow(this._now.ticks + this._embedder.dateNow() - lastLogTime);
|
||||||
|
|
||||||
|
this._log.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEpoch(epoch: Date | number | undefined): number {
|
|
||||||
if (!epoch)
|
|
||||||
return 0;
|
|
||||||
if (typeof epoch !== 'number')
|
|
||||||
return epoch.getTime();
|
|
||||||
return epoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inRange(from: number, to: number, timer: Timer): boolean {
|
|
||||||
return timer && timer.callAt >= from && timer.callAt <= to;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into
|
|
||||||
* number of milliseconds. This is used to support human-readable strings passed
|
|
||||||
* to clock.tick()
|
|
||||||
*/
|
|
||||||
function parseTime(value: number | string): number {
|
|
||||||
if (typeof value === 'number')
|
|
||||||
return value;
|
|
||||||
if (!value)
|
|
||||||
return 0;
|
|
||||||
const str = value;
|
|
||||||
|
|
||||||
const strings = str.split(':');
|
|
||||||
const l = strings.length;
|
|
||||||
let i = l;
|
|
||||||
let ms = 0;
|
|
||||||
let parsed;
|
|
||||||
|
|
||||||
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
|
|
||||||
throw new Error(
|
|
||||||
`Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (i--) {
|
|
||||||
parsed = parseInt(strings[i], 10);
|
|
||||||
if (parsed >= 60)
|
|
||||||
throw new Error(`Invalid time ${str}`);
|
|
||||||
ms += parsed * Math.pow(60, l - i - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ms * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
|
function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
|
||||||
let prop;
|
for (const prop in source) {
|
||||||
for (prop of Object.keys(source) as (keyof DateConstructor)[])
|
if (source.hasOwnProperty(prop))
|
||||||
target[prop] = source[prop];
|
target[prop] = (source as any)[prop];
|
||||||
|
}
|
||||||
target.toString = () => source.toString();
|
target.toString = () => source.toString();
|
||||||
target.prototype = source.prototype;
|
target.prototype = source.prototype;
|
||||||
target.parse = source.parse;
|
target.parse = source.parse;
|
||||||
|
|
@ -485,7 +479,7 @@ function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Int
|
||||||
* All properties of Intl are non-enumerable, so we need
|
* All properties of Intl are non-enumerable, so we need
|
||||||
* to do a bit of work to get them out.
|
* to do a bit of work to get them out.
|
||||||
*/
|
*/
|
||||||
for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[])
|
for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[])
|
||||||
ClockIntl[key] = NativeIntl[key];
|
ClockIntl[key] = NativeIntl[key];
|
||||||
|
|
||||||
ClockIntl.DateTimeFormat = function(...args: any[]) {
|
ClockIntl.DateTimeFormat = function(...args: any[]) {
|
||||||
|
|
@ -644,8 +638,8 @@ function getClearHandler(type: TimerType) {
|
||||||
function fakePerformance(clock: ClockController, performance: Performance): Performance {
|
function fakePerformance(clock: ClockController, performance: Performance): Performance {
|
||||||
const result: any = {
|
const result: any = {
|
||||||
now: () => clock.performanceNow(),
|
now: () => clock.performanceNow(),
|
||||||
timeOrigin: clock.timeOrigin,
|
|
||||||
};
|
};
|
||||||
|
result.__defineGetter__('timeOrigin', () => clock._now.origin || 0);
|
||||||
// eslint-disable-next-line no-proto
|
// eslint-disable-next-line no-proto
|
||||||
for (const key of Object.keys((performance as any).__proto__)) {
|
for (const key of Object.keys((performance as any).__proto__)) {
|
||||||
if (key === 'now' || key === 'timeOrigin')
|
if (key === 'now' || key === 'timeOrigin')
|
||||||
|
|
@ -658,19 +652,22 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
|
export function createClock(globalObject: WindowOrWorkerGlobalScope): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
|
||||||
const originals = platformOriginals(globalObject);
|
const originals = platformOriginals(globalObject);
|
||||||
const embedder = {
|
const embedder: Embedder = {
|
||||||
postTask: (task: () => void) => {
|
dateNow: () => originals.raw.Date.now(),
|
||||||
originals.bound.setTimeout(task, 0);
|
performanceNow: () => originals.raw.performance!.now(),
|
||||||
|
setTimeout: (task: () => void, timeout?: number) => {
|
||||||
|
const timerId = originals.bound.setTimeout(task, timeout);
|
||||||
|
return () => originals.bound.clearTimeout(timerId);
|
||||||
},
|
},
|
||||||
postTaskPeriodically: (task: () => void, delay: number) => {
|
setInterval: (task: () => void, delay: number) => {
|
||||||
const intervalId = globalObject.setInterval(task, delay);
|
const intervalId = originals.bound.setInterval(task, delay);
|
||||||
return () => originals.bound.clearInterval(intervalId);
|
return () => originals.bound.clearInterval(intervalId);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const clock = new ClockController(embedder, config.now, config.loopLimit);
|
const clock = new ClockController(embedder);
|
||||||
const api = createApi(clock, originals.bound);
|
const api = createApi(clock, originals.bound);
|
||||||
return { clock, api, originals: originals.raw };
|
return { clock, api, originals: originals.raw };
|
||||||
}
|
}
|
||||||
|
|
@ -682,7 +679,7 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
|
||||||
throw new TypeError(`Can't install fake timers twice on the same global object.`);
|
throw new TypeError(`Can't install fake timers twice on the same global object.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { clock, api, originals } = createClock(globalObject, config);
|
const { clock, api, originals } = createClock(globalObject);
|
||||||
const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[];
|
const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[];
|
||||||
|
|
||||||
for (const method of toFake) {
|
for (const method of toFake) {
|
||||||
|
|
@ -706,11 +703,10 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inject(globalObject: WindowOrWorkerGlobalScope) {
|
export function inject(globalObject: WindowOrWorkerGlobalScope) {
|
||||||
|
const { clock: controller } = install(globalObject);
|
||||||
|
controller.resume();
|
||||||
return {
|
return {
|
||||||
install: (config: InstallConfig) => {
|
controller,
|
||||||
const { clock } = install(globalObject, config);
|
|
||||||
return clock;
|
|
||||||
},
|
|
||||||
builtin: platformOriginals(globalObject).bound,
|
builtin: platformOriginals(globalObject).bound,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
129
packages/playwright-core/types/types.d.ts
vendored
129
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -17247,8 +17247,40 @@ export interface BrowserServer {
|
||||||
* controlled by the same clock.
|
* controlled by the same clock.
|
||||||
*/
|
*/
|
||||||
export interface Clock {
|
export interface Clock {
|
||||||
|
/**
|
||||||
|
* Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user
|
||||||
|
* closing the laptop lid for a while and reopening it later, after given time.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* await page.clock.fastForward(1000);
|
||||||
|
* await page.clock.fastForward('30:00');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
|
||||||
|
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||||
|
*/
|
||||||
|
fastForward(ticks: number|string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance the clock by jumping forward in time. Only fires due timers at most once. This is equivalent to user
|
||||||
|
* closing the laptop lid for a while and reopening it at the specified time.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* await page.clock.fastForwardTo(new Date('2020-02-02'));
|
||||||
|
* await page.clock.fastForwardTo('2020-02-02');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param time
|
||||||
|
*/
|
||||||
|
fastForwardTo(time: number|string|Date): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install fake implementations for the following time-related functions:
|
* Install fake implementations for the following time-related functions:
|
||||||
|
* - `Date`
|
||||||
* - `setTimeout`
|
* - `setTimeout`
|
||||||
* - `clearTimeout`
|
* - `clearTimeout`
|
||||||
* - `setInterval`
|
* - `setInterval`
|
||||||
|
|
@ -17261,34 +17293,33 @@ export interface Clock {
|
||||||
*
|
*
|
||||||
* Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers,
|
* Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers,
|
||||||
* and control the behavior of time-dependent functions. See
|
* and control the behavior of time-dependent functions. See
|
||||||
* [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and
|
* [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for) and
|
||||||
* [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) for more information.
|
* [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward) for more information.
|
||||||
* @param time Install fake timers with the specified base time.
|
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
installFakeTimers(time: number|Date, options?: {
|
install(options?: {
|
||||||
/**
|
/**
|
||||||
* The maximum number of timers that will be run in
|
* Time to initialize with, current system time by default.
|
||||||
* [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`.
|
|
||||||
*/
|
*/
|
||||||
loopLimit?: number;
|
time?: number|string|Date;
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be
|
* Pause timers. Once this method is called, no timers are fired unless
|
||||||
* run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch.
|
* [clock.runFor(ticks)](https://playwright.dev/docs/api/class-clock#clock-run-for),
|
||||||
*
|
* [clock.fastForward(ticks)](https://playwright.dev/docs/api/class-clock#clock-fast-forward),
|
||||||
* **Details**
|
* [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to) or
|
||||||
*
|
* [clock.resume()](https://playwright.dev/docs/api/class-clock#clock-resume) is called.
|
||||||
* This makes it easier to run asynchronous tests to completion without worrying about the number of timers they use,
|
|
||||||
* or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite
|
|
||||||
* loop of timers and throws an error.
|
|
||||||
*/
|
*/
|
||||||
runAllTimers(): Promise<number>;
|
pause(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must
|
* Resumes timers. Once this method is called, time resumes flowing, timers are fired as usual.
|
||||||
* be installed. Returns fake milliseconds since the unix epoch.
|
*/
|
||||||
|
resume(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance the clock, firing all the time-related callbacks.
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
|
|
@ -17297,55 +17328,41 @@ export interface Clock {
|
||||||
* await page.clock.runFor('30:00');
|
* await page.clock.runFor('30:00');
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
|
* @param ticks Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
|
||||||
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||||
*/
|
*/
|
||||||
runFor(time: number|string): Promise<number>;
|
runFor(ticks: number|string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as
|
* Makes `Date.now` and `new Date()` return fixed fake time at all times, keeps all the timers running.
|
||||||
* necessary. If new timers are added while it is executing they will be run only if they would occur before this
|
|
||||||
* time. This is useful when you want to run a test to completion, but the test recursively sets timers that would
|
|
||||||
* cause runAll to trigger an infinite loop warning. Fake timers must be installed. Returns fake milliseconds since
|
|
||||||
* the unix epoch.
|
|
||||||
*/
|
|
||||||
runToLastTimer(): Promise<number>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advances the clock to the moment of the first scheduled timer, firing it. Fake timers must be installed. Returns
|
|
||||||
* fake milliseconds since the unix epoch.
|
|
||||||
*/
|
|
||||||
runToNextTimer(): Promise<number>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the clock to the specified time.
|
|
||||||
*
|
|
||||||
* When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as
|
|
||||||
* a browser) being put to sleep and resumed later, skipping intermediary timers.
|
|
||||||
* @param time
|
|
||||||
*/
|
|
||||||
setTime(time: number|Date): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advance the clock by jumping forward in time, equivalent to running
|
|
||||||
* [clock.setTime(time)](https://playwright.dev/docs/api/class-clock#clock-set-time) with the new target time.
|
|
||||||
*
|
|
||||||
* When fake timers are installed, [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time)
|
|
||||||
* only fires due timers at most once, while
|
|
||||||
* [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) fires all the timers up to the
|
|
||||||
* current time. Returns fake milliseconds since the unix epoch.
|
|
||||||
*
|
*
|
||||||
* **Usage**
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* await page.clock.skipTime(1000);
|
* await page.clock.setFixedTime(Date.now());
|
||||||
* await page.clock.skipTime('30:00');
|
* await page.clock.setFixedTime(new Date('2020-02-02'));
|
||||||
|
* await page.clock.setFixedTime('2020-02-02');
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
|
* @param time Time to be set.
|
||||||
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
|
||||||
*/
|
*/
|
||||||
skipTime(time: number|string): Promise<number>;
|
setFixedTime(time: number|string|Date): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets current system time but does not trigger any timers, unlike
|
||||||
|
* [clock.fastForwardTo(time)](https://playwright.dev/docs/api/class-clock#clock-fast-forward-to).
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* await page.clock.setSystemTime(Date.now());
|
||||||
|
* await page.clock.setSystemTime(new Date('2020-02-02'));
|
||||||
|
* await page.clock.setSystemTime('2020-02-02');
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param time
|
||||||
|
*/
|
||||||
|
setSystemTime(time: number|string|Date): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1460,13 +1460,14 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
|
||||||
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
||||||
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
|
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
|
||||||
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
|
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
|
||||||
clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallFakeTimersResult>;
|
clockFastForward(params: BrowserContextClockFastForwardParams, metadata?: CallMetadata): Promise<BrowserContextClockFastForwardResult>;
|
||||||
clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllTimersResult>;
|
clockFastForwardTo(params: BrowserContextClockFastForwardToParams, metadata?: CallMetadata): Promise<BrowserContextClockFastForwardToResult>;
|
||||||
|
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>;
|
||||||
|
clockPause(params?: BrowserContextClockPauseParams, metadata?: CallMetadata): Promise<BrowserContextClockPauseResult>;
|
||||||
|
clockResume(params?: BrowserContextClockResumeParams, metadata?: CallMetadata): Promise<BrowserContextClockResumeResult>;
|
||||||
clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise<BrowserContextClockRunForResult>;
|
clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise<BrowserContextClockRunForResult>;
|
||||||
clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastTimerResult>;
|
clockSetFixedTime(params: BrowserContextClockSetFixedTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetFixedTimeResult>;
|
||||||
clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToNextTimerResult>;
|
clockSetSystemTime(params: BrowserContextClockSetSystemTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetSystemTimeResult>;
|
||||||
clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSetTimeResult>;
|
|
||||||
clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise<BrowserContextClockSkipTimeResult>;
|
|
||||||
}
|
}
|
||||||
export type BrowserContextBindingCallEvent = {
|
export type BrowserContextBindingCallEvent = {
|
||||||
binding: BindingCallChannel,
|
binding: BindingCallChannel,
|
||||||
|
|
@ -1755,58 +1756,66 @@ export type BrowserContextUpdateSubscriptionOptions = {
|
||||||
|
|
||||||
};
|
};
|
||||||
export type BrowserContextUpdateSubscriptionResult = void;
|
export type BrowserContextUpdateSubscriptionResult = void;
|
||||||
export type BrowserContextClockInstallFakeTimersParams = {
|
export type BrowserContextClockFastForwardParams = {
|
||||||
time: number,
|
ticksNumber?: number,
|
||||||
loopLimit?: number,
|
ticksString?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockInstallFakeTimersOptions = {
|
export type BrowserContextClockFastForwardOptions = {
|
||||||
loopLimit?: number,
|
ticksNumber?: number,
|
||||||
|
ticksString?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockInstallFakeTimersResult = void;
|
export type BrowserContextClockFastForwardResult = void;
|
||||||
export type BrowserContextClockRunAllTimersParams = {};
|
export type BrowserContextClockFastForwardToParams = {
|
||||||
export type BrowserContextClockRunAllTimersOptions = {};
|
|
||||||
export type BrowserContextClockRunAllTimersResult = {
|
|
||||||
fakeTime: number,
|
|
||||||
};
|
|
||||||
export type BrowserContextClockRunForParams = {
|
|
||||||
timeNumber?: number,
|
timeNumber?: number,
|
||||||
timeString?: string,
|
timeString?: string,
|
||||||
};
|
};
|
||||||
|
export type BrowserContextClockFastForwardToOptions = {
|
||||||
|
timeNumber?: number,
|
||||||
|
timeString?: string,
|
||||||
|
};
|
||||||
|
export type BrowserContextClockFastForwardToResult = void;
|
||||||
|
export type BrowserContextClockInstallParams = {
|
||||||
|
timeNumber?: number,
|
||||||
|
timeString?: string,
|
||||||
|
};
|
||||||
|
export type BrowserContextClockInstallOptions = {
|
||||||
|
timeNumber?: number,
|
||||||
|
timeString?: string,
|
||||||
|
};
|
||||||
|
export type BrowserContextClockInstallResult = void;
|
||||||
|
export type BrowserContextClockPauseParams = {};
|
||||||
|
export type BrowserContextClockPauseOptions = {};
|
||||||
|
export type BrowserContextClockPauseResult = void;
|
||||||
|
export type BrowserContextClockResumeParams = {};
|
||||||
|
export type BrowserContextClockResumeOptions = {};
|
||||||
|
export type BrowserContextClockResumeResult = void;
|
||||||
|
export type BrowserContextClockRunForParams = {
|
||||||
|
ticksNumber?: number,
|
||||||
|
ticksString?: string,
|
||||||
|
};
|
||||||
export type BrowserContextClockRunForOptions = {
|
export type BrowserContextClockRunForOptions = {
|
||||||
|
ticksNumber?: number,
|
||||||
|
ticksString?: string,
|
||||||
|
};
|
||||||
|
export type BrowserContextClockRunForResult = void;
|
||||||
|
export type BrowserContextClockSetFixedTimeParams = {
|
||||||
timeNumber?: number,
|
timeNumber?: number,
|
||||||
timeString?: string,
|
timeString?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockRunForResult = {
|
export type BrowserContextClockSetFixedTimeOptions = {
|
||||||
fakeTime: number,
|
|
||||||
};
|
|
||||||
export type BrowserContextClockRunToLastTimerParams = {};
|
|
||||||
export type BrowserContextClockRunToLastTimerOptions = {};
|
|
||||||
export type BrowserContextClockRunToLastTimerResult = {
|
|
||||||
fakeTime: number,
|
|
||||||
};
|
|
||||||
export type BrowserContextClockRunToNextTimerParams = {};
|
|
||||||
export type BrowserContextClockRunToNextTimerOptions = {};
|
|
||||||
export type BrowserContextClockRunToNextTimerResult = {
|
|
||||||
fakeTime: number,
|
|
||||||
};
|
|
||||||
export type BrowserContextClockSetTimeParams = {
|
|
||||||
time: number,
|
|
||||||
};
|
|
||||||
export type BrowserContextClockSetTimeOptions = {
|
|
||||||
|
|
||||||
};
|
|
||||||
export type BrowserContextClockSetTimeResult = void;
|
|
||||||
export type BrowserContextClockSkipTimeParams = {
|
|
||||||
timeNumber?: number,
|
timeNumber?: number,
|
||||||
timeString?: string,
|
timeString?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockSkipTimeOptions = {
|
export type BrowserContextClockSetFixedTimeResult = void;
|
||||||
|
export type BrowserContextClockSetSystemTimeParams = {
|
||||||
timeNumber?: number,
|
timeNumber?: number,
|
||||||
timeString?: string,
|
timeString?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextClockSkipTimeResult = {
|
export type BrowserContextClockSetSystemTimeOptions = {
|
||||||
fakeTime: number,
|
timeNumber?: number,
|
||||||
|
timeString?: string,
|
||||||
};
|
};
|
||||||
|
export type BrowserContextClockSetSystemTimeResult = void;
|
||||||
|
|
||||||
export interface BrowserContextEvents {
|
export interface BrowserContextEvents {
|
||||||
'bindingCall': BrowserContextBindingCallEvent;
|
'bindingCall': BrowserContextBindingCallEvent;
|
||||||
|
|
|
||||||
|
|
@ -1204,40 +1204,39 @@ BrowserContext:
|
||||||
- requestFailed
|
- requestFailed
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
clockInstallFakeTimers:
|
clockFastForward:
|
||||||
parameters:
|
parameters:
|
||||||
time: number
|
ticksNumber: number?
|
||||||
loopLimit: number?
|
ticksString: string?
|
||||||
|
|
||||||
clockRunAllTimers:
|
clockFastForwardTo:
|
||||||
returns:
|
parameters:
|
||||||
fakeTime: number
|
timeNumber: number?
|
||||||
|
timeString: string?
|
||||||
|
|
||||||
|
clockInstall:
|
||||||
|
parameters:
|
||||||
|
timeNumber: number?
|
||||||
|
timeString: string?
|
||||||
|
|
||||||
|
clockPause:
|
||||||
|
|
||||||
|
clockResume:
|
||||||
|
|
||||||
clockRunFor:
|
clockRunFor:
|
||||||
parameters:
|
parameters:
|
||||||
timeNumber: number?
|
ticksNumber: number?
|
||||||
timeString: string?
|
ticksString: string?
|
||||||
returns:
|
|
||||||
fakeTime: number
|
|
||||||
|
|
||||||
clockRunToLastTimer:
|
clockSetFixedTime:
|
||||||
returns:
|
parameters:
|
||||||
fakeTime: number
|
timeNumber: number?
|
||||||
|
timeString: string?
|
||||||
clockRunToNextTimer:
|
|
||||||
returns:
|
clockSetSystemTime:
|
||||||
fakeTime: number
|
|
||||||
|
|
||||||
clockSetTime:
|
|
||||||
parameters:
|
|
||||||
time: number
|
|
||||||
|
|
||||||
clockSkipTime:
|
|
||||||
parameters:
|
parameters:
|
||||||
timeNumber: number?
|
timeNumber: number?
|
||||||
timeString: string?
|
timeString: string?
|
||||||
returns:
|
|
||||||
fakeTime: number
|
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -18,6 +18,7 @@ import { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
it.skip(!process.env.PW_FREEZE_TIME);
|
it.skip(!process.env.PW_FREEZE_TIME);
|
||||||
|
|
||||||
it('cock should be frozen', async ({ page }) => {
|
it('clock should be frozen', async ({ page }) => {
|
||||||
|
await page.clock.setSystemTime(0);
|
||||||
expect(await page.evaluate('Date.now()')).toBe(0);
|
expect(await page.evaluate('Date.now()')).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,12 @@ const it = test.extend<{ calls: { params: any[] }[] }>({
|
||||||
});
|
});
|
||||||
|
|
||||||
it.describe('runFor', () => {
|
it.describe('runFor', () => {
|
||||||
|
it.beforeEach(async ({ page }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.clock.pause();
|
||||||
|
});
|
||||||
|
|
||||||
it('triggers immediately without specified delay', async ({ page, calls }) => {
|
it('triggers immediately without specified delay', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub);
|
setTimeout(window.stub);
|
||||||
});
|
});
|
||||||
|
|
@ -46,7 +50,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not trigger without sufficient delay', async ({ page, calls }) => {
|
it('does not trigger without sufficient delay', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub, 100);
|
setTimeout(window.stub, 100);
|
||||||
});
|
});
|
||||||
|
|
@ -55,7 +58,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('triggers after sufficient delay', async ({ page, calls }) => {
|
it('triggers after sufficient delay', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub, 100);
|
setTimeout(window.stub, 100);
|
||||||
});
|
});
|
||||||
|
|
@ -64,7 +66,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('triggers simultaneous timers', async ({ page, calls }) => {
|
it('triggers simultaneous timers', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub, 100);
|
setTimeout(window.stub, 100);
|
||||||
setTimeout(window.stub, 100);
|
setTimeout(window.stub, 100);
|
||||||
|
|
@ -74,7 +75,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('triggers multiple simultaneous timers', async ({ page, calls }) => {
|
it('triggers multiple simultaneous timers', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub, 100);
|
setTimeout(window.stub, 100);
|
||||||
setTimeout(window.stub, 100);
|
setTimeout(window.stub, 100);
|
||||||
|
|
@ -86,7 +86,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('waits after setTimeout was called', async ({ page, calls }) => {
|
it('waits after setTimeout was called', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub, 150);
|
setTimeout(window.stub, 150);
|
||||||
});
|
});
|
||||||
|
|
@ -97,18 +96,16 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('triggers event when some throw', async ({ page, calls }) => {
|
it('triggers event when some throw', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => { throw new Error(); }, 100);
|
setTimeout(() => { throw new Error(); }, 100);
|
||||||
setTimeout(window.stub, 120);
|
setTimeout(window.stub, 120);
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(page.clock.runFor(120)).rejects.toThrow();
|
await expect(page.clock.runFor(120)).rejects.toThrow();
|
||||||
expect(calls).toHaveLength(1);
|
expect(calls).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates updated Date while ticking', async ({ page, calls }) => {
|
it('creates updated Date while ticking', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
await page.clock.setSystemTime(0);
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
window.stub(new Date().getTime());
|
window.stub(new Date().getTime());
|
||||||
|
|
@ -130,7 +127,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes 8 seconds', async ({ page, calls }) => {
|
it('passes 8 seconds', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setInterval(window.stub, 4000);
|
setInterval(window.stub, 4000);
|
||||||
});
|
});
|
||||||
|
|
@ -140,7 +136,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes 1 minute', async ({ page, calls }) => {
|
it('passes 1 minute', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setInterval(window.stub, 6000);
|
setInterval(window.stub, 6000);
|
||||||
});
|
});
|
||||||
|
|
@ -150,7 +145,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
|
it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setInterval(window.stub, 10000);
|
setInterval(window.stub, 10000);
|
||||||
});
|
});
|
||||||
|
|
@ -160,7 +154,6 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws for invalid format', async ({ page, calls }) => {
|
it('throws for invalid format', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setInterval(window.stub, 10000);
|
setInterval(window.stub, 10000);
|
||||||
});
|
});
|
||||||
|
|
@ -169,332 +162,93 @@ it.describe('runFor', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the current now value', async ({ page }) => {
|
it('returns the current now value', async ({ page }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
await page.clock.setSystemTime(0);
|
||||||
const value = 200;
|
const value = 200;
|
||||||
await page.clock.runFor(value);
|
await page.clock.runFor(value);
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(value);
|
expect(await page.evaluate(() => Date.now())).toBe(value);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.describe('skipTime', () => {
|
it.describe('fastForward', () => {
|
||||||
|
it.beforeEach(async ({ page }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.clock.setSystemTime(0);
|
||||||
|
});
|
||||||
|
|
||||||
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
|
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.stub('should not be logged');
|
window.stub('should not be logged');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
await page.clock.skipTime(500);
|
await page.clock.fastForward(500);
|
||||||
expect(calls).toEqual([]);
|
expect(calls).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
|
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.stub(Date.now());
|
window.stub(Date.now());
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.clock.skipTime(2000);
|
await page.clock.fastForward(2000);
|
||||||
expect(calls).toEqual([{ params: [2000] }]);
|
expect(calls).toEqual([{ params: [2000] }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports string time arguments', async ({ page, calls }) => {
|
it('supports string time arguments', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.stub(Date.now());
|
window.stub(Date.now());
|
||||||
}, 100000); // 100000 = 1:40
|
}, 100000); // 100000 = 1:40
|
||||||
});
|
});
|
||||||
await page.clock.skipTime('01:50');
|
await page.clock.fastForward('01:50');
|
||||||
expect(calls).toEqual([{ params: [110000] }]);
|
expect(calls).toEqual([{ params: [110000] }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.describe('runAllTimers', () => {
|
it.describe('fastForwardTo', () => {
|
||||||
it('if there are no timers just return', async ({ page }) => {
|
it.beforeEach(async ({ page }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
await page.clock.install();
|
||||||
await page.clock.runAllTimers();
|
await page.clock.pause();
|
||||||
|
await page.clock.setSystemTime(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runs all timers', async ({ page, calls }) => {
|
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(window.stub, 10);
|
|
||||||
setTimeout(window.stub, 50);
|
|
||||||
});
|
|
||||||
await page.clock.runAllTimers();
|
|
||||||
expect(calls.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new timers added while running are also run', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setTimeout(window.stub, 50);
|
window.stub('should not be logged');
|
||||||
}, 10);
|
}, 1000);
|
||||||
});
|
});
|
||||||
await page.clock.runAllTimers();
|
await page.clock.fastForwardTo(500);
|
||||||
expect(calls.length).toBe(1);
|
expect(calls).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('new timers added in promises while running are also run', async ({ page, calls }) => {
|
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
void Promise.resolve().then(() => {
|
window.stub(Date.now());
|
||||||
setTimeout(window.stub, 50);
|
}, 1000);
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
});
|
});
|
||||||
await page.clock.runAllTimers();
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws before allowing infinite recursion', async ({ page, calls }) => {
|
await page.clock.fastForwardTo(2000);
|
||||||
await page.clock.installFakeTimers(0);
|
expect(calls).toEqual([{ params: [2000] }]);
|
||||||
await page.evaluate(async () => {
|
|
||||||
const recursiveCallback = () => {
|
|
||||||
window.stub();
|
|
||||||
setTimeout(recursiveCallback, 10);
|
|
||||||
};
|
|
||||||
setTimeout(recursiveCallback, 10);
|
|
||||||
});
|
|
||||||
await expect(page.clock.runAllTimers()).rejects.toThrow();
|
|
||||||
expect(calls).toHaveLength(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws before allowing infinite recursion from promises', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const recursiveCallback = () => {
|
|
||||||
window.stub();
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
setTimeout(recursiveCallback, 10);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
setTimeout(recursiveCallback, 10);
|
|
||||||
});
|
|
||||||
await expect(page.clock.runAllTimers()).rejects.toThrow();
|
|
||||||
expect(calls).toHaveLength(1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('the loop limit can be set when creating a clock', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0, { loopLimit: 1 });
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(window.stub, 10);
|
|
||||||
setTimeout(window.stub, 50);
|
|
||||||
});
|
|
||||||
await expect(page.clock.runAllTimers()).rejects.toThrow();
|
|
||||||
expect(calls).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle user-created promises', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
void Promise.resolve().then(() => window.stub());
|
|
||||||
}, 55);
|
|
||||||
});
|
|
||||||
await page.clock.runAllTimers();
|
|
||||||
expect(calls).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle nested user-created promises', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
void Promise.resolve().then(() => window.stub());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 55);
|
|
||||||
});
|
|
||||||
await page.clock.runAllTimers();
|
|
||||||
expect(calls).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle local promises before firing timers', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
void Promise.resolve().then(() => window.stub(1));
|
|
||||||
setTimeout(() => window.stub(2), 55);
|
|
||||||
});
|
|
||||||
await page.clock.runAllTimers();
|
|
||||||
expect(calls).toEqual([
|
|
||||||
{ params: [1] },
|
|
||||||
{ params: [2] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.describe('runToLastTimer', () => {
|
|
||||||
it('returns current time when there are no timers', async ({ page }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
const time = await page.clock.runToLastTimer();
|
|
||||||
expect(time).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('runs all existing timers', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(window.stub, 10);
|
|
||||||
setTimeout(window.stub, 50);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns time of the last timer', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(window.stub, 10);
|
|
||||||
setTimeout(window.stub, 50);
|
|
||||||
});
|
|
||||||
const time = await page.clock.runToLastTimer();
|
|
||||||
expect(time).toBe(50);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('runs all existing timers when two timers are matched for being last', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(window.stub, 10);
|
|
||||||
setTimeout(window.stub, 10);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new timers added with a call time later than the last existing timer are NOT run', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.stub();
|
|
||||||
setTimeout(window.stub, 50);
|
|
||||||
}, 10);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new timers added with a call time earlier than the last existing timer are run', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(window.stub, 100);
|
|
||||||
setTimeout(() => {
|
|
||||||
setTimeout(window.stub, 50);
|
|
||||||
}, 10);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new timers cannot cause an infinite loop', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const recursiveCallback = () => {
|
|
||||||
window.stub();
|
|
||||||
setTimeout(recursiveCallback, 0);
|
|
||||||
};
|
|
||||||
setTimeout(recursiveCallback, 0);
|
|
||||||
setTimeout(window.stub, 100);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(102);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support clocks with start time', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(200);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(function cb() {
|
|
||||||
window.stub();
|
|
||||||
setTimeout(cb, 50);
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new timers created from promises cannot cause an infinite loop', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
const recursiveCallback = () => {
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
setTimeout(recursiveCallback, 0);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
setTimeout(recursiveCallback, 0);
|
|
||||||
setTimeout(window.stub, 100);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle user-created promises', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
void Promise.resolve().then(() => window.stub());
|
|
||||||
}, 55);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle nested user-created promises', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
void Promise.resolve().then(() => {
|
|
||||||
void Promise.resolve().then(() => window.stub());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 55);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle local promises before firing timers', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
void Promise.resolve().then(() => window.stub(1));
|
|
||||||
setTimeout(() => window.stub(2), 55);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls).toEqual([
|
|
||||||
{ params: [1] },
|
|
||||||
{ params: [2] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should settle user-created promises before firing more timers', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
void Promise.resolve().then(() => window.stub(1));
|
|
||||||
}, 55);
|
|
||||||
setTimeout(() => window.stub(2), 75);
|
|
||||||
});
|
|
||||||
await page.clock.runToLastTimer();
|
|
||||||
expect(calls).toEqual([
|
|
||||||
{ params: [1] },
|
|
||||||
{ params: [2] },
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.describe('stubTimers', () => {
|
it.describe('stubTimers', () => {
|
||||||
|
it.beforeEach(async ({ page }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.clock.setSystemTime(0);
|
||||||
|
});
|
||||||
it('sets initial timestamp', async ({ page, calls }) => {
|
it('sets initial timestamp', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(1400);
|
await page.clock.setSystemTime(1400);
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(1400);
|
expect(await page.evaluate(() => Date.now())).toBe(1400);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces global setTimeout', async ({ page, calls }) => {
|
it('replaces global setTimeout', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(window.stub, 1000);
|
setTimeout(window.stub, 1000);
|
||||||
});
|
});
|
||||||
|
|
@ -503,13 +257,11 @@ it.describe('stubTimers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('global fake setTimeout should return id', async ({ page, calls }) => {
|
it('global fake setTimeout should return id', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
const to = await page.evaluate(() => setTimeout(window.stub, 1000));
|
const to = await page.evaluate(() => setTimeout(window.stub, 1000));
|
||||||
expect(typeof to).toBe('number');
|
expect(typeof to).toBe('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces global clearTimeout', async ({ page, calls }) => {
|
it('replaces global clearTimeout', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const to = setTimeout(window.stub, 1000);
|
const to = setTimeout(window.stub, 1000);
|
||||||
clearTimeout(to);
|
clearTimeout(to);
|
||||||
|
|
@ -519,7 +271,6 @@ it.describe('stubTimers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces global setInterval', async ({ page, calls }) => {
|
it('replaces global setInterval', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setInterval(window.stub, 500);
|
setInterval(window.stub, 500);
|
||||||
});
|
});
|
||||||
|
|
@ -528,7 +279,6 @@ it.describe('stubTimers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces global clearInterval', async ({ page, calls }) => {
|
it('replaces global clearInterval', async ({ page, calls }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
const to = setInterval(window.stub, 500);
|
const to = setInterval(window.stub, 500);
|
||||||
clearInterval(to);
|
clearInterval(to);
|
||||||
|
|
@ -538,7 +288,6 @@ it.describe('stubTimers', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces global performance.now', async ({ page }) => {
|
it('replaces global performance.now', async ({ page }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
const promise = page.evaluate(async () => {
|
const promise = page.evaluate(async () => {
|
||||||
const prev = performance.now();
|
const prev = performance.now();
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
await new Promise(f => setTimeout(f, 1000));
|
||||||
|
|
@ -549,30 +298,35 @@ it.describe('stubTimers', () => {
|
||||||
expect(await promise).toEqual({ prev: 0, next: 1000 });
|
expect(await promise).toEqual({ prev: 0, next: 1000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces global performance.timeOrigin', async ({ page }) => {
|
|
||||||
await page.clock.installFakeTimers(1000);
|
|
||||||
const promise = page.evaluate(async () => {
|
|
||||||
const prev = performance.now();
|
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
|
||||||
const next = performance.now();
|
|
||||||
return { prev, next };
|
|
||||||
});
|
|
||||||
expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000);
|
|
||||||
await page.clock.runFor(1000);
|
|
||||||
expect(await promise).toEqual({ prev: 0, next: 1000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fakes Date constructor', async ({ page }) => {
|
it('fakes Date constructor', async ({ page }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
const now = await page.evaluate(() => new Date().getTime());
|
const now = await page.evaluate(() => new Date().getTime());
|
||||||
expect(now).toBe(0);
|
expect(now).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.describe('stubTimers', () => {
|
||||||
|
it('replaces global performance.timeOrigin', async ({ page }) => {
|
||||||
|
await page.clock.install({ time: 1000 });
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.clock.setSystemTime(1000);
|
||||||
|
const promise = page.evaluate(async () => {
|
||||||
|
const prev = performance.now();
|
||||||
|
await new Promise(f => setTimeout(f, 1000));
|
||||||
|
const next = performance.now();
|
||||||
|
return { prev, next };
|
||||||
|
});
|
||||||
|
await page.clock.runFor(1000);
|
||||||
|
expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000);
|
||||||
|
expect(await promise).toEqual({ prev: 0, next: 1000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it.describe('popup', () => {
|
it.describe('popup', () => {
|
||||||
it('should tick after popup', async ({ page }) => {
|
it('should tick after popup', async ({ page }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.clock.pause();
|
||||||
const now = new Date('2015-09-25');
|
const now = new Date('2015-09-25');
|
||||||
await page.clock.installFakeTimers(now);
|
await page.clock.setSystemTime(now);
|
||||||
const [popup] = await Promise.all([
|
const [popup] = await Promise.all([
|
||||||
page.waitForEvent('popup'),
|
page.waitForEvent('popup'),
|
||||||
page.evaluate(() => window.open('about:blank')),
|
page.evaluate(() => window.open('about:blank')),
|
||||||
|
|
@ -584,11 +338,12 @@ it.describe('popup', () => {
|
||||||
expect(popupTimeAfter).toBe(now.getTime() + 1000);
|
expect(popupTimeAfter).toBe(now.getTime() + 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tick before popup', async ({ page, browserName }) => {
|
it('should tick before popup', async ({ page }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.clock.pause();
|
||||||
const now = new Date('2015-09-25');
|
const now = new Date('2015-09-25');
|
||||||
await page.clock.installFakeTimers(now);
|
await page.clock.setSystemTime(now);
|
||||||
const ticks = await page.clock.runFor(1000);
|
await page.clock.runFor(1000);
|
||||||
expect(ticks).toBe(1000);
|
|
||||||
|
|
||||||
const [popup] = await Promise.all([
|
const [popup] = await Promise.all([
|
||||||
page.waitForEvent('popup'),
|
page.waitForEvent('popup'),
|
||||||
|
|
@ -597,90 +352,47 @@ it.describe('popup', () => {
|
||||||
const popupTime = await popup.evaluate(() => Date.now());
|
const popupTime = await popup.evaluate(() => Date.now());
|
||||||
expect(popupTime).toBe(now.getTime() + 1000);
|
expect(popupTime).toBe(now.getTime() + 1000);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it.describe('runToNextTimer', () => {
|
it('should run time before popup', async ({ page, server }) => {
|
||||||
it('triggers the next timer', async ({ page, calls }) => {
|
server.setRoute('/popup.html', async (req, res) => {
|
||||||
await page.clock.installFakeTimers(0);
|
res.setHeader('Content-Type', 'text/html');
|
||||||
await page.evaluate(async () => {
|
res.end(`<script>window.time = Date.now()</script>`);
|
||||||
setTimeout(window.stub, 100);
|
|
||||||
});
|
});
|
||||||
expect(await page.clock.runToNextTimer()).toBe(100);
|
await page.clock.setSystemTime(0);
|
||||||
expect(calls).toHaveLength(1);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
// Wait for 2 second in real life to check that it is past in popup.
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
const [popup] = await Promise.all([
|
||||||
|
page.waitForEvent('popup'),
|
||||||
|
page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'),
|
||||||
|
]);
|
||||||
|
const popupTime = await popup.evaluate('time');
|
||||||
|
expect(popupTime).toBeGreaterThanOrEqual(2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not trigger simultaneous timers', async ({ page, calls }) => {
|
it('should not run time before popup on pause', async ({ page, server }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
server.setRoute('/popup.html', async (req, res) => {
|
||||||
await page.evaluate(() => {
|
res.setHeader('Content-Type', 'text/html');
|
||||||
setTimeout(() => {
|
res.end(`<script>window.time = Date.now()</script>`);
|
||||||
window.stub();
|
|
||||||
}, 100);
|
|
||||||
setTimeout(() => {
|
|
||||||
window.stub();
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
|
await page.clock.install();
|
||||||
await page.clock.runToNextTimer();
|
await page.clock.pause();
|
||||||
expect(calls).toHaveLength(1);
|
await page.clock.setSystemTime(0);
|
||||||
});
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
// Wait for 2 second in real life to check that it is past in popup.
|
||||||
it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => {
|
await page.waitForTimeout(2000);
|
||||||
await page.clock.installFakeTimers(0);
|
const [popup] = await Promise.all([
|
||||||
await page.evaluate(async () => {
|
page.waitForEvent('popup'),
|
||||||
setTimeout(() => {
|
page.evaluate(url => window.open(url), server.PREFIX + '/popup.html'),
|
||||||
window.stub();
|
]);
|
||||||
}, 100);
|
const popupTime = await popup.evaluate('time');
|
||||||
setTimeout(() => {
|
expect(popupTime).toBe(0);
|
||||||
window.stub();
|
|
||||||
}, 100);
|
|
||||||
setTimeout(() => {
|
|
||||||
window.stub();
|
|
||||||
}, 99);
|
|
||||||
setTimeout(() => {
|
|
||||||
window.stub();
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.clock.runToNextTimer();
|
|
||||||
expect(calls).toHaveLength(1);
|
|
||||||
await page.clock.runToNextTimer();
|
|
||||||
expect(calls).toHaveLength(2);
|
|
||||||
await page.clock.runToNextTimer();
|
|
||||||
expect(calls).toHaveLength(3);
|
|
||||||
await page.clock.runToNextTimer();
|
|
||||||
expect(calls).toHaveLength(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
window.stub(1);
|
|
||||||
setTimeout(() => {
|
|
||||||
setTimeout(() => window.stub(2), 0);
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.clock.runToNextTimer();
|
|
||||||
expect(calls).toEqual([{ params: [1] }]);
|
|
||||||
await page.clock.runToNextTimer();
|
|
||||||
expect(calls).toEqual([{ params: [1] }, { params: [2] }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws exception thrown by timer', async ({ page, calls }) => {
|
|
||||||
await page.clock.installFakeTimers(0);
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
throw new Error();
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(page.clock.runToNextTimer()).rejects.toThrow();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.describe('setTime', () => {
|
it.describe('setFixedTime', () => {
|
||||||
it('does not fake methods', async ({ page }) => {
|
it('does not fake methods', async ({ page }) => {
|
||||||
await page.clock.setTime(0);
|
await page.clock.setFixedTime(0);
|
||||||
|
|
||||||
// Should not stall.
|
// Should not stall.
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
|
@ -688,54 +400,145 @@ it.describe('setTime', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows setting time multiple times', async ({ page, calls }) => {
|
it('allows setting time multiple times', async ({ page }) => {
|
||||||
await page.clock.setTime(100);
|
await page.clock.setFixedTime(100);
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(100);
|
expect(await page.evaluate(() => Date.now())).toBe(100);
|
||||||
await page.clock.setTime(200);
|
await page.clock.setFixedTime(200);
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(200);
|
expect(await page.evaluate(() => Date.now())).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports skipTime w/o fake timers', async ({ page }) => {
|
it('fixed time is not affected by clock manipulation', async ({ page }) => {
|
||||||
await page.clock.setTime(100);
|
await page.clock.setFixedTime(100);
|
||||||
|
expect(await page.evaluate(() => Date.now())).toBe(100);
|
||||||
|
await page.clock.fastForward(20);
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(100);
|
expect(await page.evaluate(() => Date.now())).toBe(100);
|
||||||
await page.clock.skipTime(20);
|
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(120);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows installing fake timers after settings time', async ({ page, calls }) => {
|
it('allows installing fake timers after settings time', async ({ page, calls }) => {
|
||||||
await page.clock.setTime(100);
|
await page.clock.setFixedTime(100);
|
||||||
expect(await page.evaluate(() => Date.now())).toBe(100);
|
expect(await page.evaluate(() => Date.now())).toBe(100);
|
||||||
await page.clock.installFakeTimers(200);
|
await page.clock.setFixedTime(200);
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
setTimeout(() => window.stub(Date.now()));
|
setTimeout(() => window.stub(Date.now()));
|
||||||
});
|
});
|
||||||
await page.clock.runFor(0);
|
await page.clock.runFor(0);
|
||||||
expect(calls).toEqual([{ params: [200] }]);
|
expect(calls).toEqual([{ params: [200] }]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('allows setting time after installing fake timers', async ({ page, calls }) => {
|
it.describe('while running', () => {
|
||||||
await page.clock.installFakeTimers(200);
|
it('should progress time', async ({ page }) => {
|
||||||
await page.evaluate(async () => {
|
await page.clock.install({ time: 0 });
|
||||||
setTimeout(() => window.stub(Date.now()));
|
await page.goto('data:text/html,');
|
||||||
});
|
await page.waitForTimeout(1000);
|
||||||
await page.clock.setTime(220);
|
const now = await page.evaluate(() => Date.now());
|
||||||
expect(calls).toEqual([{ params: [220] }]);
|
expect(now).toBeGreaterThanOrEqual(1000);
|
||||||
|
expect(now).toBeLessThanOrEqual(2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not allow flowing time backwards', async ({ page, calls }) => {
|
it('should runFor', async ({ page }) => {
|
||||||
await page.clock.installFakeTimers(200);
|
await page.clock.install({ time: 0 });
|
||||||
await expect(page.clock.setTime(180)).rejects.toThrow();
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.runFor(10000);
|
||||||
|
const now = await page.evaluate(() => Date.now());
|
||||||
|
expect(now).toBeGreaterThanOrEqual(10000);
|
||||||
|
expect(now).toBeLessThanOrEqual(11000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should turn setTime into jump', async ({ page, calls }) => {
|
it('should fastForward', async ({ page }) => {
|
||||||
await page.clock.installFakeTimers(0);
|
await page.clock.install({ time: 0 });
|
||||||
await page.evaluate(async () => {
|
await page.goto('data:text/html,');
|
||||||
setTimeout(window.stub, 100);
|
await page.clock.fastForward(10000);
|
||||||
setTimeout(window.stub, 200);
|
const now = await page.evaluate(() => Date.now());
|
||||||
});
|
expect(now).toBeGreaterThanOrEqual(10000);
|
||||||
await page.clock.setTime(100);
|
expect(now).toBeLessThanOrEqual(11000);
|
||||||
expect(calls).toHaveLength(1);
|
});
|
||||||
await page.clock.setTime(200);
|
|
||||||
expect(calls).toHaveLength(2);
|
it('should fastForwardTo', async ({ page }) => {
|
||||||
|
await page.clock.install({ time: 0 });
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.fastForwardTo(10000);
|
||||||
|
const now = await page.evaluate(() => Date.now());
|
||||||
|
expect(now).toBeGreaterThanOrEqual(10000);
|
||||||
|
expect(now).toBeLessThanOrEqual(11000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause', async ({ page }) => {
|
||||||
|
await page.clock.install({ time: 0 });
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await page.clock.resume();
|
||||||
|
const now = await page.evaluate(() => Date.now());
|
||||||
|
expect(now).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(now).toBeLessThanOrEqual(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause and fastForwardTo', async ({ page }) => {
|
||||||
|
await page.clock.install({ time: 0 });
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.clock.fastForwardTo(1000);
|
||||||
|
const now = await page.evaluate(() => Date.now());
|
||||||
|
expect(now).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set system time on pause', async ({ page }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.clock.setSystemTime(1000);
|
||||||
|
const now = await page.evaluate(() => Date.now());
|
||||||
|
expect(now).toBe(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.describe('while on pause', () => {
|
||||||
|
it('fastForward should not run nested immediate', async ({ page, calls }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.evaluate(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.stub('outer');
|
||||||
|
setTimeout(() => window.stub('inner'), 0);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
await page.clock.fastForward(1000);
|
||||||
|
expect(calls).toEqual([{ params: ['outer'] }]);
|
||||||
|
await page.clock.fastForward(1);
|
||||||
|
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runFor should not run nested immediate', async ({ page, calls }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.evaluate(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.stub('outer');
|
||||||
|
setTimeout(() => window.stub('inner'), 0);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
await page.clock.runFor(1000);
|
||||||
|
expect(calls).toEqual([{ params: ['outer'] }]);
|
||||||
|
await page.clock.runFor(1);
|
||||||
|
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runFor should not run nested immediate from microtask', async ({ page, calls }) => {
|
||||||
|
await page.clock.install();
|
||||||
|
await page.goto('data:text/html,');
|
||||||
|
await page.clock.pause();
|
||||||
|
await page.evaluate(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.stub('outer');
|
||||||
|
void Promise.resolve().then(() => setTimeout(() => window.stub('inner'), 0));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
await page.clock.runFor(1000);
|
||||||
|
expect(calls).toEqual([{ params: ['outer'] }]);
|
||||||
|
await page.clock.runFor(1);
|
||||||
|
expect(calls).toEqual([{ params: ['outer'] }, { params: ['inner'] }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue