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