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>
79 lines
5.8 KiB
JavaScript
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");
|
|
}
|