feat: prefer Six Flags regular waits, show Fast Lane at 0, mark outages on chart
Three usability fixes after a day of using the ride detail page. 1. Six Flags is now the primary source for regular wait times. SF's /wait-times endpoint reports regular waits alongside Fast Lane, and it updates more promptly than Queue-Times around park-open. The sampler and the live /rides + ride-history routes all prefer SF's regularWaittime when its createdDateTime is non-empty; Queue-Times remains the fallback and the authoritative isOpen source. 2. The today chart's Fast Lane line now stays visible when its value is 0 (walk-on). Y-axis bottom padding ensures the line sits clearly above the X-axis frame instead of being clipped against it. The tooltip shows "walk-on" instead of "0 min" for that case. 3. Outages are now explicit on the chart instead of just being gaps. computeOutages walks today's samples to find contiguous closed runs and numbers them chronologically. Each outage renders as a translucent pink ReferenceArea with a "#N" label. The custom tooltip detects when the cursor is over an outage span and shows "Outage #N — Hh Mm" (e.g. "Outage #2 — 1h 28m") in place of the wait/Fast Lane rows. Includes a seed-test-samples.ts dev script for eyeballing the chart with synthetic outage data. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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");
|
||||
});
|
||||
Reference in New Issue
Block a user