This commit is contained in:
Pavel Feldman 2024-06-07 08:51:09 -07:00
parent 8bc139b27c
commit 294f0523c7
11 changed files with 104 additions and 10 deletions

View file

@ -35,6 +35,10 @@ Install fake timers with the specified base time.
The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`. The maximum number of timers that will be run in [`method: Clock.runAllTimers`]. Defaults to `1000`.
### option: Clock.installFakeTimers.speed
* since: v1.45
- `speed` <[float]>
## async method: Clock.runAllTimers ## async method: Clock.runAllTimers
* since: v1.45 * since: v1.45
- returns: <[int]> - returns: <[int]>

View file

@ -24,9 +24,9 @@ export class Clock implements api.Clock {
this._browserContext = browserContext; this._browserContext = browserContext;
} }
async installFakeTimers(time: number | Date, options: { loopLimit?: number } = {}) { async installFakeTimers(time: number | Date, options: { loopLimit?: number, speed?: number } = {}) {
const timeMs = time instanceof Date ? time.getTime() : time; const timeMs = time instanceof Date ? time.getTime() : time;
await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit }); await this._browserContext._channel.clockInstallFakeTimers({ time: timeMs, loopLimit: options.loopLimit, speed: options.speed });
} }
async runAllTimers(): Promise<number> { async runAllTimers(): Promise<number> {

View file

@ -966,6 +966,7 @@ scheme.BrowserContextUpdateSubscriptionResult = tOptional(tObject({}));
scheme.BrowserContextClockInstallFakeTimersParams = tObject({ scheme.BrowserContextClockInstallFakeTimersParams = tObject({
time: tNumber, time: tNumber,
loopLimit: tOptional(tNumber), loopLimit: tOptional(tNumber),
speed: tOptional(tNumber),
}); });
scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({})); scheme.BrowserContextClockInstallFakeTimersResult = tOptional(tObject({}));
scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({})); scheme.BrowserContextClockRunAllTimersParams = tOptional(tObject({}));

View file

@ -27,11 +27,11 @@ export class Clock {
this._browserContext = browserContext; this._browserContext = browserContext;
} }
async installFakeTimers(time: number, loopLimit: number | undefined) { async installFakeTimers(time: number, loopLimit: number | undefined, speed: number | undefined) {
await this._injectScriptIfNeeded(); await this._injectScriptIfNeeded();
await this._addAndEvaluate(`(() => { await this._addAndEvaluate(`(() => {
globalThis.__pwClock.clock?.uninstall(); globalThis.__pwClock.clock?.uninstall();
globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })}); globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit, speed })});
})();`); })();`);
this._now = time; this._now = time;
this._clockInstalled = true; this._clockInstalled = true;

View file

@ -313,7 +313,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallFakeTimersResult> { async clockInstallFakeTimers(params: channels.BrowserContextClockInstallFakeTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockInstallFakeTimersResult> {
await this._context.clock.installFakeTimers(params.time, params.loopLimit); await this._context.clock.installFakeTimers(params.time, params.loopLimit, params.speed);
} }
async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllTimersResult> { async clockRunAllTimers(params: channels.BrowserContextClockRunAllTimersParams, metadata?: CallMetadata | undefined): Promise<channels.BrowserContextClockRunAllTimersResult> {

View file

@ -31,6 +31,7 @@ export type ClockConfig = {
export type InstallConfig = ClockConfig & { export type InstallConfig = ClockConfig & {
toFake?: (keyof ClockMethods)[]; toFake?: (keyof ClockMethods)[];
speed?: number;
}; };
enum TimerType { enum TimerType {
@ -53,7 +54,8 @@ type Timer = {
}; };
interface Embedder { interface Embedder {
postTask(task: () => void): void; performanceNow(): DOMHighResTimeStamp;
postTask(task: () => void, timeout?: number): () => void;
postTaskPeriodically(task: () => void, delay: number): () => void; postTaskPeriodically(task: () => void, delay: number): () => void;
} }
@ -66,6 +68,8 @@ export class ClockController {
private _uniqueTimerId = idCounterStart; private _uniqueTimerId = idCounterStart;
private _embedder: Embedder; private _embedder: Embedder;
readonly disposables: (() => void)[] = []; readonly disposables: (() => void)[] = [];
private _realTime: { startTicks: number, speed: number, lastSyncTicks: number } | undefined;
private _currentRealTimeTimer: { callAt: number, dispose: () => void } | undefined;
constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) { constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) {
const start = Math.floor(getEpoch(startDate)); const start = Math.floor(getEpoch(startDate));
@ -145,6 +149,37 @@ export class ClockController {
return this.tick(this.getTimeToNextFrame()); return this.tick(this.getTimeToNextFrame());
} }
startRealTime(options: { speed?: number } = {}) {
const now = Math.ceil(this._embedder.performanceNow());
this._realTime = { startTicks: now, speed: options.speed || 1, lastSyncTicks: now };
}
private _ensureTimer() {
const firstTimer = this._firstTimer();
if (!firstTimer)
return;
const callAt = firstTimer.callAt;
if (this._currentRealTimeTimer && this._currentRealTimeTimer.callAt < callAt)
return;
if (this._currentRealTimeTimer) {
this._currentRealTimeTimer.dispose();
this._currentRealTimeTimer = undefined;
}
this._currentRealTimeTimer = {
callAt,
dispose: this._embedder.postTask(() => {
const now = Math.ceil(this._embedder.performanceNow());
const sinceLastSync = now - this._realTime!.lastSyncTicks;
this._realTime!.lastSyncTicks = now;
this._currentRealTimeTimer = undefined;
this.tick(sinceLastSync * this._realTime!.speed).then(() => this._ensureTimer());
}, (callAt - this._now.ticks) / this._realTime!.speed),
};
}
async runAll(): Promise<number> { async runAll(): Promise<number> {
for (let i = 0; i < this._loopLimit; i++) { for (let i = 0; i < this._loopLimit; i++) {
const numTimers = this._timers.size; const numTimers = this._timers.size;
@ -204,6 +239,8 @@ export class ClockController {
error: new Error(), error: new Error(),
}; };
this._timers.set(timer.id, timer); this._timers.set(timer.id, timer);
if (this._realTime)
this._ensureTimer();
return timer.id; return timer.id;
} }
@ -412,7 +449,7 @@ function parseTime(value: number | string): number {
function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date { function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
let prop; let prop;
for (prop of Object.keys(source) as (keyof DateConstructor)[]) for (prop of Object.getOwnPropertyNames(source) as (keyof DateConstructor)[])
target[prop] = source[prop]; target[prop] = source[prop];
target.toString = () => source.toString(); target.toString = () => source.toString();
target.prototype = source.prototype; target.prototype = source.prototype;
@ -485,7 +522,7 @@ function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Int
* All properties of Intl are non-enumerable, so we need * All properties of Intl are non-enumerable, so we need
* to do a bit of work to get them out. * to do a bit of work to get them out.
*/ */
for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[])
ClockIntl[key] = NativeIntl[key]; ClockIntl[key] = NativeIntl[key];
ClockIntl.DateTimeFormat = function(...args: any[]) { ClockIntl.DateTimeFormat = function(...args: any[]) {
@ -661,8 +698,10 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf
export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
const originals = platformOriginals(globalObject); const originals = platformOriginals(globalObject);
const embedder = { const embedder = {
postTask: (task: () => void) => { performanceNow: () => originals.raw.performance!.now(),
originals.bound.setTimeout(task, 0); postTask: (task: () => void, timeout?: number) => {
const timerId = originals.bound.setTimeout(task, timeout);
return () => originals.bound.clearTimeout(timerId);
}, },
postTaskPeriodically: (task: () => void, delay: number) => { postTaskPeriodically: (task: () => void, delay: number) => {
const intervalId = globalObject.setInterval(task, delay); const intervalId = globalObject.setInterval(task, delay);
@ -702,6 +741,9 @@ export function install(globalObject: WindowOrWorkerGlobalScope, config: Install
}); });
} }
if (typeof config.speed === 'number')
clock.startRealTime({ speed: config.speed });
return { clock, api, originals }; return { clock, api, originals };
} }

View file

@ -17271,6 +17271,8 @@ export interface Clock {
* [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`. * [clock.runAllTimers()](https://playwright.dev/docs/api/class-clock#clock-run-all-timers). Defaults to `1000`.
*/ */
loopLimit?: number; loopLimit?: number;
speed?: number;
}): Promise<void>; }): Promise<void>;
/** /**

View file

@ -1758,9 +1758,11 @@ export type BrowserContextUpdateSubscriptionResult = void;
export type BrowserContextClockInstallFakeTimersParams = { export type BrowserContextClockInstallFakeTimersParams = {
time: number, time: number,
loopLimit?: number, loopLimit?: number,
speed?: number,
}; };
export type BrowserContextClockInstallFakeTimersOptions = { export type BrowserContextClockInstallFakeTimersOptions = {
loopLimit?: number, loopLimit?: number,
speed?: number,
}; };
export type BrowserContextClockInstallFakeTimersResult = void; export type BrowserContextClockInstallFakeTimersResult = void;
export type BrowserContextClockRunAllTimersParams = {}; export type BrowserContextClockRunAllTimersParams = {};

View file

@ -1208,6 +1208,7 @@ BrowserContext:
parameters: parameters:
time: number time: number
loopLimit: number? loopLimit: number?
speed: number?
clockRunAllTimers: clockRunAllTimers:
returns: returns:

View file

@ -2326,6 +2326,40 @@ it.describe('Intl API', () => {
}); });
}); });
it.describe('startRealTime', () => {
it.only('should start real time', async ({ clock }) => {
clock.startRealTime({ speed: 2 });
const start = Date.now();
clock.setTimeout(() => {
const end = Date.now();
console.log('A', clock.Date.now(), end - start);
}, 1000);
clock.setTimeout(() => {
const end = Date.now();
console.log('B', clock.Date.now(), end - start);
}, 1100);
clock.setTimeout(() => {
const end = Date.now();
console.log('C', clock.Date.now(), end - start);
clock.setTimeout(() => {
const end = Date.now();
console.log('D', clock.Date.now(), end - start);
}, 400);
}, 300);
await new Promise(f => setTimeout(f, 2000));
});
it.only('should start real time interval', async ({ clock }) => {
clock.startRealTime({ speed: 2 });
const start = Date.now();
clock.setInterval(() => {
const end = Date.now();
console.log('A', clock.Date.now(), end - start);
}, 100);
await new Promise(f => setTimeout(f, 2000));
});
});
interface Stub { interface Stub {
called: boolean; called: boolean;
callCount: number; callCount: number;

View file

@ -738,4 +738,12 @@ it.describe('setTime', () => {
await page.clock.setTime(200); await page.clock.setTime(200);
expect(calls).toHaveLength(2); expect(calls).toHaveLength(2);
}); });
it.only('my clock', async ({ page }) => {
await page.clock.installFakeTimers(Date.now(), { speed: 0.5 });
await page.goto('https://www.online-stopwatch.com/large-online-clock/');
// await page.goto('https://www.example.com');
await new Promise(() => {});
});
}); });