Fix team page crash
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle 2026-08-06 18:00:02 +03:00
parent 04460d56d1
commit b73aac0d53
2 changed files with 131 additions and 6 deletions

View File

@ -820,6 +820,115 @@ describe("single elimination standings - third place match", () => {
});
});
describe("single elimination standings - byes in later rounds", () => {
// Brackets created before the current engine paired the padded seeding
// naturally, so the byes ended up next to each other and could fill both
// sides of a first round match. The current engine spreads byes with
// `space_between`, which makes this impossible to create today, but such
// brackets are still stored (tournament 1252's playoffs is one). A first
// round match that is a bye on both sides leaves the second round match it
// feeds with a single opponent, so that match is won against a bye. The
// semifinal won that way produces no loser, leaving only one team for the
// third place match, which can therefore never be played.
const legacyByeBracketData = (): BracketData => {
const stageId = 0;
const thirdPlaceRoundId = 3;
const match = (
id: number,
roundId: number,
number: number,
opponent1: number | null,
opponent2: number | null,
winnerSide: MatchData["winnerSide"],
): MatchData => ({
id,
stageId,
groupId: roundId === thirdPlaceRoundId ? 1 : 0,
roundId,
number,
opponent1: opponent1 === null ? null : { id: opponent1 },
opponent2: opponent2 === null ? null : { id: opponent2 },
winnerSide,
});
return {
stage: [
{
id: stageId,
type: "single_elimination",
settings: { consolationFinal: true },
number: 1,
},
],
group: [
{ id: 0, stageId, number: 1 },
{ id: 1, stageId, number: 2 },
],
round: [
{ id: 0, stageId, groupId: 0, number: 1 },
{ id: 1, stageId, groupId: 0, number: 2 },
{ id: 2, stageId, groupId: 0, number: 3 },
{ id: thirdPlaceRoundId, stageId, groupId: 1, number: 1 },
],
match: [
match(0, 0, 1, 1, 2, "opponent1"),
match(1, 0, 2, 3, 4, "opponent1"),
match(2, 0, 3, 5, 6, "opponent1"),
// six teams in an eight team bracket, both byes landed here
match(3, 0, 4, null, null, null),
match(4, 1, 1, 1, 3, "opponent1"),
// won against a bye
match(5, 1, 2, 5, null, "opponent1"),
match(6, 2, 1, 1, 5, "opponent1"),
// only one semifinal produced a loser
match(7, thirdPlaceRoundId, 1, 3, null, null),
],
};
};
const legacyByeTournament = () =>
testTournament({
ctx: {
settings: {
bracketProgression: [
{
type: "single_elimination",
name: "SE",
requiresCheckIn: false,
settings: {},
sources: [],
},
],
},
},
data: legacyByeBracketData(),
});
it("places every team when a match is won against a bye", () => {
const tournament = legacyByeTournament();
const standings = tournament.bracketByIdx(0)!.standings;
expect(standings.map((s) => [s.team.id, s.placement])).toEqual([
[1, 1],
[5, 2],
[3, 3],
[2, 4],
[4, 4],
[6, 4],
]);
});
it("gives third place to the only semifinal loser when the third place match is a bye", () => {
const tournament = legacyByeTournament();
const standings = tournament.bracketByIdx(0)!.standings;
expect(standings.find((s) => s.team.id === 3)?.placement).toBe(3);
});
});
describe("single elimination standings - projected ties", () => {
// Two semifinal losers tie for 3rd (no consolation final). Reports only one
// semifinal so the other is still in progress, mirroring the projected

View File

@ -2,6 +2,7 @@ import * as R from "remeda";
import type { Tables } from "~/db/tables";
import type {
BracketData,
MatchData,
RoundData,
} from "~/features/tournament-bracket/core/engine/types";
import invariant from "~/utils/invariant";
@ -86,6 +87,9 @@ export class SingleEliminationBracket extends Bracket {
continue;
}
// BYE
if (!match.opponent1 || !match.opponent2) continue;
const loser =
match.winnerSide === "opponent1" ? match.opponent2 : match.opponent1;
invariant(loser?.id, "Loser id not found");
@ -139,12 +143,7 @@ export class SingleEliminationBracket extends Bracket {
const thirdPlaceMatch = this.hasThirdPlaceMatch()
? this.data.match.find((m) => m.groupId !== matches[0].groupId)
: undefined;
const thirdPlaceMatchWinner =
thirdPlaceMatch?.winnerSide === "opponent1"
? thirdPlaceMatch.opponent1
: thirdPlaceMatch?.winnerSide === "opponent2"
? thirdPlaceMatch.opponent2
: undefined;
const thirdPlaceMatchWinner = winnerOfThirdPlaceMatch(thirdPlaceMatch);
const resultWithThirdPlaceTiebroken = result
.flatMap((standing) => {
@ -220,3 +219,20 @@ export class SingleEliminationBracket extends Bracket {
};
}
}
/**
* A third place match with only one opponent is decided by a BYE: the semifinal
* on the other side was itself won against a BYE, so it produced no loser and
* the lone semifinal loser takes third place without playing.
*/
function winnerOfThirdPlaceMatch(match: MatchData | undefined) {
if (!match) return undefined;
if (match.opponent1 && !match.opponent2) return match.opponent1;
if (!match.opponent1 && match.opponent2) return match.opponent2;
if (match.winnerSide === "opponent1") return match.opponent1;
if (match.winnerSide === "opponent2") return match.opponent2;
return undefined;
}