chore: rename fakeTimers to clock (#31193)

This commit is contained in:
Pavel Feldman 2024-06-06 15:56:13 -07:00 committed by GitHub
parent 3e86ebc80c
commit 826343b8a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 3223 additions and 223 deletions

View file

@ -102,7 +102,7 @@ context.BackgroundPage += (_, backgroundPage) =>
* since: v1.45 * since: v1.45
- type: <[Clock]> - 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 ## event: BrowserContext.close
* since: v1.8 * since: v1.8

View file

@ -155,7 +155,7 @@ page.Load -= PageLoadHandler;
* since: v1.45 * since: v1.45
- type: <[Clock]> - 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 ## event: Page.close
* since: v1.8 * since: v1.8

View file

@ -15,12 +15,12 @@
*/ */
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
import * as fakeTimersSource from '../generated/fakeTimersSource'; import * as clockSource from '../generated/clockSource';
export class Clock { export class Clock {
private _browserContext: BrowserContext; private _browserContext: BrowserContext;
private _scriptInjected = false; private _scriptInjected = false;
private _fakeTimersInstalled = false; private _clockInstalled = false;
private _now = 0; private _now = 0;
constructor(browserContext: BrowserContext) { constructor(browserContext: BrowserContext) {
@ -30,48 +30,48 @@ export class Clock {
async installFakeTimers(time: number, loopLimit: number | undefined) { async installFakeTimers(time: number, loopLimit: number | undefined) {
await this._injectScriptIfNeeded(); await this._injectScriptIfNeeded();
await this._addAndEvaluate(`(() => { await this._addAndEvaluate(`(() => {
globalThis.__pwFakeTimers.clock?.uninstall(); globalThis.__pwClock.clock?.uninstall();
globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, loopLimit })}); globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, loopLimit })});
})();`); })();`);
this._now = time; this._now = time;
this._fakeTimersInstalled = true; this._clockInstalled = true;
} }
async runToNextTimer(): Promise<number> { async runToNextTimer(): Promise<number> {
this._assertInstalled(); this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.next()`); await this._browserContext.addInitScript(`globalThis.__pwClock.clock.next()`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.nextAsync()`); this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.nextAsync()`);
return this._now; return this._now;
} }
async runAllTimers(): Promise<number> { async runAllTimers(): Promise<number> {
this._assertInstalled(); this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runAll()`); await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runAll()`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runAllAsync()`); this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runAllAsync()`);
return this._now; return this._now;
} }
async runToLastTimer(): Promise<number> { async runToLastTimer(): Promise<number> {
this._assertInstalled(); this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.runToLast()`); await this._browserContext.addInitScript(`globalThis.__pwClock.clock.runToLast()`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.runToLastAsync()`); this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.runToLastAsync()`);
return this._now; return this._now;
} }
async setTime(time: number) { async setTime(time: number) {
if (this._fakeTimersInstalled) { if (this._clockInstalled) {
const jump = time - this._now; const jump = time - this._now;
if (jump < 0) if (jump < 0)
throw new Error('Unable to set time into the past when fake timers are installed'); 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; this._now = time;
return this._now; return this._now;
} }
await this._injectScriptIfNeeded(); await this._injectScriptIfNeeded();
await this._addAndEvaluate(`(() => { await this._addAndEvaluate(`(() => {
globalThis.__pwFakeTimers.clock?.uninstall(); globalThis.__pwClock.clock?.uninstall();
globalThis.__pwFakeTimers.clock = globalThis.__pwFakeTimers.install(${JSON.stringify({ now: time, toFake: ['Date'] })}); globalThis.__pwClock.clock = globalThis.__pwClock.install(${JSON.stringify({ now: time, toFake: ['Date'] })});
})();`); })();`);
this._now = time; this._now = time;
return this._now; return this._now;
@ -85,8 +85,8 @@ export class Clock {
async runFor(time: number | string): Promise<number> { async runFor(time: number | string): Promise<number> {
this._assertInstalled(); this._assertInstalled();
await this._browserContext.addInitScript(`globalThis.__pwFakeTimers.clock.tick(${JSON.stringify(time)})`); await this._browserContext.addInitScript(`globalThis.__pwClock.clock.tick(${JSON.stringify(time)})`);
this._now = await this._evaluateInFrames(`globalThis.__pwFakeTimers.clock.tickAsync(${JSON.stringify(time)})`); this._now = await this._evaluateInFrames(`globalThis.__pwClock.clock.tickAsync(${JSON.stringify(time)})`);
return this._now; return this._now;
} }
@ -96,8 +96,8 @@ export class Clock {
this._scriptInjected = true; this._scriptInjected = true;
const script = `(() => { const script = `(() => {
const module = {}; const module = {};
${fakeTimersSource.source} ${clockSource.source}
globalThis.__pwFakeTimers = (module.exports.inject())(globalThis); globalThis.__pwClock = (module.exports.inject())(globalThis);
})();`; })();`;
await this._addAndEvaluate(script); await this._addAndEvaluate(script);
} }
@ -114,7 +114,7 @@ export class Clock {
} }
private _assertInstalled() { private _assertInstalled() {
if (!this._fakeTimersInstalled) if (!this._clockInstalled)
throw new Error('Clock is not installed'); throw new Error('Clock is not installed');
} }
} }

