Files
SixFlagsSuperCalendar/tests/fast-lane-matching.test.ts
josh e888261ed9
Build and Deploy / Lint, typecheck, test (push) Successful in 35s
Build and Deploy / Build & Push (push) Successful in 1m8s
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>
2026-05-30 18:54:02 -04:00

139 lines
4.3 KiB
TypeScript

/**
* Fast Lane name-join tests.
*
* The Six Flags /wait-times endpoint and Queue-Times use slightly different
* ride name conventions, so Fast Lane waits are joined onto Queue-Times rides
* by normalized name. These cases lock that join behaviour.
*
* Run with: npm test
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseWaitTimes, lookupFastLane } from "../lib/scrapers/sixflags-waittimes";
import type { WTResponse } from "../lib/scrapers/sixflags-waittimes";
function ride(
name: string,
isFastLane: boolean,
fastLaneMinutes: number | null,
regularMinutes: number | null = 20,
): Record<string, unknown> {
return {
id: 1,
name,
isFastLane,
regularWaittime:
regularMinutes === null
? { createdDateTime: "", waitTime: 0 }
: { createdDateTime: "May 29, 2026 19:00:00", waitTime: regularMinutes },
fastlaneWaittime:
fastLaneMinutes === null
? { createdDateTime: "", waitTime: 0 }
: { createdDateTime: "May 29, 2026 19:00:00", waitTime: fastLaneMinutes },
fimsId: "RIDE-906-00001",
};
}
function result(...rides: Record<string, unknown>[]) {
const json: WTResponse = {
parkId: 906,
venues: [{ venueId: 1, venueName: "Rides", details: rides as never }],
};
const r = parseWaitTimes(json);
assert.ok(r, "expected parseWaitTimes to return a result");
return r;
}
// ── Name joins across QT ↔ SF naming quirks ──────────────────────────────────
test("matches across trademark symbols, THE prefix, possessives", () => {
const r = result(
ride("Batman: The Ride", true, 5),
ride("Riddler's Revenge", true, 10),
ride("Apocalypse the Ride", true, 15),
);
// Queue-Times-style names on the left should resolve to the SF entries.
assert.deepEqual(lookupFastLane("BATMAN™ The Ride", r), {
hasFastLane: true,
fastLaneMinutes: 5,
regularMinutes: 20,
});
assert.deepEqual(lookupFastLane("THE RIDDLER™'s Revenge", r), {
hasFastLane: true,
fastLaneMinutes: 10,
regularMinutes: 20,
});
// Prefix match: "Apocalypse" is a prefix of "Apocalypse the Ride".
assert.deepEqual(lookupFastLane("Apocalypse", r), {
hasFastLane: true,
fastLaneMinutes: 15,
regularMinutes: 20,
});
});
test("a non-Fast-Lane ride resolves to hasFastLane: false", () => {
const r = result(ride("Bucaneer", false, null));
assert.deepEqual(lookupFastLane("Bucaneer", r), {
hasFastLane: false,
fastLaneMinutes: null,
regularMinutes: 20,
});
});
test("empty fastlane createdDateTime yields fastLaneMinutes: null", () => {
const r = result(ride("Batman: The Ride", true, null));
assert.deepEqual(lookupFastLane("Batman: The Ride", r), {
hasFastLane: true,
fastLaneMinutes: null,
regularMinutes: 20,
});
});
test("regular wait is surfaced when regularWaittime.createdDateTime is fresh", () => {
const r = result(ride("Goliath", true, null, 35));
assert.deepEqual(lookupFastLane("Goliath", r), {
hasFastLane: true,
fastLaneMinutes: null,
regularMinutes: 35,
});
});
test("walk-on regular wait (0) is surfaced, not null", () => {
const r = result(ride("Buccaneer", false, null, 0));
assert.deepEqual(lookupFastLane("Buccaneer", r), {
hasFastLane: false,
fastLaneMinutes: null,
regularMinutes: 0,
});
});
test("empty regular createdDateTime yields regularMinutes: null", () => {
const r = result(ride("Tatsu", true, 10, null));
assert.deepEqual(lookupFastLane("Tatsu", r), {
hasFastLane: true,
fastLaneMinutes: 10,
regularMinutes: null,
});
});
test("a ride absent from SF data returns null", () => {
const r = result(ride("Apocalypse the Ride", true, 15));
assert.equal(lookupFastLane("Some Other Coaster", r), null);
});
test("conjunction guard: compound name does not match a single ride", () => {
const r = result(ride("Joker", true, 25));
// "Joker y Harley Quinn" is a different (compound) ride, not a Joker subtitle.
assert.equal(lookupFastLane("Joker y Harley Quinn", r), null);
});
test("parseWaitTimes returns null when no ride rows present", () => {
assert.equal(parseWaitTimes({ parkId: 1, venues: [] }), null);
assert.equal(
parseWaitTimes({ parkId: 1, venues: [{ venueId: 9, venueName: "Restaurants", details: [] }] }),
null,
);
});