diff --git a/package-lock.json b/package-lock.json index 5b252cbbc..678956e8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,6 @@ "less-loader": "^12.1.0", "lint-staged": "^15.2.0", "mini-css-extract-plugin": "^2.7.7", - "mockdate": "^3.0.5", "null-loader": "^4.0.1", "patch-package": "^8.0.0", "postcss": "^8.4.33", @@ -1651,6 +1650,10 @@ "resolved": "packages/test-env-server", "link": true }, + "node_modules/@keybr/test-env-time": { + "resolved": "packages/test-env-clock", + "link": true + }, "node_modules/@keybr/textinput": { "resolved": "packages/keybr-textinput", "link": true @@ -8573,12 +8576,6 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "node_modules/mockdate": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz", - "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", - "dev": true - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -13514,7 +13511,9 @@ "@keybr/publicid": "*", "@keybr/rand": "*" }, - "devDependencies": {} + "devDependencies": { + "@keybr/test-env-time": "*" + } }, "packages/keybr-debug": { "name": "@keybr/debug", @@ -13537,7 +13536,9 @@ "@keybr/keyboard": "*", "@keybr/result": "*" }, - "devDependencies": {} + "devDependencies": { + "@keybr/test-env-time": "*" + } }, "packages/keybr-identicon": { "name": "@keybr/identicon", @@ -13671,7 +13672,9 @@ "@keybr/textinput": "*", "@keybr/timer": "*" }, - "devDependencies": {} + "devDependencies": { + "@keybr/test-env-time": "*" + } }, "packages/keybr-multiplayer-shared": { "name": "@keybr/multiplayer-shared", @@ -13696,7 +13699,6 @@ "@keybr/textinput": "*", "@keybr/textinput-events": "*", "@keybr/textinput-ui": "*", - "@keybr/timer": "*", "@keybr/widget": "*" }, "devDependencies": {} @@ -13712,7 +13714,9 @@ "packages/keybr-oauth": { "name": "@keybr/oauth", "version": "0.0.0", - "devDependencies": {} + "devDependencies": { + "@keybr/test-env-time": "*" + } }, "packages/keybr-pages-browser": { "name": "@keybr/pages-browser", @@ -14211,6 +14215,11 @@ "version": "0.0.0", "devDependencies": {} }, + "packages/test-env-clock": { + "name": "@keybr/test-env-time", + "version": "0.0.0", + "devDependencies": {} + }, "packages/test-env-server": { "name": "@keybr/test-env-server", "version": "0.0.0", diff --git a/package.json b/package.json index 10836e853..8a53b5be6 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,6 @@ "less-loader": "^12.1.0", "lint-staged": "^15.2.0", "mini-css-extract-plugin": "^2.7.7", - "mockdate": "^3.0.5", "null-loader": "^4.0.1", "patch-package": "^8.0.0", "postcss": "^8.4.33", diff --git a/packages/keybr-database/lib/model.test.ts b/packages/keybr-database/lib/model.test.ts index 9ab249517..5aabce113 100644 --- a/packages/keybr-database/lib/model.test.ts +++ b/packages/keybr-database/lib/model.test.ts @@ -1,7 +1,7 @@ import { newKnex } from "@keybr/config"; import { PublicId } from "@keybr/publicid"; +import { fake } from "@keybr/test-env-time"; import test from "ava"; -import MockDate from "mockdate"; import { ValidationError } from "objection"; import { User, UserExternalId, UserLoginRequest } from "./model.ts"; import { createSchema } from "./schema.ts"; @@ -17,11 +17,11 @@ test.beforeEach(async () => { }); test.beforeEach(() => { - MockDate.set(now); + fake.date.set(now); }); test.afterEach(() => { - MockDate.reset(); + fake.date.reset(); }); test("validate models", (t) => { diff --git a/packages/keybr-database/package.json b/packages/keybr-database/package.json index 665b1d2e7..3234f7746 100644 --- a/packages/keybr-database/package.json +++ b/packages/keybr-database/package.json @@ -12,7 +12,9 @@ "@keybr/publicid": "*", "@keybr/rand": "*" }, - "devDependencies": {}, + "devDependencies": { + "@keybr/test-env-time": "*" + }, "scripts": { "clean": "rm -fr .types", "compile": "tsc", diff --git a/packages/keybr-highscores/lib/factory.test.ts b/packages/keybr-highscores/lib/factory.test.ts index 908648e73..a60949645 100644 --- a/packages/keybr-highscores/lib/factory.test.ts +++ b/packages/keybr-highscores/lib/factory.test.ts @@ -1,9 +1,9 @@ import { DataDir } from "@keybr/config"; import { Layout } from "@keybr/keyboard"; import { ResultFaker } from "@keybr/result"; +import { fake } from "@keybr/test-env-time"; import { removeDir } from "@sosimple/fsx"; import test from "ava"; -import MockDate from "mockdate"; import { HighScoresFactory } from "./factory.ts"; import { type HighScoresRow } from "./highscores.ts"; @@ -16,11 +16,11 @@ test.beforeEach(async () => { }); test.beforeEach(() => { - MockDate.set(now); + fake.date.set(now); }); test.afterEach(() => { - MockDate.reset(); + fake.date.reset(); }); test.afterEach(async () => { diff --git a/packages/keybr-highscores/lib/highscores.test.ts b/packages/keybr-highscores/lib/highscores.test.ts index 3863bbbe3..b5dac3eaf 100644 --- a/packages/keybr-highscores/lib/highscores.test.ts +++ b/packages/keybr-highscores/lib/highscores.test.ts @@ -1,6 +1,6 @@ import { Layout } from "@keybr/keyboard"; +import { fake } from "@keybr/test-env-time"; import test from "ava"; -import MockDate from "mockdate"; import { HighScores, type HighScoresRow } from "./highscores.ts"; const now = new Date("2001-02-03T04:05:06Z"); @@ -18,11 +18,11 @@ const template: HighScoresRow = { }; test.beforeEach(() => { - MockDate.set(now); + fake.date.set(now); }); test.afterEach(() => { - MockDate.reset(); + fake.date.reset(); }); test("do not insert if result is old", (t) => { diff --git a/packages/keybr-highscores/package.json b/packages/keybr-highscores/package.json index d895e3d4c..a7133fe1f 100644 --- a/packages/keybr-highscores/package.json +++ b/packages/keybr-highscores/package.json @@ -9,7 +9,9 @@ "@keybr/keyboard": "*", "@keybr/result": "*" }, - "devDependencies": {}, + "devDependencies": { + "@keybr/test-env-time": "*" + }, "scripts": { "clean": "rm -fr .types", "compile": "tsc", diff --git a/packages/keybr-multiplayer-server/lib/room.test.ts b/packages/keybr-multiplayer-server/lib/room.test.ts index e9524288b..f7be79e2f 100644 --- a/packages/keybr-multiplayer-server/lib/room.test.ts +++ b/packages/keybr-multiplayer-server/lib/room.test.ts @@ -12,12 +12,8 @@ import { type PlayerState, } from "@keybr/multiplayer-shared"; import { type AnonymousUser } from "@keybr/pages-shared"; +import { fake } from "@keybr/test-env-time"; import { Timer } from "@keybr/timer"; -import { - resetFakeTimers, - runTimers, - setFakeTimers, -} from "@keybr/timer/lib/fake.ts"; import test from "ava"; import { Game } from "./game.ts"; import { Player } from "./player.ts"; @@ -26,12 +22,12 @@ import { Services } from "./services.ts"; import { FakeSession } from "./testing/fake-session.ts"; test.beforeEach(() => { - setFakeTimers(); + fake.timers.set(); Timer.now = () => 0; }); test.afterEach(() => { - resetFakeTimers(); + fake.timers.reset(); Timer.now = () => 0; }); @@ -48,13 +44,13 @@ test.serial("join and start", async (t) => { player0.join(room); player0.onMessage({ type: PLAYER_ANNOUNCE_ID, signature: 0xdeadbabe }); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room); t.is(player1.room, null); player1.join(room); player1.onMessage({ type: PLAYER_ANNOUNCE_ID, signature: 0xdeadbabe }); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room); t.is(player1.room, room); @@ -301,31 +297,31 @@ test.serial("join and leave", async (t) => { // Player 0 joins room 0. player0.join(room0); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room0); t.is(player1.room, null); // Repeat again. player0.join(room0); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room0); t.is(player1.room, null); // Player 1 joins room 0. room0.join(player1); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room0); t.is(player1.room, room0); // Repeat again. room0.join(player1); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room0); t.is(player1.room, room0); // Joint another room. room1.join(player1); - await runTimers(); + await fake.timers.run(); t.is(player0.room, room0); t.is(player1.room, room1); diff --git a/packages/keybr-multiplayer-server/package.json b/packages/keybr-multiplayer-server/package.json index 33eb7df57..8c25bd6ba 100644 --- a/packages/keybr-multiplayer-server/package.json +++ b/packages/keybr-multiplayer-server/package.json @@ -11,7 +11,9 @@ "@keybr/textinput": "*", "@keybr/timer": "*" }, - "devDependencies": {}, + "devDependencies": { + "@keybr/test-env-time": "*" + }, "scripts": { "clean": "rm -fr .types", "compile": "tsc", diff --git a/packages/keybr-multiplayer-ui/package.json b/packages/keybr-multiplayer-ui/package.json index ee57cf51c..c6d40882b 100644 --- a/packages/keybr-multiplayer-ui/package.json +++ b/packages/keybr-multiplayer-ui/package.json @@ -13,7 +13,6 @@ "@keybr/textinput": "*", "@keybr/textinput-events": "*", "@keybr/textinput-ui": "*", - "@keybr/timer": "*", "@keybr/widget": "*" }, "devDependencies": {}, diff --git a/packages/keybr-oauth/lib/token.test.ts b/packages/keybr-oauth/lib/token.test.ts index 9d586ec1d..788142b22 100644 --- a/packages/keybr-oauth/lib/token.test.ts +++ b/packages/keybr-oauth/lib/token.test.ts @@ -1,17 +1,13 @@ +import { fake } from "@keybr/test-env-time"; import test from "ava"; -import MockDate from "mockdate"; import { AccessToken } from "./token.ts"; -test.beforeEach(() => { - MockDate.reset(); -}); - test.afterEach(() => { - MockDate.reset(); + fake.date.reset(); }); test("construct token from response", (t) => { - MockDate.set(new Date("2001-02-03T04:05:06Z")); + fake.date.set(new Date("2001-02-03T04:05:06Z")); const token = new AccessToken({ access_token: "token", @@ -31,7 +27,7 @@ test("construct token from response", (t) => { }); test("check expired", (t) => { - MockDate.set(new Date("2001-01-01T00:00:00Z")); + fake.date.set(new Date("2001-01-01T00:00:00Z")); const token = new AccessToken({ access_token: "token", @@ -41,19 +37,19 @@ test("check expired", (t) => { t.false(token.expired()); - MockDate.set(new Date("2001-01-01T00:58:59Z")); + fake.date.set(new Date("2001-01-01T00:58:59Z")); t.false(token.expired()); t.false(token.expired(60)); t.false(token.expired(0)); - MockDate.set(new Date("2001-01-01T00:59:00Z")); + fake.date.set(new Date("2001-01-01T00:59:00Z")); t.true(token.expired()); t.true(token.expired(60)); t.false(token.expired(0)); - MockDate.set(new Date("2001-01-01T01:00:00Z")); + fake.date.set(new Date("2001-01-01T01:00:00Z")); t.true(token.expired()); t.true(token.expired(0)); diff --git a/packages/keybr-oauth/package.json b/packages/keybr-oauth/package.json index 1f1a44395..e5996d2d0 100644 --- a/packages/keybr-oauth/package.json +++ b/packages/keybr-oauth/package.json @@ -5,7 +5,9 @@ "main": "lib/index.ts", "types": ".types/index.d.ts", "dependencies": {}, - "devDependencies": {}, + "devDependencies": { + "@keybr/test-env-time": "*" + }, "scripts": { "clean": "rm -fr .types", "compile": "tsc", diff --git a/packages/keybr-timer/lib/fake.ts b/packages/keybr-timer/lib/fake.ts deleted file mode 100644 index a0260ebfe..000000000 --- a/packages/keybr-timer/lib/fake.ts +++ /dev/null @@ -1,140 +0,0 @@ -import assert from "node:assert"; - -type Global = { - setTimeout: unknown; - clearTimeout: unknown; - setInterval: unknown; - clearInterval: unknown; -}; - -const realSetTimeout = (global as Global).setTimeout; -const realClearTimeout = (global as Global).clearTimeout; -const realSetInterval = (global as Global).setInterval; -const realClearInterval = (global as Global).clearInterval; - -class Task { - cancelled = false; - - constructor( - readonly timeout: Timeout, - readonly ms: number, - readonly callback: (...args: unknown[]) => void, - readonly args: unknown[], - ) {} - - cancel(): void { - this.cancelled = true; - } - - run(): void { - if (!this.cancelled) { - this.callback.apply(null, this.args); - } - } -} - -class Timeout { - hasRef(): boolean { - return false; - } - - ref(): this { - return this; - } - - unref(): this { - return this; - } - - refresh(): this { - return this; - } - - [Symbol.toPrimitive](): number { - return 0; - } -} - -const tasks = new Map(); - -function fakeSetTimeout( - callback: (...args: unknown[]) => void, - ms: number = 0, - ...args: unknown[] -): Timeout { - assert(typeof callback === "function"); - assert(typeof ms === "number"); - assert(ms >= 0); - const timeout = new Timeout(); - tasks.set(timeout, new Task(timeout, ms, callback, args)); - return timeout; -} - -function fakeClearTimeout(timeout: Timeout): void { - assert(timeout instanceof Timeout); - const task = tasks.get(timeout); - if (task != null) { - tasks.delete(timeout); - task.cancel(); - } -} - -function fakeSetInterval( - callback: (...args: unknown[]) => void, - ms: number = 0, - ...args: unknown[] -): Timeout { - assert(typeof callback === "function"); - assert(typeof ms === "number"); - assert(ms >= 0); - const timeout = new Timeout(); - tasks.set(timeout, new Task(timeout, ms, callback, args)); - return timeout; -} - -function fakeClearInterval(timeout: Timeout): void { - assert(timeout instanceof Timeout); - const task = tasks.get(timeout); - if (task != null) { - tasks.delete(timeout); - task.cancel(); - } -} - -export function setFakeTimers(): void { - (global as Global).setTimeout = fakeSetTimeout; - (global as Global).clearTimeout = fakeClearTimeout; - (global as Global).setInterval = fakeSetInterval; - (global as Global).clearInterval = fakeClearInterval; -} - -export function resetFakeTimers(): void { - (global as Global).setTimeout = realSetTimeout; - (global as Global).clearTimeout = realClearTimeout; - (global as Global).setInterval = realSetInterval; - (global as Global).clearInterval = realClearInterval; -} - -export async function runTimers(all: boolean = true): Promise { - return new Promise((resolve, reject) => { - // First round of tasks. - let copy = [...tasks.values()]; - while (copy.length > 0) { - for (const task of copy) { - try { - task.run(); - } catch (err) { - reject(err); - return; - } - tasks.delete(task.timeout); - } - // Next round of tasks. - copy = [...tasks.values()]; - if (!all) { - break; - } - } - resolve(copy.length); - }); -} diff --git a/packages/test-env-clock/lib/date.ts b/packages/test-env-clock/lib/date.ts new file mode 100644 index 000000000..4d3b6bfc5 --- /dev/null +++ b/packages/test-env-clock/lib/date.ts @@ -0,0 +1,36 @@ +import assert from "node:assert"; +import { real } from "./real.ts"; + +const fake = { + Date(time: number) { + function now() { + return time; + } + + return new Proxy(real.Date, { + construct(target: any, args: any[]): object { + if (args.length === 0) { + return new real.Date(time); // Magic. + } + return Reflect.construct(target, args); + }, + get(target: any, p: string | symbol): any { + if (p === "now") { + return now; // Magic. + } + return Reflect.get(target, p); + }, + }); + }, +}; + +export const date = { + set(date: number | string | Date): void { + const time = new real.Date(date).getTime(); + assert(time >= 0); + global.Date = fake.Date(time); + }, + reset(): void { + global.Date = real.Date; + }, +}; diff --git a/packages/test-env-clock/lib/index.ts b/packages/test-env-clock/lib/index.ts new file mode 100644 index 000000000..3696c97fa --- /dev/null +++ b/packages/test-env-clock/lib/index.ts @@ -0,0 +1,9 @@ +import { date } from "./date.ts"; +import { timers } from "./timers.ts"; + +export { real, reset } from "./real.ts"; + +export const fake = { + date, + timers, +}; diff --git a/packages/test-env-clock/lib/real.ts b/packages/test-env-clock/lib/real.ts new file mode 100644 index 000000000..ae67f425d --- /dev/null +++ b/packages/test-env-clock/lib/real.ts @@ -0,0 +1,23 @@ +export const global = globalThis as { + Date: DateConstructor; + setTimeout: typeof setTimeout; + clearTimeout: typeof clearTimeout; + setInterval: typeof setInterval; + clearInterval: typeof clearInterval; +}; + +export const real = { + Date, + setTimeout, + clearTimeout, + setInterval, + clearInterval, +}; + +export function reset(): void { + global.Date = real.Date; + global.setTimeout = real.setTimeout; + global.clearTimeout = real.clearTimeout; + global.setInterval = real.setInterval; + global.clearInterval = real.clearInterval; +} diff --git a/packages/test-env-clock/lib/timers.ts b/packages/test-env-clock/lib/timers.ts new file mode 100644 index 000000000..bfb68511c --- /dev/null +++ b/packages/test-env-clock/lib/timers.ts @@ -0,0 +1,126 @@ +import assert from "node:assert"; +import { real } from "./real.ts"; + +class Task { + static id = 0; + + id = (Task.id += 1); + cancelled = false; + + constructor( + readonly ms: number, + readonly callback: (...args: unknown[]) => void, + readonly args: unknown[], + ) {} + + cancel(): void { + this.cancelled = true; + } + + run(): void { + if (!this.cancelled) { + this.callback.apply(null, this.args); + } + } + + hasRef(): boolean { + return false; + } + + ref(): this { + return this; + } + + unref(): this { + return this; + } + + refresh(): this { + return this; + } + + [Symbol.toPrimitive](): number { + return this.id; + } +} + +const tasks: Task[] = []; + +const fake = { + setTimeout: new Proxy(real.setTimeout, { + apply(target: any, thisArg: any, args: any[]): any { + console.assert(args.length >= 2); + const [callback, ms, ...rest] = args; + assert(typeof callback === "function"); + assert(typeof ms === "number"); + assert(ms >= 0); + const task = new Task(ms, callback, rest); + tasks.push(task); + return task; + }, + }), + clearTimeout: new Proxy(real.clearTimeout, { + apply(target: any, thisArg: any, args: any[]): any { + console.assert(args.length === 1); + const [task] = args; + assert(task instanceof Task); + task.cancel(); + }, + }), + setInterval: new Proxy(real.setInterval, { + apply(target: any, thisArg: any, args: any[]): any { + console.assert(args.length >= 2); + const [callback, ms, ...rest] = args; + assert(typeof callback === "function"); + assert(typeof ms === "number"); + assert(ms >= 0); + const task = new Task(ms, callback, rest); + tasks.push(task); + return task; + }, + }), + clearInterval: new Proxy(real.clearInterval, { + apply(target: any, thisArg: any, args: any[]): any { + console.assert(args.length === 1); + const [task] = args; + assert(task instanceof Task); + task.cancel(); + }, + }), +}; + +export const timers = { + set(): void { + global.setTimeout = fake.setTimeout; + global.clearTimeout = fake.clearTimeout; + global.setInterval = fake.setInterval; + global.clearInterval = fake.clearInterval; + }, + reset(): void { + global.setTimeout = real.setTimeout; + global.clearTimeout = real.clearTimeout; + global.setInterval = real.setInterval; + global.clearInterval = real.clearInterval; + }, + get pending(): number { + return tasks.length; + }, + clear(): void { + tasks.length = 0; + }, + async run(): Promise { + return new Promise((resolve, reject) => { + while (tasks.length > 0) { + const task = tasks.shift() as Task; + try { + task.run(); + } catch (err) { + console.error(err); + reject(err); + return; + } + } + resolve(); + }); + }, +}; diff --git a/packages/test-env-clock/package.json b/packages/test-env-clock/package.json new file mode 100644 index 000000000..b54198748 --- /dev/null +++ b/packages/test-env-clock/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "name": "@keybr/test-env-time", + "version": "0.0.0", + "main": "lib/index.ts", + "types": ".types/index.d.ts", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "clean": "rm -fr .types", + "compile": "tsc" + } +} diff --git a/packages/test-env-clock/tsconfig.json b/packages/test-env-clock/tsconfig.json new file mode 100644 index 000000000..649ce299f --- /dev/null +++ b/packages/test-env-clock/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig-template.json", + "compilerOptions": { + "rootDir": "lib", + "outDir": ".types", + "emitDeclarationOnly": true + }, + "include": [ + "lib/**/*" + ] +}