View file

@ -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. * 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; Date: DateConstructor;
setTimeout: Window['setTimeout']; setTimeout: Window['setTimeout'];
clearTimeout: Window['clearTimeout']; clearTimeout: Window['clearTimeout'];
@ -24,12 +24,12 @@ type ClockMethods = {
performance?: Window['performance']; performance?: Window['performance'];
}; };
type ClockConfig = { export type ClockConfig = {
now?: number | Date; now?: number | Date;
loopLimit?: number; loopLimit?: number;
}; };
type InstallConfig = ClockConfig & { export type InstallConfig = ClockConfig & {
toFake?: (keyof ClockMethods)[]; toFake?: (keyof ClockMethods)[];
}; };
@ -44,7 +44,7 @@ enum TimerType {
type Timer = { type Timer = {
type: TimerType; type: TimerType;
func: TimerHandler; func: TimerHandler;
args: any[]; args: () => any[];
delay: number; delay: number;
callAt: number; callAt: number;
createdAt: number; createdAt: number;
@ -57,15 +57,13 @@ interface Embedder {
postTaskPeriodically(task: () => void, delay: number): () => void; postTaskPeriodically(task: () => void, delay: number): () => void;
} }
class Clock { export class ClockController {
readonly start: number; readonly start: number;
private _now: number; private _now: number;
private _loopLimit: number; private _loopLimit: number;
private _jobs: Timer[] = [];
private _adjustedSystemTime = 0; private _adjustedSystemTime = 0;
private _duringTick = false; private _duringTick = false;
private _timers = new Map<number, Timer>(); private _timers = new Map<number, Timer>();
private _isNearInfiniteLimit = false;
private _uniqueTimerId = idCounterStart; private _uniqueTimerId = idCounterStart;
private _embedder: Embedder; private _embedder: Embedder;
readonly disposables: (() => void)[] = []; readonly disposables: (() => void)[] = [];
@ -88,10 +86,7 @@ class Clock {
} }
performanceNow(): DOMHighResTimeStamp { performanceNow(): DOMHighResTimeStamp {
const millisSinceStart = this._now - this._adjustedSystemTime - this.start; return 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 { 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 compensationCheck: () => void;
let postTimerCall: () => void; let postTimerCall: () => void;
/* eslint-enable prefer-const */
this._duringTick = true; this._duringTick = true;
// perform microtasks // perform microtasks
oldNow = this._now; oldNow = this._now;
this._runJobs();
if (oldNow !== this._now) { if (oldNow !== this._now) {
// compensate for any setSystemTime() call during microtask callback // compensate for any setSystemTime() call during microtask callback
tickFrom += this._now - oldNow; tickFrom += this._now - oldNow;
@ -138,7 +130,6 @@ class Clock {
this._now = timer.callAt; this._now = timer.callAt;
oldNow = this._now; oldNow = this._now;
try { try {
this._runJobs();
this._callTimer(timer); this._callTimer(timer);
} catch (e) { } catch (e) {
firstException = firstException || e; firstException = firstException || e;
@ -158,7 +149,6 @@ class Clock {
// perform process.nextTick()s again // perform process.nextTick()s again
oldNow = this._now; oldNow = this._now;
this._runJobs();
if (oldNow !== this._now) { if (oldNow !== this._now) {
// compensate for any setSystemTime() call during process.nextTick() callback // compensate for any setSystemTime() call during process.nextTick() callback
tickFrom += this._now - oldNow; tickFrom += this._now - oldNow;
@ -220,20 +210,12 @@ class Clock {
return this._doTick(tickValue, false)!; return this._doTick(tickValue, false)!;
} }
tickAsync(tickValue: string | number): Promise<number> { async tickAsync(tickValue: string | number): Promise<number> {
return new Promise<number>((resolve, reject) => { await new Promise<void>(f => this._embedder.postTask(f));
this._embedder.postTask(() => { return new Promise((resolve, reject) => this._doTick(tickValue, true, resolve, reject));
try {
this._doTick(tickValue, true, resolve, reject);
} catch (e) {
reject(e);
}
});
});
} }
next() { next() {
this._runJobs();
const timer = this._firstTimer(); const timer = this._firstTimer();
if (!timer) if (!timer)
return this._now; return this._now;
@ -242,117 +224,73 @@ class Clock {
try { try {
this._now = timer.callAt; this._now = timer.callAt;
this._callTimer(timer); this._callTimer(timer);
this._runJobs();
return this._now; return this._now;
} finally { } finally {
this._duringTick = false; this._duringTick = false;
} }
} }
nextAsync() { async nextAsync() {
return new Promise<number>((resolve, reject) => { await new Promise<void>(f => this._embedder.postTask(f));
this._embedder.postTask(() => { const timer = this._firstTimer();
try { if (!timer)
const timer = this._firstTimer(); return this._now;
if (!timer) {
resolve(this._now);
return;
}
let err: Error; let err: Error | undefined;
this._duringTick = true; this._duringTick = true;
this._now = timer.callAt; this._now = timer.callAt;
try { try {
this._callTimer(timer); this._callTimer(timer);
} catch (e) { } catch (e) {
err = e; err = e;
} }
this._duringTick = false; this._duringTick = false;
this._embedder.postTask(() => { await new Promise<void>(f => this._embedder.postTask(f));
if (err) if (err)
reject(err); throw err;
else return this._now;
resolve(this._now);
});
} catch (e) {
reject(e);
}
});
});
} }
runAll() { runAll() {
this._runJobs();
for (let i = 0; i < this._loopLimit; i++) { for (let i = 0; i < this._loopLimit; i++) {
const numTimers = this._timers.size; const numTimers = this._timers.size;
if (numTimers === 0) { if (numTimers === 0)
this._resetIsNearInfiniteLimit();
return this._now; return this._now;
}
this.next(); this.next();
this._checkIsNearInfiniteLimit(i);
} }
const excessJob = this._firstTimer(); const excessJob = this._firstTimer();
throw this._getInfiniteLoopError(excessJob!); if (!excessJob)
return;
throw this._getInfiniteLoopError(excessJob);
} }
runToFrame() { runToFrame() {
return this.tick(this.getTimeToNextFrame()); return this.tick(this.getTimeToNextFrame());
} }
runAllAsync() { async runAllAsync() {
return new Promise<number>((resolve, reject) => { for (let i = 0; i < this._loopLimit; i++) {
let i = 0; await new Promise<void>(f => this._embedder.postTask(f));
/** const numTimers = this._timers.size;
* if (numTimers === 0)
*/ return this._now;
const doRun = () => {
this._embedder.postTask(() => {
try {
this._runJobs();
let numTimers; this.next();
if (i < this._loopLimit) { }
if (!this._timers) { await new Promise<void>(f => this._embedder.postTask(f));
this._resetIsNearInfiniteLimit();
resolve(this._now);
return;
}
numTimers = this._timers.size; const excessJob = this._firstTimer();
if (numTimers === 0) { if (!excessJob)
this._resetIsNearInfiniteLimit(); return;
resolve(this._now); throw this._getInfiniteLoopError(excessJob);
return;
}
this.next();
i++;
doRun();
this._checkIsNearInfiniteLimit(i);
return;
}
const excessJob = this._firstTimer();
reject(this._getInfiniteLoopError(excessJob!));
} catch (e) {
reject(e);
}
});
};
doRun();
});
} }
runToLast() { runToLast() {
const timer = this._lastTimer(); const timer = this._lastTimer();
if (!timer) { if (!timer)
this._runJobs();
return this._now; return this._now;
}
return this.tick(timer.callAt - this._now); return this.tick(timer.callAt - this._now);
} }
@ -362,7 +300,6 @@ class Clock {
try { try {
const timer = this._lastTimer(); const timer = this._lastTimer();
if (!timer) { if (!timer) {
this._runJobs();
resolve(this._now); resolve(this._now);
return; return;
} }
@ -376,7 +313,6 @@ class Clock {
reset() { reset() {
this._timers.clear(); this._timers.clear();
this._jobs = [];
this._now = this.start; this._now = this.start;
} }
@ -410,34 +346,7 @@ class Clock {
return this.tick(ms); return this.tick(ms);
} }
private _checkIsNearInfiniteLimit(i: number): void { addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: () => any[] }): number {
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) if (options.func === undefined)
throw new Error('Callback must be provided to timer calls'); throw new Error('Callback must be provided to timer calls');
@ -450,12 +359,12 @@ class Clock {
const timer: Timer = { const timer: Timer = {
type: options.type, type: options.type,
func: options.func, func: options.func,
args: options.args || [], args: options.args || (() => []),
delay, delay,
callAt: this._now + (delay || (this._duringTick ? 1 : 0)), callAt: this._now + (delay || (this._duringTick ? 1 : 0)),
createdAt: this._now, createdAt: this._now,
id: this._uniqueTimerId++, id: this._uniqueTimerId++,
error: this._isNearInfiniteLimit ? new Error() : undefined, error: new Error(),
}; };
this._timers.set(timer.id, timer); this._timers.set(timer.id, timer);
return timer.id; return timer.id;
@ -472,7 +381,7 @@ class Clock {
} }
countTimers() { countTimers() {
return this._timers.size + this._jobs.length; return this._timers.size;
} }
private _firstTimer(): Timer | null { private _firstTimer(): Timer | null {
@ -500,7 +409,7 @@ class Clock {
this._timers.get(timer.id)!.callAt += timer.delay; this._timers.get(timer.id)!.callAt += timer.delay;
else else
this._timers.delete(timer.id); this._timers.delete(timer.id);
callFunction(timer.func, timer.args); callFunction(timer.func, timer.args());
} }
private _getInfiniteLoopError(job: Timer) { private _getInfiniteLoopError(job: Timer) {
@ -548,14 +457,7 @@ class Clock {
.slice(matchedLineIndex + 1) .slice(matchedLineIndex + 1)
.join('\n')}`; .join('\n')}`;
try { infiniteLoopError.stack = stack;
Object.defineProperty(infiniteLoopError, 'stack', {
value: stack,
});
} catch (e) {
// noop
}
return infiniteLoopError; return infiniteLoopError;
} }
@ -661,7 +563,7 @@ function mirrorDateProperties(target: any, source: typeof Date): DateConstructor
return target; 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 { 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. // 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. // 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 * but we need to take control of those that have a
* dependency on the current clock. * 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 = {}; const ClockIntl: any = {};
/* /*
* All properties of Intl are non-enumerable, so we need * 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)[]) for (const key of Object.keys(NativeIntl) as (keyof typeof Intl)[])
ClockIntl[key] = NativeIntl[key]; ClockIntl[key] = NativeIntl[key];
ClockIntl.DateTimeFormat = (...args: any[]) => { ClockIntl.DateTimeFormat = function(...args: any[]) {
const realFormatter = new NativeIntl.DateTimeFormat(...args); const realFormatter = new NativeIntl.DateTimeFormat(...args);
const formatter: Intl.DateTimeFormat = { const formatter: Intl.DateTimeFormat = {
formatRange: realFormatter.formatRange.bind(realFormatter), 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 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 idCounterStart = 1e12; // arbitrarily large number to avoid collisions with native timer IDs
function platformOriginals(globalObject: WindowOrWorkerGlobalScope): ClockMethods { function platformOriginals(globalObject: WindowOrWorkerGlobalScope): { raw: ClockMethods, bound: ClockMethods } {
return { const raw: ClockMethods = {
setTimeout: globalObject.setTimeout.bind(globalObject), setTimeout: globalObject.setTimeout,
clearTimeout: globalObject.clearTimeout.bind(globalObject), clearTimeout: globalObject.clearTimeout,
setInterval: globalObject.setInterval.bind(globalObject), setInterval: globalObject.setInterval,
clearInterval: globalObject.clearInterval.bind(globalObject), clearInterval: globalObject.clearInterval,
requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame.bind(globalObject) : undefined, requestAnimationFrame: (globalObject as any).requestAnimationFrame ? (globalObject as any).requestAnimationFrame : undefined,
cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame.bind(globalObject) : undefined, cancelAnimationFrame: (globalObject as any).cancelAnimationFrame ? (globalObject as any).cancelAnimationFrame : undefined,
requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback.bind(globalObject) : undefined, requestIdleCallback: (globalObject as any).requestIdleCallback ? (globalObject as any).requestIdleCallback : undefined,
cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback.bind(globalObject) : undefined, cancelIdleCallback: (globalObject as any).cancelIdleCallback ? (globalObject as any).cancelIdleCallback : undefined,
Date: (globalObject as any).Date, Date: (globalObject as any).Date,
performance: globalObject.performance, performance: globalObject.performance,
Intl: (globalObject as any).Intl, 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}`; return `set${type}`;
} }
function createApi(clock: Clock, originals: ClockMethods): ClockMethods { function createApi(clock: ClockController, originals: ClockMethods): ClockMethods {
return { return {
setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => { setTimeout: (func: TimerHandler, timeout?: number | undefined, ...args: any[]) => {
const delay = timeout ? +timeout : timeout; const delay = timeout ? +timeout : timeout;
return clock.addTimer({ return clock.addTimer({
type: TimerType.Timeout, type: TimerType.Timeout,
func, func,
args, args: () => args,
delay delay
}); });
}, },
@ -833,7 +741,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods {
return clock.addTimer({ return clock.addTimer({
type: TimerType.Interval, type: TimerType.Interval,
func, func,
args, args: () => args,
delay, delay,
}); });
}, },
@ -846,9 +754,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods {
type: TimerType.AnimationFrame, type: TimerType.AnimationFrame,
func: callback, func: callback,
delay: clock.getTimeToNextFrame(), delay: clock.getTimeToNextFrame(),
get args() { args: () => [clock.performanceNow()],
return [clock.performanceNow()];
},
}); });
}, },
cancelAnimationFrame: (timerId: number): void => { cancelAnimationFrame: (timerId: number): void => {
@ -863,7 +769,7 @@ function createApi(clock: Clock, originals: ClockMethods): ClockMethods {
return clock.addTimer({ return clock.addTimer({
type: TimerType.IdleCallback, type: TimerType.IdleCallback,
func: callback, func: callback,
args: [], args: () => [],
delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod, delay: options?.timeout ? Math.min(options?.timeout, timeToNextIdlePeriod) : timeToNextIdlePeriod,
}); });
}, },
@ -884,33 +790,41 @@ function getClearHandler(type: TimerType) {
return `clear${type}`; return `clear${type}`;
} }
function fakePerformance(clock: Clock, performance: Performance): Performance { function fakePerformance(clock: ClockController, performance: Performance): Performance {
const result: any = { const result: any = {
now: () => clock.performanceNow(), now: () => clock.performanceNow(),
timeOrigin: clock.start, 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; return result;
} }
export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: Clock, api: Partial<ClockMethods>, originals: Partial<ClockMethods> } { export function createClock(globalObject: WindowOrWorkerGlobalScope, config: ClockConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
const originals = platformOriginals(globalObject); const originals = platformOriginals(globalObject);
const embedder = { const embedder = {
postTask: (task: () => void) => { postTask: (task: () => void) => {
originals.setTimeout!(task, 0); originals.bound.setTimeout(task, 0);
}, },
postTaskPeriodically: (task: () => void, delay: number) => { postTaskPeriodically: (task: () => void, delay: number) => {
const intervalId = globalObject.setInterval(task, delay); 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 clock = new ClockController(embedder, config.now, config.loopLimit);
const api = createApi(clock, originals); const api = createApi(clock, originals.bound);
return { clock, api, originals }; return { clock, api, originals: originals.raw };
} }
export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: Clock, api: Partial<ClockMethods>, originals: Partial<ClockMethods> } { export function install(globalObject: WindowOrWorkerGlobalScope, config: InstallConfig = {}): { clock: ClockController, api: ClockMethods, originals: ClockMethods } {
if ((globalObject as any).Date?.isFake) { if ((globalObject as any).Date?.isFake) {
// Timers are already faked; this is a problem. // Timers are already faked; this is a problem.
// Make the user reset timers before continuing. // Make the user reset timers before continuing.
@ -946,6 +860,6 @@ export function inject(globalObject: WindowOrWorkerGlobalScope) {
const { clock } = install(globalObject, config); const { clock } = install(globalObject, config);
return clock; return clock;
}, },
builtin: platformOriginals(globalObject), builtin: platformOriginals(globalObject).bound,
}; };
} }

View file

@ -125,14 +125,14 @@ export class InjectedScript {
} }
builtinSetTimeout(callback: Function, timeout: number) { builtinSetTimeout(callback: Function, timeout: number) {
if (this.window.__pwFakeTimers?.builtin) if (this.window.__pwClock?.builtin)
return this.window.__pwFakeTimers.builtin.setTimeout(callback, timeout); return this.window.__pwClock.builtin.setTimeout(callback, timeout);
return setTimeout(callback, timeout); return setTimeout(callback, timeout);
} }
builtinRequestAnimationFrame(callback: FrameRequestCallback) { builtinRequestAnimationFrame(callback: FrameRequestCallback) {
if (this.window.__pwFakeTimers?.builtin) if (this.window.__pwClock?.builtin)
return this.window.__pwFakeTimers.builtin.requestAnimationFrame(callback); return this.window.__pwClock.builtin.requestAnimationFrame(callback);
return requestAnimationFrame(callback); return requestAnimationFrame(callback);
} }
@ -1525,7 +1525,7 @@ function deepEquals(a: any, b: any): boolean {
declare global { declare global {
interface Window { interface Window {
__pwFakeTimers?: { __pwClock?: {
builtin: { builtin: {
setTimeout: Window['setTimeout'], setTimeout: Window['setTimeout'],
requestAnimationFrame: Window['requestAnimationFrame'], requestAnimationFrame: Window['requestAnimationFrame'],

View file

@ -79,42 +79,42 @@ export class UtilityScript {
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
const window = (globalThis as any); const window = (globalThis as any);
window.builtinSetTimeout = (callback: Function, timeout: number) => { window.builtinSetTimeout = (callback: Function, timeout: number) => {
if (window.__pwFakeTimers?.builtin) if (window.__pwClock?.builtin)
return window.__pwFakeTimers.builtin.setTimeout(callback, timeout); return window.__pwClock.builtin.setTimeout(callback, timeout);
return setTimeout(callback, timeout); return setTimeout(callback, timeout);
}; };
window.builtinClearTimeout = (id: number) => { window.builtinClearTimeout = (id: number) => {
if (window.__pwFakeTimers?.builtin) if (window.__pwClock?.builtin)
return window.__pwFakeTimers.builtin.clearTimeout(id); return window.__pwClock.builtin.clearTimeout(id);
return clearTimeout(id); return clearTimeout(id);
}; };
window.builtinSetInterval = (callback: Function, timeout: number) => { window.builtinSetInterval = (callback: Function, timeout: number) => {
if (window.__pwFakeTimers?.builtin) if (window.__pwClock?.builtin)
return window.__pwFakeTimers.builtin.setInterval(callback, timeout); return window.__pwClock.builtin.setInterval(callback, timeout);
return setInterval(callback, timeout); return setInterval(callback, timeout);
}; };
window.builtinClearInterval = (id: number) => { window.builtinClearInterval = (id: number) => {
if (window.__pwFakeTimers?.builtin) if (window.__pwClock?.builtin)
return window.__pwFakeTimers.builtin.clearInterval(id); return window.__pwClock.builtin.clearInterval(id);
return clearInterval(id); return clearInterval(id);
}; };
window.builtinRequestAnimationFrame = (callback: FrameRequestCallback) => { window.builtinRequestAnimationFrame = (callback: FrameRequestCallback) => {
if (window.__pwFakeTimers?.builtin) if (window.__pwClock?.builtin)
return window.__pwFakeTimers.builtin.requestAnimationFrame(callback); return window.__pwClock.builtin.requestAnimationFrame(callback);
return requestAnimationFrame(callback); return requestAnimationFrame(callback);
}; };
window.builtinCancelAnimationFrame = (id: number) => { window.builtinCancelAnimationFrame = (id: number) => {
if (window.__pwFakeTimers?.builtin) if (window.__pwClock?.builtin)
return window.__pwFakeTimers.builtin.cancelAnimationFrame(id); return window.__pwClock.builtin.cancelAnimationFrame(id);
return cancelAnimationFrame(id); return cancelAnimationFrame(id);
}; };
window.builtinDate = window.__pwFakeTimers?.builtin.Date || Date; window.builtinDate = window.__pwClock?.builtin.Date || Date;
window.builtinPerformance = window.__pwFakeTimers?.builtin.performance || performance; window.builtinPerformance = window.__pwClock?.builtin.performance || performance;
} }
} }

View file

@ -4864,7 +4864,7 @@ export interface Page {
accessibility: Accessibility; 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; 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; clock: Clock;

3086
tests/library/clock.spec.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -51,7 +51,7 @@ const injectedScripts = [
true, 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', 'lib', 'server', 'injected', 'packed'),
path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'),
true, true,