SPB Git

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%
4.4 KB · 139 lines typescript
Raw Blame History
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