From 294f0523c793398361f02f59f433b4932a9d1de2 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 7 Jun 2024 08:51:09 -0700 Subject: [PATCH] wip --- docs/src/api/class-clock.md | 4 ++ packages/playwright-core/src/client/clock.ts | 4 +- .../playwright-core/src/protocol/validator.ts | 1 + packages/playwright-core/src/server/clock.ts | 4 +- .../dispatchers/browserContextDispatcher.ts | 2 +- .../src/server/injected/clock.ts | 52 +++++++++++++++++-- packages/playwright-core/types/types.d.ts | 2 + packages/protocol/src/channels.ts | 2 + packages/protocol/src/protocol.yml | 1 + tests/library/clock.spec.ts | 34 ++++++++++++ tests/page/page-clock.spec.ts | 8 +++ 11 files changed, 104 insertions(+), 10 deletions(-) diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md index 31115db41b..b4ca4a006b 100644 --- a/docs/src/api/class-clock.md +++ b/docs/src/api/class-clock.md @@ -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]> diff --git a/packages/playwright-core/src/client/clock.ts b/packages/playwright-core/src/client/clock.ts index 792e36a135..6d0b0b4db1 100644 --- a/packages/playwright-core/src/client/clock.ts +++ b/packages/playwright-core/src/client/clock.ts @@ -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 { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 0a60a92ebb..3d2556c716 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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({})); diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index dd7f194a1d..8e9cba8d42 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -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; diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index dd1f61f57b..40ee75d2e9 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -313,7 +313,7 @@ export class BrowserContextDispatcher extends Dispatcher { - 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 { diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 508d7a59e5..9836ca3072 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -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 { 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 }; } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 3753d59986..c716bd1678 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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; /** diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index c8523e8de5..dab7284496 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -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 = {}; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 7f6bd3bfe1..db55d2889c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1208,6 +1208,7 @@ BrowserContext: parameters: time: number loopLimit: number? + speed: number? clockRunAllTimers: returns: diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index 554b49d1a7..fefa34943f 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -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; diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index 4a6ca00896..71e75cd383 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -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(() => {}); + }); + });