Files
Live_RC/tests/browser_team_membership.js
larssand 15b574a37d Fix Team Race duplicate-membership bug and two related team-roster gaps
Reported by the user: creating a team, adding the wrong driver, then
moving that driver to another team by ticking them on the new team's
edit form left the driver on both teams at once. Root cause: there
was never a "move" action - the edit form only ever added a driver to
the team being saved, never removed them from wherever they already
were. Live laps from a driver on two teams were then silently
credited to only the first-matching team, with no clear error.

reassignTeamMembers (event_race_logic.js) now strips a driver/car
being saved onto one team out of every other team in the same event,
and the team checkbox list shows which team a driver is already on
before you move them.

Also fixed two related gaps found while investigating, at the user's
request to fix everything: getTeamDriverPool had regressed to
ignoring the event's class/participant scoping entirely (showing
every driver in the club), and deleting a driver or car left them
behind in team rosters forever. Added a duplicate_team_members Race
Day preflight check as defense in depth for any already-corrupted
saved state.

Verified with new unit tests for reassignTeamMembers, getTeamDriverPool
and the new preflight check, plus a new permanent browser test
reproducing the user's exact reported scenario end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 17:07:45 +02:00

79 lines
5.8 KiB
JavaScript

import assert from "node:assert/strict";
// Regression coverage for a real bug the user reported: moving a driver from one team to
// another (by ticking them on the new team's edit form - there is no separate "move"
// action) used to only ever ADD the driver to the new team, never remove them from the
// old one, so the same driver ended up listed on two teams at once. Also covers the two
// related findings from the same investigation: getTeamDriverPool used to ignore the
// event's class/participant scoping entirely (showing every driver in the club), and
// deleting a driver left them behind in any team's roster forever.
//
// The event/session fixtures are seeded directly rather than driven through the 4-step
// race wizard (already covered by browser_race_setup.js) - only team management itself is
// under test here, and the Teams section only renders once a team_race session exists
// (see showTeamsSection in event_manager_controller.js).
export async function runTeamMembershipSmoke({ evaluate, navigate, savedState, waitFor, pause, base }) {
const click = (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`);
const submit = (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).requestSubmit()`);
const findEvent = async (id) => (await savedState()).events.find((event) => event.id === id);
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 driverA = { id: "team_membership_driver_a", name: "Team Membership Driver A", classId: "test_class", transponder: "7810001" };
const driverB = { id: "team_membership_driver_b", name: "Team Membership Driver B", classId: "test_class", transponder: "7810002" };
const raceEvent = {
id: "team_membership_event", name: "Team Membership Event", classId: "test_class", mode: "race",
raceConfig: { participantsConfigured: true, driverIds: [driverA.id, driverB.id], teams: [] },
};
const teamRaceSession = { id: "team_membership_session", eventId: raceEvent.id, name: "Team Race Session", type: "team_race", mode: "race", durationMin: 45, status: "ready" };
const seeded = await savedState();
await patchState({
drivers: [...seeded.drivers, driverA, driverB],
events: [...seeded.events, raceEvent],
sessions: [...seeded.sessions, teamRaceSession],
});
await navigate("race_setup", "raceWizardStepForm");
await click("event-manage-" + raceEvent.id);
await waitFor(async () => await evaluate(`Boolean(document.getElementById("teamForm"))`), "teams section visible for a race-mode event with a team_race session");
await evaluate(`(() => { const form = document.getElementById("teamForm"); form.elements.namedItem("teamName").value = "Membership Team A"; })()`);
await evaluate(`document.querySelector('[form="teamForm"][name="teamDriverIds"][value="${driverA.id}"]').checked = true`);
await submit("teamForm");
await waitFor(async () => (await findEvent(raceEvent.id)).raceConfig.teams.some((team) => team.name === "Membership Team A"), "team A created");
await evaluate(`(() => { const form = document.getElementById("teamForm"); form.elements.namedItem("teamName").value = "Membership Team B"; })()`);
await evaluate(`document.querySelector('[form="teamForm"][name="teamDriverIds"][value="${driverB.id}"]').checked = true`);
await submit("teamForm");
await waitFor(async () => (await findEvent(raceEvent.id)).raceConfig.teams.some((team) => team.name === "Membership Team B"), "team B created");
const teamB = (await findEvent(raceEvent.id)).raceConfig.teams.find((team) => team.name === "Membership Team B");
await click("team-edit-" + teamB.id);
await waitFor(async () => await evaluate(`Boolean(document.getElementById("teamEditForm"))`), "team B edit form open");
const hintText = await evaluate(`document.querySelector('#teamEditForm [name="teamDriverIds"][value="${driverA.id}"]')?.closest("label")?.textContent || ""`);
assert.match(hintText, /Membership Team A/, "the edit form names the other team a driver is currently on, before the move happens");
await evaluate(`document.querySelector('#teamEditForm [name="teamDriverIds"][value="${driverA.id}"]').checked = true`);
await pause(5500); // outlast the periodic version poll, matching the existing team-edit browser coverage
await click("teamEditSave");
await waitFor(async () => (await findEvent(raceEvent.id)).raceConfig.teams.find((team) => team.name === "Membership Team B").driverIds.includes(driverA.id), "driver A saved onto team B");
const afterMove = await findEvent(raceEvent.id);
const teamA = afterMove.raceConfig.teams.find((team) => team.name === "Membership Team A");
const teamBAfter = afterMove.raceConfig.teams.find((team) => team.name === "Membership Team B");
assert.deepEqual(teamA.driverIds, [], "driver A was removed from team A when moved to team B - the reported duplicate-membership bug is fixed");
assert.deepEqual([...teamBAfter.driverIds].sort(), [driverA.id, driverB.id].sort(), "team B now has both drivers");
console.log("PASS moving a driver onto a different team's roster removes them from their previous team, with a warning shown before saving");
await navigate("drivers", "driverForm");
await click("driver-delete-" + driverB.id);
await waitFor(async () => !(await savedState()).drivers.some((driver) => driver.id === driverB.id), "driver B deleted");
const afterDelete = (await findEvent(raceEvent.id)).raceConfig.teams.find((team) => team.name === "Membership Team B");
assert.deepEqual(afterDelete.driverIds, [driverA.id], "a deleted driver no longer lingers in a team's roster");
console.log("PASS deleting a driver purges them from every team's roster too");
}