mirror of
https://github.com/microsoft/playwright.git
synced 2024-09-11 20:37:54 +03:00
feat(timers): a stab at fake timers (#31075)
This commit is contained in:
parent
a1db91040e
commit
170c457a61
@ -98,6 +98,12 @@ context.BackgroundPage += (_, backgroundPage) =>
|
||||
|
||||
```
|
||||
|
||||
## property: BrowserContext.clock
|
||||
* since: v1.45
|
||||
- type: <[Clock]>
|
||||
|
||||
Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
|
||||
|
||||
## event: BrowserContext.close
|
||||
* since: v1.8
|
||||
- argument: <[BrowserContext]>
|
||||
|
86
docs/src/api/class-clock.md
Normal file
86
docs/src/api/class-clock.md
Normal file
@ -0,0 +1,86 @@
|
||||
# class: Clock
|
||||
* since: v1.45
|
||||
|
||||
Playwright uses [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) for clock emulation. 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
|
||||
* since: v1.45
|
||||
|
||||
Creates a clock and installs it globally.
|
||||
|
||||
### option: Clock.install.now
|
||||
* since: v1.45
|
||||
- `now` <[int]|[Date]>
|
||||
|
||||
Install fake timers with the specified unix epoch (default: 0).
|
||||
|
||||
### 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, `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` and `Date` are faked.
|
||||
|
||||
### option: Clock.install.loopLimit
|
||||
* since: v1.45
|
||||
- `loopLimit` <[int]>
|
||||
|
||||
The maximum number of timers that will be run when calling [`method: Clock.runAll`]. 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. Returns fake milliseconds since the unix epoch.
|
||||
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.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.runAll
|
||||
* since: v1.45
|
||||
- returns: <[int]> Fake milliseconds since the unix epoch.
|
||||
|
||||
Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be run as well.
|
||||
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
|
||||
* since: v1.45
|
||||
- returns: <[int]> Fake milliseconds since the unix epoch.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## async method: Clock.tick
|
||||
* since: v1.45
|
||||
- returns: <[int]> Fake milliseconds since the unix epoch.
|
||||
|
||||
Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch.
|
||||
|
||||
### param: Clock.tick.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.
|
@ -151,6 +151,12 @@ page.Load += PageLoadHandler;
|
||||
page.Load -= PageLoadHandler;
|
||||
```
|
||||
|
||||
## property: Page.clock
|
||||
* since: v1.45
|
||||
- type: <[Clock]>
|
||||
|
||||
Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
|
||||
|
||||
## event: Page.close
|
||||
* since: v1.8
|
||||
- argument: <[Page]>
|
||||
|
@ -20,6 +20,7 @@ export { Browser } from './browser';
|
||||
export { BrowserContext } from './browserContext';
|
||||
export type { BrowserServer } from './browserType';
|
||||
export { BrowserType } from './browserType';
|
||||
export { Clock } from './clock';
|
||||
export { ConsoleMessage } from './consoleMessage';
|
||||
export { Coverage } from './coverage';
|
||||
export { Dialog } from './dialog';
|
||||
|
@ -44,6 +44,7 @@ import { ConsoleMessage } from './consoleMessage';
|
||||
import { Dialog } from './dialog';
|
||||
import { WebError } from './webError';
|
||||
import { TargetClosedError, parseError } from './errors';
|
||||
import { Clock } from './clock';
|
||||
|
||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||
_pages = new Set<Page>();
|
||||
@ -58,6 +59,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||
|
||||
readonly request: APIRequestContext;
|
||||
readonly tracing: Tracing;
|
||||
readonly clock: Clock;
|
||||
|
||||
readonly _backgroundPages = new Set<Page>();
|
||||
readonly _serviceWorkers = new Set<Worker>();
|
||||
readonly _isChromium: boolean;
|
||||
@ -82,6 +85,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||
this._isChromium = this._browser?._name === 'chromium';
|
||||
this.tracing = Tracing.from(initializer.tracing);
|
||||
this.request = APIRequestContext.from(initializer.requestContext);
|
||||
this.clock = new Clock(this);
|
||||
|
||||
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
|
||||
this._channel.on('close', () => this._onClose());
|
||||
|
57
packages/playwright-core/src/client/clock.ts
Normal file
57
packages/playwright-core/src/client/clock.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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 {
|
||||
private _browserContext: BrowserContext;
|
||||
|
||||
constructor(browserContext: BrowserContext) {
|
||||
this._browserContext = browserContext;
|
||||
}
|
||||
|
||||
async install(options?: Omit<channels.BrowserContextClockInstallOptions, 'now'> & { 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 jump(time: number | string) {
|
||||
await this._browserContext._channel.clockJump({
|
||||
timeNumber: typeof time === 'number' ? time : undefined,
|
||||
timeString: typeof time === 'string' ? time : undefined
|
||||
});
|
||||
}
|
||||
|
||||
async runAll(): Promise<number> {
|
||||
const result = await this._browserContext._channel.clockRunAll();
|
||||
return result.fakeTime;
|
||||
}
|
||||
|
||||
async runToLast(): Promise<number> {
|
||||
const result = await this._browserContext._channel.clockRunToLast();
|
||||
return result.fakeTime;
|
||||
}
|
||||
|
||||
async tick(time: number | string): Promise<number> {
|
||||
const result = await this._browserContext._channel.clockTick({
|
||||
timeNumber: typeof time === 'number' ? time : undefined,
|
||||
timeString: typeof time === 'string' ? time : undefined
|
||||
});
|
||||
return result.fakeTime;
|
||||
}
|
||||
}
|
@ -49,6 +49,7 @@ import { Video } from './video';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
import { HarRouter } from './harRouter';
|
||||
import type { Clock } from './clock';
|
||||
|
||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||
width?: string | number,
|
||||
@ -87,6 +88,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||
readonly mouse: Mouse;
|
||||
readonly request: APIRequestContext;
|
||||
readonly touchscreen: Touchscreen;
|
||||
readonly clock: Clock;
|
||||
|
||||
|
||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
@ -116,6 +119,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||
this.mouse = new Mouse(this);
|
||||
this.request = this._browserContext.request;
|
||||
this.touchscreen = new Touchscreen(this);
|
||||
this.clock = this._browserContext.clock;
|
||||
|
||||
this._mainFrame = Frame.from(initializer.mainFrame);
|
||||
this._mainFrame._page = this;
|
||||
|
@ -963,6 +963,34 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
|
||||
enabled: tBoolean,
|
||||
});
|
||||
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextClockInstallParams = tObject({
|
||||
now: tOptional(tNumber),
|
||||
toFake: tOptional(tArray(tString)),
|
||||
loopLimit: tOptional(tNumber),
|
||||
shouldAdvanceTime: tOptional(tBoolean),
|
||||
advanceTimeDelta: tOptional(tNumber),
|
||||
});
|
||||
scheme.BrowserContextClockInstallResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextClockJumpParams = tObject({
|
||||
timeNumber: tOptional(tNumber),
|
||||
timeString: tOptional(tString),
|
||||
});
|
||||
scheme.BrowserContextClockJumpResult = tOptional(tObject({}));
|
||||
scheme.BrowserContextClockRunAllParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextClockRunAllResult = tObject({
|
||||
fakeTime: tNumber,
|
||||
});
|
||||
scheme.BrowserContextClockRunToLastParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextClockRunToLastResult = tObject({
|
||||
fakeTime: tNumber,
|
||||
});
|
||||
scheme.BrowserContextClockTickParams = tObject({
|
||||
timeNumber: tOptional(tNumber),
|
||||
timeString: tOptional(tString),
|
||||
});
|
||||
scheme.BrowserContextClockTickResult = tObject({
|
||||
fakeTime: tNumber,
|
||||
});
|
||||
scheme.PageInitializer = tObject({
|
||||
mainFrame: tChannel(['Frame']),
|
||||
viewportSize: tOptional(tObject({
|
||||
|
@ -41,6 +41,7 @@ import { Recorder } from './recorder';
|
||||
import * as consoleApiSource from '../generated/consoleApiSource';
|
||||
import { BrowserContextAPIRequestContext } from './fetch';
|
||||
import type { Artifact } from './artifact';
|
||||
import { Clock } from './clock';
|
||||
|
||||
export abstract class BrowserContext extends SdkObject {
|
||||
static Events = {
|
||||
@ -87,6 +88,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||
private _routesInFlight = new Set<network.Route>();
|
||||
private _debugger!: Debugger;
|
||||
_closeReason: string | undefined;
|
||||
readonly clock: Clock;
|
||||
|
||||
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
|
||||
super(browser, 'browser-context');
|
||||
@ -103,6 +105,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||
this._harRecorders.set('', new HarRecorder(this, null, this._options.recordHar));
|
||||
|
||||
this.tracing = new Tracing(this, browser.options.tracesDir);
|
||||
this.clock = new Clock(this);
|
||||
}
|
||||
|
||||
isPersistentContext(): boolean {
|
||||
|
79
packages/playwright-core/src/server/clock.ts
Normal file
79
packages/playwright-core/src/server/clock.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* 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;
|
||||
|
||||
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;
|
||||
const script = `(() => {
|
||||
const module = {};
|
||||
${fakeTimersSource.source}
|
||||
globalThis.__pwFakeTimers = (module.exports.install())(${JSON.stringify(params)});
|
||||
})();`;
|
||||
await this._addAndEvaluate(script);
|
||||
}
|
||||
|
||||
async jump(time: number | string) {
|
||||
this._assertInstalled();
|
||||
await this._addAndEvaluate(`globalThis.__pwFakeTimers.jump(${JSON.stringify(time)}); 0`);
|
||||
}
|
||||
|
||||
async runAll(): Promise<number> {
|
||||
this._assertInstalled();
|
||||
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runAll()`);
|
||||
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runAllAsync()`);
|
||||
}
|
||||
|
||||
async runToLast(): Promise<number> {
|
||||
this._assertInstalled();
|
||||
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.runToLast()`);
|
||||
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.runToLastAsync()`);
|
||||
}
|
||||
|
||||
async tick(time: number | string): Promise<number> {
|
||||
this._assertInstalled();
|
||||
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.tick(${JSON.stringify(time)})`);
|
||||
return await this._evaluateInFrames(`globalThis.__pwFakeTimers.tickAsync(${JSON.stringify(time)})`);
|
||||
}
|
||||
|
||||
private async _addAndEvaluate(script: string) {
|
||||
await this._browserContext.addInitScript(script);
|
||||
return await this._evaluateInFrames(script);
|
||||
}
|
||||
|
||||
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)));
|
||||
return results[0];
|
||||
}
|
||||
|
||||
private _assertInstalled() {
|
||||
if (!this._installed)
|
||||
throw new Error('Clock is not installed');
|
||||
}
|
||||
}
|
@ -312,6 +312,26 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
return { artifact: ArtifactDispatcher.from(this, artifact) };
|
||||
}
|
||||
|
||||
async clockInstall(params: channels.BrowserContextClockInstallParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallResult> {
|
||||
await this._context.clock.install(params);
|
||||
}
|
||||
|
||||
async clockJump(params: channels.BrowserContextClockJumpParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockJumpResult> {
|
||||
await this._context.clock.jump(params.timeString || params.timeNumber || 0);
|
||||
}
|
||||
|
||||
async clockRunAll(params: channels.BrowserContextClockRunAllParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllResult> {
|
||||
return { fakeTime: await this._context.clock.runAll() };
|
||||
}
|
||||
|
||||
async clockRunToLast(params: channels.BrowserContextClockRunToLastParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunToLastResult> {
|
||||
return { fakeTime: await this._context.clock.runToLast() };
|
||||
}
|
||||
|
||||
async clockTick(params: channels.BrowserContextClockTickParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockTickResult> {
|
||||
return { fakeTime: await this._context.clock.tick(params.timeString || params.timeNumber || 0) };
|
||||
}
|
||||
|
||||
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {
|
||||
if (params.enabled)
|
||||
this._subscriptions.add(params.event);
|
||||
|
@ -1,4 +1,7 @@
|
||||
# Files in this folder are used in browser environment, they can only depend on isomorphic files.
|
||||
[*]
|
||||
../isomorphic/
|
||||
../../utils/isomorphic
|
||||
../../utils/isomorphic
|
||||
|
||||
[fakeTimers.ts]
|
||||
../../third_party/fake-timers-src
|
||||
|
23
packages/playwright-core/src/server/injected/fakeTimers.ts
Normal file
23
packages/playwright-core/src/server/injected/fakeTimers.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import SinonFakeTimers from '../../third_party/fake-timers-src';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export function install(params: channels.BrowserContextClockInstallOptions) {
|
||||
return SinonFakeTimers.install(params);
|
||||
}
|
1776
packages/playwright-core/src/third_party/fake-timers-src.js
vendored
Normal file
1776
packages/playwright-core/src/third_party/fake-timers-src.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
85
packages/playwright-core/types/types.d.ts
vendored
85
packages/playwright-core/types/types.d.ts
vendored
@ -4863,6 +4863,11 @@ export interface Page {
|
||||
*/
|
||||
accessibility: Accessibility;
|
||||
|
||||
/**
|
||||
* Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
|
||||
*/
|
||||
clock: Clock;
|
||||
|
||||
/**
|
||||
* **NOTE** Only available for Chromium atm.
|
||||
*
|
||||
@ -8980,6 +8985,11 @@ export interface BrowserContext {
|
||||
waitForEvent(event: 'weberror', optionsOrPredicate?: { predicate?: (webError: WebError) => boolean | Promise<boolean>, timeout?: number } | ((webError: WebError) => boolean | Promise<boolean>)): Promise<WebError>;
|
||||
|
||||
|
||||
/**
|
||||
* Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock.
|
||||
*/
|
||||
clock: Clock;
|
||||
|
||||
/**
|
||||
* API testing helper associated with this context. Requests made with this API will use context cookies.
|
||||
*/
|
||||
@ -17224,6 +17234,81 @@ export interface BrowserServer {
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playwright uses [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) for clock emulation. Clock is
|
||||
* installed for the entire {@link BrowserContext}, so the time in all the pages and iframes is controlled by the same
|
||||
* clock.
|
||||
*/
|
||||
export interface Clock {
|
||||
/**
|
||||
* Creates a clock and installs it globally.
|
||||
* @param options
|
||||
*/
|
||||
install(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`.
|
||||
*/
|
||||
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, `setTimeout`, `clearTimeout`, `setInterval`,
|
||||
* `clearInterval` and `Date` are faked.
|
||||
*/
|
||||
toFake?: Array<"setTimeout"|"clearTimeout"|"setInterval"|"clearInterval"|"Date"|"requestAnimationFrame"|"cancelAnimationFrame"|"requestIdleCallback"|"cancelIdleCallback"|"performance">;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Advance the clock by jumping forward in time, firing callbacks at most once. Returns fake milliseconds since the
|
||||
* unix epoch. 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 Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are
|
||||
* "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
|
||||
*/
|
||||
jump(time: number|string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Runs all pending timers until there are none remaining. If new timers are added while it is executing they will be
|
||||
* run as well. 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.
|
||||
*/
|
||||
runAll(): Promise<number>;
|
||||
|
||||
/**
|
||||
* This takes note of the last scheduled timer when it is run, and advances the clock to that time firing callbacks as
|
||||
* necessary. If new timers are added while it is executing they will be run only if they would occur before this
|
||||
* time. This is useful when you want to run a test to completion, but the test recursively sets timers that would
|
||||
* cause runAll to trigger an infinite loop warning.
|
||||
*/
|
||||
runToLast(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Advance the clock, firing callbacks if necessary. Returns fake milliseconds since the unix epoch.
|
||||
* @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<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ConsoleMessage} objects are dispatched by page via the
|
||||
* [page.on('console')](https://playwright.dev/docs/api/class-page#page-event-console) event. For each console message
|
||||
|
@ -1460,6 +1460,11 @@ 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>;
|
||||
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>;
|
||||
clockJump(params: BrowserContextClockJumpParams, metadata?: CallMetadata): Promise<BrowserContextClockJumpResult>;
|
||||
clockRunAll(params?: BrowserContextClockRunAllParams, metadata?: CallMetadata): Promise<BrowserContextClockRunAllResult>;
|
||||
clockRunToLast(params?: BrowserContextClockRunToLastParams, metadata?: CallMetadata): Promise<BrowserContextClockRunToLastResult>;
|
||||
clockTick(params: BrowserContextClockTickParams, metadata?: CallMetadata): Promise<BrowserContextClockTickResult>;
|
||||
}
|
||||
export type BrowserContextBindingCallEvent = {
|
||||
binding: BindingCallChannel,
|
||||
@ -1748,6 +1753,51 @@ export type BrowserContextUpdateSubscriptionOptions = {
|
||||
|
||||
};
|
||||
export type BrowserContextUpdateSubscriptionResult = void;
|
||||
export type BrowserContextClockInstallParams = {
|
||||
now?: number,
|
||||
toFake?: string[],
|
||||
loopLimit?: number,
|
||||
shouldAdvanceTime?: boolean,
|
||||
advanceTimeDelta?: number,
|
||||
};
|
||||
export type BrowserContextClockInstallOptions = {
|
||||
now?: number,
|
||||
toFake?: string[],
|
||||
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 BrowserContextClockRunAllParams = {};
|
||||
export type BrowserContextClockRunAllOptions = {};
|
||||
export type BrowserContextClockRunAllResult = {
|
||||
fakeTime: number,
|
||||
};
|
||||
export type BrowserContextClockRunToLastParams = {};
|
||||
export type BrowserContextClockRunToLastOptions = {};
|
||||
export type BrowserContextClockRunToLastResult = {
|
||||
fakeTime: number,
|
||||
};
|
||||
export type BrowserContextClockTickParams = {
|
||||
timeNumber?: number,
|
||||
timeString?: string,
|
||||
};
|
||||
export type BrowserContextClockTickOptions = {
|
||||
timeNumber?: number,
|
||||
timeString?: string,
|
||||
};
|
||||
export type BrowserContextClockTickResult = {
|
||||
fakeTime: number,
|
||||
};
|
||||
|
||||
export interface BrowserContextEvents {
|
||||
'bindingCall': BrowserContextBindingCallEvent;
|
||||
|
@ -1196,6 +1196,36 @@ BrowserContext:
|
||||
- requestFailed
|
||||
enabled: boolean
|
||||
|
||||
clockInstall:
|
||||
parameters:
|
||||
now: number?
|
||||
toFake:
|
||||
type: array?
|
||||
items: string
|
||||
loopLimit: number?
|
||||
shouldAdvanceTime: boolean?
|
||||
advanceTimeDelta: number?
|
||||
|
||||
clockJump:
|
||||
parameters:
|
||||
timeNumber: number?
|
||||
timeString: string?
|
||||
|
||||
clockRunAll:
|
||||
returns:
|
||||
fakeTime: number
|
||||
|
||||
clockRunToLast:
|
||||
returns:
|
||||
fakeTime: number
|
||||
|
||||
clockTick:
|
||||
parameters:
|
||||
timeNumber: number?
|
||||
timeString: string?
|
||||
returns:
|
||||
fakeTime: number
|
||||
|
||||
events:
|
||||
|
||||
bindingCall:
|
||||
|
614
tests/page/page-clock.spec.ts
Normal file
614
tests/page/page-clock.spec.ts
Normal file
@ -0,0 +1,614 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { test, expect } from './pageTest';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
stub: (param?: any) => void
|
||||
}
|
||||
}
|
||||
|
||||
const it = test.extend<{ calls: { params: any[] }[] }>({
|
||||
calls: async ({ page }, use) => {
|
||||
const calls = [];
|
||||
await page.exposeFunction('stub', async (...params: any[]) => {
|
||||
calls.push({ params });
|
||||
});
|
||||
await use(calls);
|
||||
}
|
||||
});
|
||||
|
||||
it.describe('tick', () => {
|
||||
it('triggers immediately without specified delay', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub);
|
||||
});
|
||||
|
||||
await page.clock.tick(0);
|
||||
expect(calls).toEqual([{ params: [] }]);
|
||||
});
|
||||
|
||||
it('does not trigger without sufficient delay', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 100);
|
||||
});
|
||||
await page.clock.tick(10);
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('triggers after sufficient delay', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 100);
|
||||
});
|
||||
await page.clock.tick(100);
|
||||
expect(calls).toEqual([{ params: [] }]);
|
||||
});
|
||||
|
||||
it('triggers simultaneous timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 100);
|
||||
setTimeout(window.stub, 100);
|
||||
});
|
||||
await page.clock.tick(100);
|
||||
expect(calls).toEqual([{ params: [] }, { params: [] }]);
|
||||
});
|
||||
|
||||
it('triggers multiple simultaneous timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
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);
|
||||
expect(calls.length).toBe(4);
|
||||
});
|
||||
|
||||
it('waits after setTimeout was called', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 150);
|
||||
});
|
||||
await page.clock.tick(50);
|
||||
expect(calls).toEqual([]);
|
||||
await page.clock.tick(100);
|
||||
expect(calls).toEqual([{ params: [] }]);
|
||||
});
|
||||
|
||||
it('triggers event when some throw', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => { throw new Error(); }, 100);
|
||||
setTimeout(window.stub, 120);
|
||||
});
|
||||
|
||||
await expect(page.clock.tick(120)).rejects.toThrow();
|
||||
expect(calls).toEqual([{ params: [] }]);
|
||||
});
|
||||
|
||||
it('creates updated Date while ticking', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setInterval(() => {
|
||||
window.stub(new Date().getTime());
|
||||
}, 10);
|
||||
});
|
||||
await page.clock.tick(100);
|
||||
expect(calls).toEqual([
|
||||
{ params: [10] },
|
||||
{ params: [20] },
|
||||
{ params: [30] },
|
||||
{ params: [40] },
|
||||
{ params: [50] },
|
||||
{ params: [60] },
|
||||
{ params: [70] },
|
||||
{ params: [80] },
|
||||
{ params: [90] },
|
||||
{ params: [100] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('passes 8 seconds', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setInterval(window.stub, 4000);
|
||||
});
|
||||
|
||||
await page.clock.tick('08');
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('passes 1 minute', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setInterval(window.stub, 6000);
|
||||
});
|
||||
|
||||
await page.clock.tick('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.evaluate(async () => {
|
||||
setInterval(window.stub, 10000);
|
||||
});
|
||||
|
||||
await page.clock.tick('02:34:10');
|
||||
expect(calls.length).toBe(925);
|
||||
});
|
||||
|
||||
it('throws for invalid format', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setInterval(window.stub, 10000);
|
||||
});
|
||||
await expect(page.clock.tick('12:02:34:10')).rejects.toThrow();
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns the current now value', async ({ page }) => {
|
||||
await page.clock.install();
|
||||
const value = 200;
|
||||
await page.clock.tick(value);
|
||||
expect(await page.evaluate(() => Date.now())).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
it.describe('jump', () => {
|
||||
it(`ignores timers which wouldn't be run`, async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
window.stub('should not be logged');
|
||||
}, 1000);
|
||||
});
|
||||
await page.clock.jump(500);
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('pushes back execution time for skipped timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
window.stub(Date.now());
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
await page.clock.jump(2000);
|
||||
expect(calls).toEqual([{ params: [2000] }]);
|
||||
});
|
||||
|
||||
it('supports string time arguments', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
window.stub(Date.now());
|
||||
}, 100000); // 100000 = 1:40
|
||||
});
|
||||
await page.clock.jump('01:50');
|
||||
expect(calls).toEqual([{ params: [110000] }]);
|
||||
});
|
||||
});
|
||||
|
||||
it.describe('runAllAsyn', () => {
|
||||
it('if there are no timers just return', async ({ page }) => {
|
||||
await page.clock.install();
|
||||
await page.clock.runAll();
|
||||
});
|
||||
|
||||
it('runs all timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 10);
|
||||
setTimeout(window.stub, 50);
|
||||
});
|
||||
await page.clock.runAll();
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('new timers added while running are also run', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
setTimeout(window.stub, 50);
|
||||
}, 10);
|
||||
});
|
||||
await page.clock.runAll();
|
||||
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.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
setTimeout(window.stub, 50);
|
||||
});
|
||||
}, 10);
|
||||
});
|
||||
await page.clock.runAll();
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('throws before allowing infinite recursion', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
const recursiveCallback = () => {
|
||||
window.stub();
|
||||
setTimeout(recursiveCallback, 10);
|
||||
};
|
||||
setTimeout(recursiveCallback, 10);
|
||||
});
|
||||
await expect(page.clock.runAll()).rejects.toThrow();
|
||||
expect(calls).toHaveLength(1000);
|
||||
});
|
||||
|
||||
it('throws before allowing infinite recursion from promises', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
const recursiveCallback = () => {
|
||||
window.stub();
|
||||
void Promise.resolve().then(() => {
|
||||
setTimeout(recursiveCallback, 10);
|
||||
});
|
||||
};
|
||||
setTimeout(recursiveCallback, 10);
|
||||
});
|
||||
await expect(page.clock.runAll()).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.evaluate(async () => {
|
||||
setTimeout(window.stub, 10);
|
||||
setTimeout(window.stub, 50);
|
||||
});
|
||||
await expect(page.clock.runAll()).rejects.toThrow();
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should settle user-created promises', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
void Promise.resolve().then(() => window.stub());
|
||||
}, 55);
|
||||
});
|
||||
await page.clock.runAll();
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should settle nested user-created promises', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => window.stub());
|
||||
});
|
||||
});
|
||||
}, 55);
|
||||
});
|
||||
await page.clock.runAll();
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should settle local promises before firing timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
void Promise.resolve().then(() => window.stub(1));
|
||||
setTimeout(() => window.stub(2), 55);
|
||||
});
|
||||
await page.clock.runAll();
|
||||
expect(calls).toEqual([
|
||||
{ params: [1] },
|
||||
{ params: [2] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.describe('runToLast', () => {
|
||||
it('returns current time when there are no timers', async ({ page }) => {
|
||||
await page.clock.install();
|
||||
const time = await page.clock.runToLast();
|
||||
expect(time).toBe(0);
|
||||
});
|
||||
|
||||
it('runs all existing timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 10);
|
||||
setTimeout(window.stub, 50);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('returns time of the last timer', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 10);
|
||||
setTimeout(window.stub, 50);
|
||||
});
|
||||
const time = await page.clock.runToLast();
|
||||
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.evaluate(async () => {
|
||||
setTimeout(window.stub, 10);
|
||||
setTimeout(window.stub, 10);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
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.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
window.stub();
|
||||
setTimeout(window.stub, 50);
|
||||
}, 10);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
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.evaluate(async () => {
|
||||
setTimeout(window.stub, 100);
|
||||
setTimeout(() => {
|
||||
setTimeout(window.stub, 50);
|
||||
}, 10);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('new timers cannot cause an infinite loop', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
const recursiveCallback = () => {
|
||||
window.stub();
|
||||
setTimeout(recursiveCallback, 0);
|
||||
};
|
||||
setTimeout(recursiveCallback, 0);
|
||||
setTimeout(window.stub, 100);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls.length).toBe(102);
|
||||
});
|
||||
|
||||
it('should support clocks with start time', async ({ page, calls }) => {
|
||||
await page.clock.install({ now: 200 });
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(function cb() {
|
||||
window.stub();
|
||||
setTimeout(cb, 50);
|
||||
}, 50);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
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.evaluate(async () => {
|
||||
const recursiveCallback = () => {
|
||||
void Promise.resolve().then(() => {
|
||||
setTimeout(recursiveCallback, 0);
|
||||
});
|
||||
};
|
||||
setTimeout(recursiveCallback, 0);
|
||||
setTimeout(window.stub, 100);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should settle user-created promises', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
void Promise.resolve().then(() => window.stub());
|
||||
}, 55);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should settle nested user-created promises', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => {
|
||||
void Promise.resolve().then(() => window.stub());
|
||||
});
|
||||
});
|
||||
}, 55);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should settle local promises before firing timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
void Promise.resolve().then(() => window.stub(1));
|
||||
setTimeout(() => window.stub(2), 55);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls).toEqual([
|
||||
{ params: [1] },
|
||||
{ params: [2] },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should settle user-created promises before firing more timers', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(() => {
|
||||
void Promise.resolve().then(() => window.stub(1));
|
||||
}, 55);
|
||||
setTimeout(() => window.stub(2), 75);
|
||||
});
|
||||
await page.clock.runToLast();
|
||||
expect(calls).toEqual([
|
||||
{ params: [1] },
|
||||
{ params: [2] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it.describe('stubTimers', () => {
|
||||
it('sets initial timestamp', async ({ page, calls }) => {
|
||||
await page.clock.install({ now: 1400 });
|
||||
expect(await page.evaluate(() => Date.now())).toBe(1400);
|
||||
});
|
||||
|
||||
it('replaces global setTimeout', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setTimeout(window.stub, 1000);
|
||||
});
|
||||
await page.clock.tick(1000);
|
||||
expect(calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('global fake setTimeout should return id', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
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.evaluate(async () => {
|
||||
const to = setTimeout(window.stub, 1000);
|
||||
clearTimeout(to);
|
||||
});
|
||||
await page.clock.tick(1000);
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('replaces global setInterval', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
setInterval(window.stub, 500);
|
||||
});
|
||||
await page.clock.tick(1000);
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('replaces global clearInterval', async ({ page, calls }) => {
|
||||
await page.clock.install();
|
||||
await page.evaluate(async () => {
|
||||
const to = setInterval(window.stub, 500);
|
||||
clearInterval(to);
|
||||
});
|
||||
await page.clock.tick(1000);
|
||||
expect(calls).toEqual([]);
|
||||
});
|
||||
|
||||
it('replaces global performance.now', async ({ page }) => {
|
||||
await page.clock.install();
|
||||
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);
|
||||
expect(await promise).toEqual({ prev: 0, next: 1000 });
|
||||
});
|
||||
|
||||
it('fakes Date constructor', async ({ page }) => {
|
||||
await page.clock.install({ now: 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<void>(f => {
|
||||
const timeoutStarted = Date.now();
|
||||
setTimeout(() => {
|
||||
window.stub(Date.now() - timeoutStarted);
|
||||
f();
|
||||
}, testDelay);
|
||||
});
|
||||
}, testDelay);
|
||||
|
||||
expect(calls).toEqual([
|
||||
{ params: [testDelay] }
|
||||
]);
|
||||
});
|
||||
|
||||
it('should test setInterval', async ({ page, calls }) => {
|
||||
const now = new Date('2015-09-25');
|
||||
await page.clock.install({ now, shouldAdvanceTime: true });
|
||||
|
||||
const timeDifference = await page.evaluate(async () => {
|
||||
return new Promise(f => {
|
||||
const interval = 20;
|
||||
const cyclesToTrigger = 3;
|
||||
const timeoutStarted = Date.now();
|
||||
let intervalsTriggered = 0;
|
||||
const intervalId = setInterval(() => {
|
||||
if (++intervalsTriggered === cyclesToTrigger) {
|
||||
clearInterval(intervalId);
|
||||
const timeDifference = Date.now() - timeoutStarted;
|
||||
f(timeDifference - interval * cyclesToTrigger);
|
||||
}
|
||||
}, interval);
|
||||
});
|
||||
});
|
||||
|
||||
expect(timeDifference).toBe(0);
|
||||
});
|
||||
});
|
@ -865,6 +865,8 @@ function csharpOptionOverloadSuffix(option, type) {
|
||||
case 'function': return 'Func';
|
||||
case 'Buffer': return 'Byte';
|
||||
case 'Serializable': return 'Object';
|
||||
case 'int': return 'Int';
|
||||
case 'Date': return 'Date';
|
||||
}
|
||||
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
|
||||
}
|
||||
|
@ -50,6 +50,12 @@ const injectedScripts = [
|
||||
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
|
||||
true,
|
||||
],
|
||||
[
|
||||
path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'fakeTimers.ts'),
|
||||
path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'),
|
||||
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
|
||||
true,
|
||||
],
|
||||
[
|
||||
path.join(ROOT, 'packages', 'playwright-ct-core', 'src', 'injected', 'index.ts'),
|
||||
path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),
|
||||
|
Loading…
Reference in New Issue
Block a user