TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import { describe, it, expect } from "vitest";2import { resolveRange, bucketKeys, bucketKey, previousPeriod, startOfDayInTz, addDays, observedDays, isValidTimeZone } from "@/lib/usage/time";34// 2026-09-11 22:30 in Montréal = 2026-09-12 02:30Z5const NOW = new Date("2026-09-12T02:30:00Z");6const TZ = "America/Toronto";78describe("usage time ranges", () => {9 it("resolves `today` to local midnight → next local midnight, hourly buckets", () => {10 const r = resolveRange({ range: "today", tz: TZ }, NOW);11 expect(r.from?.toISOString()).toBe("2026-09-11T04:00:00.000Z");12 expect(r.to.toISOString()).toBe("2026-09-12T04:00:00.000Z");13 expect(r.bucket).toBe("hour");14 expect(r.days).toBe(1);15 expect(bucketKeys(r.from!, r.to, "hour", TZ)).toHaveLength(24);16 expect(bucketKeys(r.from!, r.to, "hour", TZ)[0]).toBe("2026-09-11T00:00");17 });18 it("7d / 30d / 90d cover N whole local days ending today", () => {19 const r = resolveRange({ range: "7d", tz: TZ }, NOW);20 expect(bucketKey(r.from!, "day", TZ)).toBe("2026-09-05");21 expect(bucketKeys(r.from!, r.to, "day", TZ)).toEqual(["2026-09-05", "2026-09-06", "2026-09-07", "2026-09-08", "2026-09-09", "2026-09-10", "2026-09-11"]);22 expect(resolveRange({ range: "90d", tz: TZ }, NOW).days).toBe(90);23 expect(bucketKeys(resolveRange({ range: "90d", tz: TZ }, NOW).from!, NOW, "day", TZ)).toHaveLength(90);24 });25 it("custom ranges are inclusive of both dates, clamped to today and to one year", () => {26 const r = resolveRange({ range: "custom", from: "2026-08-01", to: "2026-08-03", tz: TZ }, NOW);27 expect(bucketKeys(r.from!, r.to, r.bucket, TZ)).toEqual(["2026-08-01", "2026-08-02", "2026-08-03"]);28 expect(r.days).toBe(3);29 const fut = resolveRange({ range: "custom", from: "2026-09-10", to: "2030-01-01", tz: TZ }, NOW);30 expect(fut.to.toISOString()).toBe("2026-09-12T04:00:00.000Z");31 const huge = resolveRange({ range: "custom", from: "2020-01-01", to: "2026-09-11", tz: TZ }, NOW);32 expect(huge.days).toBeLessThanOrEqual(366);33 const swapped = resolveRange({ range: "custom", from: "2026-08-10", to: "2026-08-01", tz: TZ }, NOW);34 expect(swapped.from! < swapped.to).toBe(true);35 // two days or less → hourly36 expect(resolveRange({ range: "custom", from: "2026-09-10", to: "2026-09-11", tz: TZ }, NOW).bucket).toBe("hour");37 });38 it("falls back to UTC / 30d on bad input", () => {39 const r = resolveRange({ range: "nope", tz: "Mars/Olympus" }, NOW);40 expect(r.key).toBe("30d");41 expect(r.tz).toBe("UTC");42 expect(isValidTimeZone("Europe/Paris")).toBe(true);43 expect(isValidTimeZone("../etc")).toBe(false);44 expect(isValidTimeZone("UTC")).toBe(true);45 });46 it("previous period has the same length and ends where the range starts", () => {47 const r = resolveRange({ range: "7d", tz: TZ }, NOW);48 const p = previousPeriod(r)!;49 expect(p.to.getTime()).toBe(r.from!.getTime());50 expect(r.to.getTime() - r.from!.getTime()).toBe(p.to.getTime() - p.from.getTime());51 expect(previousPeriod(resolveRange({ range: "all", tz: TZ }, NOW))).toBeNull();52 });53 it("handles DST when adding days", () => {54 // 2026-11-01 is the fall-back date in Toronto55 const oct31 = startOfDayInTz(new Date("2026-10-31T12:00:00Z"), TZ);56 const nov2 = addDays(oct31, 2, TZ);57 expect(bucketKey(nov2, "day", TZ)).toBe("2026-11-02");58 expect((nov2.getTime() - oct31.getTime()) / 3_600_000).toBe(49);59 });60 it("observed days is fractional for today and never below one hour", () => {61 const r = resolveRange({ range: "today", tz: TZ }, NOW);62 const d = observedDays(r, NOW);63 expect(d).toBeGreaterThan(0.9);64 expect(d).toBeLessThan(1);65 expect(observedDays(resolveRange({ range: "all", tz: TZ }, NOW), NOW, null)).toBeCloseTo(1 / 24, 5);66 });67});68