diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index c140c471f4..95fa0b69d3 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -6,211 +6,161 @@ Accurately simulating time-dependent behavior is essential for verifying the cor Note that clock is installed for the entire [BrowserContext], so the time in all the pages and iframes is controlled by the same clock. -## async method: Clock.install +## async method: Clock.installFakeTimers * since: v1.45 -Creates a clock and installs it globally. +Install fake implementations for the following time-related functions: -**Usage** +* `setTimeout` +* `clearTimeout` +* `setInterval` +* `clearInterval` +* `requestAnimationFrame` +* `cancelAnimationFrame` +* `requestIdleCallback` +* `cancelIdleCallback` +* `performance` -```js -await page.clock.install(); -await page.clock.install({ now }); -await page.clock.install({ now, toFake: ['Date'] }); -``` +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. -```python async -await page.clock.install() -await page.clock.install(now=now) -await page.clock.install(now=now, toFake=['Date']) -``` - -```python sync -page.clock.install() -page.clock.install(now=now) -page.clock.install(now=now, toFake=['Date']) -``` - -```java -page.clock().install(); -page.clock().install( - new Clock.InstallOptions() - .setNow(now)); -page.clock().install( - new Clock.InstallOptions() - .setNow(now) - .setToFake(new String[]{"Date"})); -``` - -```csharp -await page.Clock.InstallAsync(); -await page.Clock.InstallAsync( - new ClockInstallOptions { Now = now }); -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = now, - ToFake = new[] { "Date" } - }); -``` - -### option: Clock.install.now +### param: Clock.installFakeTimers.time * since: v1.45 -- `now` <[int]|[Date]> +- `time` <[int]|[Date]> -Install fake timers with the specified unix epoch (default: 0). +Install fake timers with the specified base time. -### option: Clock.install.toFake -* since: v1.45 -- `toFake` <[Array]<[FakeMethod]<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">>> - -An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: ['setTimeout'] })` will fake only `setTimeout()`. -By default, all the methods are faked. - -### option: Clock.install.loopLimit +### option: Clock.installFakeTimers.loopLimit * since: v1.45 - `loopLimit` <[int]> -The maximum number of timers that will be run when calling [`method: Clock.runAll`]. Defaults to `1000`. +The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`. -### option: Clock.install.shouldAdvanceTime -* since: v1.45 -- `shouldAdvanceTime` <[boolean]> - -Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the mocked time will be incremented by -20ms for every 20ms change in the real system time). Defaults to `false`. - -### option: Clock.install.advanceTimeDelta -* since: v1.45 -- `advanceTimeDelta` <[int]> - -Relevant only when using with [`option: shouldAdvanceTime`]. Increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change -in the real system time (default: 20). - -## async method: Clock.jump -* since: v1.45 - -Advance the clock by jumping forward in time, firing callbacks at most once. -This can be used to simulate the JS engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. - -**Usage** - -```js -await page.clock.jump(1000); -await page.clock.jump('30:00'); -``` - -```python async -await page.clock.jump(1000); -await page.clock.jump('30:00') -``` - -```python sync -page.clock.jump(1000); -page.clock.jump('30:00') -``` - -```java -page.clock().jump(1000); -page.clock().jump("30:00"); -``` - -```csharp -await page.Clock.JumpAsync(1000); -await page.Clock.JumpAsync("30:00"); -``` - -### param: Clock.jump.time -* since: v1.45 -- `time` <[int]|[string]> - -Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. - -## async method: Clock.next +## async method: Clock.runAllTimers * since: v1.45 - returns: <[int]> -Advances the clock to the the moment of the first scheduled timer, firing it. +Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. +Fake timers must be installed. Returns fake milliseconds since the unix epoch. -**Usage** - -```js -await page.clock.next(); -``` - -```python async -await page.clock.next() -``` - -```python sync -page.clock.next() -``` - -```java -page.clock().next(); -``` - -```csharp -await page.Clock.NextAsync(); -``` - -## async method: Clock.runAll -* since: v1.45 -- returns: <[int]> - -Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well. Returns fake milliseconds since the unix epoch. - **Details** 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. -## async method: Clock.runToLast +## async method: Clock.runFor +* since: v1.45 +- returns: <[int]> + +Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. +Fake timers must be installed. +Returns fake milliseconds since the unix epoch. + +**Usage** + +```js +await page.clock.runFor(1000); +await page.clock.runFor('30:00'); +``` + +```python async +await page.clock.run_for(1000); +await page.clock.run_for('30:00') +``` + +```python sync +page.clock.run_for(1000); +page.clock.run_for('30:00') +``` + +```java +page.clock().runFor(1000); +page.clock().runFor("30:00"); +``` + +```csharp +await page.Clock.RunForAsync(1000); +await page.Clock.RunForAsync("30:00"); +``` + +### param: Clock.runFor.time +* since: v1.45 +- `time` <[int]|[string]> + +Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + + +## async method: Clock.runToLastTimer * since: v1.45 - 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.tick +## async method: Clock.runToNextTimer * since: v1.45 - returns: <[int]> -Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake milliseconds since the unix epoch. +Advances the clock to the the moment of the first scheduled timer, firing it. +Fake timers must be installed. +Returns fake milliseconds since the unix epoch. + + +## async method: Clock.setTime +* since: v1.45 + +Set the clock to the specified time. + +When fake timers are installed, only fires timers at most once. This can be used to simulate the JS engine (such as a browser) +being put to sleep and resumed later, skipping intermediary timers. + +### param: Clock.setTime.time +* since: v1.45 +- `time` <[int]|[Date]> + + +## async method: Clock.skipTime +* since: v1.45 +- returns: <[int]> + +Advance the clock by jumping forward in time, equivalent to running [`method: Clock.setTime`] with the new target time. + +When fake timers are installed, [`method: Clock.skipTime`] only fires due timers at most once, while [`method: Clock.runFor`] fires all the timers up to the current time. +Returns fake milliseconds since the unix epoch. **Usage** ```js -await page.clock.tick(1000); -await page.clock.tick('30:00'); +await page.clock.skipTime(1000); +await page.clock.skipTime('30:00'); ``` ```python async -await page.clock.tick(1000); -await page.clock.tick('30:00') +await page.clock.skipTime(1000); +await page.clock.skipTime('30:00') ``` ```python sync -page.clock.tick(1000); -page.clock.tick('30:00') +page.clock.skipTime(1000); +page.clock.skipTime('30:00') ``` ```java -page.clock().tick(1000); -page.clock().tick("30:00"); +page.clock().skipTime(1000); +page.clock().skipTime("30:00"); ``` ```csharp -await page.Clock.TickAsync(1000); -await page.Clock.TickAsync("30:00"); +await page.Clock.SkipTimeAsync(1000); +await page.Clock.SkipTimeAsync("30:00"); ``` -### param: Clock.tick.time +### param: Clock.skipTime.time * since: v1.45 - `time` <[int]|[string]> diff --git a/docs/src/clock.md b/docs/src/clock.md index 95e8df0e56..4339e4bc60 100644 --- a/docs/src/clock.md +++ b/docs/src/clock.md @@ -21,254 +21,240 @@ Accurately simulating time-dependent behavior is essential for verifying the cor By default, the clock starts at the unix epoch (timestamp of 0). You can override it using the `now` option. ```js -await page.clock.install(); -await page.clock.install({ now: new Date('2020-02-02') }); +await page.clock.setTime(new Date('2020-02-02')); +await page.clock.installFakeTimers(new Date('2020-02-02')); ``` -## Freeze Date.now +## Mock Date.now -Sometimes you only need to fake `Date.now` and no other time-related functions. +Most of the time, you only need to fake `Date.now` and no other time-related functions. That way the time flows naturally, but `Date.now` returns a fixed value. ```html - +
``` ```js -// Initialize clock with a specific time, only fake Date.now. -await page.clock.install({ - now: new Date('2024-01-01T10:00:00Z'), - toFake: ['Date'], -}); +await page.clock.setTime(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); -await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); + +await page.clock.setTime(new Date('2024-02-02T10:30:00')); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); ``` ```python async -# Initialize clock with a specific time, only fake Date.now. -await page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), - toFake=['Date'], -) +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('my-time') -await expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') + +page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) +await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```python sync -# Initialize clock with a specific time, only fake Date.now. -page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), - to_fake=['Date'], -) +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('my-time') -expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +expect(locator).to_have_text('2/2/2024, 10:00:00 AM') + +page.clock.set_time(datetime.datetime(2024, 2, 2, 10, 30, 0, tzinfo=datetime.timezone.pst)) +expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```java -// Initialize clock with a specific time, only fake Date.now. -page.clock().install( - new Clock.InstallOptions() - .setNow(Instant.parse("2024-01-01T10:00:00Z")) - .setToFake(new String[]{"Date"}) -); +page.clock().setTime(Instant.parse("2024-02-02T10:00:00")); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("my-time"); -assertThat(locator).hasValue("2024-01-01T10:00"); +Locator locator = page.getByTestId("current-time"); +assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); + +page.clock().setTime(Instant.parse("2024-02-02T10:30:00")); +assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); ``` ```csharp // Initialize clock with a specific time, only fake Date.now. -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), - ToFake = new[] { "Date" } - }); +await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst)); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("my-time"); -await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); +var locator = page.GetByTestId("current-time"); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM"); + +await page.Clock.SetTimeAsync(new DateTime(2024, 2, 2, 10, 30, 0, DateTimeKind.Pst)); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); ``` -## Assert page at different points in time +## Mock Date.now consistent with the timers -More often you need to simulate the passage of time to test time-dependent behavior. -You can jump the clock forward in time to simulate the passage of time without waiting for real-time to pass. +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 - +
``` ```js // Initialize clock with a specific time, take full control over time. -await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); +await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); -await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:00'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:00:00 AM'); // Fast forward time 30 minutes without firing intermediate timers, as if the user // closed and opened the lid of the laptop. -await page.clock.jump('30:00'); -await expect(page.getByTestId('my-time')).toHaveValue('2024-01-01T10:30'); +await page.clock.skipTime('30:00'); +await expect(page.getByTestId('current-time')).toHaveText('2/2/2024, 10:30:00 AM'); ``` ```python async # Initialize clock with a specific time, take full control over time. -await page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +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('my-time') -await expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +await expect(locator).to_have_text('2/2/2024, 10:00:00 AM') # Fast forward time 30 minutes without firing intermediate timers, as if the user # closed and opened the lid of the laptop. -await page.clock.jump('30:00') -await expect(locator).to_have_value('2024-01-01T10:30') +await page.clock.skip_time('30:00') +await expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```python sync # Initialize clock with a specific time, take full control over time. -page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +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('my-time') -expect(locator).to_have_value('2024-01-01T10:00') +locator = page.get_by_test_id('current-time') +expect(locator).to_have_text('2/2/2024, 10:00:00 AM') # Fast forward time 30 minutes without firing intermediate timers, as if the user # closed and opened the lid of the laptop. -page.clock.jump('30:00') -expect(locator).to_have_value('2024-01-01T10:30') +page.clock.skip_time('30:00') +expect(locator).to_have_text('2/2/2024, 10:30:00 AM') ``` ```java // Initialize clock with a specific time, take full control over time. -page.clock().install( - new Clock.InstallOptions() - .setNow(Instant.parse("2024-01-01T10:00:00Z")) -); +page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("my-time"); -assertThat(locator).hasValue("2024-01-01T10:00"); +Locator locator = page.getByTestId("current-time"); +assertThat(locator).hasText("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. -page.clock().jump("30:00"); -assertThat(locator).hasValue("2024-01-01T10:30"); +page.clock().skipTime("30:00"); +assertThat(locator).hasText("2/2/2024, 10:30:00 AM"); ``` ```csharp // Initialize clock with a specific time, take full control over time. -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), - }); +await page.Clock.InstallFakeTimersAsync( + new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) +); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("my-time"); -await Expect(locator).ToHaveValueAsync("2024-01-01T10:00"); +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.JumpAsync("30:00"); -await Expect(locator).ToHaveValueAsync("2024-01-01T10:30"); +await page.Clock.SkipTimeAsync("30:00"); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:30:00 AM"); ``` ## Tick through time manually -In some cases, you may want to tick through time manually, firing all timers in the process. -This can be useful when you want to simulate the passage of time in a controlled manner. +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 - +
``` ```js // Initialize clock with a specific time, take full control over time. -await page.clock.install({ now: new Date('2024-01-01T10:00:00Z') }); +await page.clock.installFakeTimers(new Date('2024-02-02T10:00:00')); await page.goto('http://localhost:3333'); // 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.tick(2000); +await page.clock.runFor(2000); +await expect(locator).to_have_text('2/2/2024, 10:00:02 AM') ``` ```python async # Initialize clock with a specific time, take full control over time. -await page.clock.install( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +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('my-time') +locator = page.get_by_test_id('current-time') # 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.tick(2000) +await page.clock.run_for(2000) +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( - now=datetime.datetime(2024, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc), +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('my-time') +locator = page.get_by_test_id('current-time') # Tick through time manually, firing all timers in the process. # In this case, time will be updated in the screen 2 times. -page.clock.tick(2000) +page.clock.run_for(2000) +expect(locator).to_have_text('2/2/2024, 10:00:02 AM') ``` ```java // Initialize clock with a specific time, take full control over time. -page.clock().install( - new Clock.InstallOptions() - .setNow(Instant.parse("2024-01-01T10:00:00Z")) -); +page.clock().installFakeTimers(Instant.parse("2024-02-02T10:00:00")); page.navigate("http://localhost:3333"); -Locator locator = page.getByTestId("my-time"); +Locator locator = page.getByTestId("current-time"); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -page.clock().tick(2000); +page.clock().runFor(2000); +assertThat(locator).hasText("2/2/2024, 10:00:02 AM"); ``` ```csharp // Initialize clock with a specific time, take full control over time. -await page.Clock.InstallAsync( - new ClockInstallOptions - { - Now = new DateTime(2024, 1, 1, 10, 0, 0, DateTimeKind.Utc), - }); +await page.Clock.InstallFakeTimersAsync( + new DateTime(2024, 2, 2, 10, 0, 0, DateTimeKind.Pst) +); await page.GotoAsync("http://localhost:3333"); -var locator = page.GetByTestId("my-time"); +var locator = page.GetByTestId("current-time"); // Tick through time manually, firing all timers in the process. // In this case, time will be updated in the screen 2 times. -await page.Clock.TickAsync(2000); +await page.Clock.RunForAsync(2000); +await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM"); ``` diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 802939295c..ed903b674e 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -86,7 +86,7 @@ export class Browser extends ChannelOwner implements ap 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.install(); }, true); + await this._wrapApiCall(async () => { await context.clock.installFakeTimers(new Date(0)); }, true); return context; } diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 3287f41dbb..792e36a135 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -15,7 +15,6 @@ */ import type * as api from '../../types/types'; -import type * as channels from '@protocol/channels'; import type { BrowserContext } from './browserContext'; export class Clock implements api.Clock { @@ -25,35 +24,41 @@ export class Clock implements api.Clock { this._browserContext = browserContext; } - async install(options?: Omit & { now?: number | Date }) { - const now = options && options.now ? (options.now instanceof Date ? options.now.getTime() : options.now) : undefined; - await this._browserContext._channel.clockInstall({ ...options, now }); + 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 jump(time: number | string) { - await this._browserContext._channel.clockJump({ + async runAllTimers(): Promise { + const result = await this._browserContext._channel.clockRunAllTimers(); + return result.fakeTime; + } + + async runFor(time: number | string): Promise { + const result = await this._browserContext._channel.clockRunFor({ timeNumber: typeof time === 'number' ? time : undefined, timeString: typeof time === 'string' ? time : undefined }); - } - - async next(): Promise { - const result = await this._browserContext._channel.clockNext(); return result.fakeTime; } - async runAll(): Promise { - const result = await this._browserContext._channel.clockRunAll(); + async runToLastTimer(): Promise { + const result = await this._browserContext._channel.clockRunToLastTimer(); return result.fakeTime; } - async runToLast(): Promise { - const result = await this._browserContext._channel.clockRunToLast(); + async runToNextTimer(): Promise { + const result = await this._browserContext._channel.clockRunToNextTimer(); return result.fakeTime; } - async tick(time: number | string): Promise { - const result = await this._browserContext._channel.clockTick({ + async setTime(time: number | Date) { + const timeMs = time instanceof Date ? time.getTime() : time; + await this._browserContext._channel.clockSetTime({ time: timeMs }); + } + + async skipTime(time: number | string) { + const result = await this._browserContext._channel.clockSkipTime({ timeNumber: typeof time === 'number' ? time : undefined, timeString: typeof time === 'string' ? time : undefined }); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index ec1d4bffe2..0a60a92ebb 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -963,36 +963,39 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({ enabled: tBoolean, }); scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({})); -scheme.BrowserContextClockInstallParams = tObject({ - now: tOptional(tNumber), - toFake: tOptional(tArray(tString)), +scheme.BrowserContextClockInstallFakeTimersParams = tObject({ + time: tNumber, loopLimit: tOptional(tNumber), - shouldAdvanceTime: tOptional(tBoolean), - advanceTimeDelta: tOptional(tNumber), }); -scheme.BrowserContextClockInstallResult = tOptional(tObject({})); -scheme.BrowserContextClockJumpParams = tObject({ +scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({})); +scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({})); +scheme.BrowserContextClockRunAllTimersResult = tObject({ + fakeTime: tNumber, +}); +scheme.BrowserContextClockRunForParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockJumpResult = tOptional(tObject({})); -scheme.BrowserContextClockNextParams = tOptional(tObject({})); -scheme.BrowserContextClockNextResult = tObject({ +scheme.BrowserContextClockRunForResult = tObject({ fakeTime: tNumber, }); -scheme.BrowserContextClockRunAllParams = tOptional(tObject({})); -scheme.BrowserContextClockRunAllResult = tObject({ +scheme.BrowserContextClockRunToLastTimerParams = tOptional(tObject({})); +scheme.BrowserContextClockRunToLastTimerResult = tObject({ fakeTime: tNumber, }); -scheme.BrowserContextClockRunToLastParams = tOptional(tObject({})); -scheme.BrowserContextClockRunToLastResult = tObject({ +scheme.BrowserContextClockRunToNextTimerParams = tOptional(tObject({})); +scheme.BrowserContextClockRunToNextTimerResult = tObject({ fakeTime: tNumber, }); -scheme.BrowserContextClockTickParams = tObject({ +scheme.BrowserContextClockSetTimeParams = tObject({ + time: tNumber, +}); +scheme.BrowserContextClockSetTimeResult = tOptional(tObject({})); +scheme.BrowserContextClockSkipTimeParams = tObject({ timeNumber: tOptional(tNumber), timeString: tOptional(tString), }); -scheme.BrowserContextClockTickResult = tObject({ +scheme.BrowserContextClockSkipTimeResult = tObject({ fakeTime: tNumber, }); scheme.PageInitializer = tObject({ diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index fee151b407..f4d96965f4 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -14,59 +14,94 @@ * limitations under the License. */ -import type * as channels from '@protocol/channels'; import type { BrowserContext } from './browserContext'; import * as fakeTimersSource from '../generated/fakeTimersSource'; export class Clock { private _browserContext: BrowserContext; - private _installed = false; + private _scriptInjected = false; + private _fakeTimersInstalled = false; + private _now = 0; constructor(browserContext: BrowserContext) { this._browserContext = browserContext; } - async install(params: channels.BrowserContextClockInstallOptions) { - if (this._installed) - throw new Error('Cannot install more than one clock per context'); - this._installed = true; + async installFakeTimers(time: number, loopLimit: number | undefined) { + await this._injectScriptIfNeeded(); + await this._addAndEvaluate(`(() => { + globalThis.__pwFakeTimers.clock?.uninstall(); + globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, loopLimit })}); + })();`); + this._now = time; + this._fakeTimersInstalled = true; + } + + async runToNextTimer(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`); + return this._now; + } + + async runAllTimers(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runAll()`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runAllAsync()`); + return this._now; + } + + async runToLastTimer(): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runToLast()`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runToLastAsync()`); + return this._now; + } + + async setTime(time: number) { + if (this._fakeTimersInstalled) { + const jump = time - this._now; + if (jump < 0) + throw new Error('Unable to set time into the past when fake timers are installed'); + await this._addAndEvaluate(`globalThis.__pwFakeTimers.clock.jump(${jump})`); + this._now = time; + return this._now; + } + + await this._injectScriptIfNeeded(); + await this._addAndEvaluate(`(() => { + globalThis.__pwFakeTimers.clock?.uninstall(); + globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); + })();`); + this._now = time; + return this._now; + } + + async skipTime(time: number | string) { + const delta = parseTime(time); + await this.setTime(this._now + delta); + return this._now; + } + + async runFor(time: number | string): Promise { + this._assertInstalled(); + await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.tick(${JSON.stringify(time)})`); + this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.tickAsync(${JSON.stringify(time)})`); + return this._now; + } + + private async _injectScriptIfNeeded() { + if (this._scriptInjected) + return; + this._scriptInjected = true; const script = `(() => { const module = {}; ${fakeTimersSource.source} - globalThis.__pwFakeTimers = (module.exports.install())(${JSON.stringify(params)}); + globalThis.__pwFakeTimers = (module.exports.inject())(); })();`; await this._addAndEvaluate(script); } - async jump(time: number | string) { - this._assertInstalled(); - await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)})`); - } - - async next(): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.next()`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.nextAsync()`); - } - - async runAll(): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runAll()`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runAllAsync()`); - } - - async runToLast(): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runToLast()`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runToLastAsync()`); - } - - async tick(time: number | string): Promise { - this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.tick(${JSON.stringify(time)})`); - return await this._evaluateInFrames(`globalThis.__pwFakeTimers.tickAsync(${JSON.stringify(time)})`); - } - private async _addAndEvaluate(script: string) { await this._browserContext.addInitScript(script); return await this._evaluateInFrames(script); @@ -79,7 +114,32 @@ export class Clock { } private _assertInstalled() { - if (!this._installed) + if (!this._fakeTimersInstalled) throw new Error('Clock is not installed'); } } + +// Taken from sinonjs/fake-timerss-src. +function parseTime(time: string | number): number { + if (typeof time === 'number') + return time; + if (!time) + return 0; + + const strings = time.split(':'); + const l = strings.length; + let i = l; + let ms = 0; + let parsed; + + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time)) + throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`); + + while (i--) { + parsed = parseInt(strings[i], 10); + if (parsed >= 60) + throw new Error(`Invalid time ${time}`); + ms += parsed * Math.pow(60, l - i - 1); + } + return ms * 1000; +} diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 419b99f205..dd1f61f57b 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -312,28 +312,32 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.install(params); + async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.installFakeTimers(params.time, params.loopLimit); } - async clockJump(params: channels.BrowserContextClockJumpParams, metadata?: CallMetadata | undefined): Promise { - await this._context.clock.jump(params.timeString || params.timeNumber || 0); + async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runAllTimers() }; } - async clockNext(params: channels.BrowserContextClockNextParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.next() }; + async clockRunToLastTimer(params: channels.BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runToLastTimer() }; } - async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runAll() }; + async clockRunToNextTimer(params: channels.BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runToNextTimer() }; } - async clockRunToLast(params: channels.BrowserContextClockRunToLastParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.runToLast() }; + async clockSetTime(params: channels.BrowserContextClockSetTimeParams, metadata?: CallMetadata | undefined): Promise { + await this._context.clock.setTime(params.time); } - async clockTick(params: channels.BrowserContextClockTickParams, metadata?: CallMetadata | undefined): Promise { - return { fakeTime: await this._context.clock.tick(params.timeString || params.timeNumber || 0) }; + async clockSkipTime(params: channels.BrowserContextClockSkipTimeParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.skipTime(params.timeString || params.timeNumber || 0) }; + } + + async clockRunFor(params: channels.BrowserContextClockRunForParams, metadata?: CallMetadata | undefined): Promise { + return { fakeTime: await this._context.clock.runFor(params.timeString || params.timeNumber || 0) }; } async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise { diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/fakeTimers.ts index e3d8e5a54d..44974ab8e1 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/fakeTimers.ts @@ -16,9 +16,8 @@ // @ts-ignore import SinonFakeTimers from '../../third_party/fake-timers-src'; -import type * as channels from '@protocol/channels'; -export function install(params: channels.BrowserContextClockInstallOptions) { +export function inject() { // eslint-disable-next-line no-restricted-globals const window = globalThis; const builtin = { @@ -34,7 +33,7 @@ export function install(params: channels.BrowserContextClockInstallOptions) { Intl: window.Intl, Date: window.Date, }; - const result = SinonFakeTimers.install(params); + const result = SinonFakeTimers; result.builtin = builtin; return result; } diff --git a/packages/playwright-core/src/third_party/fake-timers-src.js b/packages/playwright-core/src/third_party/fake-timers-src.js index d4bfdd7305..9602123052 100644 --- a/packages/playwright-core/src/third_party/fake-timers-src.js +++ b/packages/playwright-core/src/third_party/fake-timers-src.js @@ -1654,6 +1654,7 @@ function withGlobal(_global) { * @returns {Clock} */ function install(config) { + console.log('INSTALL', config); if ( arguments.length > 1 || config instanceof Date || diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 11305a90d5..92a3b130df 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -17247,81 +17247,35 @@ export interface BrowserServer { */ export interface Clock { /** - * Creates a clock and installs it globally. - * - * **Usage** - * - * ```js - * await page.clock.install(); - * await page.clock.install({ now }); - * await page.clock.install({ now, toFake: ['Date'] }); - * ``` + * Install fake implementations for the following time-related functions: + * - `setTimeout` + * - `clearTimeout` + * - `setInterval` + * - `clearInterval` + * - `requestAnimationFrame` + * - `cancelAnimationFrame` + * - `requestIdleCallback` + * - `cancelIdleCallback` + * - `performance` * + * Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, + * and control the behavior of time-dependent functions. See + * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) and + * [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) for more information. + * @param time Install fake timers with the specified base time. * @param options */ - install(options?: { + installFakeTimers(time: number|Date, options?: { /** - * Relevant only when using with `shouldAdvanceTime`. Increment mocked time by advanceTimeDelta ms every - * advanceTimeDelta ms change in the real system time (default: 20). - */ - advanceTimeDelta?: number; - - /** - * The maximum number of timers that will be run when calling - * [clock.runAll()](https://playwright.dev/docs/api/class-clock#clock-run-all). Defaults to `1000`. + * 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`. */ loopLimit?: number; - - /** - * Install fake timers with the specified unix epoch (default: 0). - */ - now?: number|Date; - - /** - * Tells `@sinonjs/fake-timers` to increment mocked time automatically based on the real system time shift (e.g., the - * mocked time will be incremented by 20ms for every 20ms change in the real system time). Defaults to `false`. - */ - shouldAdvanceTime?: boolean; - - /** - * An array with names of global methods and APIs to fake. For instance, `await page.clock.install({ toFake: - * ['setTimeout'] })` will fake only `setTimeout()`. By default, all the methods are faked. - */ - toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">; }): Promise; - /** - * Advance the clock by jumping forward in time, firing callbacks at most once. This can be used to simulate the JS - * engine (such as a browser) being put to sleep and resumed later, skipping intermediary timers. - * - * **Usage** - * - * ```js - * await page.clock.jump(1000); - * await page.clock.jump('30:00'); - * ``` - * - * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are - * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. - */ - jump(time: number|string): Promise; - - /** - * Advances the clock to the the moment of the first scheduled timer, firing it. Returns fake milliseconds since the - * unix epoch. - * - * **Usage** - * - * ```js - * await page.clock.next(); - * ``` - * - */ - next(): Promise; - /** * Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be - * run as well. Returns fake milliseconds since the unix epoch. + * run as well. Fake timers must be installed. Returns fake milliseconds since the unix epoch. * * **Details** * @@ -17329,31 +17283,68 @@ export interface Clock { * or the delays in those timers. It runs a maximum of `loopLimit` times after which it assumes there is an infinite * loop of timers and throws an error. */ - runAll(): Promise; + runAllTimers(): Promise; /** - * 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. Returns fake milliseconds since the unix epoch. - */ - runToLast(): Promise; - - /** - * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Returns fake - * milliseconds since the unix epoch. + * Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch. Fake timers must + * be installed. Returns fake milliseconds since the unix epoch. * * **Usage** * * ```js - * await page.clock.tick(1000); - * await page.clock.tick('30:00'); + * await page.clock.runFor(1000); + * 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 * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. */ - tick(time: number|string): Promise; + runFor(time: number|string): Promise; + + /** + * 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; + + /** + * Advances the clock to the the moment of the first scheduled timer, firing it. Fake timers must be installed. + * Returns fake milliseconds since the unix epoch. + */ + runToNextTimer(): Promise; + + /** + * 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; + + /** + * Advance the clock by jumping forward in time, equivalent to running + * [clock.setTime(time)](https://playwright.dev/docs/api/class-clock#clock-set-time) with the new target time. + * + * When fake timers are installed, [clock.skipTime(time)](https://playwright.dev/docs/api/class-clock#clock-skip-time) + * only fires due timers at most once, while + * [clock.runFor(time)](https://playwright.dev/docs/api/class-clock#clock-run-for) fires all the timers up to the + * current time. Returns fake milliseconds since the unix epoch. + * + * **Usage** + * + * ```js + * await page.clock.skipTime(1000); + * await page.clock.skipTime('30:00'); + * ``` + * + * @param time Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are + * "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds. + */ + skipTime(time: number|string): Promise; } /** diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d5dd5c3e33..c8523e8de5 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1460,12 +1460,13 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise; createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise; updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise; - clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise; - clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise; - clockNext(params?: BrowserContextClockNextParams, metadata?: CallMetadata): Promise; - clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise; - clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise; - clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise; + clockInstallFakeTimers(params: BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata): Promise; + clockRunAllTimers(params?: BrowserContextClockRunAllTimersParams, metadata?: CallMetadata): Promise; + clockRunFor(params: BrowserContextClockRunForParams, metadata?: CallMetadata): Promise; + clockRunToLastTimer(params?: BrowserContextClockRunToLastTimerParams, metadata?: CallMetadata): Promise; + clockRunToNextTimer(params?: BrowserContextClockRunToNextTimerParams, metadata?: CallMetadata): Promise; + clockSetTime(params: BrowserContextClockSetTimeParams, metadata?: CallMetadata): Promise; + clockSkipTime(params: BrowserContextClockSkipTimeParams, metadata?: CallMetadata): Promise; } export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, @@ -1754,54 +1755,56 @@ export type BrowserContextUpdateSubscriptionOptions = { }; export type BrowserContextUpdateSubscriptionResult = void; -export type BrowserContextClockInstallParams = { - now?: number, - toFake?: string[], +export type BrowserContextClockInstallFakeTimersParams = { + time: number, loopLimit?: number, - shouldAdvanceTime?: boolean, - advanceTimeDelta?: number, }; -export type BrowserContextClockInstallOptions = { - now?: number, - toFake?: string[], +export type BrowserContextClockInstallFakeTimersOptions = { loopLimit?: number, - shouldAdvanceTime?: boolean, - advanceTimeDelta?: number, }; -export type BrowserContextClockInstallResult = void; -export type BrowserContextClockJumpParams = { - timeNumber?: number, - timeString?: string, -}; -export type BrowserContextClockJumpOptions = { - timeNumber?: number, - timeString?: string, -}; -export type BrowserContextClockJumpResult = void; -export type BrowserContextClockNextParams = {}; -export type BrowserContextClockNextOptions = {}; -export type BrowserContextClockNextResult = { +export type BrowserContextClockInstallFakeTimersResult = void; +export type BrowserContextClockRunAllTimersParams = {}; +export type BrowserContextClockRunAllTimersOptions = {}; +export type BrowserContextClockRunAllTimersResult = { fakeTime: number, }; -export type BrowserContextClockRunAllParams = {}; -export type BrowserContextClockRunAllOptions = {}; -export type BrowserContextClockRunAllResult = { - fakeTime: number, -}; -export type BrowserContextClockRunToLastParams = {}; -export type BrowserContextClockRunToLastOptions = {}; -export type BrowserContextClockRunToLastResult = { - fakeTime: number, -}; -export type BrowserContextClockTickParams = { +export type BrowserContextClockRunForParams = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockTickOptions = { +export type BrowserContextClockRunForOptions = { timeNumber?: number, timeString?: string, }; -export type BrowserContextClockTickResult = { +export type BrowserContextClockRunForResult = { + fakeTime: number, +}; +export type BrowserContextClockRunToLastTimerParams = {}; +export type BrowserContextClockRunToLastTimerOptions = {}; +export type BrowserContextClockRunToLastTimerResult = { + fakeTime: number, +}; +export type BrowserContextClockRunToNextTimerParams = {}; +export type BrowserContextClockRunToNextTimerOptions = {}; +export type BrowserContextClockRunToNextTimerResult = { + fakeTime: number, +}; +export type BrowserContextClockSetTimeParams = { + time: number, +}; +export type BrowserContextClockSetTimeOptions = { + +}; +export type BrowserContextClockSetTimeResult = void; +export type BrowserContextClockSkipTimeParams = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockSkipTimeOptions = { + timeNumber?: number, + timeString?: string, +}; +export type BrowserContextClockSkipTimeResult = { fakeTime: number, }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 8056debdcd..7f6bd3bfe1 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1204,34 +1204,35 @@ BrowserContext: - requestFailed enabled: boolean - clockInstall: + clockInstallFakeTimers: parameters: - now: number? - toFake: - type: array? - items: string + time: number loopLimit: number? - shouldAdvanceTime: boolean? - advanceTimeDelta: number? - clockJump: + clockRunAllTimers: + returns: + fakeTime: number + + clockRunFor: parameters: timeNumber: number? timeString: string? - - clockNext: returns: fakeTime: number - clockRunAll: + clockRunToLastTimer: returns: fakeTime: number - clockRunToLast: + clockRunToNextTimer: returns: fakeTime: number - clockTick: + clockSetTime: + parameters: + time: number + + clockSkipTime: parameters: timeNumber: number? timeString: string? diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 7e001e6641..ccc9e8773c 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -34,87 +34,87 @@ const it = test.extend<{ calls: { params: any[] }[] }>({ } }); -it.describe('tick', () => { +it.describe('runFor', () => { it('triggers immediately without specified delay', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub); }); - await page.clock.tick(0); + await page.clock.runFor(0); expect(calls).toHaveLength(1); }); it('does not trigger without sufficient delay', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); - await page.clock.tick(10); + await page.clock.runFor(10); expect(calls).toEqual([]); }); it('triggers after sufficient delay', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toHaveLength(1); }); it('triggers simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toHaveLength(2); }); it('triggers multiple simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + 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.tick(100); + await page.clock.runFor(100); expect(calls.length).toBe(4); }); it('waits after setTimeout was called', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 150); }); - await page.clock.tick(50); + await page.clock.runFor(50); expect(calls).toEqual([]); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toHaveLength(1); }); it('triggers event when some throw', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { throw new Error(); }, 100); setTimeout(window.stub, 120); }); - await expect(page.clock.tick(120)).rejects.toThrow(); + await expect(page.clock.runFor(120)).rejects.toThrow(); expect(calls).toHaveLength(1); }); it('creates updated Date while ticking', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(() => { window.stub(new Date().getTime()); }, 10); }); - await page.clock.tick(100); + await page.clock.runFor(100); expect(calls).toEqual([ { params: [10] }, { params: [20] }, @@ -130,117 +130,117 @@ it.describe('tick', () => { }); it('passes 8 seconds', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 4000); }); - await page.clock.tick('08'); + await page.clock.runFor('08'); expect(calls.length).toBe(2); }); it('passes 1 minute', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 6000); }); - await page.clock.tick('01:00'); + await page.clock.runFor('01:00'); expect(calls.length).toBe(10); }); it('passes 2 hours, 34 minutes and 10 seconds', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); - await page.clock.tick('02:34:10'); + await page.clock.runFor('02:34:10'); expect(calls.length).toBe(925); }); it('throws for invalid format', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 10000); }); - await expect(page.clock.tick('12:02:34:10')).rejects.toThrow(); + await expect(page.clock.runFor('12:02:34:10')).rejects.toThrow(); expect(calls).toEqual([]); }); it('returns the current now value', async ({ page }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); const value = 200; - await page.clock.tick(value); + await page.clock.runFor(value); expect(await page.evaluate(() => Date.now())).toBe(value); }); }); -it.describe('jump', () => { +it.describe('skipTime', () => { it(`ignores timers which wouldn't be run`, async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub('should not be logged'); }, 1000); }); - await page.clock.jump(500); + await page.clock.skipTime(500); expect(calls).toEqual([]); }); it('pushes back execution time for skipped timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 1000); }); - await page.clock.jump(2000); + await page.clock.skipTime(2000); expect(calls).toEqual([{ params: [2000] }]); }); it('supports string time arguments', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(Date.now()); }, 100000); // 100000 = 1:40 }); - await page.clock.jump('01:50'); + await page.clock.skipTime('01:50'); expect(calls).toEqual([{ params: [110000] }]); }); }); -it.describe('runAll', () => { +it.describe('runAllTimers', () => { it('if there are no timers just return', async ({ page }) => { - await page.clock.install(); - await page.clock.runAll(); + await page.clock.installFakeTimers(0); + await page.clock.runAllTimers(); }); it('runs all timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls.length).toBe(2); }); it('new timers added while running are also run', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { setTimeout(window.stub, 50); }, 10); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls.length).toBe(1); }); it('new timers added in promises while running are also run', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => { @@ -248,12 +248,12 @@ it.describe('runAll', () => { }); }, 10); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls.length).toBe(1); }); it('throws before allowing infinite recursion', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { window.stub(); @@ -261,12 +261,12 @@ it.describe('runAll', () => { }; setTimeout(recursiveCallback, 10); }); - await expect(page.clock.runAll()).rejects.toThrow(); + 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.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { window.stub(); @@ -276,33 +276,33 @@ it.describe('runAll', () => { }; setTimeout(recursiveCallback, 10); }); - await expect(page.clock.runAll()).rejects.toThrow(); + 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.install({ loopLimit: 1 }); + await page.clock.installFakeTimers(0, { loopLimit: 1 }); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - await expect(page.clock.runAll()).rejects.toThrow(); + await expect(page.clock.runAllTimers()).rejects.toThrow(); expect(calls).toHaveLength(1); }); it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => window.stub()); }, 55); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls).toHaveLength(1); }); it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => { @@ -312,17 +312,17 @@ it.describe('runAll', () => { }); }, 55); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls).toHaveLength(1); }); it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { void Promise.resolve().then(() => window.stub(1)); setTimeout(() => window.stub(2), 55); }); - await page.clock.runAll(); + await page.clock.runAllTimers(); expect(calls).toEqual([ { params: [1] }, { params: [2] }, @@ -330,69 +330,69 @@ it.describe('runAll', () => { }); }); -it.describe('runToLast', () => { +it.describe('runToLastTimer', () => { it('returns current time when there are no timers', async ({ page }) => { - await page.clock.install(); - const time = await page.clock.runToLast(); + 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.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(2); }); it('returns time of the last timer', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 50); }); - const time = await page.clock.runToLast(); + 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.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 10); setTimeout(window.stub, 10); }); - await page.clock.runToLast(); + 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.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(); setTimeout(window.stub, 50); }, 10); }); - await page.clock.runToLast(); + 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.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); setTimeout(() => { setTimeout(window.stub, 50); }, 10); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(2); }); it('new timers cannot cause an infinite loop', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { window.stub(); @@ -401,24 +401,24 @@ it.describe('runToLast', () => { setTimeout(recursiveCallback, 0); setTimeout(window.stub, 100); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(102); }); it('should support clocks with start time', async ({ page, calls }) => { - await page.clock.install({ now: 200 }); + await page.clock.installFakeTimers(200); await page.evaluate(async () => { setTimeout(function cb() { window.stub(); setTimeout(cb, 50); }, 50); }); - await page.clock.runToLast(); + 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.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const recursiveCallback = () => { void Promise.resolve().then(() => { @@ -428,23 +428,23 @@ it.describe('runToLast', () => { setTimeout(recursiveCallback, 0); setTimeout(window.stub, 100); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('should settle user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => window.stub()); }, 55); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('should settle nested user-created promises', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => { @@ -454,17 +454,17 @@ it.describe('runToLast', () => { }); }, 55); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls.length).toBe(1); }); it('should settle local promises before firing timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { void Promise.resolve().then(() => window.stub(1)); setTimeout(() => window.stub(2), 55); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls).toEqual([ { params: [1] }, { params: [2] }, @@ -472,14 +472,14 @@ it.describe('runToLast', () => { }); it('should settle user-created promises before firing more timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { void Promise.resolve().then(() => window.stub(1)); }, 55); setTimeout(() => window.stub(2), 75); }); - await page.clock.runToLast(); + await page.clock.runToLastTimer(); expect(calls).toEqual([ { params: [1] }, { params: [2] }, @@ -489,152 +489,92 @@ it.describe('runToLast', () => { it.describe('stubTimers', () => { it('sets initial timestamp', async ({ page, calls }) => { - await page.clock.install({ now: 1400 }); + await page.clock.installFakeTimers(1400); expect(await page.evaluate(() => Date.now())).toBe(1400); }); it('replaces global setTimeout', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 1000); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls.length).toBe(1); }); it('global fake setTimeout should return id', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); const to = await page.evaluate(() => setTimeout(window.stub, 1000)); expect(typeof to).toBe('number'); }); it('replaces global clearTimeout', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setTimeout(window.stub, 1000); clearTimeout(to); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls).toEqual([]); }); it('replaces global setInterval', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setInterval(window.stub, 500); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls.length).toBe(2); }); it('replaces global clearInterval', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { const to = setInterval(window.stub, 500); clearInterval(to); }); - await page.clock.tick(1000); + await page.clock.runFor(1000); expect(calls).toEqual([]); }); it('replaces global performance.now', async ({ page }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); 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.tick(1000); + await page.clock.runFor(1000); expect(await promise).toEqual({ prev: 0, next: 1000 }); }); it('fakes Date constructor', async ({ page }) => { - await page.clock.install({ now: 0 }); + await page.clock.installFakeTimers(0); const now = await page.evaluate(() => new Date().getTime()); expect(now).toBe(0); }); - - it('does not fake methods not provided', async ({ page }) => { - await page.clock.install({ - now: 0, - toFake: ['Date'], - }); - - // Should not stall. - await page.evaluate(() => { - return new Promise(f => setTimeout(f, 1)); - }); - }); -}); - -it.describe('shouldAdvanceTime', () => { - it('should create an auto advancing timer', async ({ page, calls }) => { - const testDelay = 29; - const now = new Date('2015-09-25'); - await page.clock.install({ now, shouldAdvanceTime: true }); - const pageNow = await page.evaluate(() => Date.now()); - expect(pageNow).toBe(1443139200000); - - await page.evaluate(async testDelay => { - return new Promise(f => { - const timeoutStarted = Date.now(); - setTimeout(() => { - window.stub(Date.now() - timeoutStarted); - f(); - }, testDelay); - }); - }, testDelay); - - expect(calls).toEqual([ - { params: [testDelay] } - ]); - }); - - it('should test setInterval', async ({ page, calls }) => { - const now = new Date('2015-09-25'); - await page.clock.install({ now, shouldAdvanceTime: true }); - - const timeDifference = await page.evaluate(async () => { - return new Promise(f => { - const interval = 20; - const cyclesToTrigger = 3; - const timeoutStarted = Date.now(); - let intervalsTriggered = 0; - const intervalId = setInterval(() => { - if (++intervalsTriggered === cyclesToTrigger) { - clearInterval(intervalId); - const timeDifference = Date.now() - timeoutStarted; - f(timeDifference - interval * cyclesToTrigger); - } - }, interval); - }); - }); - - expect(timeDifference).toBe(0); - }); }); it.describe('popup', () => { it('should tick after popup', async ({ page }) => { const now = new Date('2015-09-25'); - await page.clock.install({ now }); + await page.clock.installFakeTimers(now); const [popup] = await Promise.all([ page.waitForEvent('popup'), page.evaluate(() => window.open('about:blank')), ]); const popupTime = await popup.evaluate(() => Date.now()); expect(popupTime).toBe(now.getTime()); - await page.clock.tick(1000); + await page.clock.runFor(1000); const popupTimeAfter = await popup.evaluate(() => Date.now()); expect(popupTimeAfter).toBe(now.getTime() + 1000); }); it('should tick before popup', async ({ page, browserName }) => { - it.skip(browserName === 'chromium'); const now = new Date('2015-09-25'); - await page.clock.install({ now }); - const newNow = await page.clock.tick(1000); + await page.clock.installFakeTimers(now); + const newNow = await page.clock.runFor(1000); expect(newNow).toBe(now.getTime() + 1000); const [popup] = await Promise.all([ @@ -646,18 +586,18 @@ it.describe('popup', () => { }); }); -it.describe('next', () => { +it.describe('runToNextTimer', () => { it('triggers the next timer', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(window.stub, 100); }); - expect(await page.clock.next()).toBe(100); + expect(await page.clock.runToNextTimer()).toBe(100); expect(calls).toHaveLength(1); }); it('does not trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(() => { setTimeout(() => { window.stub(); @@ -667,12 +607,12 @@ it.describe('next', () => { }, 100); }); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(1); }); it('subsequent calls trigger simultaneous timers', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { window.stub(); @@ -688,18 +628,18 @@ it.describe('next', () => { }, 100); }); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(1); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(2); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(3); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toHaveLength(4); }); it('subsequent calls triggers simultaneous timers with zero callAt', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { window.stub(1); setTimeout(() => { @@ -707,20 +647,82 @@ it.describe('next', () => { }, 0); }); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toEqual([{ params: [1] }]); - await page.clock.next(); + await page.clock.runToNextTimer(); expect(calls).toEqual([{ params: [1] }, { params: [2] }]); }); it('throws exception thrown by timer', async ({ page, calls }) => { - await page.clock.install(); + await page.clock.installFakeTimers(0); await page.evaluate(async () => { setTimeout(() => { throw new Error(); }, 100); }); - await expect(page.clock.next()).rejects.toThrow(); + await expect(page.clock.runToNextTimer()).rejects.toThrow(); + }); +}); + +it.describe('setTime', () => { + it('does not fake methods', async ({ page }) => { + await page.clock.setTime(0); + + // Should not stall. + await page.evaluate(() => { + return new Promise(f => setTimeout(f, 1)); + }); + }); + + it('allows setting time multiple times', async ({ page, calls }) => { + await page.clock.setTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.setTime(200); + expect(await page.evaluate(() => Date.now())).toBe(200); + }); + + it('supports skipTime w/o fake timers', async ({ page }) => { + await page.clock.setTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.skipTime(20); + expect(await page.evaluate(() => Date.now())).toBe(120); + }); + + it('allows installing fake timers after settings time', async ({ page, calls }) => { + await page.clock.setTime(100); + expect(await page.evaluate(() => Date.now())).toBe(100); + await page.clock.installFakeTimers(200); + await page.evaluate(async () => { + setTimeout(() => window.stub(Date.now())); + }); + await page.clock.runFor(0); + expect(calls).toEqual([{ params: [200] }]); + }); + + it('allows setting time after installing fake timers', async ({ page, calls }) => { + await page.clock.installFakeTimers(200); + await page.evaluate(async () => { + setTimeout(() => window.stub(Date.now())); + }); + await page.clock.setTime(220); + expect(calls).toEqual([{ params: [220] }]); + }); + + it('does not allow flowing time backwards', async ({ page, calls }) => { + await page.clock.installFakeTimers(200); + await expect(page.clock.setTime(180)).rejects.toThrow(); + }); + + it('should turn setTime into jump', async ({ page, calls }) => { + await page.clock.installFakeTimers(0); + await page.evaluate(async () => { + setTimeout(window.stub, 100); + setTimeout(window.stub, 200); + }); + await page.clock.setTime(100); + expect(calls).toHaveLength(1); + await page.clock.setTime(200); + expect(calls).toHaveLength(2); }); });