/** * 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 { BrowserContext } from './browserContext'; import * as fakeTimersSource from '../generated/fakeTimersSource'; export class Clock { private _browserContext: BrowserContext; private _scriptInjected = false; private _fakeTimersInstalled = false; private _now = 0; constructor(browserContext: BrowserContext) { this._browserContext = browserContext; } async installFakeTimers(time: number, loopLimit: number | undefined) { await this._injectScriptIfNeeded(); await this._addAndEvaluate(`(() => { globalThis.__pwFakeTimers.clock?.uninstall(); globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, loopLimit })}); })();`); this._now = time; this._fakeTimersInstalled = true; } async runToNextTimer(): Promise { this._assertInstalled(); await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`); this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`); return this._now; } async runAllTimers(): Promise { this._assertInstalled(); await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runAll()`); this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runAllAsync()`); return this._now; } async runToLastTimer(): Promise { this._assertInstalled(); await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runToLast()`); this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runToLastAsync()`); return this._now; } async setTime(time: number) { if (this._fakeTimersInstalled) { const jump = time - this._now; if (jump < 0) throw new Error('Unable to set time into the past when fake timers are installed'); await this._addAndEvaluate(`globalThis.__pwFakeTimers.clock.jump(${jump})`); this._now = time; return this._now; } await this._injectScriptIfNeeded(); await this._addAndEvaluate(`(() => { globalThis.__pwFakeTimers.clock?.uninstall(); globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); })();`); this._now = time; return this._now; } async skipTime(time: number | string) { const delta = parseTime(time); await this.setTime(this._now + delta); return this._now; } async runFor(time: number | string): Promise { this._assertInstalled(); await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.tick(${JSON.stringify(time)})`); this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.tickAsync(${JSON.stringify(time)})`); return this._now; } private async _injectScriptIfNeeded() { if (this._scriptInjected) return; this._scriptInjected = true; const script = `(() => { const module = {}; ${fakeTimersSource.source} globalThis.__pwFakeTimers = (module.exports.inject())(); })();`; await this._addAndEvaluate(script); } 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._fakeTimersInstalled) throw new Error('Clock is not installed'); } } // Taken from sinonjs/fake-timerss-src. function parseTime(time: string | number): number { if (typeof time === 'number') return time; if (!time) return 0; const strings = time.split(':'); const l = strings.length; let i = l; let ms = 0; let parsed; if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(time)) throw new Error(`tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits`); while (i--) { parsed = parseInt(strings[i], 10); if (parsed >= 60) throw new Error(`Invalid time ${time}`); ms += parsed * Math.pow(60, l - i - 1); } return ms * 1000; }