mirror of
https://github.com/denoland/std.git
synced 2024-11-22 04:59:05 +00:00
feat(testing): add utility for faking time (#2069)
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
parent
5d7915ccec
commit
403f226a20
@ -796,3 +796,56 @@ Deno.test("randomMultiple uses randomInt to generate random multiples between -1
|
||||
assertSpyCalls(randomIntStub, 2);
|
||||
});
|
||||
```
|
||||
|
||||
### Faking time
|
||||
|
||||
Say we have a function that has time based behavior that we would like to test.
|
||||
With real time, that could cause tests to take much longer than they should. If
|
||||
you fake time, you could simulate how your function would behave over time
|
||||
starting from any point in time. Below is an example where we want to test that
|
||||
the callback is called every second.
|
||||
|
||||
```ts
|
||||
// https://deno.land/std@$STD_VERSION/testing/mock_examples/interval.ts
|
||||
export function secondInterval(cb: () => void): number {
|
||||
return setInterval(cb, 1000);
|
||||
}
|
||||
```
|
||||
|
||||
With `FakeTime` we can do that. When the `FakeTime` instance is created, it
|
||||
splits from real time. The `Date`, `setTimeout`, `clearTimeout`, `setInterval`
|
||||
and `clearInterval` globals are replaced with versions that use the fake time
|
||||
until real time is restored. You can control how time ticks forward with the
|
||||
`tick` method on the `FakeTime` instance.
|
||||
|
||||
```ts
|
||||
// https://deno.land/std@$STD_VERSION/testing/mock_examples/interval_test.ts
|
||||
import {
|
||||
assertSpyCalls,
|
||||
spy,
|
||||
} from "https://deno.land/std@$STD_VERSION/testing/mock.ts";
|
||||
import { FakeTime } from "https://deno.land/std@$STD_VERSION/testing/time.ts";
|
||||
import { secondInterval } from "https://deno.land/std@$STD_VERSION/testing/mock_examples/interval.ts";
|
||||
|
||||
Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
|
||||
const time = new FakeTime();
|
||||
|
||||
try {
|
||||
const cb = spy();
|
||||
const intervalId = secondInterval(cb);
|
||||
assertSpyCalls(cb, 0);
|
||||
time.tick(500);
|
||||
assertSpyCalls(cb, 0);
|
||||
time.tick(500);
|
||||
assertSpyCalls(cb, 1);
|
||||
time.tick(3500);
|
||||
assertSpyCalls(cb, 4);
|
||||
|
||||
clearInterval(intervalId);
|
||||
time.tick(1000);
|
||||
assertSpyCalls(cb, 4);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
10
testing/_time.ts
Normal file
10
testing/_time.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
/** Used internally for testing that fake time uses real time correctly. */
|
||||
export const _internals = {
|
||||
Date,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
};
|
4
testing/mock_examples/interval.ts
Normal file
4
testing/mock_examples/interval.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
export function secondInterval(cb: () => void): number {
|
||||
return setInterval(cb, 1000);
|
||||
}
|
26
testing/mock_examples/interval_test.ts
Normal file
26
testing/mock_examples/interval_test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
import { assertSpyCalls, spy } from "../mock.ts";
|
||||
import { FakeTime } from "../time.ts";
|
||||
import { secondInterval } from "./interval.ts";
|
||||
|
||||
Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
|
||||
const time = new FakeTime();
|
||||
const cb = spy();
|
||||
|
||||
try {
|
||||
const intervalId = secondInterval(cb);
|
||||
assertSpyCalls(cb, 0);
|
||||
time.tick(500);
|
||||
assertSpyCalls(cb, 0);
|
||||
time.tick(500);
|
||||
assertSpyCalls(cb, 1);
|
||||
time.tick(3500);
|
||||
assertSpyCalls(cb, 4);
|
||||
|
||||
clearInterval(intervalId);
|
||||
time.tick(1000);
|
||||
assertSpyCalls(cb, 4);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
460
testing/time.ts
Normal file
460
testing/time.ts
Normal file
@ -0,0 +1,460 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
/** This module is browser compatible. */
|
||||
|
||||
import { ascend, RBTree } from "../collections/rb_tree.ts";
|
||||
import { DelayOptions } from "../async/delay.ts";
|
||||
import { _internals } from "./_time.ts";
|
||||
|
||||
/** An error related to faking time. */
|
||||
export class TimeError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "TimeError";
|
||||
}
|
||||
}
|
||||
|
||||
function isFakeDate(instance: unknown): instance is FakeDate {
|
||||
return instance instanceof FakeDate;
|
||||
}
|
||||
|
||||
interface FakeDate extends Date {
|
||||
date: Date;
|
||||
}
|
||||
|
||||
function FakeDate(this: void): string;
|
||||
function FakeDate(this: FakeDate): void;
|
||||
function FakeDate(
|
||||
this: FakeDate,
|
||||
value: string | number | Date,
|
||||
): void;
|
||||
function FakeDate(
|
||||
this: FakeDate,
|
||||
year: number,
|
||||
month: number,
|
||||
date?: number,
|
||||
hours?: number,
|
||||
minutes?: number,
|
||||
seconds?: number,
|
||||
ms?: number,
|
||||
): void;
|
||||
function FakeDate(
|
||||
this: FakeDate | void,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
...args: any[]
|
||||
): string | void {
|
||||
if (args.length === 0) args.push(FakeDate.now());
|
||||
if (isFakeDate(this)) {
|
||||
this.date = new _internals.Date(...(args as []));
|
||||
} else {
|
||||
return new _internals.Date(args[0]).toString();
|
||||
}
|
||||
}
|
||||
|
||||
FakeDate.parse = Date.parse;
|
||||
FakeDate.UTC = Date.UTC;
|
||||
FakeDate.now = () => time?.now ?? _internals.Date.now();
|
||||
Object.getOwnPropertyNames(Date.prototype).forEach((name: string) => {
|
||||
const propName: keyof Date = name as keyof Date;
|
||||
FakeDate.prototype[propName] = function (
|
||||
this: FakeDate,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
...args: any[]
|
||||
// deno-lint-ignore no-explicit-any
|
||||
): any {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
return (this.date[propName] as (...args: any[]) => any).apply(
|
||||
this.date,
|
||||
args,
|
||||
);
|
||||
};
|
||||
});
|
||||
Object.getOwnPropertySymbols(Date.prototype).forEach((name: symbol) => {
|
||||
const propName: keyof Date = name as unknown as keyof Date;
|
||||
FakeDate.prototype[propName] = function (
|
||||
this: FakeDate,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
...args: any[]
|
||||
// deno-lint-ignore no-explicit-any
|
||||
): any {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
return (this.date[propName] as (...args: any[]) => any).apply(
|
||||
this.date,
|
||||
args,
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface Timer {
|
||||
id: number;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
callback: (...args: any[]) => void;
|
||||
delay: number;
|
||||
args: unknown[];
|
||||
due: number;
|
||||
repeat: boolean;
|
||||
}
|
||||
|
||||
export interface FakeTimeOptions {
|
||||
/**
|
||||
* The rate relative to real time at which fake time is updated.
|
||||
* By default time only moves forward through calling tick or setting now.
|
||||
* Set to 1 to have the fake time automatically tick foward at the same rate in milliseconds as real time.
|
||||
*/
|
||||
advanceRate: number;
|
||||
/**
|
||||
* The frequency in milliseconds at which fake time is updated.
|
||||
* If advanceRate is set, we will update the time every 10 milliseconds by default.
|
||||
*/
|
||||
advanceFrequency?: number;
|
||||
}
|
||||
|
||||
interface DueNode {
|
||||
due: number;
|
||||
timers: Timer[];
|
||||
}
|
||||
|
||||
let time: FakeTime | undefined = undefined;
|
||||
|
||||
function fakeSetTimeout(
|
||||
// deno-lint-ignore no-explicit-any
|
||||
callback: (...args: any[]) => void,
|
||||
delay = 0,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
...args: any[]
|
||||
): number {
|
||||
if (!time) throw new TimeError("no fake time");
|
||||
return setTimer(callback, delay, args, false);
|
||||
}
|
||||
|
||||
function fakeClearTimeout(id?: number): void {
|
||||
if (!time) throw new TimeError("no fake time");
|
||||
if (typeof id === "number" && dueNodes.has(id)) {
|
||||
dueNodes.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function fakeSetInterval(
|
||||
// deno-lint-ignore no-explicit-any
|
||||
callback: (...args: any[]) => unknown,
|
||||
delay = 0,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
...args: any[]
|
||||
): number {
|
||||
if (!time) throw new TimeError("no fake time");
|
||||
return setTimer(callback, delay, args, true);
|
||||
}
|
||||
|
||||
function fakeClearInterval(id?: number): void {
|
||||
if (!time) throw new TimeError("no fake time");
|
||||
if (typeof id === "number" && dueNodes.has(id)) {
|
||||
dueNodes.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimer(
|
||||
// deno-lint-ignore no-explicit-any
|
||||
callback: (...args: any[]) => void,
|
||||
delay = 0,
|
||||
args: unknown[],
|
||||
repeat = false,
|
||||
): number {
|
||||
const id: number = timerId.next().value;
|
||||
delay = Math.max(repeat ? 1 : 0, Math.floor(delay));
|
||||
const due: number = now + delay;
|
||||
let dueNode: DueNode | null = dueTree.find({ due } as DueNode);
|
||||
if (dueNode === null) {
|
||||
dueNode = { due, timers: [] };
|
||||
dueTree.insert(dueNode);
|
||||
}
|
||||
dueNode.timers.push({
|
||||
id,
|
||||
callback,
|
||||
args,
|
||||
delay,
|
||||
due,
|
||||
repeat,
|
||||
});
|
||||
dueNodes.set(id, dueNode);
|
||||
return id;
|
||||
}
|
||||
|
||||
function overrideGlobals(): void {
|
||||
globalThis.Date = FakeDate as DateConstructor;
|
||||
globalThis.setTimeout = fakeSetTimeout;
|
||||
globalThis.clearTimeout = fakeClearTimeout;
|
||||
globalThis.setInterval = fakeSetInterval;
|
||||
globalThis.clearInterval = fakeClearInterval;
|
||||
}
|
||||
|
||||
function restoreGlobals(): void {
|
||||
globalThis.Date = _internals.Date;
|
||||
globalThis.setTimeout = _internals.setTimeout;
|
||||
globalThis.clearTimeout = _internals.clearTimeout;
|
||||
globalThis.setInterval = _internals.setInterval;
|
||||
globalThis.clearInterval = _internals.clearInterval;
|
||||
}
|
||||
|
||||
function* timerIdGen() {
|
||||
let i = 1;
|
||||
while (true) yield i++;
|
||||
}
|
||||
|
||||
let startedAt: number;
|
||||
let now: number;
|
||||
let initializedAt: number;
|
||||
let advanceRate: number;
|
||||
let advanceFrequency: number;
|
||||
let advanceIntervalId: number | undefined;
|
||||
let timerId: Generator<number>;
|
||||
let dueNodes: Map<number, DueNode>;
|
||||
let dueTree: RBTree<DueNode>;
|
||||
|
||||
/**
|
||||
* Overrides the real Date object and timer functions with fake ones that can be
|
||||
* controlled through the fake time instance.
|
||||
*
|
||||
* ```ts
|
||||
* // https://deno.land/std@$STD_VERSION/testing/mock_examples/interval_test.ts
|
||||
* import {
|
||||
* assertSpyCalls,
|
||||
* spy,
|
||||
* } from "https://deno.land/std@$STD_VERSION/testing/mock.ts";
|
||||
* import { FakeTime } from "https://deno.land/std@$STD_VERSION/testing/time.ts";
|
||||
* import { secondInterval } from "https://deno.land/std@$STD_VERSION/testing/mock_examples/interval.ts";
|
||||
*
|
||||
* Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
|
||||
* const time = new FakeTime();
|
||||
*
|
||||
* try {
|
||||
* const cb = spy();
|
||||
* const intervalId = secondInterval(cb);
|
||||
* assertSpyCalls(cb, 0);
|
||||
* time.tick(500);
|
||||
* assertSpyCalls(cb, 0);
|
||||
* time.tick(500);
|
||||
* assertSpyCalls(cb, 1);
|
||||
* time.tick(3500);
|
||||
* assertSpyCalls(cb, 4);
|
||||
*
|
||||
* clearInterval(intervalId);
|
||||
* time.tick(1000);
|
||||
* assertSpyCalls(cb, 4);
|
||||
* } finally {
|
||||
* time.restore();
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class FakeTime {
|
||||
constructor(
|
||||
start?: number | string | Date | null,
|
||||
options?: FakeTimeOptions,
|
||||
) {
|
||||
if (time) time.restore();
|
||||
initializedAt = _internals.Date.now();
|
||||
startedAt = start instanceof Date
|
||||
? start.valueOf()
|
||||
: typeof start === "number"
|
||||
? Math.floor(start)
|
||||
: typeof start === "string"
|
||||
? (new Date(start)).valueOf()
|
||||
: initializedAt;
|
||||
if (Number.isNaN(startedAt)) throw new TimeError("invalid start");
|
||||
now = startedAt;
|
||||
|
||||
timerId = timerIdGen();
|
||||
dueNodes = new Map();
|
||||
dueTree = new RBTree(
|
||||
(a: DueNode, b: DueNode) => ascend(a.due, b.due),
|
||||
);
|
||||
|
||||
overrideGlobals();
|
||||
time = this;
|
||||
|
||||
advanceRate = Math.max(
|
||||
0,
|
||||
options?.advanceRate ? options.advanceRate : 0,
|
||||
);
|
||||
advanceFrequency = Math.max(
|
||||
0,
|
||||
options?.advanceFrequency ? options.advanceFrequency : 10,
|
||||
);
|
||||
advanceIntervalId = advanceRate > 0
|
||||
? _internals.setInterval.call(null, () => {
|
||||
this.tick(advanceRate * advanceFrequency);
|
||||
}, advanceFrequency)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Restores real time. */
|
||||
static restore(): void {
|
||||
if (!time) throw new TimeError("time already restored");
|
||||
time.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores real time temporarily until callback returns and resolves.
|
||||
*/
|
||||
static async restoreFor<T>(
|
||||
// deno-lint-ignore no-explicit-any
|
||||
callback: (...args: any[]) => Promise<T> | T,
|
||||
// deno-lint-ignore no-explicit-any
|
||||
...args: any[]
|
||||
): Promise<T> {
|
||||
if (!time) throw new TimeError("no fake time");
|
||||
let result: T;
|
||||
restoreGlobals();
|
||||
try {
|
||||
result = await callback.apply(null, args);
|
||||
} finally {
|
||||
overrideGlobals();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount of milliseconds elapsed since January 1, 1970 00:00:00 UTC for the fake time.
|
||||
* When set, it will call any functions waiting to be called between the current and new fake time.
|
||||
* If the timer callback throws, time will stop advancing forward beyond that timer.
|
||||
*/
|
||||
get now(): number {
|
||||
return now;
|
||||
}
|
||||
set now(value: number) {
|
||||
if (value < now) throw new Error("time cannot go backwards");
|
||||
let dueNode: DueNode | null = dueTree.min();
|
||||
while (dueNode && dueNode.due <= value) {
|
||||
const timer: Timer | undefined = dueNode.timers.shift();
|
||||
if (timer && dueNodes.has(timer.id)) {
|
||||
now = timer.due;
|
||||
if (timer.repeat) {
|
||||
const due: number = timer.due + timer.delay;
|
||||
let dueNode: DueNode | null = dueTree.find({ due } as DueNode);
|
||||
if (dueNode === null) {
|
||||
dueNode = { due, timers: [] };
|
||||
dueTree.insert(dueNode);
|
||||
}
|
||||
dueNode.timers.push({ ...timer, due });
|
||||
dueNodes.set(timer.id, dueNode);
|
||||
} else {
|
||||
dueNodes.delete(timer.id);
|
||||
}
|
||||
timer.callback.apply(null, timer.args);
|
||||
} else if (!timer) {
|
||||
dueTree.remove(dueNode);
|
||||
dueNode = dueTree.min();
|
||||
}
|
||||
}
|
||||
now = value;
|
||||
}
|
||||
|
||||
/** The initial amount of milliseconds elapsed since January 1, 1970 00:00:00 UTC for the fake time. */
|
||||
get start(): number {
|
||||
return startedAt;
|
||||
}
|
||||
set start(value: number) {
|
||||
throw new Error("cannot change start time after initialization");
|
||||
}
|
||||
|
||||
/** Resolves after the given number of milliseconds using real time. */
|
||||
async delay(ms: number, options: DelayOptions = {}): Promise<void> {
|
||||
const { signal } = options;
|
||||
if (signal?.aborted) {
|
||||
return Promise.reject(
|
||||
new DOMException("Delay was aborted.", "AbortError"),
|
||||
);
|
||||
}
|
||||
return await new Promise((resolve, reject) => {
|
||||
let timer: number | null = null;
|
||||
const abort = () =>
|
||||
FakeTime
|
||||
.restoreFor(() => {
|
||||
if (timer) clearTimeout(timer);
|
||||
})
|
||||
.then(() =>
|
||||
reject(new DOMException("Delay was aborted.", "AbortError"))
|
||||
);
|
||||
const done = () => {
|
||||
signal?.removeEventListener("abort", abort);
|
||||
resolve();
|
||||
};
|
||||
FakeTime.restoreFor(() => setTimeout(done, ms))
|
||||
.then((id) => timer = id);
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
/** Runs all pending microtasks. */
|
||||
async runMicrotasks(): Promise<void> {
|
||||
await this.delay(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified number of milliseconds to the fake time.
|
||||
* This will call any functions waiting to be called between the current and new fake time.
|
||||
*/
|
||||
tick(ms = 0): void {
|
||||
this.now += ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all pending microtasks then adds the specified number of milliseconds to the fake time.
|
||||
* This will call any functions waiting to be called between the current and new fake time.
|
||||
*/
|
||||
async tickAsync(ms = 0): Promise<void> {
|
||||
await this.runMicrotasks();
|
||||
this.now += ms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances time to when the next scheduled timer is due.
|
||||
* If there are no pending timers, time will not be changed.
|
||||
* Returns true when there is a scheduled timer and false when there is not.
|
||||
*/
|
||||
next(): boolean {
|
||||
const next = dueTree.min();
|
||||
if (next) this.now = next.due;
|
||||
return !!next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all pending microtasks then advances time to when the next scheduled timer is due.
|
||||
* If there are no pending timers, time will not be changed.
|
||||
*/
|
||||
async nextAsync(): Promise<boolean> {
|
||||
await this.runMicrotasks();
|
||||
return this.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances time forward to the next due timer until there are no pending timers remaining.
|
||||
* If the timers create additional timers, they will be run too. If there is an interval,
|
||||
* time will keep advancing forward until the interval is cleared.
|
||||
*/
|
||||
runAll(): void {
|
||||
while (!dueTree.isEmpty()) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances time forward to the next due timer until there are no pending timers remaining.
|
||||
* If the timers create additional timers, they will be run too. If there is an interval,
|
||||
* time will keep advancing forward until the interval is cleared.
|
||||
* Runs all pending microtasks before each timer.
|
||||
*/
|
||||
async runAllAsync(): Promise<void> {
|
||||
while (!dueTree.isEmpty()) {
|
||||
await this.nextAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/** Restores time related global functions to their original state. */
|
||||
restore(): void {
|
||||
if (!time) throw new TimeError("time already restored");
|
||||
time = undefined;
|
||||
restoreGlobals();
|
||||
if (advanceIntervalId) clearInterval(advanceIntervalId);
|
||||
}
|
||||
}
|
568
testing/time_test.ts
Normal file
568
testing/time_test.ts
Normal file
@ -0,0 +1,568 @@
|
||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||
import {
|
||||
assert,
|
||||
assertEquals,
|
||||
assertInstanceOf,
|
||||
assertNotEquals,
|
||||
assertRejects,
|
||||
assertStrictEquals,
|
||||
} from "./asserts.ts";
|
||||
import { FakeTime } from "./time.ts";
|
||||
import { _internals } from "./_time.ts";
|
||||
import { assertSpyCall, MockError, spy, SpyCall, stub } from "./mock.ts";
|
||||
|
||||
function fromNow(): () => number {
|
||||
const start: number = Date.now();
|
||||
return () => Date.now() - start;
|
||||
}
|
||||
|
||||
Deno.test("Date unchanged if FakeTime is uninitialized", () => {
|
||||
assertStrictEquals(Date, _internals.Date);
|
||||
});
|
||||
|
||||
Deno.test("Date is fake if FakeTime is initialized", () => {
|
||||
const time = new FakeTime(9001);
|
||||
try {
|
||||
assertNotEquals(Date, _internals.Date);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
assertStrictEquals(Date, _internals.Date);
|
||||
});
|
||||
|
||||
Deno.test("Fake Date parse and UTC behave the same", () => {
|
||||
const expectedUTC = Date.UTC(96, 1, 2, 3, 4, 5);
|
||||
const expectedParse = Date.parse("04 Dec 1995 00:12:00 GMT");
|
||||
|
||||
const time = new FakeTime();
|
||||
try {
|
||||
assertEquals(
|
||||
Date.UTC(96, 1, 2, 3, 4, 5),
|
||||
expectedUTC,
|
||||
);
|
||||
assertEquals(
|
||||
Date.parse("04 Dec 1995 00:12:00 GMT"),
|
||||
expectedParse,
|
||||
);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Fake Date.now returns current fake time", () => {
|
||||
const time: FakeTime = new FakeTime(9001);
|
||||
const now = spy(_internals.Date, "now");
|
||||
try {
|
||||
assertEquals(Date.now(), 9001);
|
||||
assertEquals(now.calls.length, 0);
|
||||
time.tick(1523);
|
||||
assertEquals(Date.now(), 10524);
|
||||
assertEquals(now.calls.length, 0);
|
||||
} finally {
|
||||
time.restore();
|
||||
now.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Fake Date instance methods passthrough to real Date instance methods", () => {
|
||||
const time = new FakeTime();
|
||||
try {
|
||||
const now = new Date("2020-05-25T05:00:00.12345Z");
|
||||
assertEquals(now.toISOString(), "2020-05-25T05:00:00.123Z");
|
||||
Object.getOwnPropertyNames(_internals.Date.prototype).forEach(
|
||||
(method: string) => {
|
||||
if (
|
||||
typeof _internals.Date.prototype[method as keyof Date] === "function"
|
||||
) {
|
||||
if (typeof now[method as keyof Date] !== "function") {
|
||||
throw new MockError(`FakeDate missing ${method} method`);
|
||||
}
|
||||
const returned = Symbol();
|
||||
const func = stub(
|
||||
_internals.Date.prototype,
|
||||
method as keyof Date,
|
||||
() => returned,
|
||||
);
|
||||
try {
|
||||
const args = Array(5).fill(undefined).map(() => Symbol());
|
||||
(now[method as keyof Date] as CallableFunction)(...args);
|
||||
assertSpyCall(func, 0, {
|
||||
args,
|
||||
returned,
|
||||
});
|
||||
assertInstanceOf(func.calls[0].self, _internals.Date);
|
||||
} finally {
|
||||
func.restore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
Object.getOwnPropertySymbols(_internals.Date.prototype).forEach(
|
||||
(method: symbol) => {
|
||||
if (
|
||||
typeof _internals.Date.prototype[method as keyof Date] === "function"
|
||||
) {
|
||||
if (typeof now[method as keyof Date] !== "function") {
|
||||
throw new MockError(`FakeDate missing ${method.toString()} method`);
|
||||
}
|
||||
const returned = Symbol();
|
||||
const func = stub(
|
||||
_internals.Date.prototype,
|
||||
method as keyof Date,
|
||||
() => returned,
|
||||
);
|
||||
try {
|
||||
const args = Array(5).fill(undefined).map(() => Symbol());
|
||||
(now[method as keyof Date] as CallableFunction)(...args);
|
||||
assertSpyCall(func, 0, {
|
||||
args,
|
||||
returned,
|
||||
});
|
||||
assertInstanceOf(func.calls[0].self, _internals.Date);
|
||||
} finally {
|
||||
func.restore();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("timeout functions unchanged if FakeTime is uninitialized", () => {
|
||||
assertStrictEquals(setTimeout, _internals.setTimeout);
|
||||
assertStrictEquals(clearTimeout, _internals.clearTimeout);
|
||||
});
|
||||
|
||||
Deno.test("timeout functions are fake if FakeTime is initialized", () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
try {
|
||||
assertNotEquals(setTimeout, _internals.setTimeout);
|
||||
assertNotEquals(clearTimeout, _internals.clearTimeout);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
assertStrictEquals(setTimeout, _internals.setTimeout);
|
||||
assertStrictEquals(clearTimeout, _internals.clearTimeout);
|
||||
});
|
||||
|
||||
Deno.test("FakeTime only ticks forward when setting now or calling tick", () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
|
||||
try {
|
||||
assertEquals(Date.now(), start);
|
||||
time.tick(5);
|
||||
assertEquals(Date.now(), start + 5);
|
||||
time.now = start + 1000;
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
assert(_internals.Date.now() < start + 1000);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("FakeTime controls timeouts", () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
const cb = spy(fromNow());
|
||||
const expected: SpyCall[] = [];
|
||||
|
||||
try {
|
||||
setTimeout(cb, 1000);
|
||||
time.tick(250);
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(250);
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(500);
|
||||
expected.push({ args: [], returned: 1000 });
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(2500);
|
||||
assertEquals(cb.calls, expected);
|
||||
|
||||
setTimeout(cb, 1000, "a");
|
||||
setTimeout(cb, 2000, "b");
|
||||
setTimeout(cb, 1500, "c");
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(2500);
|
||||
expected.push({ args: ["a"], returned: 4500 });
|
||||
expected.push({ args: ["c"], returned: 5000 });
|
||||
expected.push({ args: ["b"], returned: 5500 });
|
||||
assertEquals(cb.calls, expected);
|
||||
|
||||
setTimeout(cb, 1000, "a");
|
||||
setTimeout(cb, 1500, "b");
|
||||
const timeout: number = setTimeout(cb, 1750, "c");
|
||||
setTimeout(cb, 2000, "d");
|
||||
time.tick(1250);
|
||||
expected.push({ args: ["a"], returned: 7000 });
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(Date.now(), start + 7250);
|
||||
clearTimeout(timeout);
|
||||
time.tick(500);
|
||||
expected.push({ args: ["b"], returned: 7500 });
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(Date.now(), start + 7750);
|
||||
time.tick(250);
|
||||
expected.push({ args: ["d"], returned: 8000 });
|
||||
assertEquals(cb.calls, expected);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("interval functions unchanged if FakeTime is uninitialized", () => {
|
||||
assertStrictEquals(setInterval, _internals.setInterval);
|
||||
assertStrictEquals(clearInterval, _internals.clearInterval);
|
||||
});
|
||||
|
||||
Deno.test("interval functions are fake if FakeTime is initialized", () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
try {
|
||||
assertNotEquals(setInterval, _internals.setInterval);
|
||||
assertNotEquals(clearInterval, _internals.clearInterval);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
assertStrictEquals(setInterval, _internals.setInterval);
|
||||
assertStrictEquals(clearInterval, _internals.clearInterval);
|
||||
});
|
||||
|
||||
Deno.test("FakeTime controls intervals", () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const cb = spy(fromNow());
|
||||
const expected: SpyCall[] = [];
|
||||
try {
|
||||
const interval: number = setInterval(cb, 1000);
|
||||
time.tick(250);
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(250);
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(500);
|
||||
expected.push({ args: [], returned: 1000 });
|
||||
assertEquals(cb.calls, expected);
|
||||
time.tick(2500);
|
||||
expected.push({ args: [], returned: 2000 });
|
||||
expected.push({ args: [], returned: 3000 });
|
||||
assertEquals(cb.calls, expected);
|
||||
|
||||
clearInterval(interval);
|
||||
time.tick(1000);
|
||||
assertEquals(cb.calls, expected);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("FakeTime calls timeout and interval callbacks in correct order", () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const cb = spy(fromNow());
|
||||
const timeoutCb = spy(cb);
|
||||
const intervalCb = spy(cb);
|
||||
const expected: SpyCall[] = [];
|
||||
const timeoutExpected: SpyCall[] = [];
|
||||
const intervalExpected: SpyCall[] = [];
|
||||
try {
|
||||
const interval: number = setInterval(intervalCb, 1000);
|
||||
setTimeout(timeoutCb, 500);
|
||||
time.tick(250);
|
||||
assertEquals(intervalCb.calls, intervalExpected);
|
||||
time.tick(250);
|
||||
setTimeout(timeoutCb, 1000);
|
||||
let expect: SpyCall = { args: [], returned: 500 };
|
||||
expected.push(expect);
|
||||
timeoutExpected.push(expect);
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(timeoutCb.calls, timeoutExpected);
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(intervalCb.calls, intervalExpected);
|
||||
time.tick(500);
|
||||
expect = { args: [], returned: 1000 };
|
||||
expected.push(expect);
|
||||
intervalExpected.push(expect);
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(intervalCb.calls, intervalExpected);
|
||||
time.tick(2500);
|
||||
expect = { args: [], returned: 1500 };
|
||||
expected.push(expect);
|
||||
timeoutExpected.push(expect);
|
||||
expect = { args: [], returned: 2000 };
|
||||
expected.push(expect);
|
||||
intervalExpected.push(expect);
|
||||
expect = { args: [], returned: 3000 };
|
||||
expected.push(expect);
|
||||
intervalExpected.push(expect);
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(timeoutCb.calls, timeoutExpected);
|
||||
assertEquals(intervalCb.calls, intervalExpected);
|
||||
|
||||
clearInterval(interval);
|
||||
time.tick(1000);
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(timeoutCb.calls, timeoutExpected);
|
||||
assertEquals(intervalCb.calls, intervalExpected);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("FakeTime restoreFor restores real time temporarily", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
|
||||
try {
|
||||
assertEquals(Date.now(), start);
|
||||
time.tick(1000);
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
assert(_internals.Date.now() < start + 1000);
|
||||
await FakeTime.restoreFor(() => {
|
||||
assert(Date.now() < start + 1000);
|
||||
});
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
assert(_internals.Date.now() < start + 1000);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("delay uses real time", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
|
||||
try {
|
||||
assertEquals(Date.now(), start);
|
||||
await time.delay(20);
|
||||
assert(_internals.Date.now() >= start + 20);
|
||||
assertEquals(Date.now(), start);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("delay runs all microtasks before resolving", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
|
||||
try {
|
||||
const seq = [];
|
||||
queueMicrotask(() => seq.push(2));
|
||||
queueMicrotask(() => seq.push(3));
|
||||
seq.push(1);
|
||||
await time.delay(20);
|
||||
seq.push(4);
|
||||
assertEquals(seq, [1, 2, 3, 4]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("delay with abort", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
|
||||
try {
|
||||
const seq = [];
|
||||
const abort = new AbortController();
|
||||
const { signal } = abort;
|
||||
const delayedPromise = time.delay(100, { signal });
|
||||
seq.push(1);
|
||||
await FakeTime.restoreFor(() => {
|
||||
setTimeout(() => {
|
||||
seq.push(2);
|
||||
abort.abort();
|
||||
}, 0);
|
||||
});
|
||||
await assertRejects(
|
||||
() => delayedPromise,
|
||||
DOMException,
|
||||
"Delay was aborted",
|
||||
);
|
||||
seq.push(3);
|
||||
assertEquals(seq, [1, 2, 3]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("runMicrotasks runs all microtasks before resolving", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
|
||||
try {
|
||||
const seq = [];
|
||||
queueMicrotask(() => seq.push(2));
|
||||
queueMicrotask(() => seq.push(3));
|
||||
seq.push(1);
|
||||
await time.runMicrotasks();
|
||||
seq.push(4);
|
||||
assertEquals(seq, [1, 2, 3, 4]);
|
||||
assertEquals(Date.now(), start);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("tickAsync runs all microtasks and runs timers if ticks past due", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
const cb = spy(fromNow());
|
||||
const expected: SpyCall[] = [];
|
||||
const seq: number[] = [];
|
||||
|
||||
try {
|
||||
setTimeout(cb, 1000);
|
||||
queueMicrotask(() => seq.push(2));
|
||||
queueMicrotask(() => seq.push(3));
|
||||
seq.push(1);
|
||||
await time.tickAsync(250);
|
||||
seq.push(4);
|
||||
assertEquals(cb.calls, expected);
|
||||
await time.tickAsync(250);
|
||||
assertEquals(cb.calls, expected);
|
||||
queueMicrotask(() => seq.push(6));
|
||||
seq.push(5);
|
||||
await time.tickAsync(500);
|
||||
seq.push(7);
|
||||
expected.push({ args: [], returned: 1000 });
|
||||
assertEquals(cb.calls, expected);
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("runNext runs next timer without running microtasks", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
const cb = spy(fromNow());
|
||||
const seq: number[] = [];
|
||||
|
||||
try {
|
||||
setTimeout(cb, 1000);
|
||||
queueMicrotask(() => seq.push(3));
|
||||
queueMicrotask(() => seq.push(4));
|
||||
seq.push(1);
|
||||
time.next();
|
||||
seq.push(2);
|
||||
const expectedCalls = [{ args: [], returned: 1000 }];
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
await time.runMicrotasks();
|
||||
|
||||
queueMicrotask(() => seq.push(7));
|
||||
queueMicrotask(() => seq.push(8));
|
||||
seq.push(5);
|
||||
time.next();
|
||||
seq.push(6);
|
||||
await time.runMicrotasks();
|
||||
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("runNextAsync runs all microtasks and next timer", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
const cb = spy(fromNow());
|
||||
const seq: number[] = [];
|
||||
|
||||
try {
|
||||
setTimeout(cb, 1000);
|
||||
queueMicrotask(() => seq.push(2));
|
||||
queueMicrotask(() => seq.push(3));
|
||||
seq.push(1);
|
||||
await time.nextAsync();
|
||||
seq.push(4);
|
||||
const expectedCalls = [{ args: [], returned: 1000 }];
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
|
||||
queueMicrotask(() => seq.push(6));
|
||||
queueMicrotask(() => seq.push(7));
|
||||
seq.push(5);
|
||||
await time.nextAsync();
|
||||
seq.push(8);
|
||||
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1000);
|
||||
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("runAll runs all timers without running microtasks", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
const cb = spy(fromNow());
|
||||
const seq: number[] = [];
|
||||
|
||||
try {
|
||||
setTimeout(cb, 1000);
|
||||
setTimeout(cb, 1500);
|
||||
queueMicrotask(() => seq.push(3));
|
||||
queueMicrotask(() => seq.push(4));
|
||||
seq.push(1);
|
||||
time.runAll();
|
||||
seq.push(2);
|
||||
const expectedCalls = [
|
||||
{ args: [], returned: 1000 },
|
||||
{ args: [], returned: 1500 },
|
||||
];
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1500);
|
||||
await time.runMicrotasks();
|
||||
|
||||
queueMicrotask(() => seq.push(7));
|
||||
queueMicrotask(() => seq.push(8));
|
||||
seq.push(5);
|
||||
time.runAll();
|
||||
seq.push(6);
|
||||
await time.runMicrotasks();
|
||||
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1500);
|
||||
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("runAllAsync runs all microtasks and timers", async () => {
|
||||
const time: FakeTime = new FakeTime();
|
||||
const start: number = Date.now();
|
||||
const cb = spy(fromNow());
|
||||
const seq: number[] = [];
|
||||
|
||||
try {
|
||||
setTimeout(cb, 1000);
|
||||
setTimeout(cb, 1500);
|
||||
queueMicrotask(() => seq.push(2));
|
||||
queueMicrotask(() => seq.push(3));
|
||||
seq.push(1);
|
||||
await time.runAllAsync();
|
||||
seq.push(4);
|
||||
const expectedCalls = [
|
||||
{ args: [], returned: 1000 },
|
||||
{ args: [], returned: 1500 },
|
||||
];
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1500);
|
||||
|
||||
queueMicrotask(() => seq.push(6));
|
||||
queueMicrotask(() => seq.push(7));
|
||||
seq.push(5);
|
||||
await time.runAllAsync();
|
||||
seq.push(8);
|
||||
|
||||
assertEquals(cb.calls, expectedCalls);
|
||||
assertEquals(Date.now(), start + 1500);
|
||||
assertEquals(seq, [1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
} finally {
|
||||
time.restore();
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue
Block a user