mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00: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
|
## event: BrowserContext.close
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
- argument: <[BrowserContext]>
|
- 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;
|
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
|
## event: Page.close
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
- argument: <[Page]>
|
- argument: <[Page]>
|
||||||
|
@ -20,6 +20,7 @@ export { Browser } from './browser';
|
|||||||
export { BrowserContext } from './browserContext';
|
export { BrowserContext } from './browserContext';
|
||||||
export type { BrowserServer } from './browserType';
|
export type { BrowserServer } from './browserType';
|
||||||
export { BrowserType } from './browserType';
|
export { BrowserType } from './browserType';
|
||||||
|
export { Clock } from './clock';
|
||||||
export { ConsoleMessage } from './consoleMessage';
|
export { ConsoleMessage } from './consoleMessage';
|
||||||
export { Coverage } from './coverage';
|
export { Coverage } from './coverage';
|
||||||
export { Dialog } from './dialog';
|
export { Dialog } from './dialog';
|
||||||
|
@ -44,6 +44,7 @@ import { ConsoleMessage } from './consoleMessage';
|
|||||||
import { Dialog } from './dialog';
|
import { Dialog } from './dialog';
|
||||||
import { WebError } from './webError';
|
import { WebError } from './webError';
|
||||||
import { TargetClosedError, parseError } from './errors';
|
import { TargetClosedError, parseError } from './errors';
|
||||||
|
import { Clock } from './clock';
|
||||||
|
|
||||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||||
_pages = new Set<Page>();
|
_pages = new Set<Page>();
|
||||||
@ -58,6 +59,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||||||
|
|
||||||
readonly request: APIRequestContext;
|
readonly request: APIRequestContext;
|
||||||
readonly tracing: Tracing;
|
readonly tracing: Tracing;
|
||||||
|
readonly clock: Clock;
|
||||||
|
|
||||||
readonly _backgroundPages = new Set<Page>();
|
readonly _backgroundPages = new Set<Page>();
|
||||||
readonly _serviceWorkers = new Set<Worker>();
|
readonly _serviceWorkers = new Set<Worker>();
|
||||||
readonly _isChromium: boolean;
|
readonly _isChromium: boolean;
|
||||||
@ -82,6 +85,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||||||
this._isChromium = this._browser?._name === 'chromium';
|
this._isChromium = this._browser?._name === 'chromium';
|
||||||
this.tracing = Tracing.from(initializer.tracing);
|
this.tracing = Tracing.from(initializer.tracing);
|
||||||
this.request = APIRequestContext.from(initializer.requestContext);
|
this.request = APIRequestContext.from(initializer.requestContext);
|
||||||
|
this.clock = new Clock(this);
|
||||||
|
|
||||||
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
|
this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding)));
|
||||||
this._channel.on('close', () => this._onClose());
|
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 { Waiter } from './waiter';
|
||||||
import { Worker } from './worker';
|
import { Worker } from './worker';
|
||||||
import { HarRouter } from './harRouter';
|
import { HarRouter } from './harRouter';
|
||||||
|
import type { Clock } from './clock';
|
||||||
|
|
||||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||||
width?: string | number,
|
width?: string | number,
|
||||||
@ -87,6 +88,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||||||
readonly mouse: Mouse;
|
readonly mouse: Mouse;
|
||||||
readonly request: APIRequestContext;
|
readonly request: APIRequestContext;
|
||||||
readonly touchscreen: Touchscreen;
|
readonly touchscreen: Touchscreen;
|
||||||
|
readonly clock: Clock;
|
||||||
|
|
||||||
|
|
||||||
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
|
||||||
readonly _timeoutSettings: TimeoutSettings;
|
readonly _timeoutSettings: TimeoutSettings;
|
||||||
@ -116,6 +119,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||||||
this.mouse = new Mouse(this);
|
this.mouse = new Mouse(this);
|
||||||
this.request = this._browserContext.request;
|
this.request = this._browserContext.request;
|
||||||
this.touchscreen = new Touchscreen(this);
|
this.touchscreen = new Touchscreen(this);
|
||||||
|
this.clock = this._browserContext.clock;
|
||||||
|
|
||||||
this._mainFrame = Frame.from(initializer.mainFrame);
|
this._mainFrame = Frame.from(initializer.mainFrame);
|
||||||
this._mainFrame._page = this;
|
this._mainFrame._page = this;
|
||||||
|
@ -963,6 +963,34 @@ scheme.BrowserContextUpdateSubscriptionParams = tObject({
|
|||||||
enabled: tBoolean,
|
enabled: tBoolean,
|
||||||
});
|
});
|
||||||
scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
|
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({
|
scheme.PageInitializer = tObject({
|
||||||
mainFrame: tChannel(['Frame']),
|
mainFrame: tChannel(['Frame']),
|
||||||
viewportSize: tOptional(tObject({
|
viewportSize: tOptional(tObject({
|
||||||
|
@ -41,6 +41,7 @@ import { Recorder } from './recorder';
|
|||||||
import * as consoleApiSource from '../generated/consoleApiSource';
|
import * as consoleApiSource from '../generated/consoleApiSource';
|
||||||
import { BrowserContextAPIRequestContext } from './fetch';
|
import { BrowserContextAPIRequestContext } from './fetch';
|
||||||
import type { Artifact } from './artifact';
|
import type { Artifact } from './artifact';
|
||||||
|
import { Clock } from './clock';
|
||||||
|
|
||||||
export abstract class BrowserContext extends SdkObject {
|
export abstract class BrowserContext extends SdkObject {
|
||||||
static Events = {
|
static Events = {
|
||||||
@ -87,6 +88,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||||||
private _routesInFlight = new Set<network.Route>();
|
private _routesInFlight = new Set<network.Route>();
|
||||||
private _debugger!: Debugger;
|
private _debugger!: Debugger;
|
||||||
_closeReason: string | undefined;
|
_closeReason: string | undefined;
|
||||||
|
readonly clock: Clock;
|
||||||
|
|
||||||
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
|
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
|
||||||
super(browser, 'browser-context');
|
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._harRecorders.set('', new HarRecorder(this, null, this._options.recordHar));
|
||||||
|
|
||||||
this.tracing = new Tracing(this, browser.options.tracesDir);
|
this.tracing = new Tracing(this, browser.options.tracesDir);
|
||||||
|
this.clock = new Clock(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isPersistentContext(): boolean {
|
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) };
|
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> {
|
async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams): Promise<void> {
|
||||||
if (params.enabled)
|
if (params.enabled)
|
||||||
this._subscriptions.add(params.event);
|
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.
|
# Files in this folder are used in browser environment, they can only depend on isomorphic files.
|
||||||
[*]
|
[*]
|
||||||
../isomorphic/
|
../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;
|
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.
|
* **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>;
|
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.
|
* 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>;
|
[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
|
* {@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
|
* [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>;
|
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
||||||
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
|
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: CallMetadata): Promise<BrowserContextCreateTempFileResult>;
|
||||||
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
|
updateSubscription(params: BrowserContextUpdateSubscriptionParams, metadata?: CallMetadata): Promise<BrowserContextUpdateSubscriptionResult>;
|
||||||
|
clockInstall(params: BrowserContextClockInstallParams, metadata?: CallMetadata): Promise<BrowserContextClockInstallResult>;
|
||||||
|
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 = {
|
export type BrowserContextBindingCallEvent = {
|
||||||
binding: BindingCallChannel,
|
binding: BindingCallChannel,
|
||||||
@ -1748,6 +1753,51 @@ export type BrowserContextUpdateSubscriptionOptions = {
|
|||||||
|
|
||||||
};
|
};
|
||||||
export type BrowserContextUpdateSubscriptionResult = void;
|
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 {
|
export interface BrowserContextEvents {
|
||||||
'bindingCall': BrowserContextBindingCallEvent;
|
'bindingCall': BrowserContextBindingCallEvent;
|
||||||
|
@ -1196,6 +1196,36 @@ BrowserContext:
|
|||||||
- requestFailed
|
- requestFailed
|
||||||
enabled: boolean
|
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:
|
events:
|
||||||
|
|
||||||
bindingCall:
|
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 'function': return 'Func';
|
||||||
case 'Buffer': return 'Byte';
|
case 'Buffer': return 'Byte';
|
||||||
case 'Serializable': return 'Object';
|
case 'Serializable': return 'Object';
|
||||||
|
case 'int': return 'Int';
|
||||||
|
case 'Date': return 'Date';
|
||||||
}
|
}
|
||||||
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
|
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'),
|
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
|
||||||
true,
|
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', 'src', 'injected', 'index.ts'),
|
||||||
path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),
|
path.join(ROOT, 'packages', 'playwright-ct-core', 'lib', 'injected', 'packed'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user