/** * Outage detection tests for the ride detail page's Today chart. * * Pure unit tests against lib/outage.ts — no DOM, no Recharts. * * Run with: npm test */ import { test } from "node:test"; import assert from "node:assert/strict"; import { computeOutages, formatOutageDuration, outageLookup } from "../lib/outage"; import type { OutageSample } from "../lib/outage"; /** Build a sample at the given UTC time (just a convenience). */ function s(t: string, isOpen: boolean): OutageSample { return { recordedAt: `2026-05-30T${t}:00Z`, isOpen }; } // ── computeOutages ─────────────────────────────────────────────────────────── test("no outages when every sample is open", () => { const out = computeOutages([s("12:00", true), s("12:05", true), s("12:10", true)]); assert.deepEqual(out, []); }); test("single outage in the middle of the day → #1, duration matches", () => { // closed at 13:00, 13:05, 13:10; reopens at 13:15 const out = computeOutages([ s("12:55", true), s("13:00", false), s("13:05", false), s("13:10", false), s("13:15", true), s("13:20", true), ]); assert.equal(out.length, 1); assert.equal(out[0].n, 1); assert.equal(out[0].durationMin, 15); assert.equal(out[0].startISO, "2026-05-30T13:00:00Z"); assert.equal(out[0].endISO, "2026-05-30T13:15:00Z"); }); test("two separate outages → numbered 1 then 2 in chronological order", () => { const out = computeOutages([ s("12:00", true), s("12:05", false), s("12:10", false), s("12:15", true), s("14:00", false), s("14:05", false), s("14:10", false), s("14:15", true), ]); assert.equal(out.length, 2); assert.equal(out[0].n, 1); assert.equal(out[0].durationMin, 10); assert.equal(out[1].n, 2); assert.equal(out[1].durationMin, 15); }); test("outage at start of day is still #1", () => { const out = computeOutages([ s("10:00", false), s("10:05", false), s("10:10", true), ]); assert.equal(out.length, 1); assert.equal(out[0].n, 1); assert.equal(out[0].durationMin, 10); }); test("outage that never reopens uses the last sample as the end", () => { const out = computeOutages([ s("16:00", true), s("16:05", false), s("16:10", false), s("16:15", false), ]); assert.equal(out.length, 1); assert.equal(out[0].n, 1); // start 16:05 → end 16:15 → 10 min (in-progress at end of day) assert.equal(out[0].durationMin, 10); assert.equal(out[0].endISO, "2026-05-30T16:15:00Z"); }); test("empty sample array → no outages", () => { assert.deepEqual(computeOutages([]), []); }); test("single closed sample → 0-minute outage, still numbered #1", () => { const out = computeOutages([s("12:00", true), s("12:05", false), s("12:10", true)]); assert.equal(out.length, 1); assert.equal(out[0].n, 1); assert.equal(out[0].durationMin, 5); }); // ── outageLookup ───────────────────────────────────────────────────────────── test("outageLookup tags each closed sample's time label with its outage", () => { const samples = [ s("12:00", true), s("12:05", false), s("12:10", false), s("12:15", true), s("14:00", false), s("14:05", false), s("14:10", true), ]; const outages = computeOutages(samples); const lookup = outageLookup(samples, outages); // 12:05 and 12:10 belong to outage #1; 14:00 and 14:05 belong to #2. // The exact HH:MM labels depend on the runner's local timezone, so we // assert membership using the same TIME_FMT that outage.ts uses. const TIME_FMT = new Intl.DateTimeFormat([], { hour: "2-digit", minute: "2-digit", hour12: false, }); const fmt = (iso: string) => TIME_FMT.format(new Date(iso)); assert.equal(lookup.get(fmt("2026-05-30T12:05:00Z"))?.n, 1); assert.equal(lookup.get(fmt("2026-05-30T12:10:00Z"))?.n, 1); assert.equal(lookup.get(fmt("2026-05-30T14:00:00Z"))?.n, 2); assert.equal(lookup.get(fmt("2026-05-30T14:05:00Z"))?.n, 2); // Open samples are not in the lookup. assert.equal(lookup.get(fmt("2026-05-30T12:00:00Z")), undefined); assert.equal(lookup.get(fmt("2026-05-30T14:10:00Z")), undefined); }); // ── formatOutageDuration ───────────────────────────────────────────────────── test("formatOutageDuration renders minutes-only under 1 hour", () => { assert.equal(formatOutageDuration(0), "0m"); assert.equal(formatOutageDuration(1), "1m"); assert.equal(formatOutageDuration(47), "47m"); assert.equal(formatOutageDuration(59), "59m"); }); test("formatOutageDuration renders exact hours as 'Xh'", () => { assert.equal(formatOutageDuration(60), "1h"); assert.equal(formatOutageDuration(120), "2h"); }); test("formatOutageDuration renders hours + minutes", () => { assert.equal(formatOutageDuration(88), "1h 28m"); assert.equal(formatOutageDuration(150), "2h 30m"); });