From 826343b8a032eddb5e4a492625b0c35ec50ef775 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 6 Jun 2024 15:56:13 -0700 Subject: [PATCH] chore: rename fakeTimers to clock (#31193) --- docs/src/api/class-browsercontext.md | 2 +- docs/src/api/class-page.md | 2 +- packages/playwright-core/src/server/clock.ts | 40 +- .../injected/{fakeTimers.ts => clock.ts} | 272 +- .../src/server/injected/injectedScript.ts | 10 +- .../src/server/injected/utilityScript.ts | 28 +- packages/playwright-core/types/types.d.ts | 4 +- tests/library/clock.spec.ts | 3086 +++++++++++++++++ utils/generate_injected.js | 2 +- 9 files changed, 3223 insertions(+), 223 deletions(-) rename packages/playwright-core/src/server/injected/{fakeTimers.ts => clock.ts} (81%) create mode 100644 tests/library/clock.spec.ts diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 8cb11dbc0c..f9ab6694c3 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -102,7 +102,7 @@ context.BackgroundPage += (_, backgroundPage) => * since: v1.45 - type: <[Clock]> -Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. +Playwright has ability to mock clock and passage of time. ## event: BrowserContext.close * since: v1.8 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index b4ee91eb1b..6f8d04ceb4 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -155,7 +155,7 @@ page.Load -= PageLoadHandler; * since: v1.45 - type: <[Clock]> -Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. +Playwright has ability to mock clock and passage of time. ## event: Page.close * since: v1.8 diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index a366288476..09ac9cfec3 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -15,12 +15,12 @@ */ import type { BrowserContext } from './browserContext'; -import * as fakeTimersSource from '../generated/fakeTimersSource'; +import * as clockSource from '../generated/clockSource'; export class Clock { private _browserContext: BrowserContext; private _scriptInjected = false; - private _fakeTimersInstalled = false; + private _clockInstalled = false; private _now = 0; constructor(browserContext: BrowserContext) { @@ -30,48 +30,48 @@ export class Clock { 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 })}); + globalThis.__pwClock.clock?.uninstall(); + globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })}); })();`); this._now = time; - this._fakeTimersInstalled = true; + this._clockInstalled = true; } async runToNextTimer(): Promise { this._assertInstalled(); - await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`); - this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.next()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.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()`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runAll()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.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()`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runToLast()`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLastAsync()`); return this._now; } async setTime(time: number) { - if (this._fakeTimersInstalled) { + if (this._clockInstalled) { 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})`); + await this._addAndEvaluate(`globalThis.__pwClock.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'] })}); + globalThis.__pwClock.clock?.uninstall(); + globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); })();`); this._now = time; return this._now; @@ -85,8 +85,8 @@ export class Clock { 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)})`); + await this._browserContext.addInitScript(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`); + this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tickAsync(${JSON.stringify(time)})`); return this._now; } @@ -96,8 +96,8 @@ export class Clock { this._scriptInjected = true; const script = `(() => { const module = {}; - ${fakeTimersSource.source} - globalThis.__pwFakeTimers = (module.exports.inject())(globalThis); + ${clockSource.source} + globalThis.__pwClock = (module.exports.inject())(globalThis); })();`; await this._addAndEvaluate(script); } @@ -114,7 +114,7 @@ export class Clock { } private _assertInstalled() { - if (!this._fakeTimersInstalled) + if (!this._clockInstalled) throw new Error('Clock is not installed'); } } diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/clock.ts similarity index 81% rename from packages/playwright-core/src/server/injected/fakeTimers.ts rename to packages/playwright-core/src/server/injected/clock.ts index fbec480980..6d9f0a2036 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -10,7 +10,7 @@ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -type ClockMethods = { +export type ClockMethods = { Date: DateConstructor; setTimeout: Window['setTimeout']; clearTimeout: Window['clearTimeout']; @@ -24,12 +24,12 @@ type ClockMethods = { performance?: Window['performance']; }; -type ClockConfig = { +export type ClockConfig = { now?: number | Date; loopLimit?: number; }; -type InstallConfig = ClockConfig & { +export type InstallConfig = ClockConfig & { toFake?: (keyof ClockMethods)[]; }; @@ -44,7 +44,7 @@ enum TimerType { type Timer = { type: TimerType; func: TimerHandler; - args: any[]; + args: () => any[]; delay: number; callAt: number; createdAt: number; @@ -57,15 +57,13 @@ interface Embedder { postTaskPeriodically(task: () => void, delay: number): () => void; } -class Clock { +export class ClockController { readonly start: number; private _now: number; private _loopLimit: number; - private _jobs: Timer[] = []; private _adjustedSystemTime = 0; private _duringTick = false; private _timers = new Map(); - private _isNearInfiniteLimit = false; private _uniqueTimerId = idCounterStart; private _embedder: Embedder; readonly disposables: (() => void)[] = []; @@ -88,10 +86,7 @@ class Clock { } performanceNow(): DOMHighResTimeStamp { - const millisSinceStart = this._now - this._adjustedSystemTime - this.start; - const secsSinceStart = Math.floor(millisSinceStart / 1000); - const millis = secsSinceStart * 1000; - return millis; + return this._now - this._adjustedSystemTime - this.start; } private _doTick(tickValue: number | string, isAsync: boolean, resolve?: (time: number) => void, reject?: (error: Error) => void): number | undefined { @@ -116,13 +111,10 @@ class Clock { let compensationCheck: () => void; let postTimerCall: () => void; - /* eslint-enable prefer-const */ - this._duringTick = true; // perform microtasks oldNow = this._now; - this._runJobs(); if (oldNow !== this._now) { // compensate for any setSystemTime() call during microtask callback tickFrom += this._now - oldNow; @@ -138,7 +130,6 @@ class Clock { this._now = timer.callAt; oldNow = this._now; try { - this._runJobs(); this._callTimer(timer); } catch (e) { firstException = firstException || e; @@ -158,7 +149,6 @@ class Clock { // perform process.nextTick()s again oldNow = this._now; - this._runJobs(); if (oldNow !== this._now) { // compensate for any setSystemTime() call during process.nextTick() callback tickFrom += this._now - oldNow; @@ -220,20 +210,12 @@ class Clock { return this._doTick(tickValue, false)!; } - tickAsync(tickValue: string | number): Promise { - return new Promise((resolve, reject) => { - this._embedder.postTask(() => { - try { - this._doTick(tickValue, true, resolve, reject); - } catch (e) { - reject(e); - } - }); - }); + async tickAsync(tickValue: string | number): Promise { + await new Promise(f => this._embedder.postTask(f)); + return new Promise((resolve, reject) => this._doTick(tickValue, true, resolve, reject)); } next() { - this._runJobs(); const timer = this._firstTimer(); if (!timer) return this._now; @@ -242,117 +224,73 @@ class Clock { try { this._now = timer.callAt; this._callTimer(timer); - this._runJobs(); return this._now; } finally { this._duringTick = false; } } - nextAsync() { - return new Promise((resolve, reject) => { - this._embedder.postTask(() => { - try { - const timer = this._firstTimer(); - if (!timer) { - resolve(this._now); - return; - } + async nextAsync() { + await new Promise(f => this._embedder.postTask(f)); + const timer = this._firstTimer(); + if (!timer) + return this._now; - let err: Error; - this._duringTick = true; - this._now = timer.callAt; - try { - this._callTimer(timer); - } catch (e) { - err = e; - } - this._duringTick = false; + let err: Error | undefined; + this._duringTick = true; + this._now = timer.callAt; + try { + this._callTimer(timer); + } catch (e) { + err = e; + } + this._duringTick = false; - this._embedder.postTask(() => { - if (err) - reject(err); - else - resolve(this._now); - }); - } catch (e) { - reject(e); - } - }); - }); + await new Promise(f => this._embedder.postTask(f)); + if (err) + throw err; + return this._now; } runAll() { - this._runJobs(); for (let i = 0; i < this._loopLimit; i++) { const numTimers = this._timers.size; - if (numTimers === 0) { - this._resetIsNearInfiniteLimit(); + if (numTimers === 0) return this._now; - } - this.next(); - this._checkIsNearInfiniteLimit(i); } const excessJob = this._firstTimer(); - throw this._getInfiniteLoopError(excessJob!); + if (!excessJob) + return; + throw this._getInfiniteLoopError(excessJob); } runToFrame() { return this.tick(this.getTimeToNextFrame()); } - runAllAsync() { - return new Promise((resolve, reject) => { - let i = 0; - /** - * - */ - const doRun = () => { - this._embedder.postTask(() => { - try { - this._runJobs(); + async runAllAsync() { + for (let i = 0; i < this._loopLimit; i++) { + await new Promise(f => this._embedder.postTask(f)); + const numTimers = this._timers.size; + if (numTimers === 0) + return this._now; - let numTimers; - if (i < this._loopLimit) { - if (!this._timers) { - this._resetIsNearInfiniteLimit(); - resolve(this._now); - return; - } + this.next(); + } + await new Promise(f => this._embedder.postTask(f)); - numTimers = this._timers.size; - if (numTimers === 0) { - this._resetIsNearInfiniteLimit(); - resolve(this._now); - return; - } - - this.next(); - i++; - doRun(); - this._checkIsNearInfiniteLimit(i); - return; - } - - const excessJob = this._firstTimer(); - reject(this._getInfiniteLoopError(excessJob!)); - } catch (e) { - reject(e); - } - }); - }; - doRun(); - }); + const excessJob = this._firstTimer(); + if (!excessJob) + return; + throw this._getInfiniteLoopError(excessJob); } runToLast() { const timer = this._lastTimer(); - if (!timer) { - this._runJobs(); + if (!timer) return this._now; - } return this.tick(timer.callAt - this._now); } @@ -362,7 +300,6 @@ class Clock { try { const timer = this._lastTimer(); if (!timer) { - this._runJobs(); resolve(this._now); return; } @@ -376,7 +313,6 @@ class Clock { reset() { this._timers.clear(); - this._jobs = []; this._now = this.start; } @@ -410,34 +346,7 @@ class Clock { return this.tick(ms); } - private _checkIsNearInfiniteLimit(i: number): void { - if (this._loopLimit && i === this._loopLimit - 1) - this._isNearInfiniteLimit = true; - - } - - private _resetIsNearInfiniteLimit() { - this._isNearInfiniteLimit = false; - } - - private _runJobs() { - // runs all microtick-deferred tasks - ecma262/#sec-runjobs - if (!this._jobs) - return; - for (let i = 0; i < this._jobs.length; i++) { - const job = this._jobs[i]; - callFunction(job.func, job.args); - - this._checkIsNearInfiniteLimit(i); - if (this._loopLimit && i > this._loopLimit) - throw this._getInfiniteLoopError(job); - - } - this._resetIsNearInfiniteLimit(); - this._jobs = []; - } - - addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { + addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number { if (options.func === undefined) throw new Error('Callback must be provided to timer calls'); @@ -450,12 +359,12 @@ class Clock { const timer: Timer = { type: options.type, func: options.func, - args: options.args || [], + args: options.args || (() => []), delay, callAt: this._now + (delay || (this._duringTick ? 1 : 0)), createdAt: this._now, id: this._uniqueTimerId++, - error: this._isNearInfiniteLimit ? new Error() : undefined, + error: new Error(), }; this._timers.set(timer.id, timer); return timer.id; @@ -472,7 +381,7 @@ class Clock { } countTimers() { - return this._timers.size + this._jobs.length; + return this._timers.size; } private _firstTimer(): Timer | null { @@ -500,7 +409,7 @@ class Clock { this._timers.get(timer.id)!.callAt += timer.delay; else this._timers.delete(timer.id); - callFunction(timer.func, timer.args); + callFunction(timer.func, timer.args()); } private _getInfiniteLoopError(job: Timer) { @@ -548,14 +457,7 @@ class Clock { .slice(matchedLineIndex + 1) .join('\n')}`; - try { - Object.defineProperty(infiniteLoopError, 'stack', { - value: stack, - }); - } catch (e) { - // noop - } - + infiniteLoopError.stack = stack; return infiniteLoopError; } @@ -661,7 +563,7 @@ function mirrorDateProperties(target: any, source: typeof Date): DateConstructor return target; } -function createDate(clock: Clock, NativeDate: typeof Date): DateConstructor & Date { +function createDate(clock: ClockController, NativeDate: typeof Date): DateConstructor & Date { function ClockDate(this: typeof ClockDate, year: number, month: number, date: number, hour: number, minute: number, second: number, ms: number): Date | string { // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. // This remains so in the 10th edition of 2019 as well. @@ -717,7 +619,7 @@ function createDate(clock: Clock, NativeDate: typeof Date): DateConstructor & Da * but we need to take control of those that have a * dependency on the current clock. */ -function createIntl(clock: Clock, NativeIntl: typeof Intl): typeof Intl { +function createIntl(clock: ClockController, NativeIntl: typeof Intl): typeof Intl { const ClockIntl: any = {}; /* * All properties of Intl are non-enumerable, so we need @@ -726,7 +628,7 @@ function createIntl(clock: Clock, NativeIntl: typeof Intl): typeof Intl { for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) ClockIntl[key] = NativeIntl[key]; - ClockIntl.DateTimeFormat = (...args: any[]) => { + ClockIntl.DateTimeFormat = function(...args: any[]) { const realFormatter = new NativeIntl.DateTimeFormat(...args); const formatter: Intl.DateTimeFormat = { formatRange: realFormatter.formatRange.bind(realFormatter), @@ -787,20 +689,26 @@ function callFunction(func: TimerHandler, args: any[]) { const maxTimeout = Math.pow(2, 31) - 1; // see https://heycam.github.io/webidl/#abstract-opdef-converttoint const idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs -function platformOriginals(globalObject: WindowOrWorkerGlobalScope): ClockMethods { - return { - setTimeout: globalObject.setTimeout.bind(globalObject), - clearTimeout: globalObject.clearTimeout.bind(globalObject), - setInterval: globalObject.setInterval.bind(globalObject), - clearInterval: globalObject.clearInterval.bind(globalObject), - requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame.bind(globalObject) : undefined, - cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame.bind(globalObject) : undefined, - requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback.bind(globalObject) : undefined, - cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback.bind(globalObject) : undefined, +function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: ClockMethods, bound: ClockMethods } { + const raw: ClockMethods = { + setTimeout: globalObject.setTimeout, + clearTimeout: globalObject.clearTimeout, + setInterval: globalObject.setInterval, + clearInterval: globalObject.clearInterval, + requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame : undefined, + cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame : undefined, + requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback : undefined, + cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback : undefined, Date: (globalObject as any).Date, performance: globalObject.performance, Intl: (globalObject as any).Intl, }; + const bound = { ...raw }; + for (const key of Object.keys(bound) as (keyof ClockMethods)[]) { + if (key !== 'Date' && typeof bound[key] === 'function') + bound[key] = (bound[key] as any).bind(globalObject); + } + return { raw, bound }; } /** @@ -813,14 +721,14 @@ function getScheduleHandler(type: TimerType) { return `set${type}`; } -function createApi(clock: Clock, originals: ClockMethods): ClockMethods { +function createApi(clock: ClockController, originals: ClockMethods): ClockMethods { return { setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { const delay = timeout ? +timeout : timeout; return clock.addTimer({ type: TimerType.Timeout, func, - args, + args: () => args, delay }); }, @@ -833,7 +741,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods { return clock.addTimer({ type: TimerType.Interval, func, - args, + args: () => args, delay, }); }, @@ -846,9 +754,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods { type: TimerType.AnimationFrame, func: callback, delay: clock.getTimeToNextFrame(), - get args() { - return [clock.performanceNow()]; - }, + args: () => [clock.performanceNow()], }); }, cancelAnimationFrame: (timerId: number): void => { @@ -863,7 +769,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods { return clock.addTimer({ type: TimerType.IdleCallback, func: callback, - args: [], + args: () => [], delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod, }); }, @@ -884,33 +790,41 @@ function getClearHandler(type: TimerType) { return `clear${type}`; } -function fakePerformance(clock: Clock, performance: Performance): Performance { +function fakePerformance(clock: ClockController, performance: Performance): Performance { const result: any = { now: () => clock.performanceNow(), timeOrigin: clock.start, - __proto__: performance, }; + // eslint-disable-next-line no-proto + for (const key of Object.keys((performance as any).__proto__)) { + if (key === 'now' || key === 'timeOrigin') + continue; + if (key === 'getEntries' || key === 'getEntriesByName' || key === 'getEntriesByType') + result[key] = () => []; + else + result[key] = () => {}; + } return result; } -export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: Clock, api: Partial, originals: Partial } { +export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { const originals = platformOriginals(globalObject); const embedder = { postTask: (task: () => void) => { - originals.setTimeout!(task, 0); + originals.bound.setTimeout(task, 0); }, postTaskPeriodically: (task: () => void, delay: number) => { const intervalId = globalObject.setInterval(task, delay); - return () => originals.clearInterval!(intervalId); + return () => originals.bound.clearInterval(intervalId); }, }; - const clock = new Clock(embedder, config.now, config.loopLimit); - const api = createApi(clock, originals); - return { clock, api, originals }; + const clock = new ClockController(embedder, config.now, config.loopLimit); + const api = createApi(clock, originals.bound); + return { clock, api, originals: originals.raw }; } -export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: Clock, api: Partial, originals: Partial } { +export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } { if ((globalObject as any).Date?.isFake) { // Timers are already faked; this is a problem. // Make the user reset timers before continuing. @@ -946,6 +860,6 @@ export function inject(globalObject: WindowOrWorkerGlobalScope) { const { clock } = install(globalObject, config); return clock; }, - builtin: platformOriginals(globalObject), + builtin: platformOriginals(globalObject).bound, }; } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 347d5cb40c..b7a395cfed 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -125,14 +125,14 @@ export class InjectedScript { } builtinSetTimeout(callback: Function, timeout: number) { - if (this.window.__pwFakeTimers?.builtin) - return this.window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + if (this.window.__pwClock?.builtin) + return this.window.__pwClock.builtin.setTimeout(callback, timeout); return setTimeout(callback, timeout); } builtinRequestAnimationFrame(callback: FrameRequestCallback) { - if (this.window.__pwFakeTimers?.builtin) - return this.window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + if (this.window.__pwClock?.builtin) + return this.window.__pwClock.builtin.requestAnimationFrame(callback); return requestAnimationFrame(callback); } @@ -1525,7 +1525,7 @@ function deepEquals(a: any, b: any): boolean { declare global { interface Window { - __pwFakeTimers?: { + __pwClock?: { builtin: { setTimeout: Window['setTimeout'], requestAnimationFrame: Window['requestAnimationFrame'], diff --git a/packages/playwright-core/src/server/injected/utilityScript.ts b/packages/playwright-core/src/server/injected/utilityScript.ts index e8dbbc6ba1..7b046a529a 100644 --- a/packages/playwright-core/src/server/injected/utilityScript.ts +++ b/packages/playwright-core/src/server/injected/utilityScript.ts @@ -79,42 +79,42 @@ export class UtilityScript { // eslint-disable-next-line no-restricted-globals const window = (globalThis as any); window.builtinSetTimeout = (callback: Function, timeout: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.setTimeout(callback, timeout); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.setTimeout(callback, timeout); return setTimeout(callback, timeout); }; window.builtinClearTimeout = (id: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.clearTimeout(id); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.clearTimeout(id); return clearTimeout(id); }; window.builtinSetInterval = (callback: Function, timeout: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.setInterval(callback, timeout); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.setInterval(callback, timeout); return setInterval(callback, timeout); }; window.builtinClearInterval = (id: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.clearInterval(id); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.clearInterval(id); return clearInterval(id); }; window.builtinRequestAnimationFrame = (callback: FrameRequestCallback) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.requestAnimationFrame(callback); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.requestAnimationFrame(callback); return requestAnimationFrame(callback); }; window.builtinCancelAnimationFrame = (id: number) => { - if (window.__pwFakeTimers?.builtin) - return window.__pwFakeTimers.builtin.cancelAnimationFrame(id); + if (window.__pwClock?.builtin) + return window.__pwClock.builtin.cancelAnimationFrame(id); return cancelAnimationFrame(id); }; - window.builtinDate = window.__pwFakeTimers?.builtin.Date || Date; - window.builtinPerformance = window.__pwFakeTimers?.builtin.performance || performance; + window.builtinDate = window.__pwClock?.builtin.Date || Date; + window.builtinPerformance = window.__pwClock?.builtin.performance || performance; } } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index dc91c655f0..3753d59986 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -4864,7 +4864,7 @@ export interface Page { accessibility: Accessibility; /** - * Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. + * Playwright has ability to mock clock and passage of time. */ clock: Clock; @@ -8986,7 +8986,7 @@ export interface BrowserContext { /** - * Playwright is using [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) to fake timers and clock. + * Playwright has ability to mock clock and passage of time. */ clock: Clock; diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts new file mode 100644 index 0000000000..bb351186b5 --- /dev/null +++ b/tests/library/clock.spec.ts @@ -0,0 +1,3086 @@ +/** + * 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 '@playwright/test'; +import { createClock as rawCreateClock, install as rawInstall } from '../../packages/playwright-core/src/server/injected/clock'; +import type { InstallConfig, ClockController, ClockMethods } from '../../packages/playwright-core/src/server/injected/clock'; + +const createClock = (now?: Date | number, loopLimit?: number): ClockController & ClockMethods => { + const { clock, api } = rawCreateClock(globalThis, { now, loopLimit }); + for (const key of Object.keys(api)) + clock[key] = api[key]; + return clock as ClockController & ClockMethods; +}; + +type ClockFixtures = { + clock: ClockController & ClockMethods; + now: Date | number | undefined; + loopLimit: number | undefined; + install: (config?: InstallConfig) => ClockController & ClockMethods; + installEx: (config?: InstallConfig) => { clock: ClockController, api: ClockMethods, originals: ClockMethods }; +}; + +const it = test.extend({ + clock: async ({ now, loopLimit }, use) => { + const clock = createClock(now, loopLimit); + await use(clock); + }, + + now: undefined, + + loopLimit: undefined, + + install: async ({}, use) => { + let clockObject: ClockController & ClockMethods; + const install = (config?: InstallConfig) => { + const { clock, api } = rawInstall(globalThis, config); + for (const key of Object.keys(api)) + clock[key] = api[key]; + clockObject = clock as ClockController & ClockMethods; + return clockObject; + }; + await use(install); + clockObject?.uninstall(); + }, + + installEx: async ({}, use) => { + let clock: ClockController; + await use((config?: InstallConfig) => { + const result = rawInstall(globalThis, config); + clock = result.clock; + return result; + }); + clock?.uninstall(); + }, +}); + +it.describe('setTimeout', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.setTimeout(); + }).toThrow(); + }); + + it('returns numeric id or object with numeric id', async ({ clock }) => { + const result = clock.setTimeout(() => { }, 10); + expect(result).toEqual(expect.any(Number)); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.setTimeout(() => { }, 10); + const id2 = clock.setTimeout(() => { }, 10); + expect(id2).not.toBe(id1); + }); + + it('starts id from a large number', async ({ clock }) => { + const timer = clock.setTimeout(() => { }, 10); + expect(timer).toBeGreaterThanOrEqual(1e12); + }); + + it('sets timers on instance', async ({ clock }) => { + const clock1 = createClock(); + const clock2 = createClock(); + const stubs = [createStub(), createStub()]; + + clock1.setTimeout(stubs[0], 100); + clock2.setTimeout(stubs[1], 100); + clock2.tick(200); + + expect(stubs[0].called).toBeFalsy(); + expect(stubs[1].called).toBeTruthy(); + }); + + it('parses numeric string times', async ({ clock }) => { + let evalCalled = false; + clock.setTimeout(() => { + evalCalled = true; + // @ts-expect-error + }, '10'); + clock.tick(10); + expect(evalCalled).toBeTruthy(); + }); + + it('parses no-numeric string times', async ({ clock }) => { + let evalCalled = false; + clock.setTimeout(() => { + evalCalled = true; + // @ts-expect-error + }, 'string'); + clock.tick(10); + + expect(evalCalled).toBeTruthy(); + }); + + it('passes setTimeout parameters', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 2, 'the first', 'the second'); + clock.tick(3); + expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); + }); + + it('calls correct timeout on recursive tick', async ({ clock }) => { + const stub = createStub(); + const recurseCallback = () => { + clock.tick(100); + }; + + clock.setTimeout(recurseCallback, 50); + clock.setTimeout(stub, 100); + + clock.tick(50); + expect(stub.called).toBeTruthy(); + }); + + it('does not depend on this', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + clock.tick(100); + expect(stub.called).toBeTruthy(); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 5000); + clock.tick(1000); + clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.tick(3990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by backward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 5000); + clock.tick(1000); + clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.tick(3990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('handles Infinity and negative Infinity correctly', async ({ clock }) => { + const calls = []; + clock.setTimeout(() => { + calls.push('NaN'); + }, NaN); + clock.setTimeout(() => { + calls.push('Infinity'); + }, Number.POSITIVE_INFINITY); + clock.setTimeout(() => { + calls.push('-Infinity'); + }, Number.NEGATIVE_INFINITY); + clock.runAll(); + expect(calls).toEqual(['NaN', 'Infinity', '-Infinity']); + }); + + it.describe('use of eval when not in node', () => { + it.beforeEach(() => { + globalThis.evalCalled = false; + }); + + it.afterEach(() => { + delete globalThis.evalCalled.evalCalled; + }); + + it('evals non-function callbacks', async ({ clock }) => { + clock.setTimeout('globalThis.evalCalled = true', 10); + clock.tick(10); + + expect(globalThis.evalCalled).toBeTruthy(); + }); + + it('only evals on global scope', async ({ clock }) => { + const x = 15; + try { + clock.setTimeout('x', x); + clock.tick(x); + expect(true).toBeFalsy(); + } catch (e) { + expect(e).toBeInstanceOf(ReferenceError); + } + }); + }); +}); + +it.describe('tick', () => { + it('triggers immediately without specified delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub); + clock.tick(0); + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger without sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + clock.tick(10); + expect(stub.called).toBeFalsy(); + }); + + it('triggers after sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + clock.tick(100); + expect(stub.called).toBeTruthy(); + }); + + it('triggers simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.tick(100); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + clock.tick(100); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + expect(spies[3].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + clock.tick(10); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('waits after setTimeout was called', async ({ clock }) => { + clock.tick(100); + const stub = createStub(); + clock.setTimeout(stub, 150); + clock.tick(50); + expect(stub.called).toBeFalsy(); + clock.tick(100); + expect(stub.called).toBeTruthy(); + }); + + it('mini integration test', async ({ clock }) => { + const stubs = [createStub(), createStub(), createStub()]; + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + clock.tick(10); + clock.tick(89); + expect(stubs[0].called).toBeFalsy(); + expect(stubs[1].called).toBeFalsy(); + clock.setTimeout(stubs[2], 20); + clock.tick(1); + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeFalsy(); + clock.tick(19); + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeTruthy(); + clock.tick(1); + expect(stubs[1].called).toBeTruthy(); + }); + + it('triggers even when some throw', async ({ clock }) => { + const stubs = [createStub().throws(), createStub()]; + + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + + expect(() => { + clock.tick(120); + }).toThrow(); + + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + + expect(() => { + clock.tick(100); + }).toThrow(); + + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('triggers in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + clock.tick(15); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + clock.tick(100); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('fires timer in intervals of 13', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 13); + clock.tick(500); + expect(spy.callCount).toBe(38); + }); + + it('fires timer in intervals of "13"', async ({ clock }) => { + const spy = createStub(); + // @ts-expect-error + clock.setInterval(spy, '13'); + clock.tick(500); + expect(spy.callCount).toBe(38); + }); + + it('fires timers in correct order', async ({ clock }) => { + const spy13 = createStub(); + const spy10 = createStub(); + + clock.setInterval(() => { + spy13(new clock.Date().getTime()); + }, 13); + + clock.setInterval(() => { + spy10(new clock.Date().getTime()); + }, 10); + + clock.tick(500); + + expect(spy13.callCount).toBe(38); + expect(spy10.callCount).toBe(50); + + expect(spy13.calledWith(416)).toBeTruthy(); + expect(spy10.calledWith(320)).toBeTruthy(); + }); + + it('triggers timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + clock.tick(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(10); + expect(spies[1].callCount).toBe(1); + }); + + it('does not fire canceled intervals', async ({ clock }) => { + // eslint-disable-next-line prefer-const + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + clock.tick(100); + + expect(callback.callCount).toBe(3); + }); + + it('passes 8 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 4000); + + clock.tick('08'); + + expect(spy.callCount).toBe(2); + }); + + it('passes 1 minute', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 6000); + + clock.tick('01:00'); + + expect(spy.callCount).toBe(10); + }); + + it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + clock.tick('02:34:10'); + + expect(spy.callCount).toBe(925); + }); + + it('throws for invalid format', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + expect(() => { + clock.tick('12:02:34:10'); + }).toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('throws for invalid minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + expect(() => { + clock.tick('67:10'); + }).toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('throws for negative minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + expect(() => { + clock.tick('-7:10'); + }).toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('treats missing argument as 0', async ({ clock }) => { + // @ts-expect-error + clock.tick(); + + expect(clock.now()).toBe(0); + }); + + it('fires nested setTimeout calls properly', async ({ clock }) => { + let i = 0; + const callback = () => { + ++i; + clock.setTimeout(() => { + callback(); + }, 100); + }; + + callback(); + + clock.tick(1000); + + expect(i).toBe(11); + }); + + it('does not silently catch errors', async ({ clock }) => { + const callback = () => { + throw new Error('oh no!'); + }; + + clock.setTimeout(callback, 1000); + + expect(() => { + clock.tick(1000); + }).toThrow(); + }); + + it('returns the current now value', async ({ clock }) => { + const value = clock.tick(200); + expect(clock.now()).toBe(value); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + clock.tick(1990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes 2', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() - 1000); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + clock.tick(1990); + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + throw new Error(); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + + expect(() => { + clock.tick(1990); + }).toThrow(); + + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes when an error is thrown 2', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() - 1000); + throw new Error(); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + + expect(() => { + clock.tick(1990); + }).toThrow(); + + expect(stub.callCount).toBe(0); + clock.tick(20); + expect(stub.callCount).toBe(1); + }); + + it('throws on negative ticks', async ({ clock }) => { + expect(() => { + clock.tick(-500); + }).toThrow('Negative ticks are not supported'); + }); +}); + +it.describe('tickAsync', () => { + it('triggers immediately without specified delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub); + + await clock.tickAsync(0); + + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger without sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + await clock.tickAsync(10); + + expect(stub.called).toBeFalsy(); + }); + + it('triggers after sufficient delay', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + await clock.tickAsync(100); + + expect(stub.called).toBeTruthy(); + }); + + it('triggers simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + expect(spies[3].called).toBeTruthy(); + }); + + it('triggers multiple simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + + await clock.tickAsync(10); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('waits after setTimeout was called', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 150); + + await clock.tickAsync(50); + + expect(stub.called).toBeFalsy(); + + await clock.tickAsync(100); + + expect(stub.called).toBeTruthy(); + }); + + it('mini integration test', async ({ clock }) => { + const stubs = [createStub(), createStub(), createStub()]; + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + + await clock.tickAsync(10); + await clock.tickAsync(89); + + expect(stubs[0].called).toBeFalsy(); + expect(stubs[1].called).toBeFalsy(); + + clock.setTimeout(stubs[2], 20); + await clock.tickAsync(1); + + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeFalsy(); + + await clock.tickAsync(19); + + expect(stubs[1].called).toBeFalsy(); + expect(stubs[2].called).toBeTruthy(); + + await clock.tickAsync(1); + + expect(stubs[1].called).toBeTruthy(); + }); + + it('triggers even when some throw', async ({ clock }) => { + const stubs = [createStub().throws(), createStub()]; + + clock.setTimeout(stubs[0], 100); + clock.setTimeout(stubs[1], 120); + + await expect(clock.tickAsync(120)).rejects.toThrow(); + + expect(stubs[0].called).toBeTruthy(); + expect(stubs[1].called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + + await expect(clock.tickAsync(100)).rejects.toThrow(); + + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('triggers in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + await clock.tickAsync(15); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + await clock.tickAsync(100); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('creates updated Date while ticking promises', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + void Promise.resolve().then(() => { + spy(new clock.Date().getTime()); + }); + }, 10); + + await clock.tickAsync(100); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('fires timer in intervals of 13', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 13); + + await clock.tickAsync(500); + + expect(spy.callCount).toBe(38); + }); + + it('fires timers in correct order', async ({ clock }) => { + const spy13 = createStub(); + const spy10 = createStub(); + + clock.setInterval(() => { + spy13(new clock.Date().getTime()); + }, 13); + + clock.setInterval(() => { + spy10(new clock.Date().getTime()); + }, 10); + + await clock.tickAsync(500); + + expect(spy13.callCount).toBe(38); + expect(spy10.callCount).toBe(50); + + expect(spy13.calledWith(416)).toBeTruthy(); + expect(spy10.calledWith(320)).toBeTruthy(); + }); + + it('fires promise timers in correct order', async ({ clock }) => { + const spy13 = createStub(); + const spy10 = createStub(); + + clock.setInterval(() => { + void Promise.resolve().then(() => { + spy13(new clock.Date().getTime()); + }); + }, 13); + + clock.setInterval(() => { + void Promise.resolve().then(() => { + spy10(new clock.Date().getTime()); + }); + }, 10); + + await clock.tickAsync(500); + + expect(spy13.callCount).toBe(38); + expect(spy10.callCount).toBe(50); + + expect(spy13.calledWith(416)).toBeTruthy(); + expect(spy10.calledWith(320)).toBeTruthy(); + }); + + it('triggers timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + await clock.tickAsync(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(10); + expect(spies[1].callCount).toBe(1); + }); + + it('does not fire canceled intervals', async ({ clock }) => { + // eslint-disable-next-line prefer-const + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + await clock.tickAsync(100); + + expect(callback.callCount).toBe(3); + }); + + it('does not fire intervals canceled in a promise', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) { + void Promise.resolve().then(() => { + clock.clearInterval(id); + }); + } + }); + + id = clock.setInterval(callback, 10); + await clock.tickAsync(100); + + expect(callback.callCount).toBe(3); + }); + + it('passes 8 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 4000); + + await clock.tickAsync('08'); + + expect(spy.callCount).toBe(2); + }); + + it('passes 1 minute', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 6000); + + await clock.tickAsync('01:00'); + + expect(spy.callCount).toBe(10); + }); + + it('passes 2 hours, 34 minutes and 10 seconds', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + await clock.tickAsync('02:34:10'); + + expect(spy.callCount).toBe(925); + }); + + it('throws for invalid format', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + await expect(clock.tickAsync('12:02:34:10')).rejects.toThrow(); + expect(spy.callCount).toBe(0); + }); + + it('throws for invalid minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + await expect(clock.tickAsync('67:10')).rejects.toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('throws for negative minutes', async ({ clock }) => { + const spy = createStub(); + clock.setInterval(spy, 10000); + + await expect(clock.tickAsync('-7:10')).rejects.toThrow(); + + expect(spy.callCount).toBe(0); + }); + + it('treats missing argument as 0', async ({ clock }) => { + // @ts-expect-error + await clock.tickAsync(); + + expect(clock.now()).toBe(0); + }); + + it('fires nested setTimeout calls properly', async ({ clock }) => { + let i = 0; + const callback = () => { + ++i; + clock.setTimeout(() => { + callback(); + }, 100); + }; + + callback(); + + await clock.tickAsync(1000); + + expect(i).toBe(11); + }); + + it('fires nested setTimeout calls in user-created promises properly', async ({ clock }) => { + let i = 0; + const callback = () => { + void Promise.resolve().then(() => { + ++i; + clock.setTimeout(() => { + void Promise.resolve().then(() => { + callback(); + }); + }, 100); + }); + }; + + callback(); + + await clock.tickAsync(1000); + + expect(i).toBe(11); + }); + + it('does not silently catch errors', async ({ clock }) => { + const callback = () => { + throw new Error('oh no!'); + }; + + clock.setTimeout(callback, 1000); + + await expect(clock.tickAsync(1000)).rejects.toThrow(); + }); + + it('returns the current now value', async ({ clock }) => { + const value = await clock.tickAsync(200); + expect(clock.now()).toBe(value); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + await clock.tickAsync(1990); + expect(stub.callCount).toBe(0); + await clock.tickAsync(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes in promises', async ({ clock }) => { + const callback = () => { + void Promise.resolve().then(() => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + }); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + await clock.tickAsync(1990); + expect(stub.callCount).toBe(0); + await clock.tickAsync(20); + expect(stub.callCount).toBe(1); + }); + + it('is not influenced by forward system clock changes when an error is thrown', async ({ clock }) => { + const callback = () => { + clock.setSystemTime(new clock.Date().getTime() + 1000); + throw new Error(); + }; + const stub = createStub(); + clock.setTimeout(callback, 1000); + clock.setTimeout(stub, 2000); + + await expect(clock.tickAsync(1990)).rejects.toThrow(); + + expect(stub.callCount).toBe(0); + await clock.tickAsync(20); + expect(stub.callCount).toBe(1); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 100); + + await clock.tickAsync(100); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle chained user-created promises', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.resolve() + .then(spies[0]) + .then(spies[1]) + .then(spies[2]); + }, 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('should settle multiple user-created promises', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + void Promise.resolve().then(spies[1]); + void Promise.resolve().then(spies[2]); + }, 100); + + await clock.tickAsync(100); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 100); + + await clock.tickAsync(100); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle user-created promises even if some throw', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.reject().then(spies[0]).catch(spies[1]); + void Promise.resolve().then(spies[2]).catch(spies[3]); + }, 100); + + await clock.tickAsync(100); + + expect(spies[0].callCount).toBe(0); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + expect(spies[3].callCount).toBe(0); + }); + + it('should settle user-created promises before calling more timeouts', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + }, 100); + + clock.setTimeout(spies[1], 200); + + await clock.tickAsync(200); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle local promises before calling timeouts', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + void Promise.resolve().then(spies[0]); + + clock.setTimeout(spies[1], 100); + + await clock.tickAsync(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle local nested promises before calling timeouts', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spies[0]); + }); + }); + + clock.setTimeout(spies[1], 100); + + await clock.tickAsync(100); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('next', () => { + it('triggers the next timer', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + clock.next(); + + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + + clock.next(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('subsequent calls trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + + clock.next(); + + expect(spies[2].called).toBeTruthy(); + expect(spies[0].called).toBeFalsy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + clock.next(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + clock.next(); + + expect(spies[1].called).toBeTruthy(); + expect(spies[3].called).toBeFalsy(); + + clock.next(); + + expect(spies[3].called).toBeTruthy(); + }); + + it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + + clock.next(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + + clock.next(); + + expect(spies[1].called).toBeTruthy(); + + clock.next(); + + expect(spies[2].called).toBeTruthy(); + }); + + it('throws exception thrown by timer', async ({ clock }) => { + const stub = createStub().throws(); + + clock.setTimeout(stub, 100); + + expect(() => { + clock.next(); + }).toThrow(); + + expect(stub.called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + + expect(() => { + clock.next(); + }).toThrow(); + + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('subsequent calls trigger in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + clock.next(); + clock.next(); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(5); + expect(spies[1].callCount).toBe(1); + }); + + it('subsequent calls do not fire canceled intervals', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + clock.next(); + clock.next(); + clock.next(); + clock.next(); + + expect(callback.callCount).toBe(3); + }); + + it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + clock.next(); + + expect(clock.now()).toBe(55); + }); + + it('returns the current now value', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + const value = clock.next(); + + expect(clock.now()).toBe(value); + }); +}); + +it.describe('nextAsync', () => { + it('triggers the next timer', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100); + + await clock.nextAsync(); + + expect(stub.called).toBeTruthy(); + }); + + it('does not trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + + await clock.nextAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('subsequent calls trigger simultaneous timers', async ({ clock }) => { + const spies = [createStub(), createStub(), createStub(), createStub()]; + clock.setTimeout(spies[0], 100); + clock.setTimeout(spies[1], 100); + clock.setTimeout(spies[2], 99); + clock.setTimeout(spies[3], 100); + + await clock.nextAsync(); + + expect(spies[2].called).toBeTruthy(); + expect(spies[0].called).toBeFalsy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + expect(spies[3].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[1].called).toBeTruthy(); + expect(spies[3].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[3].called).toBeTruthy(); + }); + + it('subsequent calls trigger simultaneous timers with zero callAt', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 0); + }), + createStub(), + createStub(), + ]; + + // First spy calls another setTimeout with delay=0 + clock.setTimeout(spies[0], 0); + clock.setTimeout(spies[2], 10); + + await clock.nextAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + + await clock.nextAsync(); + + expect(spies[1].called).toBeTruthy(); + + await clock.nextAsync(); + + expect(spies[2].called).toBeTruthy(); + }); + + it('throws exception thrown by timer', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + await expect(clock.nextAsync()).rejects.toThrow(); + expect(stub.called).toBeTruthy(); + }); + + it('calls function with global object or null (strict mode) as this', async ({ clock }) => { + const stub = createStub().throws(); + clock.setTimeout(stub, 100); + await expect(clock.nextAsync()).rejects.toThrow(); + expect(stub.calledOn(global) || stub.calledOn(null)).toBeTruthy(); + }); + + it('subsequent calls trigger in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 13); + clock.setTimeout(spies[1], 11); + + await clock.nextAsync(); + await clock.nextAsync(); + + expect(spies[1].calledBefore(spies[0])).toBeTruthy(); + }); + + it('creates updated Date while ticking', async ({ clock }) => { + const spy = createStub(); + + clock.setInterval(() => { + spy(new clock.Date().getTime()); + }, 10); + + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(spy.callCount).toBe(10); + expect(spy.calledWith(10)).toBeTruthy(); + expect(spy.calledWith(20)).toBeTruthy(); + expect(spy.calledWith(30)).toBeTruthy(); + expect(spy.calledWith(40)).toBeTruthy(); + expect(spy.calledWith(50)).toBeTruthy(); + expect(spy.calledWith(60)).toBeTruthy(); + expect(spy.calledWith(70)).toBeTruthy(); + expect(spy.calledWith(80)).toBeTruthy(); + expect(spy.calledWith(90)).toBeTruthy(); + expect(spy.calledWith(100)).toBeTruthy(); + }); + + it('subsequent calls trigger timeouts and intervals in the order scheduled', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setInterval(spies[0], 10); + clock.setTimeout(spies[1], 50); + + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + expect(spies[0].callCount).toBe(5); + expect(spies[1].callCount).toBe(1); + }); + + it('does not fire canceled intervals', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) + clock.clearInterval(id); + }); + + id = clock.setInterval(callback, 10); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(callback.callCount).toBe(3); + }); + + it('does not fire intervals canceled in promises', async ({ clock }) => { + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let id; + const callback = createStub(() => { + if (callback.callCount === 3) { + void Promise.resolve().then(() => { + clock.clearInterval(id); + }); + } + }); + + id = clock.setInterval(callback, 10); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + await clock.nextAsync(); + + expect(callback.callCount).toBe(3); + }); + + it('advances the clock based on when the timer was supposed to be called', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + await clock.nextAsync(); + + expect(clock.now()).toBe(55); + }); + + it('returns the current now value', async ({ clock }) => { + clock.setTimeout(createStub(), 55); + const value = await clock.nextAsync(); + + expect(clock.now()).toBe(value); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 55); + + await clock.nextAsync(); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 55); + + await clock.nextAsync(); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle local promises before firing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + + void Promise.resolve().then(spies[0]); + + clock.setTimeout(spies[1], 55); + + await clock.nextAsync(); + + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('runAll', () => { + it('if there are no timers just return', async ({ clock }) => { + clock.runAll(); + }); + + it('runs all timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + + clock.runAll(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added while running are also run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + + clock.runAll(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('throws before allowing infinite recursion', async ({ clock }) => { + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 10); + }; + recursiveCallback(); + expect(() => clock.runAll()).toThrow(); + }); + + it('the loop limit can be set when creating a clock', async ({}) => { + const clock = createClock(0, 1); + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + expect(() => clock.runAll()).toThrow(); + }); + + it('the loop limit can be set when installing a clock', async ({ install }) => { + const clock = install({ loopLimit: 1 }); + const spies = [createStub(), createStub()]; + setTimeout(spies[0], 10); + setTimeout(spies[1], 50); + + expect(() => clock.runAll()).toThrow(); + }); +}); + +it.describe('runAllAsync', () => { + it('if there are no timers just return', async ({ clock }) => { + await clock.runAllAsync(); + }); + + it('runs all timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + + await clock.runAllAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added while running are also run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + + await clock.runAllAsync(); + + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added in promises while running are also run', async ({ clock }) => { + const spies = [ + createStub(() => { + void Promise.resolve().then(() => { + clock.setTimeout(spies[1], 50); + }); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + await clock.runAllAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('throws before allowing infinite recursion', async ({ clock }) => { + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 10); + }; + recursiveCallback(); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('throws before allowing infinite recursion from promises', async ({ clock }) => { + const recursiveCallback = () => { + void Promise.resolve().then(() => { + clock.setTimeout(recursiveCallback, 10); + }); + }; + recursiveCallback(); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('the loop limit can be set when creating a clock', async ({}) => { + const clock = createClock(0, 1); + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('the loop limit can be set when installing a clock', async ({ install }) => { + const clock = install({ loopLimit: 1 }); + const spies = [createStub(), createStub()]; + setTimeout(spies[0], 10); + setTimeout(spies[1], 50); + await expect(clock.runAllAsync()).rejects.toThrow(); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 55); + await clock.runAllAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 55); + + await clock.runAllAsync(); + + expect(spy.called).toBeTruthy(); + }); + + it('should settle local promises before firing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + void Promise.resolve().then(spies[0]); + clock.setTimeout(spies[1], 55); + await clock.runAllAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle user-created promises before firing more timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + }, 55); + clock.setTimeout(spies[1], 75); + await clock.runAllAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('runToLast', () => { + it('returns current time when there are no timers', async ({ clock }) => { + const time = clock.runToLast(); + expect(time).toBe(0); + }); + + it('runs all existing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('returns time of the last timer', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + const time = clock.runToLast(); + expect(time).toBe(50); + }); + + it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 10); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { + const spies = [ + createStub(), + createStub(() => { + clock.setTimeout(spies[2], 50); + }), + createStub(), + ]; + + clock.setTimeout(spies[0], 100); + // Spy calls another setTimeout + clock.setTimeout(spies[1], 10); + clock.runToLast(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('new timers cannot cause an infinite loop', async ({ clock }) => { + const spy = createStub(); + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 0); + }; + + clock.setTimeout(recursiveCallback, 0); + clock.setTimeout(spy, 100); + clock.runToLast(); + expect(spy.called).toBeTruthy(); + }); + + it('should support clocks with start time', async ({ clock }) => { + let invocations = 0; + + clock.setTimeout(function cb() { + invocations++; + clock.setTimeout(cb, 50); + }, 50); + + clock.runToLast(); + + expect(invocations).toBe(1); + }); +}); + +it.describe('runToLastAsync', () => { + it('returns current time when there are no timers', async ({ clock }) => { + const time = await clock.runToLastAsync(); + expect(time).toBe(0); + }); + + it('runs all existing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('returns time of the last timer', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 50); + const time = await clock.runToLastAsync(); + expect(time).toBe(50); + }); + + it('runs all existing timers when two timers are matched for being last', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(spies[0], 10); + clock.setTimeout(spies[1], 10); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + }); + + it('new timers added with a call time later than the last existing timer are NOT run', async ({ clock }) => { + const spies = [ + createStub(() => { + clock.setTimeout(spies[1], 50); + }), + createStub(), + ]; + + // Spy calls another setTimeout + clock.setTimeout(spies[0], 10); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeFalsy(); + }); + + it('new timers added with a call time earlier than the last existing timer are run', async ({ clock }) => { + const spies = [ + createStub(), + createStub(() => { + clock.setTimeout(spies[2], 50); + }), + createStub(), + ]; + + clock.setTimeout(spies[0], 100); + // Spy calls another setTimeout + clock.setTimeout(spies[1], 10); + await clock.runToLastAsync(); + expect(spies[0].called).toBeTruthy(); + expect(spies[1].called).toBeTruthy(); + expect(spies[2].called).toBeTruthy(); + }); + + it('new timers cannot cause an infinite loop', async ({ clock }) => { + const spy = createStub(); + const recursiveCallback = () => { + clock.setTimeout(recursiveCallback, 0); + }; + + clock.setTimeout(recursiveCallback, 0); + clock.setTimeout(spy, 100); + await clock.runToLastAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle user-created promises', async ({ clock }) => { + const spy = createStub(); + clock.setTimeout(() => { + void Promise.resolve().then(spy); + }, 55); + await clock.runToLastAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle nested user-created promises', async ({ clock }) => { + const spy = createStub(); + + clock.setTimeout(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(() => { + void Promise.resolve().then(spy); + }); + }); + }, 55); + + await clock.runToLastAsync(); + expect(spy.called).toBeTruthy(); + }); + + it('should settle local promises before firing timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + void Promise.resolve().then(spies[0]); + clock.setTimeout(spies[1], 55); + await clock.runToLastAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); + + it('should settle user-created promises before firing more timers', async ({ clock }) => { + const spies = [createStub(), createStub()]; + clock.setTimeout(() => { + void Promise.resolve().then(spies[0]); + }, 55); + clock.setTimeout(spies[1], 75); + await clock.runToLastAsync(); + expect(spies[0].calledBefore(spies[1])).toBeTruthy(); + }); +}); + +it.describe('clearTimeout', () => { + it('removes timeout', async ({ clock }) => { + const stub = createStub(); + const id = clock.setTimeout(stub, 50); + clock.clearTimeout(id); + await clock.tickAsync(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub, 50); + clock.clearTimeout(id); + await clock.tickAsync(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes interval with undefined interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub); + clock.clearTimeout(id); + await clock.tickAsync(50); + expect(stub.called).toBeFalsy(); + }); + + it('ignores null argument', async ({ clock }) => { + clock.clearTimeout(null); + }); +}); + +it.describe('reset', () => { + it('resets to the time install with - issue #183', async ({ clock }) => { + clock.tick(100); + clock.reset(); + expect(clock.now()).toBe(0); + }); + + it('resets hrTime - issue #206', async ({ clock }) => { + clock.tick(100); + expect(clock.performance.now()).toEqual(100); + clock.reset(); + expect(clock.performance.now()).toEqual(0); + }); +}); + +it.describe('setInterval', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.setInterval(); + }).toThrow(); + }); + + it('returns numeric id or object with numeric id', async ({ clock }) => { + const result = clock.setInterval(() => {}, 10); + expect(result).toBeGreaterThan(0); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.setInterval(() => {}, 10); + const id2 = clock.setInterval(() => {}, 10); + + expect(id2).not.toEqual(id1); + }); + + it('schedules recurring timeout', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 10); + clock.tick(99); + + expect(stub.callCount).toBe(9); + }); + + it('is not influenced by forward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 10); + clock.tick(11); + expect(stub.callCount).toBe(1); + clock.setSystemTime(new clock.Date().getTime() + 1000); + clock.tick(8); + expect(stub.callCount).toBe(1); + clock.tick(3); + expect(stub.callCount).toBe(2); + }); + + it('is not influenced by backward system clock changes', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 10); + clock.tick(5); + clock.setSystemTime(new clock.Date().getTime() - 1000); + clock.tick(6); + expect(stub.callCount).toBe(1); + clock.tick(10); + expect(stub.callCount).toBe(2); + }); + + it('does not schedule recurring timeout when cleared', async ({ clock }) => { + const stub = createStub(() => { + if (stub.callCount === 3) + clock.clearInterval(id); + }); + + const id = clock.setInterval(stub, 10); + clock.tick(100); + + expect(stub.callCount).toBe(3); + }); + + it('passes setTimeout parameters', async ({ clock }) => { + const stub = createStub(); + clock.setInterval(stub, 2, 'the first', 'the second'); + clock.tick(3); + expect(stub.calledWithExactly('the first', 'the second')).toBeTruthy(); + }); +}); + +it.describe('clearInterval', () => { + it('removes interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub, 50); + clock.clearInterval(id); + clock.tick(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes interval with undefined interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub); + clock.clearInterval(id); + clock.tick(50); + expect(stub.called).toBeFalsy(); + }); + + it('removes timeout', async ({ clock }) => { + const stub = createStub(); + const id = clock.setTimeout(stub, 50); + clock.clearInterval(id); + clock.tick(50); + expect(stub.called).toBeFalsy(); + }); + + it('ignores null argument', async ({ clock }) => { + clock.clearInterval(null); + }); +}); + +it.describe('date', () => { + it('provides date constructor', async ({ clock }) => { + expect(clock.Date).toEqual(expect.any(Function)); + }); + + it('creates real Date objects', async ({ clock }) => { + const date = new clock.Date(); + expect(Date.prototype.isPrototypeOf(date)).toBeTruthy(); + }); + + it('returns date as string when called as function', async ({ clock }) => { + const date = clock.Date(); + expect(typeof date).toBe('string'); + }); + + it('creates Date objects representing clock time', async ({ clock }) => { + const date = new clock.Date(); + expect(date.getTime()).toBe(new Date(clock.now()).getTime()); + }); + + it('returns date as string representing clock time', async ({ clock }) => { + const date = clock.Date(); + expect(date).toBe(new Date(clock.now()).toString()); + }); + + it('listens to ticking clock', async ({ clock }) => { + const date1 = new clock.Date(); + clock.tick(3); + const date2 = new clock.Date(); + expect(date2.getTime() - date1.getTime()).toBe(3); + }); + + it('listens to system clock changes', async ({ clock }) => { + const date1 = new clock.Date(); + clock.setSystemTime(date1.getTime() + 1000); + const date2 = new clock.Date(); + expect(date2.getTime() - date1.getTime()).toBe(1000); + }); + + it('creates regular date when passing timestamp', async ({ clock }) => { + const date = new Date(); + const fakeDate = new clock.Date(date.getTime()); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing a date as string', async ({ clock }) => { + const date = new Date(); + const fakeDate = new clock.Date(date.toISOString()); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing a date as RFC 2822 string', async ({ clock }) => { + const date = new Date('Sat Apr 12 2014 12:22:00 GMT+1000'); + const fakeDate = new clock.Date('Sat Apr 12 2014 12:22:00 GMT+1000'); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing year, month', async ({ clock }) => { + const date = new Date(2010, 4); + const fakeDate = new clock.Date(2010, 4); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d', async ({ clock }) => { + const date = new Date(2010, 4, 2); + const fakeDate = new clock.Date(2010, 4, 2); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12); + const fakeDate = new clock.Date(2010, 4, 2, 12); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h, m', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12, 42); + const fakeDate = new clock.Date(2010, 4, 2, 12, 42); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h, m, s', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12, 42, 53); + const fakeDate = new clock.Date(2010, 4, 2, 12, 42, 53); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('creates regular date when passing y, m, d, h, m, s, ms', async ({ clock }) => { + const date = new Date(2010, 4, 2, 12, 42, 53, 498); + const fakeDate = new clock.Date(2010, 4, 2, 12, 42, 53, 498); + expect(fakeDate.getTime()).toBe(date.getTime()); + }); + + it('returns date as string when calling with arguments', async ({ clock }) => { + // @ts-expect-error + const fakeDateStr = clock.Date(2010, 4, 2, 12, 42, 53, 498); + expect(fakeDateStr).toBe(new clock.Date().toString()); + }); + + it('returns date as string when calling with timestamp', async ({ clock }) => { + // @ts-expect-error + const fakeDateStr = clock.Date(1); + expect(fakeDateStr).toBe(new clock.Date().toString()); + }); + + it('mirrors native Date.prototype', async ({ clock }) => { + expect(clock.Date.prototype).toEqual(Date.prototype); + }); + + it('supports now method if present', async ({ clock }) => { + expect(typeof clock.Date.now).toEqual(typeof Date.now); + }); + + it('returns clock.now()', async ({ clock }) => { + const clock_now = clock.Date.now(); + const global_now = Date.now(); + expect(clock_now).toBeGreaterThanOrEqual(clock.now()); + expect(clock_now).toBeLessThanOrEqual(global_now); + }); + + it('mirrors parse method', async ({ clock }) => { + expect(clock.Date.parse).toEqual(Date.parse); + }); + + it('mirrors UTC method', async ({ clock }) => { + expect(clock.Date.UTC).toEqual(Date.UTC); + }); + + it('mirrors toUTCString method', async ({ clock }) => { + expect(clock.Date.prototype.toUTCString).toEqual(Date.prototype.toUTCString); + }); +}); + +it.describe('stubTimers', () => { + it('returns clock object', ({ install }) => { + const clock = install(); + expect(clock).toEqual(expect.any(Object)); + expect(clock.tick).toEqual(expect.any(Function)); + }); + + it('takes an object parameter', ({ install }) => { + const clock = install({}); + expect(clock).toEqual(expect.any(Object)); + }); + + it('sets initial timestamp', ({ install }) => { + const clock = install({ now: 1400 }); + expect(clock.now()).toBe(1400); + }); + + it('replaces global setTimeout', ({ install }) => { + const clock = install(); + const stub = createStub(); + + setTimeout(stub, 1000); + clock.tick(1000); + + expect(stub.called).toBeTruthy(); + }); + + it('global fake setTimeout should return id', ({ install }) => { + install(); + const stub = createStub(); + const to = setTimeout(stub, 1000); + expect(to).toEqual(expect.any(Number)); + }); + + it('replaces global clearTimeout', ({ install }) => { + const clock = install(); + const stub = createStub(); + + clearTimeout(setTimeout(stub, 1000)); + clock.tick(1000); + + expect(stub.called).toBeFalsy(); + }); + + it('replaces global setInterval', ({ install }) => { + const clock = install(); + const stub = createStub(); + + setInterval(stub, 500); + clock.tick(1000); + + expect(stub.callCount).toBe(2); + }); + + it('replaces global clearInterval', ({ install }) => { + const clock = install(); + const stub = createStub(); + + clearInterval(setInterval(stub, 500)); + clock.tick(1000); + + expect(stub.called).toBeFalsy(); + }); + + it('replaces global performance.now', ({ install }) => { + const clock = install(); + const prev = performance.now(); + clock.tick(1000); + const next = performance.now(); + expect(next).toBe(1000); + expect(prev).toBe(0); + }); + + it('uninstalls global performance.now', ({ install }) => { + const oldNow = performance.now; + const clock = install(); + expect(performance.now).toBe(clock.performance.now); + clock.uninstall(); + expect(performance.now).toBe(oldNow); + }); + + it('should let performance.mark still be callable after install() (#136)', ({ install }) => { + it.skip(nodeMajorVersion < 20); + install(); + expect(() => { + performance.mark('a name'); + }).not.toThrow(); + }); + + it('should not alter the global performance properties and methods', ({ install }) => { + it.skip(nodeMajorVersion < 20); + (Performance.prototype as any).someFunc1 = () => {}; + (Performance.prototype as any).someFunc2 = () => {}; + (Performance.prototype as any).someFunc3 = () => {}; + + const clock = install(); + expect((performance as any).someFunc1).toEqual(expect.any(Function)); + expect((performance as any).someFunc2).toEqual(expect.any(Function)); + expect((performance as any).someFunc3).toEqual(expect.any(Function)); + clock.uninstall(); + delete (Performance.prototype as any).someFunc1; + delete (Performance.prototype as any).someFunc2; + delete (Performance.prototype as any).someFunc3; + }); + + it('should replace the getEntries, getEntriesByX methods with noops that return []', ({ install }) => { + it.skip(nodeMajorVersion < 20); + const backupDescriptors = Object.getOwnPropertyDescriptors(Performance); + + function noop() { + return ['foo']; + } + + for (const propName of ['getEntries', 'getEntriesByName', 'getEntriesByType']) { + Object.defineProperty(Performance.prototype, propName, { + writable: true, + }); + } + + (Performance.prototype as any).getEntries = noop; + (Performance.prototype as any).getEntriesByName = noop; + (Performance.prototype as any).getEntriesByType = noop; + + const clock = install(); + + expect(performance.getEntries()).toEqual([]); + expect((performance as any).getEntriesByName()).toEqual([]); + expect((performance as any).getEntriesByType()).toEqual([]); + + clock.uninstall(); + + expect(performance.getEntries()).toEqual(['foo']); + expect((performance as any).getEntriesByName()).toEqual(['foo']); + expect((performance as any).getEntriesByType()).toEqual(['foo']); + + Object.keys(backupDescriptors).forEach(key => { + Object.defineProperty(Performance.prototype, key, backupDescriptors[key]); + }); + }); + + it.fixme('deletes global property on uninstall if it was inherited onto the global object', ({}) => { + // Give the global object an inherited 'setTimeout' method + const proto = { Date, + setTimeout: () => {}, + clearTimeout: () => {}, + setInterval: () => {}, + clearInterval: () => {}, + }; + const myGlobal = Object.create(proto); + + const { clock } = rawInstall(myGlobal, { now: 0, toFake: ['setTimeout'] }); + expect(myGlobal.hasOwnProperty('setTimeout')).toBeTruthy(); + clock.uninstall(); + expect(myGlobal.hasOwnProperty('setTimeout')).toBeFalsy(); + }); + + it('fakes Date constructor', ({ installEx }) => { + const { originals } = installEx({ now: 0 }); + const now = new Date(); + + expect(Date).not.toBe(originals.Date); + expect(now.getTime()).toBe(0); + }); + + it(`fake Date constructor should mirror Date's properties`, ({ clock }) => { + expect(Date).not.toBe(clock.Date); + expect(Date.prototype).toEqual(clock.Date.prototype); + }); + + it('decide on Date.now support at call-time when supported', ({ install }) => { + (Date.now as any) = () => {}; + install({ now: 0 }); + expect(Date.now).toEqual(expect.any(Function)); + }); + + it('mirrors custom Date properties', ({ install }) => { + const f = () => { + return ''; + }; + (Date as any).format = f; + install(); + + expect((Date as any).format).toEqual(f); + }); + + it('uninstalls Date constructor', () => { + const { clock, originals } = rawInstall(globalThis, { now: 0 }); + clock.uninstall(); + expect(Date).toBe(originals.Date); + }); + + it('fakes provided methods', ({ installEx }) => { + const { originals } = installEx({ now: 0, toFake: ['setTimeout', 'Date'] }); + expect(setTimeout).not.toBe(originals.setTimeout); + expect(Date).not.toBe(originals.Date); + }); + + it('resets faked methods', ({ install }) => { + const { clock, originals } = rawInstall(globalThis, { + now: 0, + toFake: ['setTimeout', 'Date'], + }); + clock.uninstall(); + + expect(setTimeout).toBe(originals.setTimeout); + expect(Date).toBe(originals.Date); + }); + + it('does not fake methods not provided', ({ installEx }) => { + const { originals } = installEx({ + now: 0, + toFake: ['setTimeout', 'Date'], + }); + + expect(clearTimeout).toBe(originals.clearTimeout); + expect(setInterval).toBe(originals.setInterval); + expect(clearInterval).toBe(originals.clearInterval); + }); +}); + +it.describe('shouldAdvanceTime', () => { + it('should create an auto advancing timer', async () => { + const testDelay = 29; + const date = new Date('2015-09-25'); + const clock = createClock(date); + clock.advanceAutomatically(); + expect(clock.Date.now()).toBe(1443139200000); + const timeoutStarted = clock.Date.now(); + + let callback: (r: number) => void; + const promise = new Promise(r => callback = r); + + clock.setTimeout(() => { + const timeDifference = clock.Date.now() - timeoutStarted; + callback(timeDifference); + }, testDelay); + expect(await promise).toBe(testDelay); + + }); + + it('should test setInterval', async () => { + const interval = 20; + let intervalsTriggered = 0; + const cyclesToTrigger = 3; + const date = new Date('2015-09-25'); + const clock = createClock(date); + clock.advanceAutomatically(); + expect(clock.Date.now()).toBe(1443139200000); + const timeoutStarted = clock.Date.now(); + + let callback: (r: number) => void; + const promise = new Promise(r => callback = r); + + const intervalId = clock.setInterval(() => { + if (++intervalsTriggered === cyclesToTrigger) { + clock.clearInterval(intervalId); + const timeDifference = clock.Date.now() - timeoutStarted; + callback(timeDifference); + } + }, interval); + + expect(await promise).toBe(interval * cyclesToTrigger); + }); + + it('should not depend on having to stub setInterval or clearInterval to work', async ({ install }) => { + const origSetInterval = globalThis.setInterval; + const origClearInterval = globalThis.clearInterval; + + install({ toFake: ['setTimeout'] }); + expect(globalThis.setInterval).toBe(origSetInterval); + expect(globalThis.clearInterval).toBe(origClearInterval); + }); +}); + +it.describe('requestAnimationFrame', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.requestAnimationFrame(); + }).toThrow(); + }); + + it('returns numeric id or object with numeric id', async ({ clock }) => { + const result = clock.requestAnimationFrame(() => {}); + expect(result).toEqual(expect.any(Number)); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.requestAnimationFrame(() => {}); + const id2 = clock.requestAnimationFrame(() => {}); + expect(id2).not.toEqual(id1); + }); + + it('should run every 16ms', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(15); + expect(stub.callCount).toBe(0); + clock.tick(1); + expect(stub.callCount).toBe(1); + }); + + it('should be called with performance.now() when available', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(20); + expect(stub.calledWith(16)).toBeTruthy(); + }); + + it('should be called with performance.now() even when performance unavailable', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(20); + expect(stub.calledWith(16)).toBeTruthy(); + }); + + it('should call callback once', async ({ clock }) => { + const stub = createStub(); + clock.requestAnimationFrame(stub); + clock.tick(32); + expect(stub.callCount).toBe(1); + }); + + it('should schedule two callbacks before the next frame at the same time', async ({ clock }) => { + const stub1 = createStub(); + const stub2 = createStub(); + clock.requestAnimationFrame(stub1); + clock.tick(5); + clock.requestAnimationFrame(stub2); + clock.tick(11); + expect(stub1.calledWith(16)).toBeTruthy(); + expect(stub2.calledWith(16)).toBeTruthy(); + }); + + it('should properly schedule callback for 3rd frame', async ({ clock }) => { + const stub1 = createStub(); + const stub2 = createStub(); + clock.requestAnimationFrame(stub1); + clock.tick(57); + clock.requestAnimationFrame(stub2); + clock.tick(10); + expect(stub1.calledWith(16)).toBeTruthy(); + expect(stub2.calledWith(64)).toBeTruthy(); + }); + + it('should schedule for next frame if on current frame', ({ clock }) => { + const stub = createStub(); + clock.tick(16); + clock.requestAnimationFrame(stub); + clock.tick(16); + expect(stub.calledWith(32)).toBeTruthy(); + }); +}); + +it.describe('cancelAnimationFrame', () => { + it('removes animation frame', async ({ clock }) => { + const stub = createStub(); + const id = clock.requestAnimationFrame(stub); + clock.cancelAnimationFrame(id); + clock.tick(16); + expect(stub.called).toBeFalsy(); + }); + + it('does not remove timeout', async ({ clock }) => { + const stub = createStub(); + const id = clock.setTimeout(stub, 50); + expect(() => { + clock.cancelAnimationFrame(id); + }).toThrow(); + clock.tick(50); + expect(stub.called).toBeTruthy(); + }); + + it('does not remove interval', async ({ clock }) => { + const stub = createStub(); + const id = clock.setInterval(stub, 50); + expect(() => { + clock.cancelAnimationFrame(id); + }).toThrow(); + clock.tick(50); + expect(stub.called).toBeTruthy(); + }); + + it('ignores null argument', async ({ clock }) => { + clock.cancelAnimationFrame(null); + }); +}); + +it.describe('runToFrame', () => { + it('should tick next frame', async ({ clock }) => { + clock.runToFrame(); + expect(clock.now()).toBe(16); + clock.tick(3); + clock.runToFrame(); + expect(clock.now()).toBe(32); + }); +}); + +it.describe('jump', () => { + it('ignores timers which wouldn\'t be run', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 1000); + clock.jump(500); + expect(stub.called).toBeFalsy(); + }); + + it('pushes back execution time for skipped timers', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => { + stub(clock.Date.now()); + }, 1000); + clock.jump(2000); + expect(stub.callCount).toBe(1); + expect(stub.calledWith(2000)).toBeTruthy(); + }); + + it('handles multiple pending timers and types', async ({ clock }) => { + const longTimers = [createStub(), createStub()]; + const shortTimers = [createStub(), createStub(), createStub()]; + clock.setTimeout(longTimers[0], 2000); + clock.setInterval(longTimers[1], 2500); + clock.setTimeout(shortTimers[0], 250); + clock.setInterval(shortTimers[1], 100); + clock.requestAnimationFrame(shortTimers[2]); + clock.jump(1500); + for (const stub of longTimers) + expect(stub.called).toBeFalsy(); + for (const stub of shortTimers) + expect(stub.callCount).toBe(1); + }); + + it('supports string time arguments', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(stub, 100000); // 100000 = 1:40 + clock.jump('01:50'); + expect(stub.callCount).toBe(1); + }); +}); + +it.describe('performance.now()', () => { + it('should start at 0', async ({ clock }) => { + const result = clock.performance.now(); + expect(result).toBe(0); + }); + + it('should run along with clock.tick', async ({ clock }) => { + clock.tick(5000); + const result = clock.performance.now(); + expect(result).toBe(5000); + }); + + it('should listen to multiple ticks in performance.now', async ({ clock }) => { + for (let i = 0; i < 10; i++) { + const next = clock.performance.now(); + expect(next).toBe(1000 * i); + clock.tick(1000); + } + }); + + it('should run with ticks with timers set', async ({ clock }) => { + clock.setTimeout(() => { + const result = clock.performance.now(); + expect(result).toBe(2500); + }, 2500); + clock.tick(5000); + }); +}); + +it.describe('requestIdleCallback', () => { + it('throws if no arguments', async ({ clock }) => { + expect(() => { + // @ts-expect-error + clock.requestIdleCallback(); + }).toThrow(); + }); + + it('returns numeric id', async ({ clock }) => { + const result = clock.requestIdleCallback(() => {}); + expect(result).toEqual(expect.any(Number)); + }); + + it('returns unique id', async ({ clock }) => { + const id1 = clock.requestIdleCallback(() => {}); + const id2 = clock.requestIdleCallback(() => {}); + expect(id2).not.toEqual(id1); + }); + + it('runs after all timers', async ({ clock }) => { + const stub = createStub(); + clock.requestIdleCallback(stub); + clock.tick(1000); + expect(stub.called).toBeTruthy(); + }); + + it('runs no later than timeout option even if there are any timers', async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => {}, 10); + clock.setTimeout(() => {}, 30); + clock.requestIdleCallback(stub, { timeout: 20 }); + clock.tick(20); + expect(stub.called).toBeTruthy(); + }); + + it(`doesn't runs if there are any timers and no timeout option`, async ({ clock }) => { + const stub = createStub(); + clock.setTimeout(() => {}, 30); + clock.requestIdleCallback(stub); + clock.tick(35); + expect(stub.called).toBeFalsy(); + }); +}); + +it.describe('cancelIdleCallback', () => { + it('removes idle callback', async ({ clock }) => { + const stub = createStub(); + const callbackId = clock.requestIdleCallback(stub, { timeout: 0 }); + clock.cancelIdleCallback(callbackId); + clock.tick(0); + expect(stub.called).toBeFalsy(); + }); +}); + +it.describe('loop limit stack trace', () => { + const expectedMessage = + 'Aborting after running 5 timers, assuming an infinite loop!'; + it.use({ loopLimit: 5 }); + + it.describe('setTimeout', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.setTimeout(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.setTimeout(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Timeout - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); + + it.describe('requestIdleCallback', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.requestIdleCallback(recursiveCreateTimer, { timeout: 10 }); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+IdleCallback - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); + + it.describe('setInterval', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.setInterval(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.setInterval(recursiveCreateTimer, 10); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+Interval - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); + + it.describe('requestAnimationFrame', () => { + it('provides a stack trace for running all async', async ({ clock }) => { + const catchSpy = createStub(); + const recursiveCreateTimer = () => { + clock.requestAnimationFrame(recursiveCreateTimer); + }; + + recursiveCreateTimer(); + await clock.runAllAsync().catch(catchSpy); + expect(catchSpy.callCount).toBe(1); + const err = catchSpy.firstCall.args[0]; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); + }); + + it('provides a stack trace for running all sync', ({ clock }) => { + let caughtError = false; + const recursiveCreateTimer = () => { + clock.requestAnimationFrame(recursiveCreateTimer); + }; + + recursiveCreateTimer(); + try { + clock.runAll(); + } catch (err) { + caughtError = true; + expect(err.message).toBe(expectedMessage); + expect(err.stack).toMatch(new RegExp(`Error: ${expectedMessage}\\s+AnimationFrame - recursiveCreateTimer`)); + } + expect(caughtError).toBeTruthy(); + }); + }); +}); + +it.describe('Intl API', () => { + function isFirstOfMonth(ianaTimeZone, timestamp?: number) { + return ( + new Intl.DateTimeFormat(undefined, { timeZone: ianaTimeZone }) + .formatToParts(timestamp) + .find(part => part.type === 'day').value === '1' + ); + } + + it('Executes formatRange like normal', async ({ clock }) => { + const start = new Date(Date.UTC(2020, 0, 1, 0, 0)); + const end = new Date(Date.UTC(2020, 0, 1, 0, 1)); + const options: Intl.DateTimeFormatOptions = { + timeZone: 'UTC', + hour12: false, + hour: 'numeric', + minute: 'numeric', + }; + expect( + new Intl.DateTimeFormat('en-GB', options).formatRange(start, end), + ).toBe('00:00–00:01'); + }); + + it('Executes formatRangeToParts like normal', async ({ clock }) => { + const start = new Date(Date.UTC(2020, 0, 1, 0, 0)); + const end = new Date(Date.UTC(2020, 0, 1, 0, 1)); + const options: Intl.DateTimeFormatOptions = { + timeZone: 'UTC', + hour12: false, + hour: 'numeric', + minute: 'numeric', + }; + expect(new Intl.DateTimeFormat('en-GB', options).formatRangeToParts(start, end)).toEqual([ + { type: 'hour', value: '00', source: 'startRange' }, + { type: 'literal', value: ':', source: 'startRange' }, + { type: 'minute', value: '00', source: 'startRange' }, + { type: 'literal', value: '–', source: 'shared' }, + { type: 'hour', value: '00', source: 'endRange' }, + { type: 'literal', value: ':', source: 'endRange' }, + { type: 'minute', value: '01', source: 'endRange' }, + ]); + }); + + it('Executes resolvedOptions like normal', async ({ clock }) => { + const options: Intl.DateTimeFormatOptions = { + timeZone: 'UTC', + hour12: false, + hour: '2-digit', + minute: '2-digit', + }; + expect(new Intl.DateTimeFormat('en-GB', options).resolvedOptions()).toEqual({ + locale: 'en-GB', + calendar: 'gregory', + numberingSystem: 'latn', + timeZone: 'UTC', + hour12: false, + hourCycle: 'h23', + hour: '2-digit', + minute: '2-digit', + }); + }); + + it('formatToParts via isFirstOfMonth -> Returns true when passed a timestamp argument that is first of the month', async ({ clock }) => { + // June 1 04:00 UTC - Toronto is June 1 00:00 + expect(isFirstOfMonth('America/Toronto', Date.UTC(2022, 5, 1, 4))).toBeTruthy(); + }); + + it('formatToParts via isFirstOfMonth -> Returns false when passed a timestamp argument that is not first of the month', async ({ clock }) => { + // June 1 00:00 UTC - Toronto is May 31 20:00 + expect(isFirstOfMonth('America/Toronto', Date.UTC(2022, 5, 1))).toBeFalsy(); + }); + + it('formatToParts via isFirstOfMonth -> Returns true when passed no timestamp and system time is first of the month', async ({ install }) => { + // June 1 04:00 UTC - Toronto is June 1 00:00 + install({ now: Date.UTC(2022, 5, 1, 4) }); + expect(isFirstOfMonth('America/Toronto')).toBeTruthy(); + }); + + it('formatToParts via isFirstOfMonth -> Returns false when passed no timestamp and system time is not first of the month', async ({ install }) => { + // June 1 00:00 UTC - Toronto is May 31 20:00 + install({ now: Date.UTC(2022, 5, 1) }); + expect(isFirstOfMonth('America/Toronto')).toBeFalsy(); + }); + + it('Executes supportedLocalesOf like normal', async ({ installEx }) => { + const { originals } = installEx(); + expect(Intl.DateTimeFormat.supportedLocalesOf([])).toEqual( + originals.Intl.DateTimeFormat.supportedLocalesOf([]), + ); + }); + + it('Creates a RelativeTimeFormat like normal', async ({ clock }) => { + const rtf = new Intl.RelativeTimeFormat('en-GB', { + numeric: 'auto', + }); + expect(rtf.format(2, 'day')).toBe('in 2 days'); + }); +}); + +interface Stub { + called: boolean; + callCount: number; + calls: { receiver: any, args: any[], time: bigint }[]; + firstCall: { args: any[] } | undefined; + calledOn: (thisObj: any) => boolean; + calledBefore: (other: Stub) => boolean; + calledWithExactly: (...args: any[]) => void; + calledWith(arg: any): void; + (...args: any[]): void; + throws: () => Stub; +} + +const createStub = (body?: () => void): Stub => { + const allFirstArgs = new Set(); + const stub: Stub = function(...args: any[]) { + stub.calls.push({ receiver: this, args, time: process.hrtime.bigint() }); + allFirstArgs.add(args[0]); + if (body) + body(); + } as any; + + stub.calls = []; + const stubAny = stub as any; + stubAny.__defineGetter__('callCount', () => stub.calls.length); + stubAny.__defineGetter__('called', () => stub.calls.length > 0); + stubAny.__defineGetter__('firstCall', () => stub.calls[0]); + + stub.calledOn = thisObj => stub.calls[0].receiver === thisObj; + + stub.calledWithExactly = (...args) => { + expect(stub.calls[0].args).toEqual(args); + return true; + }; + stub.calledWith = arg => { + expect(allFirstArgs).toContain(arg); + return true; + }; + stub.calledBefore = other => { + expect(other.calls[0].time).toBeGreaterThan(stub.calls[0].time); + return true; + }; + stub.throws = () => createStub(() => { throw new Error(''); }); + return stub; +}; + +const nodeMajorVersion = +process.versions.node.split('.')[0]; diff --git a/utils/generate_injected.js b/utils/generate_injected.js index a39583ab38..80c3be5ac0 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -51,7 +51,7 @@ const injectedScripts = [ true, ], [ - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'fakeTimers.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'clock.ts'), path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), true,