spb/earth-now Public License
earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.
TypeScript 93%
Shell 2.3%
SQL 1.4%
JavaScript 1.3%
Dockerfile 1.2%
CSS 0.8%
1/**2 * earth-now.co3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: packages/widget/test/widget.test.ts6 * Purpose: Unit tests for the widget's pure helpers — config resolution, payload parsing, display text7 */89import { describe, expect, it } from "vitest";10import type { CounterModel } from "@earth-now/counter";11import {12 DEFAULT_API,13 buildDisplayText,14 modelUrl,15 parseModelPayload,16 resolveConfig,17} from "../src/render";1819const ANCHOR = "2026-01-01T00:00:00Z";20const ANCHOR_MS = Date.parse(ANCHOR);2122const linearModel: CounterModel = {23 metricId: "co2_emissions_ytd",24 anchorValue: 1_000_000,25 anchorTime: ANCHOR,26 rateFn: { kind: "linear", perSecond: 2 },27 observedAt: "2025-12-15T00:00:00Z",28 sourceId: "gcp_2025",29 modelVersion: "linear-v1",30 displayHints: { decimals: 0, sigFigs: 4, unit: "t" },31};3233describe("resolveConfig", () => {34 it("resolves a full dataset", () => {35 const config = resolveConfig({36 earthNowMetric: "co2_emissions_ytd",37 window: "today",38 lang: "fr",39 api: "https://www.earth-now.co/",40 });41 expect(config).toEqual({42 metricId: "co2_emissions_ytd",43 window: "today",44 lang: "fr",45 api: "https://www.earth-now.co",46 });47 });4849 it("applies defaults (window total, lang en, official api)", () => {50 const config = resolveConfig({ earthNowMetric: "world_population" });51 expect(config).toEqual({52 metricId: "world_population",53 window: "total",54 lang: "en",55 api: DEFAULT_API,56 });57 });5859 it("falls back to 'total' on an unknown window", () => {60 expect(resolveConfig({ earthNowMetric: "x", window: "weird" })?.window).toBe("total");61 });6263 it("returns null when no metric id is declared", () => {64 expect(resolveConfig({})).toBeNull();65 expect(resolveConfig({ earthNowMetric: " " })).toBeNull();66 });67});6869describe("modelUrl", () => {70 it("builds the /v1 model endpoint and escapes the metric id", () => {71 const config = resolveConfig({ earthNowMetric: "a/b", api: "https://example.org" });72 expect(config && modelUrl(config)).toBe("https://example.org/v1/metrics/a%2Fb/model");73 });74});7576describe("parseModelPayload", () => {77 it("accepts a bare model and an { model } envelope", () => {78 expect(parseModelPayload(linearModel).metricId).toBe("co2_emissions_ytd");79 expect(parseModelPayload({ model: linearModel }).anchorValue).toBe(1_000_000);80 });8182 it("throws descriptive errors on malformed payloads", () => {83 expect(() => parseModelPayload(null)).toThrow(/not an object/);84 expect(() => parseModelPayload({})).toThrow(/metricId/);85 expect(() => parseModelPayload({ ...linearModel, anchorValue: "nope" })).toThrow(86 /anchorValue/,87 );88 expect(() => parseModelPayload({ ...linearModel, anchorTime: "not-a-date" })).toThrow(89 /anchorTime/,90 );91 expect(() => parseModelPayload({ ...linearModel, rateFn: 42 })).toThrow(/rateFn/);92 });93});9495describe("buildDisplayText", () => {96 const config = {97 metricId: "co2_emissions_ytd",98 window: "total" as const,99 lang: "en",100 api: DEFAULT_API,101 };102103 it("renders the animated value WITHOUT the sigFigs cap (ticker mode)", () => {104 // 100 s after anchor: 1_000_000 + 2 * 100 = 1_000_200; sigFigs 4 would flatten to 1_000_000.105 const text = buildDisplayText(linearModel, ANCHOR_MS + 100_000, config);106 expect(text.value).toBe("1,000,200");107 expect(text.unit).toBe("t");108 });109110 it("renders the per-second rate line", () => {111 const text = buildDisplayText(linearModel, ANCHOR_MS + 100_000, config);112 expect(text.rate).toBe("2/s");113 });114115 it("resolves the 'today' window from the same model", () => {116 // 2026-01-02T00:00:30Z → 30 s into the UTC day at 2/s = 60.117 const t = Date.parse("2026-01-02T00:00:30Z");118 const text = buildDisplayText(linearModel, t, { ...config, window: "today" });119 expect(text.value).toBe("60");120 });121122 it("uses the caller-provided session start for 'session' windows", () => {123 const sessionStart = ANCHOR_MS + 10_000;124 const text = buildDisplayText(125 linearModel,126 sessionStart + 45_000,127 { ...config, window: "session" },128 sessionStart,129 );130 expect(text.value).toBe("90");131 });132133 it("formats according to the requested locale", () => {134 const text = buildDisplayText(linearModel, ANCHOR_MS + 100_000, { ...config, lang: "fr" });135 // fr uses narrow no-break spaces as grouping separators.136 expect(text.value.replace(/[ ]/g, " ")).toBe("1 000 200");137 });138});139