/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: packages/widget/test/widget.test.ts * Purpose: Unit tests for the widget's pure helpers — config resolution, payload parsing, display text */ import { describe, expect, it } from "vitest"; import type { CounterModel } from "@earth-now/counter"; import { DEFAULT_API, buildDisplayText, modelUrl, parseModelPayload, resolveConfig, } from "../src/render"; const ANCHOR = "2026-01-01T00:00:00Z"; const ANCHOR_MS = Date.parse(ANCHOR); const linearModel: CounterModel = { metricId: "co2_emissions_ytd", anchorValue: 1_000_000, anchorTime: ANCHOR, rateFn: { kind: "linear", perSecond: 2 }, observedAt: "2025-12-15T00:00:00Z", sourceId: "gcp_2025", modelVersion: "linear-v1", displayHints: { decimals: 0, sigFigs: 4, unit: "t" }, }; describe("resolveConfig", () => { it("resolves a full dataset", () => { const config = resolveConfig({ earthNowMetric: "co2_emissions_ytd", window: "today", lang: "fr", api: "https://www.earth-now.co/", }); expect(config).toEqual({ metricId: "co2_emissions_ytd", window: "today", lang: "fr", api: "https://www.earth-now.co", }); }); it("applies defaults (window total, lang en, official api)", () => { const config = resolveConfig({ earthNowMetric: "world_population" }); expect(config).toEqual({ metricId: "world_population", window: "total", lang: "en", api: DEFAULT_API, }); }); it("falls back to 'total' on an unknown window", () => { expect(resolveConfig({ earthNowMetric: "x", window: "weird" })?.window).toBe("total"); }); it("returns null when no metric id is declared", () => { expect(resolveConfig({})).toBeNull(); expect(resolveConfig({ earthNowMetric: " " })).toBeNull(); }); }); describe("modelUrl", () => { it("builds the /v1 model endpoint and escapes the metric id", () => { const config = resolveConfig({ earthNowMetric: "a/b", api: "https://example.org" }); expect(config && modelUrl(config)).toBe("https://example.org/v1/metrics/a%2Fb/model"); }); }); describe("parseModelPayload", () => { it("accepts a bare model and an { model } envelope", () => { expect(parseModelPayload(linearModel).metricId).toBe("co2_emissions_ytd"); expect(parseModelPayload({ model: linearModel }).anchorValue).toBe(1_000_000); }); it("throws descriptive errors on malformed payloads", () => { expect(() => parseModelPayload(null)).toThrow(/not an object/); expect(() => parseModelPayload({})).toThrow(/metricId/); expect(() => parseModelPayload({ ...linearModel, anchorValue: "nope" })).toThrow( /anchorValue/, ); expect(() => parseModelPayload({ ...linearModel, anchorTime: "not-a-date" })).toThrow( /anchorTime/, ); expect(() => parseModelPayload({ ...linearModel, rateFn: 42 })).toThrow(/rateFn/); }); }); describe("buildDisplayText", () => { const config = { metricId: "co2_emissions_ytd", window: "total" as const, lang: "en", api: DEFAULT_API, }; it("renders the animated value WITHOUT the sigFigs cap (ticker mode)", () => { // 100 s after anchor: 1_000_000 + 2 * 100 = 1_000_200; sigFigs 4 would flatten to 1_000_000. const text = buildDisplayText(linearModel, ANCHOR_MS + 100_000, config); expect(text.value).toBe("1,000,200"); expect(text.unit).toBe("t"); }); it("renders the per-second rate line", () => { const text = buildDisplayText(linearModel, ANCHOR_MS + 100_000, config); expect(text.rate).toBe("2/s"); }); it("resolves the 'today' window from the same model", () => { // 2026-01-02T00:00:30Z → 30 s into the UTC day at 2/s = 60. const t = Date.parse("2026-01-02T00:00:30Z"); const text = buildDisplayText(linearModel, t, { ...config, window: "today" }); expect(text.value).toBe("60"); }); it("uses the caller-provided session start for 'session' windows", () => { const sessionStart = ANCHOR_MS + 10_000; const text = buildDisplayText( linearModel, sessionStart + 45_000, { ...config, window: "session" }, sessionStart, ); expect(text.value).toBe("90"); }); it("formats according to the requested locale", () => { const text = buildDisplayText(linearModel, ANCHOR_MS + 100_000, { ...config, lang: "fr" }); // fr uses narrow no-break spaces as grouping separators. expect(text.value.replace(/[  ]/g, " ")).toBe("1 000 200"); }); });