diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index f4d96965f4644..a36628847609e 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -97,7 +97,7 @@ export class Clock { const script = `(() => { const module = {}; ${fakeTimersSource.source} - globalThis.__pwFakeTimers = (module.exports.inject())(); + globalThis.__pwFakeTimers = (module.exports.inject())(globalThis); })();`; await this._addAndEvaluate(script); } diff --git a/packages/playwright-core/src/server/injected/DEPS.list b/packages/playwright-core/src/server/injected/DEPS.list index 32da641c93acd..7c5f41d8c901c 100644 --- a/packages/playwright-core/src/server/injected/DEPS.list +++ b/packages/playwright-core/src/server/injected/DEPS.list @@ -2,6 +2,3 @@ [*] ../isomorphic/ ../../utils/isomorphic - -[fakeTimers.ts] -../../third_party/fake-timers-src diff --git a/packages/playwright-core/src/server/injected/fakeTimers.ts b/packages/playwright-core/src/server/injected/fakeTimers.ts index 44974ab8e1a11..fbec480980802 100644 --- a/packages/playwright-core/src/server/injected/fakeTimers.ts +++ b/packages/playwright-core/src/server/injected/fakeTimers.ts @@ -1,39 +1,951 @@ /** - * Copyright (c) Microsoft Corporation. + * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved. + * Modifications 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 + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 = { + Date: DateConstructor; + setTimeout: Window['setTimeout']; + clearTimeout: Window['clearTimeout']; + setInterval: Window['setInterval']; + clearInterval: Window['clearInterval']; + requestAnimationFrame?: Window['requestAnimationFrame']; + cancelAnimationFrame?: (id: number) => void; + requestIdleCallback?: Window['requestIdleCallback']; + cancelIdleCallback?: (id: number) => void; + Intl?: typeof Intl; + performance?: Window['performance']; +}; + +type ClockConfig = { + now?: number | Date; + loopLimit?: number; +}; + +type InstallConfig = ClockConfig & { + toFake?: (keyof ClockMethods)[]; +}; + +enum TimerType { + Timeout = 'Timeout', + Interval = 'Interval', + Immediate = 'Immediate', + AnimationFrame = 'AnimationFrame', + IdleCallback = 'IdleCallback', +} + +type Timer = { + type: TimerType; + func: TimerHandler; + args: any[]; + delay: number; + callAt: number; + createdAt: number; + id: number; + error?: Error; +}; + +interface Embedder { + postTask(task: () => void): void; + postTaskPeriodically(task: () => void, delay: number): () => void; +} + +class Clock { + 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)[] = []; + + constructor(embedder: Embedder, startDate: Date | number | undefined, loopLimit: number = 1000) { + const start = Math.floor(getEpoch(startDate)); + this.start = start; + this._now = start; + this._embedder = embedder; + this._loopLimit = loopLimit; + } + + uninstall() { + this.disposables.forEach(dispose => dispose()); + this.disposables.length = 0; + } + + now(): number { + return this._now; + } + + performanceNow(): DOMHighResTimeStamp { + const millisSinceStart = this._now - this._adjustedSystemTime - this.start; + const secsSinceStart = Math.floor(millisSinceStart / 1000); + const millis = secsSinceStart * 1000; + return millis; + } + + private _doTick(tickValue: number | string, isAsync: boolean, resolve?: (time: number) => void, reject?: (error: Error) => void): number | undefined { + const msFloat = + typeof tickValue === 'number' + ? tickValue + : parseTime(tickValue); + const ms = Math.floor(msFloat); + let tickTo = this._now + ms; + + if (msFloat < 0) + throw new TypeError('Negative ticks are not supported'); + + let tickFrom = this._now; + let previous = this._now; + // ESLint fails to detect this correctly + /* eslint-disable prefer-const */ + let timer; + let firstException: Error; + let oldNow: number; + let nextPromiseTick: (() => void) | null; + 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; + tickTo += this._now - oldNow; + } + + const doTickInner = (): number | undefined => { + // perform each timer in the requested range + timer = this._firstTimerInRange(tickFrom, tickTo); + while (timer && tickFrom <= tickTo) { + if (this._timers.has(timer.id)) { + tickFrom = timer.callAt; + this._now = timer.callAt; + oldNow = this._now; + try { + this._runJobs(); + this._callTimer(timer); + } catch (e) { + firstException = firstException || e; + } + + if (isAsync) { + // finish up after native setImmediate callback to allow + // all native es6 promises to process their callbacks after + // each timer fires. + this._embedder.postTask(nextPromiseTick!); + return; + } + compensationCheck(); + } + postTimerCall(); + } + + // 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; + tickTo += this._now - oldNow; + } + this._duringTick = false; + + // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo] + timer = this._firstTimerInRange(tickFrom, tickTo); + if (timer) { + try { + this.tick(tickTo - this._now); // do it all again - for the remainder of the requested range + } catch (e) { + firstException = firstException || e; + } + } else { + // no timers remaining in the requested range: move the clock all the way to the end + this._now = tickTo; + } + if (firstException) + throw firstException; + + if (isAsync) + resolve!(this._now); + else + return this._now; + }; + + nextPromiseTick = + isAsync ? + () => { + try { + compensationCheck(); + postTimerCall(); + doTickInner(); + } catch (e) { + reject!(e); + } + } : null; + + compensationCheck = () => { + // compensate for any setSystemTime() call during timer callback + if (oldNow !== this._now) { + tickFrom += this._now - oldNow; + tickTo += this._now - oldNow; + previous += this._now - oldNow; + } + }; + + postTimerCall = () => { + timer = this._firstTimerInRange(previous, tickTo); + previous = tickFrom; + }; + + return doTickInner(); + } + + tick(tickValue: string | number): number { + 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); + } + }); + }); + } + + next() { + this._runJobs(); + const timer = this._firstTimer(); + if (!timer) + return this._now; + + this._duringTick = true; + 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; + } + + let err: Error; + 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); + } + }); + }); + } + + runAll() { + this._runJobs(); + for (let i = 0; i < this._loopLimit; i++) { + const numTimers = this._timers.size; + if (numTimers === 0) { + this._resetIsNearInfiniteLimit(); + return this._now; + } + + this.next(); + this._checkIsNearInfiniteLimit(i); + } + + const excessJob = this._firstTimer(); + 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(); + + let numTimers; + if (i < this._loopLimit) { + if (!this._timers) { + this._resetIsNearInfiniteLimit(); + resolve(this._now); + return; + } + + 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(); + }); + } + + runToLast() { + const timer = this._lastTimer(); + if (!timer) { + this._runJobs(); + return this._now; + } + return this.tick(timer.callAt - this._now); + } + + runToLastAsync() { + return new Promise((resolve, reject) => { + this._embedder.postTask(() => { + try { + const timer = this._lastTimer(); + if (!timer) { + this._runJobs(); + resolve(this._now); + return; + } + this.tickAsync(timer.callAt - this._now).then(resolve); + } catch (e) { + reject(e); + } + }); + }); + } + + reset() { + this._timers.clear(); + this._jobs = []; + this._now = this.start; + } + + setSystemTime(systemTime: Date | number) { + // determine time difference + const newNow = getEpoch(systemTime); + const difference = newNow - this._now; + + this._adjustedSystemTime = this._adjustedSystemTime + difference; + // update 'system clock' + this._now = newNow; + + // update timers and intervals to keep them stable + for (const timer of this._timers.values()) { + timer.createdAt += difference; + timer.callAt += difference; + } + } + + jump(tickValue: string | number): number { + const msFloat = + typeof tickValue === 'number' + ? tickValue + : parseTime(tickValue); + const ms = Math.floor(msFloat); + + for (const timer of this._timers.values()) { + if (this._now + ms > timer.callAt) + timer.callAt = this._now + ms; + } + 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 { + if (options.func === undefined) + throw new Error('Callback must be provided to timer calls'); + + let delay = options.delay ? +options.delay : 0; + if (!Number.isFinite(delay)) + delay = 0; + delay = delay > maxTimeout ? 1 : delay; + delay = Math.max(0, delay); + + const timer: Timer = { + type: options.type, + func: options.func, + args: options.args || [], + delay, + callAt: this._now + (delay || (this._duringTick ? 1 : 0)), + createdAt: this._now, + id: this._uniqueTimerId++, + error: this._isNearInfiniteLimit ? new Error() : undefined, + }; + this._timers.set(timer.id, timer); + return timer.id; + } + + private _firstTimerInRange(from: number, to: number): Timer | null { + let firstTimer: Timer | null = null; + for (const timer of this._timers.values()) { + const isInRange = inRange(from, to, timer); + if (isInRange && (!firstTimer || compareTimers(firstTimer, timer) === 1)) + firstTimer = timer; + } + return firstTimer; + } + + countTimers() { + return this._timers.size + this._jobs.length; + } + + private _firstTimer(): Timer | null { + let firstTimer: Timer | null = null; + + for (const timer of this._timers.values()) { + if (!firstTimer || compareTimers(firstTimer, timer) === 1) + firstTimer = timer; + } + return firstTimer; + } + + private _lastTimer(): Timer | null { + let lastTimer: Timer | null = null; + + for (const timer of this._timers.values()) { + if (!lastTimer || compareTimers(lastTimer, timer) === -1) + lastTimer = timer; + } + return lastTimer; + } + + private _callTimer(timer: Timer) { + if (timer.type === TimerType.Interval) + this._timers.get(timer.id)!.callAt += timer.delay; + else + this._timers.delete(timer.id); + callFunction(timer.func, timer.args); + } + + private _getInfiniteLoopError(job: Timer) { + const infiniteLoopError = new Error( + `Aborting after running ${this._loopLimit} timers, assuming an infinite loop!`, + ); + + if (!job.error) + return infiniteLoopError; + + // pattern never matched in Node + const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; + const clockMethodPattern = new RegExp( + String(Object.keys(this).join('|')), + ); + + let matchedLineIndex = -1; + job.error.stack!.split('\n').some((line, i) => { + // If we've matched a computed target line (e.g. setTimeout) then we + // don't need to look any further. Return true to stop iterating. + const matchedComputedTarget = line.match(computedTargetPattern); + /* istanbul ignore if */ + if (matchedComputedTarget) { + matchedLineIndex = i; + return true; + } + + // If we've matched a clock method line, then there may still be + // others further down the trace. Return false to keep iterating. + const matchedClockMethod = line.match(clockMethodPattern); + if (matchedClockMethod) { + matchedLineIndex = i; + return false; + } + + // If we haven't matched anything on this line, but we matched + // previously and set the matched line index, then we can stop. + // If we haven't matched previously, then we should keep iterating. + return matchedLineIndex >= 0; + }); + + const funcName = typeof job.func === 'function' ? job.func.name : 'anonymous'; + const stack = `${infiniteLoopError}\n${job.type || 'Microtask'} - ${funcName}\n${job.error.stack! + .split('\n') + .slice(matchedLineIndex + 1) + .join('\n')}`; + + try { + Object.defineProperty(infiniteLoopError, 'stack', { + value: stack, + }); + } catch (e) { + // noop + } + + return infiniteLoopError; + } + + getTimeToNextFrame() { + return 16 - ((this._now - this.start) % 16); + } + + clearTimer(timerId: number, type: TimerType) { + if (!timerId) { + // null appears to be allowed in most browsers, and appears to be + // relied upon by some libraries, like Bootstrap carousel + return; + } + + // in Node, the ID is stored as the primitive value for `Timeout` objects + // for `Immediate` objects, no ID exists, so it gets coerced to NaN + const id = Number(timerId); + + if (Number.isNaN(id) || id < idCounterStart) { + const handlerName = getClearHandler(type); + new Error(`Clock: ${handlerName} was invoked to clear a native timer instead of one created by the clock library.`); + } + + const timer = this._timers.get(id); + if (timer) { + if ( + timer.type === type || + (timer.type === 'Timeout' && type === 'Interval') || + (timer.type === 'Interval' && type === 'Timeout') + ) { + this._timers.delete(id); + } else { + const clear = getClearHandler(type); + const schedule = getScheduleHandler(timer.type); + throw new Error( + `Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()`, + ); + } + } + } + + advanceAutomatically(advanceTimeDelta: number = 20): () => void { + return this._embedder.postTaskPeriodically( + () => this.tick(advanceTimeDelta!), + advanceTimeDelta, + ); + } +} + +function getEpoch(epoch: Date | number | undefined): number { + if (!epoch) + return 0; + if (typeof epoch !== 'number') + return epoch.getTime(); + return epoch; +} + +function inRange(from: number, to: number, timer: Timer): boolean { + return timer && timer.callAt >= from && timer.callAt <= to; +} + +/** + * Parse strings like '01:10:00' (meaning 1 hour, 10 minutes, 0 seconds) into + * number of milliseconds. This is used to support human-readable strings passed + * to clock.tick() + */ +function parseTime(str: string): number { + if (!str) + return 0; + + const strings = str.split(':'); + const l = strings.length; + let i = l; + let ms = 0; + let parsed; + + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { + throw new Error( + `Clock only understands numbers, 'mm:ss' and 'hh:mm:ss'`, + ); + } + + while (i--) { + parsed = parseInt(strings[i], 10); + if (parsed >= 60) + throw new Error(`Invalid time ${str}`); + ms += parsed * Math.pow(60, l - i - 1); + } + + return ms * 1000; +} + +function mirrorDateProperties(target: any, source: typeof Date): DateConstructor & Date { + let prop; + for (prop of Object.keys(source) as (keyof DateConstructor)[]) + target[prop] = source[prop]; + target.toString = () => source.toString(); + target.prototype = source.prototype; + target.parse = source.parse; + target.UTC = source.UTC; + target.prototype.toUTCString = source.prototype.toUTCString; + target.isFake = true; + return target; +} + +function createDate(clock: Clock, 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. + if (!(this instanceof ClockDate)) + return new NativeDate(clock.now()).toString(); + + // if Date is called as a constructor with 'new' keyword + // Defensive and verbose to avoid potential harm in passing + // explicit undefined when user does not pass argument + switch (arguments.length) { + case 0: + return new NativeDate(clock.now()); + case 1: + return new NativeDate(year); + case 2: + return new NativeDate(year, month); + case 3: + return new NativeDate(year, month, date); + case 4: + return new NativeDate(year, month, date, hour); + case 5: + return new NativeDate(year, month, date, hour, minute); + case 6: + return new NativeDate( + year, + month, + date, + hour, + minute, + second, + ); + default: + return new NativeDate( + year, + month, + date, + hour, + minute, + second, + ms, + ); + } + } + + ClockDate.now = () => clock.now(); + return mirrorDateProperties(ClockDate, NativeDate); +} + +/** + * Mirror Intl by default on our fake implementation * - * 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. + * Most of the properties are the original native ones, + * 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 { + const ClockIntl: any = {}; + /* + * All properties of Intl are non-enumerable, so we need + * to do a bit of work to get them out. + */ + for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[]) + ClockIntl[key] = NativeIntl[key]; + + ClockIntl.DateTimeFormat = (...args: any[]) => { + const realFormatter = new NativeIntl.DateTimeFormat(...args); + const formatter: Intl.DateTimeFormat = { + formatRange: realFormatter.formatRange.bind(realFormatter), + formatRangeToParts: realFormatter.formatRangeToParts.bind(realFormatter), + resolvedOptions: realFormatter.resolvedOptions.bind(realFormatter), + format: date => realFormatter.format(date || clock.now()), + formatToParts: date => realFormatter.formatToParts(date || clock.now()), + }; + + return formatter; + }; + + ClockIntl.DateTimeFormat.prototype = Object.create( + NativeIntl.DateTimeFormat.prototype, + ); + + ClockIntl.DateTimeFormat.supportedLocalesOf = + NativeIntl.DateTimeFormat.supportedLocalesOf; + + return ClockIntl; +} + +function compareTimers(a: Timer, b: Timer) { + // Sort first by absolute timing + if (a.callAt < b.callAt) + return -1; + if (a.callAt > b.callAt) + return 1; + + // Sort next by immediate, immediate timers take precedence + if (a.type === TimerType.Immediate && b.type !== TimerType.Immediate) + return -1; + if (a.type !== TimerType.Immediate && b.type === TimerType.Immediate) + return 1; + + // Sort next by creation time, earlier-created timers take precedence + if (a.createdAt < b.createdAt) + return -1; + if (a.createdAt > b.createdAt) + return 1; + + // Sort next by id, lower-id timers take precedence + if (a.id < b.id) + return -1; + if (a.id > b.id) + return 1; + + // As timer ids are unique, no fallback `0` is necessary +} + +function callFunction(func: TimerHandler, args: any[]) { + if (typeof func === 'function') + func.apply(null, args); + else + (() => { eval(func); })(); +} + +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, + Date: (globalObject as any).Date, + performance: globalObject.performance, + Intl: (globalObject as any).Intl, + }; +} + +/** + * Gets schedule handler name for a given timer type */ +function getScheduleHandler(type: TimerType) { + if (type === 'IdleCallback' || type === 'AnimationFrame') + return `request${type}`; + + return `set${type}`; +} + +function createApi(clock: Clock, 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, + delay + }); + }, + clearTimeout: (timerId: number | undefined): void => { + if (timerId) + clock.clearTimer(timerId, TimerType.Timeout); + }, + setInterval: (func: TimerHandler, timeout?: number | undefined, ...args: any[]): number => { + const delay = timeout ? +timeout : timeout; + return clock.addTimer({ + type: TimerType.Interval, + func, + args, + delay, + }); + }, + clearInterval: (timerId: number | undefined): void => { + if (timerId) + return clock.clearTimer(timerId, TimerType.Interval); + }, + requestAnimationFrame: (callback: FrameRequestCallback): number => { + return clock.addTimer({ + type: TimerType.AnimationFrame, + func: callback, + delay: clock.getTimeToNextFrame(), + get args() { + return [clock.performanceNow()]; + }, + }); + }, + cancelAnimationFrame: (timerId: number): void => { + if (timerId) + return clock.clearTimer(timerId, TimerType.AnimationFrame); + }, + requestIdleCallback: (callback: IdleRequestCallback, options?: IdleRequestOptions | undefined): number => { + let timeToNextIdlePeriod = 0; -// @ts-ignore -import SinonFakeTimers from '../../third_party/fake-timers-src'; - -export function inject() { - // eslint-disable-next-line no-restricted-globals - const window = globalThis; - const builtin = { - setTimeout: window.setTimeout.bind(window), - clearTimeout: window.clearTimeout.bind(window), - setInterval: window.setInterval.bind(window), - clearInterval: window.clearInterval.bind(window), - requestAnimationFrame: window.requestAnimationFrame.bind(window), - cancelAnimationFrame: window.cancelAnimationFrame.bind(window), - requestIdleCallback: window.requestIdleCallback?.bind(window), - cancelIdleCallback: window.cancelIdleCallback?.bind(window), - performance: window.performance, - Intl: window.Intl, - Date: window.Date, + if (clock.countTimers() > 0) + timeToNextIdlePeriod = 50; // const for now + return clock.addTimer({ + type: TimerType.IdleCallback, + func: callback, + args: [], + delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod, + }); + }, + cancelIdleCallback: (timerId: number): void => { + if (timerId) + return clock.clearTimer(timerId, TimerType.IdleCallback); + }, + Intl: originals.Intl ? createIntl(clock, originals.Intl) : undefined, + Date: createDate(clock, originals.Date), + performance: originals.performance ? fakePerformance(clock, originals.performance) : undefined, + }; +} + +function getClearHandler(type: TimerType) { + if (type === 'IdleCallback' || type === 'AnimationFrame') + return `cancel${type}`; + + return `clear${type}`; +} + +function fakePerformance(clock: Clock, performance: Performance): Performance { + const result: any = { + now: () => clock.performanceNow(), + timeOrigin: clock.start, + __proto__: performance, }; - const result = SinonFakeTimers; - result.builtin = builtin; return result; } + +export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: Clock, api: Partial, originals: Partial } { + const originals = platformOriginals(globalObject); + const embedder = { + postTask: (task: () => void) => { + originals.setTimeout!(task, 0); + }, + postTaskPeriodically: (task: () => void, delay: number) => { + const intervalId = globalObject.setInterval(task, delay); + return () => originals.clearInterval!(intervalId); + }, + }; + + const clock = new Clock(embedder, config.now, config.loopLimit); + const api = createApi(clock, originals); + return { clock, api, originals }; +} + +export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: Clock, api: Partial, originals: Partial } { + if ((globalObject as any).Date?.isFake) { + // Timers are already faked; this is a problem. + // Make the user reset timers before continuing. + throw new TypeError(`Can't install fake timers twice on the same global object.`); + } + + const { clock, api, originals } = createClock(globalObject, config); + const toFake = config.toFake?.length ? config.toFake : Object.keys(originals) as (keyof ClockMethods)[]; + + for (const method of toFake) { + if (method === 'Date') { + (globalObject as any).Date = mirrorDateProperties(api.Date, (globalObject as any).Date); + } else if (method === 'Intl') { + (globalObject as any).Intl = api[method]!; + } else if (method === 'performance') { + (globalObject as any).performance = api[method]!; + } else { + (globalObject as any)[method] = (...args: any[]) => { + return (api[method] as any).apply(api, args); + }; + } + clock.disposables.push(() => { + (globalObject as any)[method] = originals[method]; + }); + } + + return { clock, api, originals }; +} + +export function inject(globalObject: WindowOrWorkerGlobalScope) { + return { + install: (config: InstallConfig) => { + const { clock } = install(globalObject, config); + return clock; + }, + builtin: platformOriginals(globalObject), + }; +} diff --git a/packages/playwright-core/src/third_party/fake-timers-src.js b/packages/playwright-core/src/third_party/fake-timers-src.js deleted file mode 100644 index 9602123052d17..0000000000000 --- a/packages/playwright-core/src/third_party/fake-timers-src.js +++ /dev/null @@ -1,1776 +0,0 @@ -/* - * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved. - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - * 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. -/ - -"use strict"; - -/* - * Local modifications: - * - removed global.process-related code. - * - removed require("@sinonjs/commons") dependency. - */ - -/** - * @typedef {object} IdleDeadline - * @property {boolean} didTimeout - whether or not the callback was called before reaching the optional timeout - * @property {function():number} timeRemaining - a floating-point value providing an estimate of the number of milliseconds remaining in the current idle period - */ - -/** - * Queues a function to be called during a browser's idle periods - * - * @callback RequestIdleCallback - * @param {function(IdleDeadline)} callback - * @param {{timeout: number}} options - an options object - * @returns {number} the id - */ - -/** - * @callback NextTick - * @param {VoidVarArgsFunc} callback - the callback to run - * @param {...*} args - optional arguments to call the callback with - * @returns {void} - */ - -/** - * @callback SetImmediate - * @param {VoidVarArgsFunc} callback - the callback to run - * @param {...*} args - optional arguments to call the callback with - * @returns {NodeImmediate} - */ - -/** - * @callback VoidVarArgsFunc - * @param {...*} callback - the callback to run - * @returns {void} - */ - -/** - * @typedef RequestAnimationFrame - * @property {function(number):void} requestAnimationFrame - * @returns {number} - the id - */ - -/** - * @typedef Performance - * @property {function(): number} now - */ - -/* eslint-disable jsdoc/require-property-description */ -/** - * @typedef {object} Clock - * @property {number} now - the current time - * @property {Date} Date - the Date constructor - * @property {number} loopLimit - the maximum number of timers before assuming an infinite loop - * @property {RequestIdleCallback} requestIdleCallback - * @property {function(number):void} cancelIdleCallback - * @property {setTimeout} setTimeout - * @property {clearTimeout} clearTimeout - * @property {NextTick} nextTick - * @property {queueMicrotask} queueMicrotask - * @property {setInterval} setInterval - * @property {clearInterval} clearInterval - * @property {SetImmediate} setImmediate - * @property {function(NodeImmediate):void} clearImmediate - * @property {function():number} countTimers - * @property {RequestAnimationFrame} requestAnimationFrame - * @property {function(number):void} cancelAnimationFrame - * @property {function():void} runMicrotasks - * @property {function(string | number): number} tick - * @property {function(string | number): Promise} tickAsync - * @property {function(): number} next - * @property {function(): Promise} nextAsync - * @property {function(): number} runAll - * @property {function(): number} runToFrame - * @property {function(): Promise} runAllAsync - * @property {function(): number} runToLast - * @property {function(): Promise} runToLastAsync - * @property {function(): void} reset - * @property {function(number | Date): void} setSystemTime - * @property {function(number): void} jump - * @property {Performance} performance - * @property {function(number[]): number[]} hrtime - process.hrtime (legacy) - * @property {function(): void} uninstall Uninstall the clock. - * @property {Function[]} methods - the methods that are faked - * @property {boolean} [shouldClearNativeTimers] inherited from config - */ -/* eslint-enable jsdoc/require-property-description */ - -/** - * Configuration object for the `install` method. - * - * @typedef {object} Config - * @property {number|Date} [now] a number (in milliseconds) or a Date object (default epoch) - * @property {string[]} [toFake] names of the methods that should be faked. - * @property {number} [loopLimit] the maximum number of timers that will be run when calling runAll() - * @property {boolean} [shouldAdvanceTime] tells FakeTimers to increment mocked time automatically (default false) - * @property {number} [advanceTimeDelta] increment mocked time every <> ms (default: 20ms) - * @property {boolean} [shouldClearNativeTimers] forwards clear timer calls to native functions if they are not fakes (default: false) - */ - -/* eslint-disable jsdoc/require-property-description */ -/** - * The internal structure to describe a scheduled fake timer - * - * @typedef {object} Timer - * @property {Function} func - * @property {*[]} args - * @property {number} delay - * @property {number} callAt - * @property {number} createdAt - * @property {boolean} immediate - * @property {number} id - * @property {Error} [error] - */ - -/** - * A Node timer - * - * @typedef {object} NodeImmediate - * @property {function(): boolean} hasRef - * @property {function(): NodeImmediate} ref - * @property {function(): NodeImmediate} unref - */ -/* eslint-enable jsdoc/require-property-description */ - -/* eslint-disable complexity */ - -/** - * Mocks available features in the specified global namespace. - * - * @param {*} _global Namespace to mock (e.g. `window`) - * @returns {FakeTimers} - */ -function withGlobal(_global) { - 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 - const NOOP = function () { - return undefined; - }; - const NOOP_ARRAY = function () { - return []; - }; - const timeoutResult = _global.setTimeout(NOOP, 0); - const addTimerReturnsObject = typeof timeoutResult === "object"; - const performancePresent = - _global.performance && typeof _global.performance.now === "function"; - const hasPerformancePrototype = - _global.Performance && - (typeof _global.Performance).match(/^(function|object)$/); - const hasPerformanceConstructorPrototype = - _global.performance && - _global.performance.constructor && - _global.performance.constructor.prototype; - const queueMicrotaskPresent = _global.hasOwnProperty("queueMicrotask"); - const requestAnimationFramePresent = - _global.requestAnimationFrame && - typeof _global.requestAnimationFrame === "function"; - const cancelAnimationFramePresent = - _global.cancelAnimationFrame && - typeof _global.cancelAnimationFrame === "function"; - const requestIdleCallbackPresent = - _global.requestIdleCallback && - typeof _global.requestIdleCallback === "function"; - const cancelIdleCallbackPresent = - _global.cancelIdleCallback && - typeof _global.cancelIdleCallback === "function"; - const setImmediatePresent = - _global.setImmediate && typeof _global.setImmediate === "function"; - const intlPresent = _global.Intl && typeof _global.Intl === "object"; - - _global.clearTimeout(timeoutResult); - - const NativeDate = _global.Date; - const NativeIntl = _global.Intl; - let uniqueTimerId = idCounterStart; - - /** - * @param {number} num - * @returns {boolean} - */ - function isNumberFinite(num) { - if (Number.isFinite) { - return Number.isFinite(num); - } - - return isFinite(num); - } - - let isNearInfiniteLimit = false; - - /** - * @param {Clock} clock - * @param {number} i - */ - function checkIsNearInfiniteLimit(clock, i) { - if (clock.loopLimit && i === clock.loopLimit - 1) { - isNearInfiniteLimit = true; - } - } - - /** - * - */ - function resetIsNearInfiniteLimit() { - isNearInfiniteLimit = false; - } - - /** - * Parse strings like "01:10:00" (meaning 1 hour, 10 minutes, 0 seconds) into - * number of milliseconds. This is used to support human-readable strings passed - * to clock.tick() - * - * @param {string} str - * @returns {number} - */ - function parseTime(str) { - if (!str) { - return 0; - } - - const strings = str.split(":"); - const l = strings.length; - let i = l; - let ms = 0; - let parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error( - "tick only understands numbers, 'm:s' and 'h:m:s'. Each part must be two digits", - ); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - - if (parsed >= 60) { - throw new Error(`Invalid time ${str}`); - } - - ms += parsed * Math.pow(60, l - i - 1); - } - - return ms * 1000; - } - - /** - * Get the decimal part of the millisecond value as nanoseconds - * - * @param {number} msFloat the number of milliseconds - * @returns {number} an integer number of nanoseconds in the range [0,1e6) - * - * Example: nanoRemainer(123.456789) -> 456789 - */ - function nanoRemainder(msFloat) { - const modulo = 1e6; - const remainder = (msFloat * 1e6) % modulo; - const positiveRemainder = - remainder < 0 ? remainder + modulo : remainder; - - return Math.floor(positiveRemainder); - } - - /** - * Used to grok the `now` parameter to createClock. - * - * @param {Date|number} epoch the system time - * @returns {number} - */ - function getEpoch(epoch) { - if (!epoch) { - return 0; - } - if (typeof epoch.getTime === "function") { - return epoch.getTime(); - } - if (typeof epoch === "number") { - return epoch; - } - throw new TypeError("now should be milliseconds since UNIX epoch"); - } - - /** - * @param {number} from - * @param {number} to - * @param {Timer} timer - * @returns {boolean} - */ - function inRange(from, to, timer) { - return timer && timer.callAt >= from && timer.callAt <= to; - } - - /** - * @param {Clock} clock - * @param {Timer} job - */ - function getInfiniteLoopError(clock, job) { - const infiniteLoopError = new Error( - `Aborting after running ${clock.loopLimit} timers, assuming an infinite loop!`, - ); - - if (!job.error) { - return infiniteLoopError; - } - - // pattern never matched in Node - const computedTargetPattern = /target\.*[<|(|[].*?[>|\]|)]\s*/; - let clockMethodPattern = new RegExp( - String(Object.keys(clock).join("|")), - ); - - if (addTimerReturnsObject) { - // node.js environment - clockMethodPattern = new RegExp( - `\\s+at (Object\\.)?(?:${Object.keys(clock).join("|")})\\s+`, - ); - } - - let matchedLineIndex = -1; - job.error.stack.split("\n").some(function (line, i) { - // If we've matched a computed target line (e.g. setTimeout) then we - // don't need to look any further. Return true to stop iterating. - const matchedComputedTarget = line.match(computedTargetPattern); - /* istanbul ignore if */ - if (matchedComputedTarget) { - matchedLineIndex = i; - return true; - } - - // If we've matched a clock method line, then there may still be - // others further down the trace. Return false to keep iterating. - const matchedClockMethod = line.match(clockMethodPattern); - if (matchedClockMethod) { - matchedLineIndex = i; - return false; - } - - // If we haven't matched anything on this line, but we matched - // previously and set the matched line index, then we can stop. - // If we haven't matched previously, then we should keep iterating. - return matchedLineIndex >= 0; - }); - - const stack = `${infiniteLoopError}\n${job.type || "Microtask"} - ${ - job.func.name || "anonymous" - }\n${job.error.stack - .split("\n") - .slice(matchedLineIndex + 1) - .join("\n")}`; - - try { - Object.defineProperty(infiniteLoopError, "stack", { - value: stack, - }); - } catch (e) { - // noop - } - - return infiniteLoopError; - } - - /** - * @param {Date} target - * @param {Date} source - * @returns {Date} the target after modifications - */ - function mirrorDateProperties(target, source) { - let prop; - for (prop in source) { - if (source.hasOwnProperty(prop)) { - target[prop] = source[prop]; - } - } - - // set special now implementation - if (source.now) { - target.now = function now() { - return target.clock.now; - }; - } else { - delete target.now; - } - - // set special toSource implementation - if (source.toSource) { - target.toSource = function toSource() { - return source.toSource(); - }; - } else { - delete target.toSource; - } - - // set special toString implementation - target.toString = function toString() { - return source.toString(); - }; - - target.prototype = source.prototype; - target.parse = source.parse; - target.UTC = source.UTC; - target.prototype.toUTCString = source.prototype.toUTCString; - target.isFake = true; - - return target; - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function createDate() { - /** - * @param {number} year - * @param {number} month - * @param {number} date - * @param {number} hour - * @param {number} minute - * @param {number} second - * @param {number} ms - * @returns {Date} - */ - function ClockDate(year, month, date, hour, minute, second, ms) { - // 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. - if (!(this instanceof ClockDate)) { - return new NativeDate(ClockDate.clock.now).toString(); - } - - // if Date is called as a constructor with 'new' keyword - // Defensive and verbose to avoid potential harm in passing - // explicit undefined when user does not pass argument - switch (arguments.length) { - case 0: - return new NativeDate(ClockDate.clock.now); - case 1: - return new NativeDate(year); - case 2: - return new NativeDate(year, month); - case 3: - return new NativeDate(year, month, date); - case 4: - return new NativeDate(year, month, date, hour); - case 5: - return new NativeDate(year, month, date, hour, minute); - case 6: - return new NativeDate( - year, - month, - date, - hour, - minute, - second, - ); - default: - return new NativeDate( - year, - month, - date, - hour, - minute, - second, - ms, - ); - } - } - - return mirrorDateProperties(ClockDate, NativeDate); - } - - /** - * Mirror Intl by default on our fake implementation - * - * Most of the properties are the original native ones, - * but we need to take control of those that have a - * dependency on the current clock. - * - * @returns {object} the partly fake Intl implementation - */ - function createIntl() { - const ClockIntl = {}; - /* - * All properties of Intl are non-enumerable, so we need - * to do a bit of work to get them out. - */ - Object.getOwnPropertyNames(NativeIntl).forEach( - (property) => (ClockIntl[property] = NativeIntl[property]), - ); - - ClockIntl.DateTimeFormat = function (...args) { - const realFormatter = new NativeIntl.DateTimeFormat(...args); - const formatter = {}; - - ["formatRange", "formatRangeToParts", "resolvedOptions"].forEach( - (method) => { - formatter[method] = - realFormatter[method].bind(realFormatter); - }, - ); - - ["format", "formatToParts"].forEach((method) => { - formatter[method] = function (date) { - return realFormatter[method](date || ClockIntl.clock.now); - }; - }); - - return formatter; - }; - - ClockIntl.DateTimeFormat.prototype = Object.create( - NativeIntl.DateTimeFormat.prototype, - ); - - ClockIntl.DateTimeFormat.supportedLocalesOf = - NativeIntl.DateTimeFormat.supportedLocalesOf; - - return ClockIntl; - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function enqueueJob(clock, job) { - // enqueues a microtick-deferred task - ecma262/#sec-enqueuejob - if (!clock.jobs) { - clock.jobs = []; - } - clock.jobs.push(job); - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function runJobs(clock) { - // runs all microtick-deferred tasks - ecma262/#sec-runjobs - if (!clock.jobs) { - return; - } - for (let i = 0; i < clock.jobs.length; i++) { - const job = clock.jobs[i]; - job.func.apply(null, job.args); - - checkIsNearInfiniteLimit(clock, i); - if (clock.loopLimit && i > clock.loopLimit) { - throw getInfiniteLoopError(clock, job); - } - } - resetIsNearInfiniteLimit(); - clock.jobs = []; - } - - /** - * @param {Clock} clock - * @param {Timer} timer - * @returns {number} id of the created timer - */ - function addTimer(clock, timer) { - if (timer.func === undefined) { - throw new Error("Callback must be provided to timer calls"); - } - - if (addTimerReturnsObject) { - // Node.js environment - if (typeof timer.func !== "function") { - throw new TypeError( - `[ERR_INVALID_CALLBACK]: Callback must be a function. Received ${ - timer.func - } of type ${typeof timer.func}`, - ); - } - } - - if (isNearInfiniteLimit) { - timer.error = new Error(); - } - - timer.type = timer.immediate ? "Immediate" : "Timeout"; - - if (timer.hasOwnProperty("delay")) { - if (typeof timer.delay !== "number") { - timer.delay = parseInt(timer.delay, 10); - } - - if (!isNumberFinite(timer.delay)) { - timer.delay = 0; - } - timer.delay = timer.delay > maxTimeout ? 1 : timer.delay; - timer.delay = Math.max(0, timer.delay); - } - - if (timer.hasOwnProperty("interval")) { - timer.type = "Interval"; - timer.interval = timer.interval > maxTimeout ? 1 : timer.interval; - } - - if (timer.hasOwnProperty("animation")) { - timer.type = "AnimationFrame"; - timer.animation = true; - } - - if (timer.hasOwnProperty("idleCallback")) { - timer.type = "IdleCallback"; - timer.idleCallback = true; - } - - if (!clock.timers) { - clock.timers = {}; - } - - timer.id = uniqueTimerId++; - timer.createdAt = clock.now; - timer.callAt = - clock.now + (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); - - clock.timers[timer.id] = timer; - - if (addTimerReturnsObject) { - const res = { - refed: true, - ref: function () { - this.refed = true; - return res; - }, - unref: function () { - this.refed = false; - return res; - }, - hasRef: function () { - return this.refed; - }, - refresh: function () { - timer.callAt = - clock.now + - (parseInt(timer.delay) || (clock.duringTick ? 1 : 0)); - - // it _might_ have been removed, but if not the assignment is perfectly fine - clock.timers[timer.id] = timer; - - return res; - }, - [Symbol.toPrimitive]: function () { - return timer.id; - }, - }; - return res; - } - - return timer.id; - } - - /* eslint consistent-return: "off" */ - /** - * Timer comparitor - * - * @param {Timer} a - * @param {Timer} b - * @returns {number} - */ - function compareTimers(a, b) { - // Sort first by absolute timing - if (a.callAt < b.callAt) { - return -1; - } - if (a.callAt > b.callAt) { - return 1; - } - - // Sort next by immediate, immediate timers take precedence - if (a.immediate && !b.immediate) { - return -1; - } - if (!a.immediate && b.immediate) { - return 1; - } - - // Sort next by creation time, earlier-created timers take precedence - if (a.createdAt < b.createdAt) { - return -1; - } - if (a.createdAt > b.createdAt) { - return 1; - } - - // Sort next by id, lower-id timers take precedence - if (a.id < b.id) { - return -1; - } - if (a.id > b.id) { - return 1; - } - - // As timer ids are unique, no fallback `0` is necessary - } - - /** - * @param {Clock} clock - * @param {number} from - * @param {number} to - * @returns {Timer} - */ - function firstTimerInRange(clock, from, to) { - const timers = clock.timers; - let timer = null; - let id, isInRange; - - for (id in timers) { - if (timers.hasOwnProperty(id)) { - isInRange = inRange(from, to, timers[id]); - - if ( - isInRange && - (!timer || compareTimers(timer, timers[id]) === 1) - ) { - timer = timers[id]; - } - } - } - - return timer; - } - - /** - * @param {Clock} clock - * @returns {Timer} - */ - function firstTimer(clock) { - const timers = clock.timers; - let timer = null; - let id; - - for (id in timers) { - if (timers.hasOwnProperty(id)) { - if (!timer || compareTimers(timer, timers[id]) === 1) { - timer = timers[id]; - } - } - } - - return timer; - } - - /** - * @param {Clock} clock - * @returns {Timer} - */ - function lastTimer(clock) { - const timers = clock.timers; - let timer = null; - let id; - - for (id in timers) { - if (timers.hasOwnProperty(id)) { - if (!timer || compareTimers(timer, timers[id]) === -1) { - timer = timers[id]; - } - } - } - - return timer; - } - - /** - * @param {Clock} clock - * @param {Timer} timer - */ - function callTimer(clock, timer) { - if (typeof timer.interval === "number") { - clock.timers[timer.id].callAt += timer.interval; - } else { - delete clock.timers[timer.id]; - } - - if (typeof timer.func === "function") { - timer.func.apply(null, timer.args); - } else { - /* eslint no-eval: "off" */ - const eval2 = eval; - (function () { - eval2(timer.func); - })(); - } - } - - /** - * Gets clear handler name for a given timer type - * - * @param {string} ttype - */ - function getClearHandler(ttype) { - if (ttype === "IdleCallback" || ttype === "AnimationFrame") { - return `cancel${ttype}`; - } - return `clear${ttype}`; - } - - /** - * Gets schedule handler name for a given timer type - * - * @param {string} ttype - */ - function getScheduleHandler(ttype) { - if (ttype === "IdleCallback" || ttype === "AnimationFrame") { - return `request${ttype}`; - } - return `set${ttype}`; - } - - /** - * Creates an anonymous function to warn only once - */ - function createWarnOnce() { - let calls = 0; - return function (msg) { - // eslint-disable-next-line - !calls++ && console.warn(msg); - }; - } - const warnOnce = createWarnOnce(); - - /** - * @param {Clock} clock - * @param {number} timerId - * @param {string} ttype - */ - function clearTimer(clock, timerId, ttype) { - if (!timerId) { - // null appears to be allowed in most browsers, and appears to be - // relied upon by some libraries, like Bootstrap carousel - return; - } - - if (!clock.timers) { - clock.timers = {}; - } - - // in Node, the ID is stored as the primitive value for `Timeout` objects - // for `Immediate` objects, no ID exists, so it gets coerced to NaN - const id = Number(timerId); - - if (Number.isNaN(id) || id < idCounterStart) { - const handlerName = getClearHandler(ttype); - - if (clock.shouldClearNativeTimers === true) { - const nativeHandler = clock[`_${handlerName}`]; - return typeof nativeHandler === "function" - ? nativeHandler(timerId) - : undefined; - } - warnOnce( - `FakeTimers: ${handlerName} was invoked to clear a native timer instead of one created by this library.` + - "\nTo automatically clean-up native timers, use `shouldClearNativeTimers`.", - ); - } - - if (clock.timers.hasOwnProperty(id)) { - // check that the ID matches a timer of the correct type - const timer = clock.timers[id]; - if ( - timer.type === ttype || - (timer.type === "Timeout" && ttype === "Interval") || - (timer.type === "Interval" && ttype === "Timeout") - ) { - delete clock.timers[id]; - } else { - const clear = getClearHandler(ttype); - const schedule = getScheduleHandler(timer.type); - throw new Error( - `Cannot clear timer: timer created with ${schedule}() but cleared with ${clear}()`, - ); - } - } - } - - /** - * @param {Clock} clock - * @param {Config} config - * @returns {Timer[]} - */ - function uninstall(clock, config) { - let method, i, l; - const installedHrTime = "_hrtime"; - const installedNextTick = "_nextTick"; - - for (i = 0, l = clock.methods.length; i < l; i++) { - method = clock.methods[i]; - if (method === "performance") { - const originalPerfDescriptor = Object.getOwnPropertyDescriptor( - clock, - `_${method}`, - ); - if ( - originalPerfDescriptor && - originalPerfDescriptor.get && - !originalPerfDescriptor.set - ) { - Object.defineProperty( - _global, - method, - originalPerfDescriptor, - ); - } else if (originalPerfDescriptor.configurable) { - _global[method] = clock[`_${method}`]; - } - } else { - if (_global[method] && _global[method].hadOwnProperty) { - _global[method] = clock[`_${method}`]; - } else { - try { - delete _global[method]; - } catch (ignore) { - /* eslint no-empty: "off" */ - } - } - } - } - - if (config.shouldAdvanceTime === true) { - _global.clearInterval(clock.attachedInterval); - } - - // Prevent multiple executions which will completely remove these props - clock.methods = []; - - // return pending timers, to enable checking what timers remained on uninstall - if (!clock.timers) { - return []; - } - return Object.keys(clock.timers).map(function mapper(key) { - return clock.timers[key]; - }); - } - - /** - * @param {object} target the target containing the method to replace - * @param {string} method the keyname of the method on the target - * @param {Clock} clock - */ - function hijackMethod(target, method, clock) { - clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call( - target, - method, - ); - clock[`_${method}`] = target[method]; - - if (method === "Date") { - const date = mirrorDateProperties(clock[method], target[method]); - target[method] = date; - } else if (method === "Intl") { - target[method] = clock[method]; - } else if (method === "performance") { - const originalPerfDescriptor = Object.getOwnPropertyDescriptor( - target, - method, - ); - // JSDOM has a read only performance field so we have to save/copy it differently - if ( - originalPerfDescriptor && - originalPerfDescriptor.get && - !originalPerfDescriptor.set - ) { - Object.defineProperty( - clock, - `_${method}`, - originalPerfDescriptor, - ); - - const perfDescriptor = Object.getOwnPropertyDescriptor( - clock, - method, - ); - Object.defineProperty(target, method, perfDescriptor); - } else { - target[method] = clock[method]; - } - } else { - target[method] = function () { - return clock[method].apply(clock, arguments); - }; - - Object.defineProperties( - target[method], - Object.getOwnPropertyDescriptors(clock[method]), - ); - } - - target[method].clock = clock; - } - - /** - * @param {Clock} clock - * @param {number} advanceTimeDelta - */ - function doIntervalTick(clock, advanceTimeDelta) { - clock.tick(advanceTimeDelta); - } - - /** - * @typedef {object} Timers - * @property {setTimeout} setTimeout - * @property {clearTimeout} clearTimeout - * @property {setInterval} setInterval - * @property {clearInterval} clearInterval - * @property {Date} Date - * @property {Intl} Intl - * @property {SetImmediate=} setImmediate - * @property {function(NodeImmediate): void=} clearImmediate - * @property {function(number[]):number[]=} hrtime - * @property {NextTick=} nextTick - * @property {Performance=} performance - * @property {RequestAnimationFrame=} requestAnimationFrame - * @property {boolean=} queueMicrotask - * @property {function(number): void=} cancelAnimationFrame - * @property {RequestIdleCallback=} requestIdleCallback - * @property {function(number): void=} cancelIdleCallback - */ - - /** @type {Timers} */ - const timers = { - setTimeout: _global.setTimeout, - clearTimeout: _global.clearTimeout, - setInterval: _global.setInterval, - clearInterval: _global.clearInterval, - Date: _global.Date, - }; - - if (setImmediatePresent) { - timers.setImmediate = _global.setImmediate; - timers.clearImmediate = _global.clearImmediate; - } - - if (performancePresent) { - timers.performance = _global.performance; - } - - if (requestAnimationFramePresent) { - timers.requestAnimationFrame = _global.requestAnimationFrame; - } - - if (queueMicrotaskPresent) { - timers.queueMicrotask = true; - } - - if (cancelAnimationFramePresent) { - timers.cancelAnimationFrame = _global.cancelAnimationFrame; - } - - if (requestIdleCallbackPresent) { - timers.requestIdleCallback = _global.requestIdleCallback; - } - - if (cancelIdleCallbackPresent) { - timers.cancelIdleCallback = _global.cancelIdleCallback; - } - - if (intlPresent) { - timers.Intl = _global.Intl; - } - - const originalSetTimeout = _global.setImmediate || _global.setTimeout; - - /** - * @param {Date|number} [start] the system time - non-integer values are floored - * @param {number} [loopLimit] maximum number of timers that will be run when calling runAll() - * @returns {Clock} - */ - function createClock(start, loopLimit) { - // eslint-disable-next-line no-param-reassign - start = Math.floor(getEpoch(start)); - // eslint-disable-next-line no-param-reassign - loopLimit = loopLimit || 1000; - let nanos = 0; - const adjustedSystemTime = [0, 0]; // [millis, nanoremainder] - - if (NativeDate === undefined) { - throw new Error( - "The global scope doesn't have a `Date` object" + - " (see https://github.com/sinonjs/sinon/issues/1852#issuecomment-419622780)", - ); - } - - const clock = { - now: start, - Date: createDate(), - loopLimit: loopLimit, - }; - - clock.Date.clock = clock; - - //eslint-disable-next-line jsdoc/require-jsdoc - function getTimeToNextFrame() { - return 16 - ((clock.now - start) % 16); - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function hrtime(prev) { - const millisSinceStart = clock.now - adjustedSystemTime[0] - start; - const secsSinceStart = Math.floor(millisSinceStart / 1000); - const remainderInNanos = - (millisSinceStart - secsSinceStart * 1e3) * 1e6 + - nanos - - adjustedSystemTime[1]; - - if (Array.isArray(prev)) { - if (prev[1] > 1e9) { - throw new TypeError( - "Number of nanoseconds can't exceed a billion", - ); - } - - const oldSecs = prev[0]; - let nanoDiff = remainderInNanos - prev[1]; - let secDiff = secsSinceStart - oldSecs; - - if (nanoDiff < 0) { - nanoDiff += 1e9; - secDiff -= 1; - } - - return [secDiff, nanoDiff]; - } - return [secsSinceStart, remainderInNanos]; - } - - /** - * A high resolution timestamp in milliseconds. - * - * @typedef {number} DOMHighResTimeStamp - */ - - /** - * performance.now() - * - * @returns {DOMHighResTimeStamp} - */ - function fakePerformanceNow() { - const hrt = hrtime(); - const millis = hrt[0] * 1000 + hrt[1] / 1e6; - return millis; - } - - if (intlPresent) { - clock.Intl = createIntl(); - clock.Intl.clock = clock; - } - - clock.requestIdleCallback = function requestIdleCallback( - func, - timeout, - ) { - let timeToNextIdlePeriod = 0; - - if (clock.countTimers() > 0) { - timeToNextIdlePeriod = 50; // const for now - } - - const result = addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 2), - delay: - typeof timeout === "undefined" - ? timeToNextIdlePeriod - : Math.min(timeout, timeToNextIdlePeriod), - idleCallback: true, - }); - - return Number(result); - }; - - clock.cancelIdleCallback = function cancelIdleCallback(timerId) { - return clearTimer(clock, timerId, "IdleCallback"); - }; - - clock.setTimeout = function setTimeout(func, timeout) { - return addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 2), - delay: timeout, - }); - }; - - clock.clearTimeout = function clearTimeout(timerId) { - return clearTimer(clock, timerId, "Timeout"); - }; - - clock.nextTick = function nextTick(func) { - return enqueueJob(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 1), - error: isNearInfiniteLimit ? new Error() : null, - }); - }; - - clock.queueMicrotask = function queueMicrotask(func) { - return clock.nextTick(func); // explicitly drop additional arguments - }; - - clock.setInterval = function setInterval(func, timeout) { - // eslint-disable-next-line no-param-reassign - timeout = parseInt(timeout, 10); - return addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 2), - delay: timeout, - interval: timeout, - }); - }; - - clock.clearInterval = function clearInterval(timerId) { - return clearTimer(clock, timerId, "Interval"); - }; - - if (setImmediatePresent) { - clock.setImmediate = function setImmediate(func) { - return addTimer(clock, { - func: func, - args: Array.prototype.slice.call(arguments, 1), - immediate: true, - }); - }; - - clock.clearImmediate = function clearImmediate(timerId) { - return clearTimer(clock, timerId, "Immediate"); - }; - } - - clock.countTimers = function countTimers() { - return ( - Object.keys(clock.timers || {}).length + - (clock.jobs || []).length - ); - }; - - clock.requestAnimationFrame = function requestAnimationFrame(func) { - const result = addTimer(clock, { - func: func, - delay: getTimeToNextFrame(), - get args() { - return [fakePerformanceNow()]; - }, - animation: true, - }); - - return Number(result); - }; - - clock.cancelAnimationFrame = function cancelAnimationFrame(timerId) { - return clearTimer(clock, timerId, "AnimationFrame"); - }; - - clock.runMicrotasks = function runMicrotasks() { - runJobs(clock); - }; - - /** - * @param {number|string} tickValue milliseconds or a string parseable by parseTime - * @param {boolean} isAsync - * @param {Function} resolve - * @param {Function} reject - * @returns {number|undefined} will return the new `now` value or nothing for async - */ - function doTick(tickValue, isAsync, resolve, reject) { - const msFloat = - typeof tickValue === "number" - ? tickValue - : parseTime(tickValue); - const ms = Math.floor(msFloat); - const remainder = nanoRemainder(msFloat); - let nanosTotal = nanos + remainder; - let tickTo = clock.now + ms; - - if (msFloat < 0) { - throw new TypeError("Negative ticks are not supported"); - } - - // adjust for positive overflow - if (nanosTotal >= 1e6) { - tickTo += 1; - nanosTotal -= 1e6; - } - - nanos = nanosTotal; - let tickFrom = clock.now; - let previous = clock.now; - // ESLint fails to detect this correctly - /* eslint-disable prefer-const */ - let timer, - firstException, - oldNow, - nextPromiseTick, - compensationCheck, - postTimerCall; - /* eslint-enable prefer-const */ - - clock.duringTick = true; - - // perform microtasks - oldNow = clock.now; - runJobs(clock); - if (oldNow !== clock.now) { - // compensate for any setSystemTime() call during microtask callback - tickFrom += clock.now - oldNow; - tickTo += clock.now - oldNow; - } - - //eslint-disable-next-line jsdoc/require-jsdoc - function doTickInner() { - // perform each timer in the requested range - timer = firstTimerInRange(clock, tickFrom, tickTo); - // eslint-disable-next-line no-unmodified-loop-condition - while (timer && tickFrom <= tickTo) { - if (clock.timers[timer.id]) { - tickFrom = timer.callAt; - clock.now = timer.callAt; - oldNow = clock.now; - try { - runJobs(clock); - callTimer(clock, timer); - } catch (e) { - firstException = firstException || e; - } - - if (isAsync) { - // finish up after native setImmediate callback to allow - // all native es6 promises to process their callbacks after - // each timer fires. - originalSetTimeout(nextPromiseTick); - return; - } - - compensationCheck(); - } - - postTimerCall(); - } - - // perform process.nextTick()s again - oldNow = clock.now; - runJobs(clock); - if (oldNow !== clock.now) { - // compensate for any setSystemTime() call during process.nextTick() callback - tickFrom += clock.now - oldNow; - tickTo += clock.now - oldNow; - } - clock.duringTick = false; - - // corner case: during runJobs new timers were scheduled which could be in the range [clock.now, tickTo] - timer = firstTimerInRange(clock, tickFrom, tickTo); - if (timer) { - try { - clock.tick(tickTo - clock.now); // do it all again - for the remainder of the requested range - } catch (e) { - firstException = firstException || e; - } - } else { - // no timers remaining in the requested range: move the clock all the way to the end - clock.now = tickTo; - - // update nanos - nanos = nanosTotal; - } - if (firstException) { - throw firstException; - } - - if (isAsync) { - resolve(clock.now); - } else { - return clock.now; - } - } - - nextPromiseTick = - isAsync && - function () { - try { - compensationCheck(); - postTimerCall(); - doTickInner(); - } catch (e) { - reject(e); - } - }; - - compensationCheck = function () { - // compensate for any setSystemTime() call during timer callback - if (oldNow !== clock.now) { - tickFrom += clock.now - oldNow; - tickTo += clock.now - oldNow; - previous += clock.now - oldNow; - } - }; - - postTimerCall = function () { - timer = firstTimerInRange(clock, previous, tickTo); - previous = tickFrom; - }; - - return doTickInner(); - } - - /** - * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" - * @returns {number} will return the new `now` value - */ - clock.tick = function tick(tickValue) { - return doTick(tickValue, false); - }; - - if (typeof _global.Promise !== "undefined") { - /** - * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" - * @returns {Promise} - */ - clock.tickAsync = function tickAsync(tickValue) { - return new _global.Promise(function (resolve, reject) { - originalSetTimeout(function () { - try { - doTick(tickValue, true, resolve, reject); - } catch (e) { - reject(e); - } - }); - }); - }; - } - - clock.next = function next() { - runJobs(clock); - const timer = firstTimer(clock); - if (!timer) { - return clock.now; - } - - clock.duringTick = true; - try { - clock.now = timer.callAt; - callTimer(clock, timer); - runJobs(clock); - return clock.now; - } finally { - clock.duringTick = false; - } - }; - - if (typeof _global.Promise !== "undefined") { - clock.nextAsync = function nextAsync() { - return new _global.Promise(function (resolve, reject) { - originalSetTimeout(function () { - try { - const timer = firstTimer(clock); - if (!timer) { - resolve(clock.now); - return; - } - - let err; - clock.duringTick = true; - clock.now = timer.callAt; - try { - callTimer(clock, timer); - } catch (e) { - err = e; - } - clock.duringTick = false; - - originalSetTimeout(function () { - if (err) { - reject(err); - } else { - resolve(clock.now); - } - }); - } catch (e) { - reject(e); - } - }); - }); - }; - } - - clock.runAll = function runAll() { - let numTimers, i; - runJobs(clock); - for (i = 0; i < clock.loopLimit; i++) { - if (!clock.timers) { - resetIsNearInfiniteLimit(); - return clock.now; - } - - numTimers = Object.keys(clock.timers).length; - if (numTimers === 0) { - resetIsNearInfiniteLimit(); - return clock.now; - } - - clock.next(); - checkIsNearInfiniteLimit(clock, i); - } - - const excessJob = firstTimer(clock); - throw getInfiniteLoopError(clock, excessJob); - }; - - clock.runToFrame = function runToFrame() { - return clock.tick(getTimeToNextFrame()); - }; - - if (typeof _global.Promise !== "undefined") { - clock.runAllAsync = function runAllAsync() { - return new _global.Promise(function (resolve, reject) { - let i = 0; - /** - * - */ - function doRun() { - originalSetTimeout(function () { - try { - runJobs(clock); - - let numTimers; - if (i < clock.loopLimit) { - if (!clock.timers) { - resetIsNearInfiniteLimit(); - resolve(clock.now); - return; - } - - numTimers = Object.keys( - clock.timers, - ).length; - if (numTimers === 0) { - resetIsNearInfiniteLimit(); - resolve(clock.now); - return; - } - - clock.next(); - - i++; - - doRun(); - checkIsNearInfiniteLimit(clock, i); - return; - } - - const excessJob = firstTimer(clock); - reject(getInfiniteLoopError(clock, excessJob)); - } catch (e) { - reject(e); - } - }); - } - doRun(); - }); - }; - } - - clock.runToLast = function runToLast() { - const timer = lastTimer(clock); - if (!timer) { - runJobs(clock); - return clock.now; - } - - return clock.tick(timer.callAt - clock.now); - }; - - if (typeof _global.Promise !== "undefined") { - clock.runToLastAsync = function runToLastAsync() { - return new _global.Promise(function (resolve, reject) { - originalSetTimeout(function () { - try { - const timer = lastTimer(clock); - if (!timer) { - runJobs(clock); - resolve(clock.now); - } - - resolve(clock.tickAsync(timer.callAt - clock.now)); - } catch (e) { - reject(e); - } - }); - }); - }; - } - - clock.reset = function reset() { - nanos = 0; - clock.timers = {}; - clock.jobs = []; - clock.now = start; - }; - - clock.setSystemTime = function setSystemTime(systemTime) { - // determine time difference - const newNow = getEpoch(systemTime); - const difference = newNow - clock.now; - let id, timer; - - adjustedSystemTime[0] = adjustedSystemTime[0] + difference; - adjustedSystemTime[1] = adjustedSystemTime[1] + nanos; - // update 'system clock' - clock.now = newNow; - nanos = 0; - - // update timers and intervals to keep them stable - for (id in clock.timers) { - if (clock.timers.hasOwnProperty(id)) { - timer = clock.timers[id]; - timer.createdAt += difference; - timer.callAt += difference; - } - } - }; - - /** - * @param {string|number} tickValue number of milliseconds or a human-readable value like "01:11:15" - * @returns {number} will return the new `now` value - */ - clock.jump = function jump(tickValue) { - const msFloat = - typeof tickValue === "number" - ? tickValue - : parseTime(tickValue); - const ms = Math.floor(msFloat); - - for (const timer of Object.values(clock.timers)) { - if (clock.now + ms > timer.callAt) { - timer.callAt = clock.now + ms; - } - } - clock.tick(ms); - }; - - if (performancePresent) { - clock.performance = Object.create(null); - clock.performance.now = fakePerformanceNow; - } - - return clock; - } - - /* eslint-disable complexity */ - - /** - * @param {Config=} [config] Optional config - * @returns {Clock} - */ - function install(config) { - console.log('INSTALL', config); - if ( - arguments.length > 1 || - config instanceof Date || - Array.isArray(config) || - typeof config === "number" - ) { - throw new TypeError( - `FakeTimers.install called with ${String( - config, - )} install requires an object parameter`, - ); - } - - if (_global.Date.isFake === true) { - // Timers are already faked; this is a problem. - // Make the user reset timers before continuing. - throw new TypeError( - "Can't install fake timers twice on the same global object.", - ); - } - - // eslint-disable-next-line no-param-reassign - config = typeof config !== "undefined" ? config : {}; - config.shouldAdvanceTime = config.shouldAdvanceTime || false; - config.advanceTimeDelta = config.advanceTimeDelta || 20; - config.shouldClearNativeTimers = - config.shouldClearNativeTimers || false; - - if (config.target) { - throw new TypeError( - "config.target is no longer supported. Use `withGlobal(target)` instead.", - ); - } - - let i, l; - const clock = createClock(config.now, config.loopLimit); - clock.shouldClearNativeTimers = config.shouldClearNativeTimers; - - clock.uninstall = function () { - return uninstall(clock, config); - }; - - clock.methods = config.toFake || []; - - if (clock.methods.length === 0) { - // do not fake nextTick by default - GitHub#126 - clock.methods = Object.keys(timers).filter(function (key) { - return key !== "nextTick" && key !== "queueMicrotask"; - }); - } - - if (config.shouldAdvanceTime === true) { - const intervalTick = doIntervalTick.bind( - null, - clock, - config.advanceTimeDelta, - ); - const intervalId = _global.setInterval( - intervalTick, - config.advanceTimeDelta, - ); - clock.attachedInterval = intervalId; - } - - if (clock.methods.includes("performance")) { - const proto = (() => { - if (hasPerformanceConstructorPrototype) { - return _global.performance.constructor.prototype; - } - if (hasPerformancePrototype) { - return _global.Performance.prototype; - } - })(); - if (proto) { - Object.getOwnPropertyNames(proto).forEach(function (name) { - if (name !== "now") { - clock.performance[name] = - name.indexOf("getEntries") === 0 - ? NOOP_ARRAY - : NOOP; - } - }); - } else if ((config.toFake || []).includes("performance")) { - // user explicitly tried to fake performance when not present - throw new ReferenceError( - "non-existent performance object cannot be faked", - ); - } - } - - for (i = 0, l = clock.methods.length; i < l; i++) { - const nameOfMethodToReplace = clock.methods[i]; - hijackMethod(_global, nameOfMethodToReplace, clock); - } - - return clock; - } - - /* eslint-enable complexity */ - - return { - timers: timers, - createClock: createClock, - install: install, - withGlobal: withGlobal, - }; -} - -/** - * @typedef {object} FakeTimers - * @property {Function} install - * @property {withGlobal} withGlobal - */ - -/* eslint-enable complexity */ - -/** @type {FakeTimers} */ -const defaultImplementation = withGlobal(globalThis); -exports.install = defaultImplementation.install; diff --git a/tests/page/page-clock.frozen.spec.ts b/tests/page/page-clock.frozen.spec.ts new file mode 100644 index 0000000000000..3d763a2cc3ddc --- /dev/null +++ b/tests/page/page-clock.frozen.spec.ts @@ -0,0 +1,23 @@ +/** + * 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 as it, expect } from './pageTest'; + +it.skip(!process.env.PW_FREEZE_TIME); + +it('cock should be frozen', async ({ page }) => { + expect(await page.evaluate('Date.now()')).toBe(0); +}); diff --git a/tests/page/page-clock.spec.ts b/tests/page/page-clock.spec.ts index ccc9e8773c381..649ab250bd746 100644 --- a/tests/page/page-clock.spec.ts +++ b/tests/page/page-clock.spec.ts @@ -549,6 +549,19 @@ it.describe('stubTimers', () => { expect(await promise).toEqual({ prev: 0, next: 1000 }); }); + it('replaces global performance.timeOrigin', async ({ page }) => { + await page.clock.installFakeTimers(1000); + const promise = page.evaluate(async () => { + const prev = performance.now(); + await new Promise(f => setTimeout(f, 1000)); + const next = performance.now(); + return { prev, next }; + }); + expect(await page.evaluate(() => performance.timeOrigin)).toBe(1000); + await page.clock.runFor(1000); + expect(await promise).toEqual({ prev: 0, next: 1000 }); + }); + it('fakes Date constructor', async ({ page }) => { await page.clock.installFakeTimers(0); const now = await page.evaluate(() => new Date().getTime()); diff --git a/utils/build/build.js b/utils/build/build.js index 980802735d061..56442c094847b 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -311,6 +311,7 @@ steps.push({ onChanges.push({ inputs: [ 'packages/playwright-core/src/server/injected/**', + 'packages/playwright-core/src/third_party/**', 'packages/playwright-ct-core/src/injected/**', 'packages/playwright-core/src/utils/isomorphic/**', 'utils/generate_injected.js',