feat(series): run each round from the series page

Step 3 becomes a rounds table: pick drivers inline (copy from the
previous round, add a late entry), set qualifying rounds/finals/max
cars per heat per round, see the status, and create qualifying and
finals from there. Qualifying heats are now split evenly so nobody
drives alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-10-11 18:55:36 +02:00
parent 581ec2c781
commit b5707f92d1
17 changed files with 447 additions and 39 deletions

View File

@@ -53,13 +53,49 @@ export async function runSeriesSmoke({ evaluate, navigate, savedState, waitFor,
assert.equal(saved.scoring, "club");
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 evaluate(`(() => { const form = document.getElementById("seriesCreateEvents"); form.elements.dates.value = "2026-11-15\\n2026-11-01"; form.elements.qualifyingRounds.value = "2"; form.elements.finalLegs.value = "1"; form.elements.carsPerHeat.value = "2"; 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"]]);
assert.deepEqual(rounds.map((event) => [event.raceConfig.qualifyingRounds, event.raceConfig.finalLegs, event.raceConfig.carsPerHeat]), [[2, 1, 2], [2, 1, 2]], "the chosen format is applied to every new round");
console.log("PASS a series is created with club, class and count rule, and its rounds are created from dates");
// The series is the hub for each round: drivers, a late entry, copy to the next round,
// format per round and qualifying - heats split by how many take part.
const roundOne = rounds.find((event) => event.date === "2026-11-01");
const roundTwo = rounds.find((event) => event.date === "2026-11-15");
const inRound = (eventId, js) => evaluate(`(() => { const row = document.querySelector('tr[data-round="${eventId}"]'); ${js} })()`);
await inRound(roundOne.id, `row.querySelector('[data-round-action="toggle"]').click();`);
await waitFor(async () => Boolean(await evaluate(`document.querySelector('[data-round-participants="${roundOne.id}"]')`)), "round 1 drivers open");
await evaluate(`(() => { const box = document.querySelector('[data-round-driver="sr_bo"]'); box.checked = false; box.dispatchEvent(new Event("change")); })()`);
await waitFor(async () => { const event = (await savedState()).events.find((item) => item.id === roundOne.id); return event.raceConfig.participantsConfigured && !event.raceConfig.driverIds.includes("sr_bo"); }, "Bo unticked for round 1");
await evaluate(`(() => { const form = document.querySelector('[data-round-add-driver]'); form.elements.name.value = "SR Cia"; form.elements.transponder.value = "8660004"; form.requestSubmit(); })()`);
await waitFor(async () => (await savedState()).drivers.some((driver) => driver.name === "SR Cia"), "late entry registered");
const cia = (await savedState()).drivers.find((driver) => driver.name === "SR Cia");
assert.deepEqual([cia.classId, cia.club, cia.transponder], ["sr_2wd", "Östersunds RC", "8660004"], "in the round's class and the series' club");
const roundOneIds = (await savedState()).events.find((item) => item.id === roundOne.id).raceConfig.driverIds;
assert.ok(roundOneIds.includes(cia.id) && !roundOneIds.includes("sr_bo"), "the late entry is ticked for this round");
await inRound(roundTwo.id, `row.querySelector('[data-round-action="toggle"]').click();`);
await waitFor(async () => Boolean(await evaluate(`document.querySelector('[data-round-participants="${roundTwo.id}"]')`)), "round 2 drivers open");
await evaluate(`document.querySelector('[data-round-participants="${roundTwo.id}"] [data-round-action="copy"]').click()`);
await waitFor(async () => JSON.stringify([...(await savedState()).events.find((item) => item.id === roundTwo.id).raceConfig.driverIds].sort()) === JSON.stringify([...roundOneIds].sort()), "round 2 copied from round 1");
// Round 1 gets three qualifying rounds (round 2 keeps two). Three drivers with max two per
// heat give two heats - of two and one - in each round.
await inRound(roundOne.id, `const input = row.querySelector('input[name="qualifyingRounds"]'); input.value = "3"; input.dispatchEvent(new Event("change"));`);
await waitFor(async () => (await savedState()).events.find((item) => item.id === roundOne.id).raceConfig.qualifyingRounds === 3, "format changed for round 1 only");
assert.equal((await savedState()).events.find((item) => item.id === roundTwo.id).raceConfig.qualifyingRounds, 2, "round 2 keeps its own format");
await inRound(roundOne.id, `row.querySelector('[data-round-action="qualifying"]').click();`);
await waitFor(async () => (await savedState()).sessions.filter((session) => session.eventId === roundOne.id && session.type === "qualification").length === 6, "3 rounds x 2 heats created");
const quals = (await savedState()).sessions.filter((session) => session.eventId === roundOne.id && session.type === "qualification");
assert.deepEqual([...new Set(quals.map((session) => session.driverIds.length))].sort(), [1, 2], "3 drivers, max 2 per heat: heats of 2 and 1");
await waitFor(async () => (await inRound(roundOne.id, `return row.querySelector('.pill').textContent.trim() + "|" + row.querySelector('input[name="qualifyingRounds"]').disabled;`)) === "Qualifying created|true", "status shown and format locked");
console.log("PASS each round is run from the series: drivers, late entry, copy from previous, own format, qualifying split by the field");
// The rest of this scenario seeds a finished final for round 1 - drop these unrun heats first.
await patchState({ sessions: (await savedState()).sessions.filter((session) => !(session.eventId === roundOne.id && session.type === "qualification")) });
// 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");
@@ -106,8 +142,8 @@ export async function runSeriesSmoke({ evaluate, navigate, savedState, waitFor,
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_")),
sessions: current.sessions.filter((session) => session.id !== "sr_final" && !seriesEventIds.has(session.eventId)),
drivers: current.drivers.filter((driver) => !driver.id.startsWith("sr_") && !String(driver.name).startsWith("SR ")),
classes: current.classes.filter((item) => !item.id.startsWith("sr_")),
});
}