feat: add Fast Lane wait times toggle on park pages
Build and Deploy / Build & Push (push) Successful in 1m3s

Join Fast Lane waits from the Six Flags /wait-times endpoint onto
Queue-Times rides by name. A new toggle on the live ride panel swaps
the shown wait to the Fast Lane number; regular waits and open status
still come from Queue-Times.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 22:51:52 -04:00
parent aa46cc1b3d
commit bfe099322f
6 changed files with 455 additions and 70 deletions
+102
View File
@@ -0,0 +1,102 @@
/**
* 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,
): Record<string, unknown> {
return {
id: 1,
name,
isFastLane,
regularWaittime: { createdDateTime: "May 29, 2026 19:00:00", waitTime: 20 },
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,
});
assert.deepEqual(lookupFastLane("THE RIDDLER™'s Revenge", r), {
hasFastLane: true,
fastLaneMinutes: 10,
});
// Prefix match: "Apocalypse" is a prefix of "Apocalypse the Ride".
assert.deepEqual(lookupFastLane("Apocalypse", r), {
hasFastLane: true,
fastLaneMinutes: 15,
});
});
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,
});
});
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,
});
});
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,
);
});