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`.
### option: Clock.installFakeTimers.speed
* since: v1.45
- `speed` <[float]>
## async method: Clock.runAllTimers
* since: v1.45
- returns: <[int]>

View file

@ -24,9 +24,9 @@ export class Clock implements api.Clock {
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;
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> {

View file

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

View file

@ -27,11 +27,11 @@ export class Clock {
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._addAndEvaluate(`(() => {
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._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> {
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> {

View file

@ -31,6 +31,7 @@ export type ClockConfig = {
export type InstallConfig = ClockConfig & {
toFake?: (keyof ClockMethods)[];
speed?: number;
};
enum TimerType {
@ -53,7 +54,8 @@ type Timer = {
};
interface Embedder {
postTask(task: () => void): void;
performanceNow(): DOMHighResTimeStamp;
postTask(task: () => void, timeout?: number): () => void;
postTaskPeriodically(task: () => void, delay: number): () => void;
}
@ -66,6 +68,8 @@ export class ClockController {
private _uniqueTimerId = idCounterStart;
private _embedder: Embedder;
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) {
const start = Math.floor(getEpoch(startDate));
@ -145,6 +149,37 @@ export class ClockController {
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> {
for (let i = 0; i < this._loopLimit; i++) {
const numTimers = this._timers.size;
@ -204,6 +239,8 @@ export class ClockController {
error: new Error(),
};
this._timers.set(timer.id, timer);
if (this._realTime)
this._ensureTimer();
return timer.id;
}
@ -412,7 +449,7 @@ function parseTime(value: number | string): number {
function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date {
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.toString = () => source.toString();
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
* to do a bit of work to get them out.
*/
for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[])
for (const key of Object.getOwnPropertyNames(NativeIntl) as (keyof typeof Intl)[])
ClockIntl[key] = NativeIntl[key];
ClockIntl.DateTimeFormat = function(...args: any[]) {
@ -661,8 +698,10 @@ function fakePerformance(clock: ClockController, performance: Performance): Perf
export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
const originals = platformOriginals(globalObject);
const embedder = {
postTask: (task: () => void) => {
originals.bound.setTimeout(task, 0);
performanceNow: () => originals.raw.performance!.now(),
postTask: (task: () => void, timeout?: number) => {
const timerId = originals.bound.setTimeout(task, timeout);
return () => originals.bound.clearTimeout(timerId);
},
postTaskPeriodically: (task: () => void, delay: number) => {
const intervalId = globalObject.setInterval(task, delay);
@ -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 };
}

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`.
*/
loopLimit?: number;
speed?: number;
}): Promise<void>;
/**

View file

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

View file

@ -1208,6 +1208,7 @@ BrowserContext:
parameters:
time: number
loopLimit: number?
speed: number?
clockRunAllTimers:
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 {
called: boolean;
callCount: number;

View file

@ -738,4 +738,12 @@ it.describe('setTime', () => {
await page.clock.setTime(200);
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(() => {});
});
});