Files
Live_RC/tests/browser_series.js
larssand 9a9379aa83 feat(series): add series/cup championships with points tables and standings
Group any number of races in freely chosen classes into a series with a
points table (ÖRC/100-80-70/F1/custom), best-N or drop-X counting, a
club filter that re-ranks members without guests, tie-breaks, rounds
created from a date list, and per-class standings with print and PDF.

Also fixes PDF export of wide tables: they are compacted and switched
to landscape instead of truncating names and dropping columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:49:23 +02:00

95 lines
6.7 KiB
JavaScript

import assert from "node:assert/strict";
// Series / cup end to end: create a series on the Series page, pick its club and class,
// create two rounds from a list of dates, run the first round (seeded final results), and
// check the standings: only club drivers score (re-ranked without the guest), the round not
// run yet is an empty column, and the PDF export carries the same table.
export async function runSeriesSmoke({ evaluate, navigate, savedState, waitFor, base, dialogs }) {
const patchState = async (patch) => {
const current = await savedState();
Object.assign(current, patch);
const response = await fetch(base + "/api/state", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(current) });
assert.equal(response.ok, true);
};
const dialogsAtStart = dialogs.length;
const seeded = await savedState();
await patchState({
classes: [...seeded.classes, { id: "sr_2wd", name: "SR 2WD" }, { id: "sr_4wd", name: "SR 4WD" }],
drivers: [
...seeded.drivers,
{ id: "sr_guest", name: "SR Guest", classId: "sr_2wd", club: "Other RC", transponder: "8660001" },
{ id: "sr_anna", name: "SR Anna", classId: "sr_2wd", club: "Östersunds RC", transponder: "8660002" },
{ id: "sr_bo", name: "SR Bo", classId: "sr_2wd", club: "Östersunds RC", transponder: "8660003" },
],
});
try {
await navigate("series", "seriesNew");
await evaluate(`document.getElementById("seriesNew").click()`);
await waitFor(async () => Boolean(await evaluate(`document.getElementById("seriesForm")`)), "series form");
await evaluate(`(() => {
const form = document.getElementById("seriesForm");
form.elements.name.value = "SR Winter Cup";
form.elements.club.value = "Östersunds RC";
form.querySelectorAll('input[name="classIds"]').forEach((box) => { box.checked = box.value === "sr_2wd"; });
form.elements.countMode.value = "drop";
form.elements.countValue.value = "1";
form.requestSubmit();
})()`);
await waitFor(async () => (await savedState()).series?.some((series) => series.name === "SR Winter Cup"), "series saved");
const saved = (await savedState()).series.find((series) => series.name === "SR Winter Cup");
assert.deepEqual(saved.classIds, ["sr_2wd"]);
assert.equal(saved.club, "Östersunds RC");
assert.deepEqual(saved.pointsTable.slice(0, 3), [100, 90, 82], "the ÖRC table is the default");
await evaluate(`(() => { const form = document.getElementById("seriesCreateEvents"); form.elements.dates.value = "2026-11-15\\n2026-11-01"; form.requestSubmit(); })()`);
await waitFor(async () => ((await savedState()).series.find((series) => series.id === saved.id)?.eventIds.length) === 2, "two rounds created");
const state = await savedState();
const rounds = state.series.find((series) => series.id === saved.id).eventIds.map((id) => state.events.find((event) => event.id === id));
assert.deepEqual(rounds.map((event) => [event.name, event.date, event.classId]).sort(), [["SR Winter Cup #1 SR 2WD", "2026-11-01", "sr_2wd"], ["SR Winter Cup #2 SR 2WD", "2026-11-15", "sr_2wd"]]);
console.log("PASS a series is created with club, class and count rule, and its rounds are created from dates");
// Round 1 is run: a finished A-final where the guest wins, Anna 2nd, Bo 3rd.
const round1 = rounds.find((event) => event.date === "2026-11-01");
const startedAt = Date.parse("2026-11-01T10:00:00Z");
const finalSession = { id: "sr_final", eventId: round1.id, name: "A Final", type: "final", durationMin: 5, status: "finished", startedAt, endedAt: startedAt + 300000, driverIds: ["sr_guest", "sr_anna", "sr_bo"] };
const competitor = (driverId, laps) => ({ key: `driver_${driverId}`, driverId, driverName: driverId, laps, startTimestamp: startedAt, lastTimestamp: startedAt + laps * 20000, bestLapMs: 20000 });
const passingsFor = (driverId, laps) => Array.from({ length: laps }, (_, index) => ({ driverId, competitorKey: `driver_${driverId}`, lapMs: 20000, timestamp: startedAt + (index + 1) * 20000, validLap: true }));
await patchState({
sessions: [...(await savedState()).sessions, finalSession],
resultsBySession: { ...(await savedState()).resultsBySession, sr_final: { passings: [...passingsFor("sr_guest", 15), ...passingsFor("sr_anna", 14), ...passingsFor("sr_bo", 13)], competitors: { driver_sr_guest: competitor("sr_guest", 15), driver_sr_anna: competitor("sr_anna", 14), driver_sr_bo: competitor("sr_bo", 13) }, adjustments: [] } },
});
await navigate("series", "seriesNew");
await evaluate(`document.querySelector('[data-series-select="${saved.id}"]')?.click()`);
await waitFor(async () => Boolean(await evaluate(`document.querySelector('[data-series-class="sr_2wd"] tbody tr')`)), "standings table");
const rows = await evaluate(`[...document.querySelectorAll('[data-series-class="sr_2wd"] tbody tr')].map((tr) => [...tr.children].map((td) => td.textContent.trim()))`);
assert.deepEqual(rows, [["1", "SR Anna", "100", "", "1", "100"], ["2", "SR Bo", "90", "", "1", "90"]], "the guest races but doesn't score; Anna is re-ranked 1st; round 2 is empty");
console.log("PASS standings: only club drivers score, re-ranked without guests; unplayed rounds stay empty");
// The PDF export carries the same table (request captured in the page, not downloaded).
const payload = await evaluate(`(async () => {
const realFetch = window.fetch;
let body = null;
window.fetch = async (url, options) => { if (String(url).includes("/api/export/pdf")) { body = options.body; return new Response(new Blob(["%PDF"]), { status: 200 }); } return realFetch(url, options); };
try { document.getElementById("seriesPdf").click(); for (let i = 0; i < 50 && !body; i += 1) await new Promise((r) => setTimeout(r, 20)); return body; }
finally { window.fetch = realFetch; }
})()`);
const pdf = JSON.parse(payload);
assert.equal(pdf.title, "SR Winter Cup");
assert.deepEqual(pdf.sections[0].rows[0], ["1", "SR Anna", "100", "", "1", "100"]);
console.log("PASS the series PDF export contains the standings");
} finally {
dialogs.splice(dialogsAtStart);
const current = await savedState();
const seriesEventIds = new Set((current.series || []).filter((series) => series.name === "SR Winter Cup").flatMap((series) => series.eventIds));
await patchState({
series: (current.series || []).filter((series) => series.name !== "SR Winter Cup"),
events: current.events.filter((event) => !seriesEventIds.has(event.id)),
sessions: current.sessions.filter((session) => session.id !== "sr_final"),
drivers: current.drivers.filter((driver) => !driver.id.startsWith("sr_")),
classes: current.classes.filter((item) => !item.id.startsWith("sr_")),
});
}
}