alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
253 lines (188 loc) • 7.18 kB
text/typescript
import { Alepha } from "alepha";
import { describe, expect, it } from "vitest";
import { DateTimeProvider } from "../index.ts";
describe("DateTimeProvider", () => {
it("should pause time and reset", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const clock = () => dt.nowISOString();
const n1 = clock();
await new Promise((resolve) => setTimeout(resolve, 10));
dt.pause();
const n2 = clock();
expect(n1).not.toBe(n2);
expect(n2).toBe(clock());
expect(n2).toBe(clock());
expect(n2).toBe(clock());
dt.reset();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(n2).not.toBe(clock());
});
it("should time travel and trigger scheduled callbacks", async () => {
const stack: string[] = [];
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
stack.push("A");
dt.wait([10, "minutes"]).then(() => stack.push("B"));
dt.wait([20, "minutes"]).then(() => stack.push("C"));
expect(stack).toEqual(["A"]);
await dt.travel([5, "minutes"]);
expect(stack).toEqual(["A"]);
await dt.travel([30, "minutes"]);
expect(stack).toEqual(["A", "B", "C"]);
});
it("should handle timeouts with clearTimeout", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const stack: string[] = [];
dt.createTimeout(() => stack.push("A"), [10, "minutes"]);
const n2 = dt.createTimeout(() => stack.push("B"), [10, "minutes"]);
expect(stack).toEqual([]);
await dt.travel([5, "minutes"]);
expect(stack).toEqual([]);
dt.clearTimeout(n2);
await dt.travel([5, "minutes"]);
expect(stack).toEqual(["A"]);
});
it("should wait with abort signal support", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const stack: string[] = [];
const abortController = new AbortController();
dt.wait([10, "minutes"], { signal: abortController.signal }).then(() =>
stack.push("A"),
);
expect(stack).toEqual([]);
abortController.abort();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(stack).toEqual(["A"]);
});
});
describe("DateTimeProvider.nowMillis", () => {
it("should return frozen millis when paused", () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
dt.pause();
const a = dt.nowMillis();
const b = dt.nowMillis();
expect(a).toBe(b);
});
it("should advance millis after travel", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
dt.pause();
const before = dt.nowMillis();
await dt.travel([5, "minutes"]);
const after = dt.nowMillis();
expect(after - before).toBe(5 * 60 * 1000);
});
});
describe("DateTimeProvider.deadline", () => {
it("should return result when handler completes in time", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const result = await dt.deadline(async () => "done", [1, "seconds"]);
expect(result).toBe("done");
});
it("should abort signal when handler exceeds deadline", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
let aborted = false;
const promise = dt.deadline(
async (signal) => {
signal.addEventListener("abort", () => {
aborted = true;
});
await dt.wait([10, "minutes"]);
return "done";
},
[5, "minutes"],
);
// Travel past deadline (5min) but handler is still waiting (10min)
await dt.travel([5, "minutes"]);
expect(aborted).toBe(true);
// Travel remaining time so handler resolves
await dt.travel([5, "minutes"]);
await expect(promise).resolves.toBe("done");
});
});
describe("DateTimeProvider.isDurationLike", () => {
it("should return true for valid durations", () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
expect(dt.isDurationLike(1000)).toBe(true);
expect(dt.isDurationLike([5, "minutes"])).toBe(true);
});
it("should return false for invalid values", () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
expect(dt.isDurationLike("not-a-duration")).toBe(false);
expect(dt.isDurationLike(null)).toBe(false);
expect(dt.isDurationLike(undefined)).toBe(false);
expect(dt.isDurationLike({})).toBe(false);
});
});
describe("DateTimeProvider.duration", () => {
it("should pass through an existing Duration object", () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const dur = dt.duration([5, "seconds"]);
const same = dt.duration(dur);
expect(same).toBe(dur);
expect(same.asMilliseconds()).toBe(5000);
});
});
describe("DateTimeProvider.travel — reschedule surviving timeout", () => {
it("should reschedule a timeout that has not yet expired", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const stack: string[] = [];
dt.createTimeout(() => stack.push("A"), [10, "minutes"]);
// travel less than 10 minutes — timeout survives
await dt.travel([3, "minutes"]);
expect(stack).toEqual([]);
// travel past the remaining time — timeout fires
await dt.travel([8, "minutes"]);
expect(stack).toEqual(["A"]);
});
});
describe("DateTimeProvider.createTimeout — replay with ref", () => {
it("should fire immediately when replaying a past timeout", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const stack: string[] = [];
// Pause time at a known point, then travel forward
dt.pause();
await dt.travel([30, "minutes"]);
// Create a timeout with a `now` in the past — should fire immediately
const pastNow = dt.now().subtract(20, "minutes").valueOf();
dt.createTimeout(() => stack.push("replayed"), [10, "minutes"], pastNow);
expect(stack).toEqual(["replayed"]);
});
it("should not fire when replay timeout is still in the future", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
const stack: string[] = [];
dt.pause();
await dt.travel([5, "minutes"]);
// Create a timeout with a `now` that puts expiry in the future
const recentNow = dt.now().subtract(1, "minutes").valueOf();
dt.createTimeout(() => stack.push("future"), [10, "minutes"], recentNow);
expect(stack).toEqual([]);
});
});
describe("DateTimeProvider.clearInterval", () => {
it("should stop a running interval", async () => {
const app = Alepha.create();
const dt = app.inject(DateTimeProvider);
let count = 0;
const interval = dt.createInterval(() => count++, [10, "seconds"], true);
await new Promise((r) => setTimeout(r, 10));
dt.clearInterval(interval);
const countAfterClear = count;
await new Promise((r) => setTimeout(r, 20));
expect(count).toBe(countAfterClear);
expect(interval.duration).toBe(0);
expect(interval.timer).toBeNull();
});
});