Skip to content

Tournament bracket · E2E tests

11 modules · 98 exports · 2 repositories · 35 e2e tests · 3 routes

  • SSendou
  • NN-ZAP
  • Users a test creates itself are coloured per test
  • Arrange data setup
  • Act what the user does
  • Assert what is expected

Tournament bracket elimination

reports score and sees bracket update

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression DOUBLE_ELIMINATION → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DOUBLE_ELIMINATION,
    });
  2. Create teams tournament.id, members N-ZAP, ...teamSeeds(3) → teams
    const teams = await createTeams(factories, tournament.id, [
    	{ members: [NZAP_TEST_ID] },
    	...teamSeeds(3),
    ]);
  3. Start bracket tournament tournament.id → matches
    const matches = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
  4. nzapsMatchId = matches[0].id
    const nzapsMatchId = matches[0].id;
  5. adjacentMatchId = matches[1].id
    const adjacentMatchId = matches[1].id;
  6. losersMatchId = matches[3].id
    const losersMatchId = matches[3].id;
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. report winner of N-ZAP's first match

    Open match nzapsMatchId → match
    let match = await brackets.openMatch(nzapsMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. report winner of the adjacent match using admin powers

    Open match adjacentMatchId → match
    match = await brackets.openMatch(adjacentMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. report one map of the only losers side match available

    Open match losersMatchId → match
    match = await brackets.openMatch(losersMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 1 set ends false
    await match.reportResult({ mapsToReport: 1, setEnds: false });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. N-ZAP's first match can't be reopened while the losers match depends on it

    Open match nzapsMatchId → match
    match = await brackets.openMatch(nzapsMatchId);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
Assert
  1. reopen match button is not visible
    await isNotVisible(match.locators.reopenMatchButton);
Act
  1. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. undo the losers match score

    Open match losersMatchId → match
    match = await brackets.openMatch(losersMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Undo last report
    await match.undoLastReport();
Assert
  1. score 0, 0 is visible
    await expect(match.score([0, 0])).toBeVisible();
Act
  1. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. now the reopen succeeds

    Open match nzapsMatchId → match
    match = await brackets.openMatch(nzapsMatchId);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
  2. Reopen
    await match.reopen();
Assert
  1. score 1, 0 is visible
    await expect(match.score([1, 0])).toBeVisible();
NN-ZAP
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open match nzapsMatchId → match
    match = await brackets.openMatch(nzapsMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Undo last report
    await match.undoLastReport();
Assert
  1. score 0, 0 is visible
    await expect(match.score([0, 0])).toBeVisible();
Act
  1. Report result maps to report 2 winner 2
    await match.reportResult({ mapsToReport: 2, winner: 2 });
  2. Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. participant in round 3, teams[0].id is visible
    await expect(
    	brackets.participantInRound(DE_LOSERS_ROUND_ID, teams[0].id),
    ).toBeVisible();
Source
test("reports score and sees bracket update", async ({ page, factories }) => {
	test.slow();
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DOUBLE_ELIMINATION,
	});
	const teams = await createTeams(factories, tournament.id, [
		{ members: [NZAP_TEST_ID] },
		...teamSeeds(3),
	]);
	const matches = await factories.TournamentFactory.startBracket(
		tournament.id,
	);
	const nzapsMatchId = matches[0].id;
	const adjacentMatchId = matches[1].id;
	const losersMatchId = matches[3].id;

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	// report winner of N-ZAP's first match
	let match = await brackets.openMatch(nzapsMatchId);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	// report winner of the adjacent match using admin powers
	match = await brackets.openMatch(adjacentMatchId);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	// report one map of the only losers side match available
	match = await brackets.openMatch(losersMatchId);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, setEnds: false });
	await match.backToBracket();

	// N-ZAP's first match can't be reopened while the losers match depends on it
	match = await brackets.openMatch(nzapsMatchId);
	await match.openTab("admin");
	await isNotVisible(match.locators.reopenMatchButton);
	await match.backToBracket();

	// undo the losers match score
	match = await brackets.openMatch(losersMatchId);
	await match.openTab("action");
	await match.undoLastReport();
	await expect(match.score([0, 0])).toBeVisible();
	await match.backToBracket();

	// now the reopen succeeds
	match = await brackets.openMatch(nzapsMatchId);
	await match.openTab("admin");
	await match.reopen();
	await expect(match.score([1, 0])).toBeVisible();

	// as N-ZAP, undo every score and let the other team sweep instead
	await impersonate(page, NZAP_TEST_ID);
	await brackets.goto(tournament.id);
	match = await brackets.openMatch(nzapsMatchId);
	await match.openTab("action");
	await match.undoLastReport();
	await expect(match.score([0, 0])).toBeVisible();
	await match.reportResult({ mapsToReport: 2, winner: 2 });
	await match.backToBracket();
	await expect(
		brackets.participantInRound(DE_LOSERS_ROUND_ID, teams[0].id),
	).toBeVisible();
})

losers bracket team winning grand finals forces a bracket reset

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression DOUBLE_ELIMINATION → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DOUBLE_ELIMINATION,
    });
  2. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
  3. Start bracket tournament tournament.id → [wbSemiOne, wbSemiTwo, wbFinal, lbSemi, lbFinal, grandFinals, reset]
    const [wbSemiOne, wbSemiTwo, wbFinal, lbSemi, lbFinal, grandFinals, reset] =
    	await factories.TournamentFactory.startBracket(tournament.id);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. every match up to grand finals is swept by whichever team is on the alpha side, leaving the winners bracket team facing the team it beat in the winners final

    For each { id } of wbSemiOne, wbSemiTwo, wbFinal, lbSemi, lbFinal
    for (const { id } of [wbSemiOne, wbSemiTwo, wbFinal, lbSemi, lbFinal]) {
    	const match = await brackets.openMatch(id);
    	await match.openTab("action");
    	await match.reportResult({ mapsToReport: 2 });
    	await match.backToBracket();
    }
    1. Open match id → match
      const match = await brackets.openMatch(id);
    2. Open tab “action”
      await match.openTab("action");
    3. Report result maps to report 2
      await match.reportResult({ mapsToReport: 2 });
    4. Back to bracket
      await match.backToBracket();
  3. the losers bracket team (bravo side of grand finals) takes the first set, which is only enough to even out the sets lost

    Open match grandFinals.id → match
    let match = await brackets.openMatch(grandFinals.id);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2 winner 2
    await match.reportResult({ mapsToReport: 2, winner: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. round header TOURNAMENT.ROUND_NAMES.BRACKET_RESET is visible
    await expect(
    	brackets.roundHeader(TOURNAMENT.ROUND_NAMES.BRACKET_RESET),
    ).toBeVisible();
  2. finalize tournament button is not visible
    await isNotVisible(brackets.locators.finalizeTournamentButton);
Act
  1. Open match reset.id → match
    match = await brackets.openMatch(reset.id);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. finalize tournament button is visible
    await expect(brackets.locators.finalizeTournamentButton).toBeVisible();
Source
test("losers bracket team winning grand finals forces a bracket reset", async ({
	page,
	factories,
}) => {
	test.slow();

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DOUBLE_ELIMINATION,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));
	const [wbSemiOne, wbSemiTwo, wbFinal, lbSemi, lbFinal, grandFinals, reset] =
		await factories.TournamentFactory.startBracket(tournament.id);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	// every match up to grand finals is swept by whichever team is on the alpha
	// side, leaving the winners bracket team facing the team it beat in the
	// winners final
	for (const { id } of [wbSemiOne, wbSemiTwo, wbFinal, lbSemi, lbFinal]) {
		const match = await brackets.openMatch(id);
		await match.openTab("action");
		await match.reportResult({ mapsToReport: 2 });
		await match.backToBracket();
	}

	// the losers bracket team (bravo side of grand finals) takes the first set,
	// which is only enough to even out the sets lost
	let match = await brackets.openMatch(grandFinals.id);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2, winner: 2 });
	await match.backToBracket();

	await expect(
		brackets.roundHeader(TOURNAMENT.ROUND_NAMES.BRACKET_RESET),
	).toBeVisible();
	await isNotVisible(brackets.locators.finalizeTournamentButton);

	match = await brackets.openMatch(reset.id);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	await expect(brackets.locators.finalizeTournamentButton).toBeVisible();
})

completes and finalizes a small tournament with badge assigning

Renders/notifications/to/:id/to/:id/brackets/to/:id/brackets/finalize/to/:id/matches/:mid/to/:id/results/u/:identifier/u/:identifier/results

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create 2 × badge → badges
    const badges = await factories.BadgeFactory.createMany(2);
  2. Create tournament name “In The Zone 22” author Sendou start times startedTournamentTimes() bracket progression DOUBLE_ELIMINATION map pool maps TO_MAP_POOL badges badges.map((badge) => badge.id) → tournament
    const tournament = await factories.TournamentFactory.create({
    	name: "In The Zone 22",
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DOUBLE_ELIMINATION,
    	mapPoolMaps: TO_MAP_POOL,
    	badges: badges.map((badge) => badge.id),
    });
  3. Create teams tournament.id, members Sendou, → teams
    const teams = await createTeams(factories, tournament.id, [
    	{ members: [ADMIN_ID] },
    	{},
    ]);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. Open match 1 → match
    const match = await brackets.openMatch(1);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. Open finalize tournament dialog → finalizeDialog
    const finalizeDialog = await brackets.openFinalizeTournamentDialog();
Finalize tournament dialog
Act
  1. Select badge receiver 0, teams[0].id
    await finalizeDialog.selectBadgeReceiver(0, teams[0].id);
  2. Select badge receiver 1, teams[1].id
    await finalizeDialog.selectBadgeReceiver(1, teams[1].id);
  3. Confirm
    await finalizeDialog.confirm();
Tournament brackets page
Act
  1. Open results → results
    const results = await brackets.nav.openResults();
Assert
  1. seed performance rating shows up after tournament is finalized

    results.locators.sprHeader is visible
    await expect(results.locators.sprHeader).toBeVisible();
User results page
Act
  1. Open the user results page Sendou
    await userResults.goto(ADMIN_ID);
Assert
  1. tournament name “In The Zone 22” is visible
    await expect(userResults.tournamentName("In The Zone 22")).toBeVisible();
Notifications page
Act
  1. Open the notifications page
    await notifications.goto();
Assert
  1. first items contains “New badge”
    await expect(notifications.locators.items.first()).toContainText(
    	"New badge",
    );
Source
test("completes and finalizes a small tournament with badge assigning", async ({
	page,
	factories,
}) => {
	test.slow();

	const badges = await factories.BadgeFactory.createMany(2);
	const tournament = await factories.TournamentFactory.create({
		name: "In The Zone 22",
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DOUBLE_ELIMINATION,
		mapPoolMaps: TO_MAP_POOL,
		badges: badges.map((badge) => badge.id),
	});
	const teams = await createTeams(factories, tournament.id, [
		{ members: [ADMIN_ID] },
		{},
	]);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	const match = await brackets.openMatch(1);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	const finalizeDialog = await brackets.openFinalizeTournamentDialog();
	await finalizeDialog.selectBadgeReceiver(0, teams[0].id);
	await finalizeDialog.selectBadgeReceiver(1, teams[1].id);
	await finalizeDialog.confirm();

	const results = await brackets.nav.openResults();
	// seed performance rating shows up after tournament is finalized
	await expect(results.locators.sprHeader).toBeVisible();

	const userResults = new UserResultsPage(page);
	await userResults.goto(ADMIN_ID);

	await expect(userResults.tournamentName("In The Zone 22")).toBeVisible();

	const notifications = new NotificationsPage(page);
	await notifications.goto();

	await expect(notifications.locators.items.first()).toContainText(
		"New badge",
	);
})

locks/unlocks matches & sets match as casted

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/stream/to/:id/brackets/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression DOUBLE_ELIMINATION map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DOUBLE_ELIMINATION,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Open stream → stream
    const stream = await admin.openStream();
Tournament admin stream page
Act
  1. an empty array field already renders one placeholder input

    Fill account 0, “test”
    await stream.fillAccount(0, "test");
  2. Save
    await stream.save();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. Open match 1 → match
    let match = await brackets.openMatch(1);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. match 3 is the winners' final the winner of match 1 waits in

    Open match 3 → match
    match = await brackets.openMatch(3);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
  2. picking a chip auto-submits the cast channel

    Set casted by “test”
    await match.setCastedBy("test");
  3. Submit cast info
    await match.submitCastInfo();
  4. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. Open match 2 → match
    match = await brackets.openMatch(2);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. first cast badges is visible
    await expect(brackets.locators.castBadges.first()).toBeVisible();
Act
  1. Open match 3 → match
    match = await brackets.openMatch(3);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
Assert
  1. the toggle reading "Unlock" is what signals the locked state

    unlock button is visible
    await expect(match.locators.unlockButton).toBeVisible();
  2. a locked match still shows the pool & room pass so players can join

    pool label is visible
    await expect(match.locators.poolLabel).toBeVisible();
  3. room pass is visible
    await expect(match.locators.roomPass).toBeVisible();
Act
  1. Submit cast info
    await match.submitCastInfo();
Assert
  1. stage banner is visible
    await expect(match.locators.stageBanner).toBeVisible();
Act
  1. the cast channel persists across unlock; the bracket badge flips from CAST to LIVE

    Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. first live badges is visible
    await expect(brackets.locators.liveBadges.first()).toBeVisible();
Source
test("locks/unlocks matches & sets match as casted", async ({
	page,
	factories,
}) => {
	test.slow();

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DOUBLE_ELIMINATION,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));

	await impersonate(page);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);

	const stream = await admin.openStream();
	// an empty array field already renders one placeholder input
	await stream.fillAccount(0, "test");
	await stream.save();

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	let match = await brackets.openMatch(1);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	// match 3 is the winners' final the winner of match 1 waits in
	match = await brackets.openMatch(3);
	await match.openTab("admin");
	// picking a chip auto-submits the cast channel
	await match.setCastedBy("test");
	await match.submitCastInfo();
	await match.backToBracket();

	match = await brackets.openMatch(2);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	await expect(brackets.locators.castBadges.first()).toBeVisible();
	match = await brackets.openMatch(3);
	await match.openTab("admin");
	// the toggle reading "Unlock" is what signals the locked state
	await expect(match.locators.unlockButton).toBeVisible();
	// a locked match still shows the pool & room pass so players can join
	await expect(match.locators.poolLabel).toBeVisible();
	await expect(match.locators.roomPass).toBeVisible();
	await match.submitCastInfo();
	await expect(match.locators.stageBanner).toBeVisible();

	// the cast channel persists across unlock; the bracket badge flips from CAST to LIVE
	await match.backToBracket();
	await expect(brackets.locators.liveBadges.first()).toBeVisible();
})

resets bracket

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/brackets/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression DOUBLE_ELIMINATION map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DOUBLE_ELIMINATION,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. the top seed has not checked in, leaving 15 teams and a first round bye

    Create teams tournament.id, is checked in false, ...teamSeeds(15)
    await createTeams(factories, tournament.id, [
    	{ isCheckedIn: false },
    	...teamSeeds(15),
    ]);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
Assert
  1. match 1 is not visible
    await isNotVisible(brackets.match(1));
Act
  1. Open match 2 → match
    const match = await brackets.openMatch(2);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Open admin → admin
    const admin = await match.nav.openAdmin();
  4. Reset bracket “Main bracket”
    await admin.resetBracket("Main bracket");
  5. Click admin.adminTab("Teams")
    await admin.adminTab("Teams").click();
  6. Check team in 0
    await admin.checkTeamIn(0);
  7. Open brackets → bracketsAfterReset
    const bracketsAfterReset = await admin.nav.openBrackets();
  8. Finalize
    await bracketsAfterReset.finalize();
Assert
  1. bye is gone

    bracketsAfterReset.firstRoundTeamNames(16) does not contain “”
    expect(await bracketsAfterReset.firstRoundTeamNames(16)).not.toContain("");
Source
test("resets bracket", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DOUBLE_ELIMINATION,
		mapPoolMaps: TO_MAP_POOL,
	});
	// the top seed has not checked in, leaving 15 teams and a first round bye
	await createTeams(factories, tournament.id, [
		{ isCheckedIn: false },
		...teamSeeds(15),
	]);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	await isNotVisible(brackets.match(1));
	const match = await brackets.openMatch(2);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });

	const admin = await match.nav.openAdmin();
	await admin.resetBracket("Main bracket");

	await admin.adminTab("Teams").click();
	await admin.checkTeamIn(0);

	const bracketsAfterReset = await admin.nav.openBrackets();
	await bracketsAfterReset.finalize();
	// bye is gone
	expect(await bracketsAfterReset.firstRoundTeamNames(16)).not.toContain("");
})

dropping team out ends ongoing match early and auto-forfeits losers bracket match

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression DOUBLE_ELIMINATION → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DOUBLE_ELIMINATION,
    });
  2. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
  3. Start bracket tournament tournament.id → matches
    const matches = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
  4. ongoingMatchId = matches[0].id
    const ongoingMatchId = matches[0].id;
  5. adjacentMatchId = matches[1].id
    const adjacentMatchId = matches[1].id;
  6. losersMatchId = matches[3].id
    const losersMatchId = matches[3].id;
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open match ongoingMatchId → match
    let match = await brackets.openMatch(ongoingMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 1 winner 1 set ends false
    await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
  3. Back to bracket
    await match.backToBracket();
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Drop out team 3
    await admin.dropOutTeam(3);
Tournament match page
Act
  1. dropping out ended the ongoing match early

    Open the tournament match page tournament id tournament.id match id ongoingMatchId
    await match.goto({ tournamentId: tournament.id, matchId: ongoingMatchId });
Assert
  1. final banner is visible
    await expect(match.locators.finalBanner).toBeVisible();
Act
  1. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. completing the adjacent match sends its loser to the losers bracket

    Open match adjacentMatchId → match
    match = await brackets.openMatch(adjacentMatchId);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. the losers match, now holding the dropped team, ended early as well

    Open match losersMatchId → match
    match = await brackets.openMatch(losersMatchId);
Tournament match page
Assert
  1. final banner is visible
    await expect(match.locators.finalBanner).toBeVisible();
Source
test("dropping team out ends ongoing match early and auto-forfeits losers bracket match", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DOUBLE_ELIMINATION,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));
	const matches = await factories.TournamentFactory.startBracket(
		tournament.id,
	);
	const ongoingMatchId = matches[0].id;
	const adjacentMatchId = matches[1].id;
	const losersMatchId = matches[3].id;

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	let match = await brackets.openMatch(ongoingMatchId);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
	await match.backToBracket();

	// the fourth team is the bravo side of the ongoing match
	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);
	await admin.dropOutTeam(3);

	// dropping out ended the ongoing match early
	await match.goto({ tournamentId: tournament.id, matchId: ongoingMatchId });
	await expect(match.locators.finalBanner).toBeVisible();
	await match.backToBracket();

	// completing the adjacent match sends its loser to the losers bracket
	match = await brackets.openMatch(adjacentMatchId);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });
	await match.backToBracket();

	// the losers match, now holding the dropped team, ended early as well
	match = await brackets.openMatch(losersMatchId);
	await expect(match.locators.finalBanner).toBeVisible();
})

Tournament bracket grouped elimination

plays double elimination groups into a redemption and a top cut

Renders/to/:id/to/:id/brackets/to/:id/brackets/finalize

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression DE_GROUPS_TO_REDEMPTION_AND_TOP_CUT → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: DE_GROUPS_TO_REDEMPTION_AND_TOP_CUT,
    });
  2. Create teams tournament.id, teamSeeds(16)
    await createTeams(factories, tournament.id, teamSeeds(16));
  3. the higher seed wins every match: Team 1 and Team 2 win their pools unbeaten

    Play out tournament tournament.id 0
    await factories.TournamentFactory.playOut(tournament.id, 0);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id, 0
    await brackets.goto(tournament.id, 0);
Assert
  1. group button “A” is visible
    await expect(brackets.groupButton("A")).toBeVisible();
  2. group button “B” is visible
    await expect(brackets.groupButton("B")).toBeVisible();
  3. round header “WB Finals” is visible
    await expect(brackets.roundHeader("WB Finals")).toBeVisible();
  4. round header “LB Finals” is not visible
    await isNotVisible(brackets.roundHeader("LB Finals"));
  5. round header “Grand Finals” is not visible
    await isNotVisible(brackets.roundHeader("Grand Finals"));
  6. exit label “Top Cut [W]” is visible
    await expect(brackets.exitLabel("Top Cut [W]")).toBeVisible();
  7. exit label “Redemption [L]” is visible
    await expect(brackets.exitLabel("Redemption [L]")).toBeVisible();
Act
  1. Open group “B”
    await brackets.openGroup("B");
Assert
  1. first bracket team name “Team 2” is visible
    await expect(brackets.bracketTeamName("Team 2").first()).toBeVisible();
  2. bracket team name “Team 1” is not visible
    await isNotVisible(brackets.bracketTeamName("Team 1"));
  3. a pool's winners final loser is linked to the redemption group it lands in, before and after it starts

    expectExitLinksToRedemptionGroups = async () => { for (const [pool, team, otherGroupTeam] of [ ["A", "Team 4", "T…
    const expectExitLinksToRedemptionGroups = async () => {
    	for (const [pool, team, otherGroupTeam] of [
    		["A", "Team 4", "Team 3"],
    		["B", "Team 3", "Team 4"],
    	]) {
    		await brackets.goto(tournament.id, 0);
    		await brackets.openGroup(pool);
    		await brackets.exitLabel("Redemption [L]").click();
    		await expect(page).toHaveURL(/idx=1.*group=\d+/);
    		await expect(brackets.bracketTeamName(team).first()).toBeVisible();
    		await isNotVisible(brackets.bracketTeamName(otherGroupTeam));
    	}
    };
  4. exit links to redemption groups
    await expectExitLinksToRedemptionGroups();
Arrange
  1. Team 3 (pool B) and Team 4 (pool A) win their redemption groups, having lost their pool's winners final

    Play out tournament tournament.id 1
    await factories.TournamentFactory.playOut(tournament.id, 1);
Assert
  1. exit links to redemption groups
    await expectExitLinksToRedemptionGroups();
Act
  1. Open the tournament brackets page tournament.id, 2
    await brackets.goto(tournament.id, 2);
Assert
  1. pool winners take the top seeds and play a redemption winner from the other pool, not a rematch

    first round team names 4 equals “Team 1”, “Team 3”, “Team 2”, “Team 4”
    expect(await brackets.firstRoundTeamNames(4)).toEqual([
    	"Team 1",
    	"Team 3",
    	"Team 2",
    	"Team 4",
    ]);
Arrange
  1. Play out tournament tournament.id 2
    await factories.TournamentFactory.playOut(tournament.id, 2);
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open finalize tournament dialog → finalizeDialog
    const finalizeDialog = await brackets.openFinalizeTournamentDialog();
Finalize tournament dialog
Act
  1. Confirm
    await finalizeDialog.confirm();
Assert
  1. finalize tournament button is not visible
    await isNotVisible(brackets.locators.finalizeTournamentButton);
Source
test("plays double elimination groups into a redemption and a top cut", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: DE_GROUPS_TO_REDEMPTION_AND_TOP_CUT,
	});
	await createTeams(factories, tournament.id, teamSeeds(16));
	// the higher seed wins every match: Team 1 and Team 2 win their pools unbeaten
	await factories.TournamentFactory.playOut(tournament.id, 0);

	await impersonate(page);
	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id, 0);

	await expect(brackets.groupButton("A")).toBeVisible();
	await expect(brackets.groupButton("B")).toBeVisible();
	await expect(brackets.roundHeader("WB Finals")).toBeVisible();
	await isNotVisible(brackets.roundHeader("LB Finals"));
	await isNotVisible(brackets.roundHeader("Grand Finals"));
	await expect(brackets.exitLabel("Top Cut [W]")).toBeVisible();
	await expect(brackets.exitLabel("Redemption [L]")).toBeVisible();

	await brackets.openGroup("B");
	await expect(brackets.bracketTeamName("Team 2").first()).toBeVisible();
	await isNotVisible(brackets.bracketTeamName("Team 1"));

	// a pool's winners final loser is linked to the redemption group it lands in, before and after it starts
	const expectExitLinksToRedemptionGroups = async () => {
		for (const [pool, team, otherGroupTeam] of [
			["A", "Team 4", "Team 3"],
			["B", "Team 3", "Team 4"],
		]) {
			await brackets.goto(tournament.id, 0);
			await brackets.openGroup(pool);
			await brackets.exitLabel("Redemption [L]").click();
			await expect(page).toHaveURL(/idx=1.*group=\d+/);
			await expect(brackets.bracketTeamName(team).first()).toBeVisible();
			await isNotVisible(brackets.bracketTeamName(otherGroupTeam));
		}
	};
	await expectExitLinksToRedemptionGroups();

	// Team 3 (pool B) and Team 4 (pool A) win their redemption groups, having lost their pool's winners final
	await factories.TournamentFactory.playOut(tournament.id, 1);

	await expectExitLinksToRedemptionGroups();

	await brackets.goto(tournament.id, 2);
	// pool winners take the top seeds and play a redemption winner from the other pool, not a rematch
	expect(await brackets.firstRoundTeamNames(4)).toEqual([
		"Team 1",
		"Team 3",
		"Team 2",
		"Team 4",
	]);

	await factories.TournamentFactory.playOut(tournament.id, 2);

	await brackets.goto(tournament.id);
	const finalizeDialog = await brackets.openFinalizeTournamentDialog();
	await finalizeDialog.confirm();
	await isNotVisible(brackets.locators.finalizeTournamentButton);
})

finalizes a tournament with co-winners, each of them getting the trophy

Renders/to/:id/to/:id/brackets/to/:id/brackets/finalize

Setup
Arrange
  1. Create trophy name “Co-winner Trophy” → trophy
    const trophy = await factories.TrophyFactory.create({
    	name: "Co-winner Trophy",
    });
  2. Create tournament author Sendou start times startedTournamentTimes() bracket progression SE_WITHOUT_FINALS trophy id trophy.id → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: SE_WITHOUT_FINALS,
    	trophyId: trophy.id,
    });
  3. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
  4. Play out tournament tournament.id 0
    await factories.TournamentFactory.playOut(tournament.id, 0);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
Assert
  1. round header “Finals” is not visible
    await isNotVisible(brackets.roundHeader("Finals"));
Act
  1. Open finalize tournament dialog → finalizeDialog
    const finalizeDialog = await brackets.openFinalizeTournamentDialog();
Finalize tournament dialog
Assert
  1. one trophy receiver picker per 1st place team, full rosters picked by default

    trophy team heading “Team 1” is visible
    await expect(finalizeDialog.trophyTeamHeading("Team 1")).toBeVisible();
  2. trophy team heading “Team 2” is visible
    await expect(finalizeDialog.trophyTeamHeading("Team 2")).toBeVisible();
Act
  1. Confirm
    await finalizeDialog.confirm();
Assert
  1. finalize tournament button is not visible
    await isNotVisible(brackets.locators.finalizeTournamentButton);
Source
test("finalizes a tournament with co-winners, each of them getting the trophy", async ({
	page,
	factories,
}) => {
	const trophy = await factories.TrophyFactory.create({
		name: "Co-winner Trophy",
	});
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: SE_WITHOUT_FINALS,
		trophyId: trophy.id,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));
	await factories.TournamentFactory.playOut(tournament.id, 0);

	await impersonate(page);
	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await isNotVisible(brackets.roundHeader("Finals"));

	const finalizeDialog = await brackets.openFinalizeTournamentDialog();
	// one trophy receiver picker per 1st place team, full rosters picked by default
	await expect(finalizeDialog.trophyTeamHeading("Team 1")).toBeVisible();
	await expect(finalizeDialog.trophyTeamHeading("Team 2")).toBeVisible();
	await finalizeDialog.confirm();
	await isNotVisible(brackets.locators.finalizeTournamentButton);
})

builds a grouped double elimination with rounds left unplayed

Renders/calendar/new/to/:id/to/:id/info

Setup
Arrange
  1. Create user null roles “TOURNAMENT_ORGANIZER” → organizer
    const organizer = await factories.UserFactory.create(null, {
    	roles: ["TOURNAMENT_ORGANIZER"],
    });
Oorganizer
Calendar new event page
Act
  1. Go to new tournament
    await newTournament.gotoNewTournament();
  2. Fill Name with “Grouped Tournament”
    await newTournament.form.fill("name", "Grouped Tournament");
  3. Set first date new Date(2027, 0, 15, 17, 0)
    await newTournament.setFirstDate(new Date(2027, 0, 15, 17, 0));
  4. Go to step “maps”
    await newTournament.goToStep("maps");
  5. Check items map picking style “TO”
    await newTournament.form.checkItems("mapPickingStyle", ["TO"]);
  6. Pick map pool stage “Scorch Gorge” mode “Splat Zones”, stage “Eeltail Alley” mode “Tower Control”
    await newTournament.pickMapPool([
    	{ stage: "Scorch Gorge", mode: "Splat Zones" },
    	{ stage: "Eeltail Alley", mode: "Tower Control" },
    ]);
  7. Select bracket 0
    await newTournament.selectBracket(0);
  8. Select “4” in group count select
    await newTournament.locators.groupCountSelect.selectOption("4");
  9. roundPlayed = (round: string) => newTournament.roundPlayedCheckbox(round)
    const roundPlayed = (round: string) =>
    	newTournament.roundPlayedCheckbox(round);
  10. a round can't be played without the rounds feeding teams to it

    Uncheck roundPlayed("LB_SEMIS")
    await roundPlayed("LB_SEMIS").uncheck();
Assert
  1. For each round of “LB_FINALS”, “GRAND_FINALS”, “BRACKET_RESET”
    for (const round of ["LB_FINALS", "GRAND_FINALS", "BRACKET_RESET"]) {
    	await expect(roundPlayed(round)).not.toBeChecked();
    }
    1. roundPlayed(round) is not checked
      await expect(roundPlayed(round)).not.toBeChecked();
  2. roundPlayed("WB_FINALS") is checked
    await expect(roundPlayed("WB_FINALS")).toBeChecked();
  3. round not played hint “Losers bracket semifinals” is visible
    await expect(
    	newTournament.roundNotPlayedHint("Losers bracket semifinals"),
    ).toBeVisible();
Act
  1. playing a round again plays what it depends on too

    Check roundPlayed("GRAND_FINALS")
    await roundPlayed("GRAND_FINALS").check();
Assert
  1. For each round of “LB_SEMIS”, “LB_FINALS”, “GRAND_FINALS”
    for (const round of ["LB_SEMIS", "LB_FINALS", "GRAND_FINALS"]) {
    	await expect(roundPlayed(round)).toBeChecked();
    }
    1. roundPlayed(round) is checked
      await expect(roundPlayed(round)).toBeChecked();
  2. roundPlayed("BRACKET_RESET") is not checked
    await expect(roundPlayed("BRACKET_RESET")).not.toBeChecked();
Act
  1. Save
    await newTournament.save();
Assert
  1. the page URL is /\/to\/\d+/
    await expect(page).toHaveURL(/\/to\/\d+/);
Source
test("builds a grouped double elimination with rounds left unplayed", async ({
	page,
	factories,
}) => {
	const organizer = await factories.UserFactory.create(null, {
		roles: ["TOURNAMENT_ORGANIZER"],
	});
	await impersonate(page, organizer.id);

	const newTournament = new CalendarNewEventPage(page);
	await newTournament.gotoNewTournament();

	await newTournament.form.fill("name", "Grouped Tournament");
	await newTournament.setFirstDate(new Date(2027, 0, 15, 17, 0));

	await newTournament.goToStep("maps");
	await newTournament.form.checkItems("mapPickingStyle", ["TO"]);
	await newTournament.pickMapPool([
		{ stage: "Scorch Gorge", mode: "Splat Zones" },
		{ stage: "Eeltail Alley", mode: "Tower Control" },
	]);

	await newTournament.selectBracket(0);
	await newTournament.locators.groupCountSelect.selectOption("4");

	const roundPlayed = (round: string) =>
		newTournament.roundPlayedCheckbox(round);

	// a round can't be played without the rounds feeding teams to it
	await roundPlayed("LB_SEMIS").uncheck();
	for (const round of ["LB_FINALS", "GRAND_FINALS", "BRACKET_RESET"]) {
		await expect(roundPlayed(round)).not.toBeChecked();
	}
	await expect(roundPlayed("WB_FINALS")).toBeChecked();
	await expect(
		newTournament.roundNotPlayedHint("Losers bracket semifinals"),
	).toBeVisible();

	// playing a round again plays what it depends on too
	await roundPlayed("GRAND_FINALS").check();
	for (const round of ["LB_SEMIS", "LB_FINALS", "GRAND_FINALS"]) {
		await expect(roundPlayed(round)).toBeChecked();
	}
	await expect(roundPlayed("BRACKET_RESET")).not.toBeChecked();

	await newTournament.save();
	await expect(page).toHaveURL(/\/to\/\d+/);
})

Tournament bracket multi stage

completes and finalizes a small tournament (RR->SE w/ underground bracket)

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/brackets/to/:id/brackets/finalize/to/:id/matches/:mid

Setup
Arrange
  1. Create 2 × badge → badges
    const badges = await factories.BadgeFactory.createMany(2);
  2. Create tournament author Sendou start times startedTournamentTimes() bracket progression RR_TO_SE_WITH_UNDERGROUND badges badges.map((badge) => badge.id) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: RR_TO_SE_WITH_UNDERGROUND,
    	badges: badges.map((badge) => badge.id),
    });
  3. groups of 3 and 4; the factory plays every match with the higher seed winning, dropping teams 5, 6 and 7 into the underground bracket's placements

    Create teams tournament.id, teamSeeds(7) → teams
    const teams = await createTeams(factories, tournament.id, teamSeeds(7));
  4. Play out tournament tournament.id
    await factories.TournamentFactory.playOut(tournament.id);
Tteams[4].owner
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Select bracket “Underground”
    await brackets.selectBracket("Underground");
  3. Check in bracket
    await brackets.checkInBracket();
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Check team in to bracket teams[6].id, “Underground bracket”
    await admin.checkTeamInToBracket(teams[6].id, "Underground bracket");
Arrange
  1. the checked in teams meet in the underground bracket; score reporting and starting brackets from the UI are covered by other tests

    Play out tournament tournament.id 2, 1
    await factories.TournamentFactory.playOut(tournament.id, [2, 1]);
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open finalize tournament dialog → finalizeDialog
    const finalizeDialog = await brackets.openFinalizeTournamentDialog();
Finalize tournament dialog
Act
  1. Assign badges later
    await finalizeDialog.assignBadgesLater();
  2. Confirm
    await finalizeDialog.confirm();
Tournament brackets page
Act
  1. after finalizing the tournament, the admin tab disappears so the reopen action is no longer reachable

    Open match 11 → match
    const match = await brackets.openMatch(11);
Assert
  1. admin tab is not visible
    await isNotVisible(match.locators.adminTab);
  2. reopen match button is not visible
    await isNotVisible(match.locators.reopenMatchButton);
Tournament match page
Act
  1. Back to bracket
    await match.backToBracket();
Source
test("completes and finalizes a small tournament (RR->SE w/ underground bracket)", async ({
	page,
	factories,
}) => {
	const badges = await factories.BadgeFactory.createMany(2);
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: RR_TO_SE_WITH_UNDERGROUND,
		badges: badges.map((badge) => badge.id),
	});
	// groups of 3 and 4; the factory plays every match with the higher seed
	// winning, dropping teams 5, 6 and 7 into the underground bracket's placements
	const teams = await createTeams(factories, tournament.id, teamSeeds(7));
	await factories.TournamentFactory.playOut(tournament.id);

	// captain of one of the underground bracket teams
	await impersonate(page, teams[4].ownerUserId);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	await brackets.selectBracket("Underground");
	await brackets.checkInBracket();

	await impersonate(page);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);
	await admin.checkTeamInToBracket(teams[6].id, "Underground bracket");

	// the checked in teams meet in the underground bracket; score reporting and
	// starting brackets from the UI are covered by other tests
	await factories.TournamentFactory.playOut(tournament.id, [2, 1]);

	await brackets.goto(tournament.id);
	const finalizeDialog = await brackets.openFinalizeTournamentDialog();
	await finalizeDialog.assignBadgesLater();
	await finalizeDialog.confirm();

	// after finalizing the tournament, the admin tab disappears so the
	// reopen action is no longer reachable
	const match = await brackets.openMatch(11);
	await isNotVisible(match.locators.adminTab);
	await isNotVisible(match.locators.reopenMatchButton);
	await match.backToBracket();
})

seeds the follow-up single elimination so group rivals rematch as late as possible

Renders/to/:id/to/:id/brackets

SSendou
Arrange
  1. 4 groups of 4, every team advancing -> 16 team single elimination

    Create tournament author Sendou start times startedTournamentTimes() bracket progression RR_TOP_4_TO_SE → topFourTournament
    const topFourTournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: RR_TOP_4_TO_SE,
    });
  2. Create teams topFourTournament.id, teamSeeds(16)
    await createTeams(factories, topFourTournament.id, teamSeeds(16));
  3. Play out tournament topFourTournament.id
    await factories.TournamentFactory.playOut(topFourTournament.id);
Tournament brackets page
Act
  1. Open the tournament brackets page topFourTournament.id
    await brackets.goto(topFourTournament.id);
  2. Select bracket “Groups stage”
    await brackets.selectBracket("Groups stage");
  3. Group standings team names 4 → topFourGroups
    const topFourGroups = await brackets.groupStandingsTeamNames(4);
  4. Select bracket “Final stage”
    await brackets.selectBracket("Final stage");
  5. First round team names 16 → topFourLineup
    const topFourLineup = await brackets.firstRoundTeamNames(16);
Assert
  1. topFourLineup groups spread across bracket topFourGroups
    expectGroupsSpreadAcrossBracket(topFourLineup, topFourGroups);
  2. topFourLineup seed order respected topFourGroups
    expectSeedOrderRespected(topFourLineup, topFourGroups);
Arrange
  1. 4 groups of 4, top 2 advancing -> 8 team single elimination

    Create tournament author Sendou start times startedTournamentTimes() bracket progression RR_TO_SE → topTwoTournament
    const topTwoTournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: RR_TO_SE,
    });
  2. Create teams topTwoTournament.id, teamSeeds(16)
    await createTeams(factories, topTwoTournament.id, teamSeeds(16));
  3. Play out tournament topTwoTournament.id
    await factories.TournamentFactory.playOut(topTwoTournament.id);
Act
  1. Open the tournament brackets page topTwoTournament.id
    await brackets.goto(topTwoTournament.id);
  2. Select bracket “Groups stage”
    await brackets.selectBracket("Groups stage");
  3. Group standings team names 4 → topTwoGroups
    const topTwoGroups = await brackets.groupStandingsTeamNames(4);
  4. Select bracket “Final stage”
    await brackets.selectBracket("Final stage");
  5. First round team names 8 → topTwoLineup
    const topTwoLineup = await brackets.firstRoundTeamNames(8);
Assert
  1. topTwoLineup groups spread across bracket topTwoGroups.map((group) => group.slice(0, 2))
    expectGroupsSpreadAcrossBracket(
    	topTwoLineup,
    	topTwoGroups.map((group) => group.slice(0, 2)),
    );
  2. topTwoLineup seed order respected topTwoGroups.map((group) => group.slice(0, 2))
    expectSeedOrderRespected(
    	topTwoLineup,
    	topTwoGroups.map((group) => group.slice(0, 2)),
    );
Source
test("seeds the follow-up single elimination so group rivals rematch as late as possible", async ({
	page,
	factories,
}) => {
	await impersonate(page);
	const brackets = new TournamentBracketsPage(page);

	// 4 groups of 4, every team advancing -> 16 team single elimination
	const topFourTournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: RR_TOP_4_TO_SE,
	});
	await createTeams(factories, topFourTournament.id, teamSeeds(16));
	await factories.TournamentFactory.playOut(topFourTournament.id);

	await brackets.goto(topFourTournament.id);
	await brackets.selectBracket("Groups stage");
	const topFourGroups = await brackets.groupStandingsTeamNames(4);

	await brackets.selectBracket("Final stage");
	const topFourLineup = await brackets.firstRoundTeamNames(16);

	expectGroupsSpreadAcrossBracket(topFourLineup, topFourGroups);
	expectSeedOrderRespected(topFourLineup, topFourGroups);

	// 4 groups of 4, top 2 advancing -> 8 team single elimination
	const topTwoTournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: RR_TO_SE,
	});
	await createTeams(factories, topTwoTournament.id, teamSeeds(16));
	await factories.TournamentFactory.playOut(topTwoTournament.id);

	await brackets.goto(topTwoTournament.id);
	await brackets.selectBracket("Groups stage");
	const topTwoGroups = await brackets.groupStandingsTeamNames(4);

	await brackets.selectBracket("Final stage");
	const topTwoLineup = await brackets.firstRoundTeamNames(8);

	expectGroupsSpreadAcrossBracket(
		topTwoLineup,
		topTwoGroups.map((group) => group.slice(0, 2)),
	);
	expectSeedOrderRespected(
		topTwoLineup,
		topTwoGroups.map((group) => group.slice(0, 2)),
	);
})

shows tournament results on user profile after finalized tournament

Renders/to/:id/to/:id/brackets/to/:id/results/to/:id/teams/:tid/u/:identifier/u/:identifier/u/:identifier/results/u/:identifier/seasons/u/:identifier/seasons

Setup
Arrange
  1. progression editing, score reporting and the finalize dialog are covered by other tests; only the result pages themselves are under test here

    Create ROSTER_SIZE * 2 × user → players
    const players = await factories.UserFactory.createMany(ROSTER_SIZE * 2);
  2. teamRosters = [ players.slice(0, ROSTER_SIZE), players.slice(ROSTER_SIZE), ].map((roster) =…
    const teamRosters = [
    	players.slice(0, ROSTER_SIZE),
    	players.slice(ROSTER_SIZE),
    ].map((roster) => roster.map((player) => player.id));
  3. Create played tournament name “Swim or Sink 101” author Sendou start times startedTournamentTimes() bracket progression RR_TO_SE teamRosters played out “all” → tournament
    const tournament = await factories.TournamentFactory.createPlayed(
    	{
    		name: "Swim or Sink 101",
    		authorId: ADMIN_ID,
    		startTimes: startedTournamentTimes(),
    		bracketProgression: RR_TO_SE,
    	},
    	{ teamRosters, playedOut: "all" },
    );
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open results → results
    const results = await brackets.nav.openResults();
  3. Expand team 0
    await results.expandTeam(0);
  4. Open member 0 → userPage
    const userPage = await results.openMember(0);
  5. Open seasons
    await userPage.openSeasons();
Assert
  1. userPage.locators.seasonsTournamentResult is visible
    await expect(userPage.locators.seasonsTournamentResult).toBeVisible();
Act
  1. Back to profile
    await userPage.backToProfile();
  2. Open results → userResults
    const userResults = await userPage.openResults();
Assert
  1. userResults.locators.tournamentNameCells.first() contains “Swim or Sink 101”
    await expect(
    	userResults.locators.tournamentNameCells.first(),
    ).toContainText("Swim or Sink 101");
Act
  1. Open mates 0
    await userResults.openMates(0);
Assert
  1. userResults.matesListItems(0) count is 3
    await expect(userResults.matesListItems(0)).toHaveCount(3);
Source
test("shows tournament results on user profile after finalized tournament", async ({
	page,
	factories,
}) => {
	// progression editing, score reporting and the finalize dialog are covered
	// by other tests; only the result pages themselves are under test here
	const players = await factories.UserFactory.createMany(ROSTER_SIZE * 2);
	const teamRosters = [
		players.slice(0, ROSTER_SIZE),
		players.slice(ROSTER_SIZE),
	].map((roster) => roster.map((player) => player.id));

	const tournament = await factories.TournamentFactory.createPlayed(
		{
			name: "Swim or Sink 101",
			authorId: ADMIN_ID,
			startTimes: startedTournamentTimes(),
			bracketProgression: RR_TO_SE,
		},
		{ teamRosters, playedOut: "all" },
	);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	const results = await brackets.nav.openResults();
	await results.expandTeam(0);
	const userPage = await results.openMember(0);

	await userPage.openSeasons();
	await expect(userPage.locators.seasonsTournamentResult).toBeVisible();

	await userPage.backToProfile();

	const userResults = await userPage.openResults();
	await expect(
		userResults.locators.tournamentNameCells.first(),
	).toContainText("Swim or Sink 101");

	await userResults.openMates(0);
	await expect(userResults.matesListItems(0)).toHaveCount(3);
})

changes SOS format and progresses with it & adds a member to another team

Renders/calendar/new/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/registration/:tid?/to/:id/brackets/to/:id/info/to/:id/matches/:mid/to/:id/teams

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression SOS_BRACKETS map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: SOS_BRACKETS,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. the to-be double registrant plays in Sendou's team; the admin add flow requires a friend code, which the admin user itself lacks

    Create user discord name “Duplicate Dave” → doubleRegistrant
    const doubleRegistrant = await factories.UserFactory.create({
    	discordName: "Duplicate Dave",
    });
  3. Create teams tournament.id, members Sendou, doubleRegistrant.id, ...teamSeeds(3) → teams
    const teams = await createTeams(factories, tournament.id, [
    	{ members: [ADMIN_ID, doubleRegistrant.id] },
    	...teamSeeds(3),
    ]);
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Edit event info → eventEdit
    const eventEdit = await admin.editEventInfo();
Calendar new event page
Act
  1. Delete last bracket
    await eventEdit.deleteLastBracket();
  2. Pick last placements 3, 4
    await eventEdit.pickLastPlacements([3, 4]);
  3. Save
    await eventEdit.save();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. every match of the group but one that Sendou's team is not part of

    For each matchId of 1, 2, 3, 4, 6
    for (const matchId of [1, 2, 3, 4, 6]) {
    	const match = await brackets.openMatch(matchId);
    	await match.openTab("action");
    	await match.reportResult({ mapsToReport: 2 });
    	await match.backToBracket();
    }
    1. Open match matchId → match
      const match = await brackets.openMatch(matchId);
    2. Open tab “action”
      await match.openTab("action");
    3. Report result maps to report 2
      await match.reportResult({ mapsToReport: 2 });
    4. Back to bracket
      await match.backToBracket();
Top right buttons
Assert
  1. Sendou's team has finished its group's matches, the header says so while the last one plays out

    global status “Waiting for match” is visible
    await expect(
    	new TopRightButtons(page).globalStatus("Waiting for match"),
    ).toBeVisible();
Tournament brackets page
Act
  1. Open match 5 → lastGroupsMatch
    const lastGroupsMatch = await brackets.openMatch(5);
Tournament match page
Act
  1. Open tab “action”
    await lastGroupsMatch.openTab("action");
  2. Report result maps to report 2
    await lastGroupsMatch.reportResult({ mapsToReport: 2 });
  3. Back to bracket
    await lastGroupsMatch.backToBracket();
Tournament brackets page
Act
  1. Select bracket “Hammerhead”
    await brackets.selectBracket("Hammerhead");
Assert
  1. brackets viewer is not visible
    await isNotVisible(brackets.locators.bracketsViewer);
Act
  1. Select bracket “Mako”
    await brackets.selectBracket("Mako");
Assert
  1. brackets viewer is visible
    await expect(brackets.locators.bracketsViewer).toBeVisible();
Act
  1. Finalize
    await brackets.finalize();
  2. Open match 7 → makoMatch
    const makoMatch = await brackets.openMatch(7);
Tournament match page
Assert
  1. back to bracket button is visible
    await expect(makoMatch.locators.backToBracketButton).toBeVisible();
Tournament admin registration page
Act
  1. Go to edit tournament.id, teams[2].id
    await registration.gotoEdit(tournament.id, teams[2].id);
  2. Add member “Duplicate Dave”
    await registration.addMember("Duplicate Dave");
  3. Save
    await registration.save();
Tournament teams page
Act
  1. Open the tournament teams page tournament.id
    await teamsPage.goto(tournament.id);
Assert
  1. member named “Duplicate Dave” count is 2
    await expect(teamsPage.memberNamed("Duplicate Dave")).toHaveCount(2);
Source
test("changes SOS format and progresses with it & adds a member to another team", async ({
	page,
	factories,
}) => {
	test.slow();
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: SOS_BRACKETS,
		mapPoolMaps: TO_MAP_POOL,
	});
	// the to-be double registrant plays in Sendou's team; the admin add
	// flow requires a friend code, which the admin user itself lacks
	const doubleRegistrant = await factories.UserFactory.create({
		discordName: "Duplicate Dave",
	});
	const teams = await createTeams(factories, tournament.id, [
		{ members: [ADMIN_ID, doubleRegistrant.id] },
		...teamSeeds(3),
	]);

	await impersonate(page);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);

	const eventEdit = await admin.editEventInfo();
	await eventEdit.deleteLastBracket();
	await eventEdit.pickLastPlacements([3, 4]);
	await eventEdit.save();

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	// every match of the group but one that Sendou's team is not part of
	for (const matchId of [1, 2, 3, 4, 6]) {
		const match = await brackets.openMatch(matchId);
		await match.openTab("action");
		await match.reportResult({ mapsToReport: 2 });
		await match.backToBracket();
	}

	// Sendou's team has finished its group's matches, the header says so while the last one plays out
	await expect(
		new TopRightButtons(page).globalStatus("Waiting for match"),
	).toBeVisible();

	const lastGroupsMatch = await brackets.openMatch(5);
	await lastGroupsMatch.openTab("action");
	await lastGroupsMatch.reportResult({ mapsToReport: 2 });
	await lastGroupsMatch.backToBracket();

	await brackets.selectBracket("Hammerhead");
	await isNotVisible(brackets.locators.bracketsViewer);

	await brackets.selectBracket("Mako");
	await expect(brackets.locators.bracketsViewer).toBeVisible();

	await brackets.finalize();

	const makoMatch = await brackets.openMatch(7);
	await expect(makoMatch.locators.backToBracketButton).toBeVisible();

	// add a player of the first team also to the third team (a team in the Mako bracket)
	const registration = new TournamentAdminRegistrationPage(page);
	await registration.gotoEdit(tournament.id, teams[2].id);
	await registration.addMember("Duplicate Dave");
	await registration.save();

	const teamsPage = new TournamentTeamsPage(page);
	await teamsPage.goto(tournament.id);

	await expect(teamsPage.memberNamed("Duplicate Dave")).toHaveCount(2);
})

conducts a tournament with many starting brackets

Renders/calendar/new/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/seeds/to/:id/brackets/to/:id/info

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression SOS_BRACKETS map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: SOS_BRACKETS,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(16)
    await createTeams(factories, tournament.id, teamSeeds(16));
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Edit event info → eventEdit
    const eventEdit = await admin.editEventInfo();
Calendar new event page
Act
  1. Delete last bracket
    await eventEdit.deleteLastBracket();
  2. Make all brackets starting brackets
    await eventEdit.makeAllBracketsStartingBrackets();
  3. Set bracket format 0, “Single elimination”
    await eventEdit.setBracketFormat(0, "Single elimination");
  4. Set bracket format 1, “Single elimination”
    await eventEdit.setBracketFormat(1, "Single elimination");
  5. Set bracket format 2, “Swiss”
    await eventEdit.setBracketFormat(2, "Swiss");
  6. Set bracket format 3, “Swiss”
    await eventEdit.setBracketFormat(3, "Swiss");
  7. Save
    await eventEdit.save();
Tournament seeds page
Act
  1. Open the tournament seeds page tournament.id
    await seeds.goto(tournament.id);
  2. Open starting brackets dialog
    await seeds.openStartingBracketsDialog();
  3. Repeat for (let i = 0; i < 16; i++)
    for (let i = 0; i < 16; i++) {
    	let bracketName: string;
    	if (i < 4) {
    		bracketName = "Groups stage";
    	} else if (i < 8) {
    		bracketName = "Great White";
    	} else if (i < 12) {
    		bracketName = "Hammerhead";
    	} else {
    		bracketName = "Mako";
    	}
    
    	await seeds.setStartingBracket(i, bracketName);
    }
    1. let bracketName: string;
      let bracketName: string;
    2. If i < 4
      if (i < 4) {
      	bracketName = "Groups stage";
      } else if (i < 8) {
      	bracketName = "Great White";
      } else if (i < 12) {
      	bracketName = "Hammerhead";
      } else {
      	bracketName = "Mako";
      }
      1. bracketName = "Groups stage"
        bracketName = "Groups stage";
    3. Set starting bracket i, bracketName
      await seeds.setStartingBracket(i, bracketName);
  4. Save starting brackets
    await seeds.saveStartingBrackets();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. For each bracketName of “Groups stage”, “Great White”, “Hammerhead”, “Mako”
    for (const bracketName of [
    	"Groups stage",
    	"Great White",
    	"Hammerhead",
    	"Mako",
    ]) {
    	await brackets.selectBracket(bracketName);
    	await brackets.finalize();
    }
    1. Select bracket bracketName
      await brackets.selectBracket(bracketName);
    2. Finalize
      await brackets.finalize();
Assert
  1. match 11 is visible
    await expect(brackets.match(11)).toBeVisible();
Source
test("conducts a tournament with many starting brackets", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: SOS_BRACKETS,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(16));

	await impersonate(page);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);

	const eventEdit = await admin.editEventInfo();
	await eventEdit.deleteLastBracket();
	await eventEdit.makeAllBracketsStartingBrackets();

	await eventEdit.setBracketFormat(0, "Single elimination");
	await eventEdit.setBracketFormat(1, "Single elimination");
	await eventEdit.setBracketFormat(2, "Swiss");
	await eventEdit.setBracketFormat(3, "Swiss");

	await eventEdit.save();

	const seeds = new TournamentSeedsPage(page);
	await seeds.goto(tournament.id);
	await seeds.openStartingBracketsDialog();

	for (let i = 0; i < 16; i++) {
		let bracketName: string;
		if (i < 4) {
			bracketName = "Groups stage";
		} else if (i < 8) {
			bracketName = "Great White";
		} else if (i < 12) {
			bracketName = "Hammerhead";
		} else {
			bracketName = "Mako";
		}

		await seeds.setStartingBracket(i, bracketName);
	}

	await seeds.saveStartingBrackets();

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	for (const bracketName of [
		"Groups stage",
		"Great White",
		"Hammerhead",
		"Mako",
	]) {
		await brackets.selectBracket(bracketName);
		await brackets.finalize();
	}

	await expect(brackets.match(11)).toBeVisible();
})

offers check out until the team's starting bracket starts and drop out after

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/brackets

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression TWO_STARTING_BRACKETS map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: TWO_STARTING_BRACKETS,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, [0, 0, 0, 0, 1, 1, 1, 1].map((startingBracketIdx) => ({ startingBracketIdx, })) → teams
    const teams = await createTeams(
    	factories,
    	tournament.id,
    	[0, 0, 0, 0, 1, 1, 1, 1].map((startingBracketIdx) => ({
    		startingBracketIdx,
    	})),
    );
  3. Start bracket tournament tournament.id bracket idx 0
    await factories.TournamentFactory.startBracket(tournament.id, {
    	bracketIdx: 0,
    });
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id, 1
    await brackets.goto(tournament.id, 1);
Assert
  1. bracket team name “Team 5” is visible
    await expect(brackets.bracketTeamName("Team 5")).toBeVisible();
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Open team actions teams[0].id
    await admin.openTeamActions(teams[0].id);
Assert
  1. team actions menu item “Drop out” is visible
    await expect(admin.teamActionsMenuItem("Drop out")).toBeVisible();
  2. team actions menu item /^Check out/ count is 0
    await expect(admin.teamActionsMenuItem(/^Check out/)).toHaveCount(0);
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Open team actions teams[4].id
    await admin.openTeamActions(teams[4].id);
Assert
  1. team actions menu item /^Check out/ is visible
    await expect(admin.teamActionsMenuItem(/^Check out/)).toBeVisible();
  2. team actions menu item “Drop out” count is 0
    await expect(admin.teamActionsMenuItem("Drop out")).toHaveCount(0);
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Check started team out teams[4].id
    await admin.checkStartedTeamOut(teams[4].id);
Assert
  1. team row teams[4].id count is 0
    await expect(admin.teamRow(teams[4].id)).toHaveCount(0);
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id, 1
    await brackets.goto(tournament.id, 1);
Assert
  1. bracket team name “Team 6” is visible
    await expect(brackets.bracketTeamName("Team 6")).toBeVisible();
  2. bracket team name “Team 5” count is 0
    await expect(brackets.bracketTeamName("Team 5")).toHaveCount(0);
Source
test("offers check out until the team's starting bracket starts and drop out after", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: TWO_STARTING_BRACKETS,
		mapPoolMaps: TO_MAP_POOL,
	});
	const teams = await createTeams(
		factories,
		tournament.id,
		[0, 0, 0, 0, 1, 1, 1, 1].map((startingBracketIdx) => ({
			startingBracketIdx,
		})),
	);
	await factories.TournamentFactory.startBracket(tournament.id, {
		bracketIdx: 0,
	});

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id, 1);
	await expect(brackets.bracketTeamName("Team 5")).toBeVisible();

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);
	await admin.openTeamActions(teams[0].id);
	await expect(admin.teamActionsMenuItem("Drop out")).toBeVisible();
	await expect(admin.teamActionsMenuItem(/^Check out/)).toHaveCount(0);

	await admin.goto(tournament.id);
	await admin.openTeamActions(teams[4].id);
	await expect(admin.teamActionsMenuItem(/^Check out/)).toBeVisible();
	await expect(admin.teamActionsMenuItem("Drop out")).toHaveCount(0);

	await admin.goto(tournament.id);
	await admin.checkStartedTeamOut(teams[4].id);

	await expect(admin.teamRow(teams[4].id)).toHaveCount(0);

	await brackets.goto(tournament.id, 1);
	await expect(brackets.bracketTeamName("Team 6")).toBeVisible();
	await expect(brackets.bracketTeamName("Team 5")).toHaveCount(0);
})

starts a starting bracket with the maps prepared for another starting bracket

Renders/to/:id/to/:id/brackets

Setup
Arrange
  1. [division1, division2] = TWO_STARTING_BRACKETS
    const [division1, division2] = TWO_STARTING_BRACKETS;
  2. Create tournament author Sendou start times startedTournamentTimes() bracket progression division1, ...division2 start time dateToDatabaseTimestamp(addHours(new Date(), 1)) map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	// maps can only be prepared for a bracket that can't be started yet
    	bracketProgression: [
    		division1,
    		{
    			...division2,
    			startTime: dateToDatabaseTimestamp(addHours(new Date(), 1)),
    		},
    	],
    	mapPoolMaps: TO_MAP_POOL,
    });
  3. Create teams tournament.id, [0, 0, 0, 0, 1, 1, 1, 1].map((startingBracketIdx) => ({ startingBracketIdx, }))
    await createTeams(
    	factories,
    	tournament.id,
    	[0, 0, 0, 0, 1, 1, 1, 1].map((startingBracketIdx) => ({
    		startingBracketIdx,
    	})),
    );
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id, 1
    await brackets.goto(tournament.id, 1);
  2. Open prepare maps dialog → prepareDialog
    const prepareDialog = await brackets.openPrepareMapsDialog();
Bracket map list dialog
Act
  1. Confirm
    await prepareDialog.confirm();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id, 1
    await brackets.goto(tournament.id, 1);
Assert
  1. prepared maps check icon is visible
    await expect(brackets.locators.preparedMapsCheckIcon).toBeVisible();
Act
  1. Open the tournament brackets page tournament.id, 0
    await brackets.goto(tournament.id, 0);
  2. Open finalize dialog → startDialog
    const startDialog = await brackets.openFinalizeDialog();
Bracket map list dialog
Assert
  1. prepared by text is visible
    await expect(startDialog.locators.preparedByText).toBeVisible();
Act
  1. Confirm
    await startDialog.confirm();
Tournament brackets page
Assert
  1. first matches is visible
    await expect(brackets.locators.matches.first()).toBeVisible();
Source
test("starts a starting bracket with the maps prepared for another starting bracket", async ({
	page,
	factories,
}) => {
	const [division1, division2] = TWO_STARTING_BRACKETS;
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		// maps can only be prepared for a bracket that can't be started yet
		bracketProgression: [
			division1,
			{
				...division2,
				startTime: dateToDatabaseTimestamp(addHours(new Date(), 1)),
			},
		],
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(
		factories,
		tournament.id,
		[0, 0, 0, 0, 1, 1, 1, 1].map((startingBracketIdx) => ({
			startingBracketIdx,
		})),
	);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id, 1);

	const prepareDialog = await brackets.openPrepareMapsDialog();
	await prepareDialog.confirm();

	await brackets.goto(tournament.id, 1);
	await expect(brackets.locators.preparedMapsCheckIcon).toBeVisible();

	await brackets.goto(tournament.id, 0);

	const startDialog = await brackets.openFinalizeDialog();
	await expect(startDialog.locators.preparedByText).toBeVisible();
	await startDialog.confirm();

	await expect(brackets.locators.matches.first()).toBeVisible();
})

plays out a redemption bracket set up in the tournament creation form

Renders/calendar/new/to/:id/to/:id/brackets/to/:id/info/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create user null roles “TOURNAMENT_ORGANIZER” → organizer
    const organizer = await factories.UserFactory.create(null, {
    	roles: ["TOURNAMENT_ORGANIZER"],
    });
Oorganizer
Calendar new event page
Act
  1. Go to new tournament
    await newTournament.gotoNewTournament();
  2. Fill Name with “Redemption Arc”
    await newTournament.form.fill("name", "Redemption Arc");
  3. start time in the past so the brackets can be started right away

    Set first date subMinutes(new Date(), 30)
    await newTournament.setFirstDate(subMinutes(new Date(), 30));
  4. Go to step “maps”
    await newTournament.goToStep("maps");
  5. Check items map picking style “TO”
    await newTournament.form.checkItems("mapPickingStyle", ["TO"]);
  6. Paste map pool MapPool.SZ
    await newTournament.pasteMapPool(MapPool.SZ);
  7. groups of 4: top 2 advance to the finals directly, 3rd placers get another shot at the last finals spot through the redemption bracket

    Rename bracket 0, “Groups”
    await newTournament.renameBracket(0, "Groups");
  8. Set bracket format 0, “Round robin”
    await newTournament.setBracketFormat(0, "Round robin");
  9. Add follow up bracket name “Redemption” format “Single elimination” placements 3
    await newTournament.addFollowUpBracket({
    	name: "Redemption",
    	format: "Single elimination",
    	placements: [3],
    });
  10. Add follow up bracket name “Finals” format “Single elimination” placements 1, 2
    await newTournament.addFollowUpBracket({
    	name: "Finals",
    	format: "Single elimination",
    	placements: [1, 2],
    });
  11. Connect 1, 2, 1
    await newTournament.connect(1, 2, [1]);
  12. Save
    await newTournament.save();
Assert
  1. the page URL is /\/to\/\d+/
    await expect(page).toHaveURL(/\/to\/\d+/);
Arrange
  1. tournamentId = Number(page.url().match(/\/to\/(\d+)/)![1])
    const tournamentId = Number(page.url().match(/\/to\/(\d+)/)![1]);
  2. Create teams tournamentId, teamSeeds(8)
    await createTeams(factories, tournamentId, teamSeeds(8));
  3. Play out tournament tournamentId 0
    await factories.TournamentFactory.playOut(tournamentId, 0);
Tournament brackets page
Act
  1. Open the tournament brackets page tournamentId
    await brackets.goto(tournamentId);
  2. Select bracket “Groups”
    await brackets.selectBracket("Groups");
  3. Group standings team names 2 → groups
    const groups = await brackets.groupStandingsTeamNames(2);
  4. redemptionTeamNames = groups.map((group) => group[2])
    const redemptionTeamNames = groups.map((group) => group[2]);
  5. the finals can not be started before the redemption bracket has been played out

    Select bracket “Finals”
    await brackets.selectBracket("Finals");
Assert
  1. teams pending from sources text is visible
    await expect(brackets.locators.teamsPendingFromSourcesText).toBeVisible();
  2. finalize bracket button is not visible
    await isNotVisible(brackets.locators.finalizeBracketButton);
Act
  1. Select bracket “Redemption”
    await brackets.selectBracket("Redemption");
  2. Finalize
    await brackets.finalize();
  3. redemptionMatchId = Number( await brackets.locators.matches.first().getAttribute("data-match-id"), )
    const redemptionMatchId = Number(
    	await brackets.locators.matches.first().getAttribute("data-match-id"),
    );
  4. Open match redemptionMatchId → redemptionMatch
    const redemptionMatch = await brackets.openMatch(redemptionMatchId);
Tournament match page
Act
  1. Open tab “action”
    await redemptionMatch.openTab("action");
  2. Report result for team team name redemptionTeamNames[0] maps to report 3
    await redemptionMatch.reportResultForTeam({
    	teamName: redemptionTeamNames[0],
    	mapsToReport: 3,
    });
  3. Back to bracket
    await redemptionMatch.backToBracket();
Tournament brackets page
Act
  1. Select bracket “Finals”
    await brackets.selectBracket("Finals");
Assert
  1. teams pending from sources text is not visible
    await isNotVisible(brackets.locators.teamsPendingFromSourcesText);
Act
  1. Finalize
    await brackets.finalize();
Assert
  1. the redemption bracket's winner took the last spot in the finals

    first brackets viewer › text redemptionTeamNames[0] is visible
    await expect(
    	brackets.locators.bracketsViewer
    		.getByText(redemptionTeamNames[0])
    		.first(),
    ).toBeVisible();
Source
test("plays out a redemption bracket set up in the tournament creation form", async ({
	page,
	factories,
}) => {
	test.slow();
	const organizer = await factories.UserFactory.create(null, {
		roles: ["TOURNAMENT_ORGANIZER"],
	});

	await impersonate(page, organizer.id);

	const newTournament = new CalendarNewEventPage(page);
	await newTournament.gotoNewTournament();

	await newTournament.form.fill("name", "Redemption Arc");
	// start time in the past so the brackets can be started right away
	await newTournament.setFirstDate(subMinutes(new Date(), 30));

	await newTournament.goToStep("maps");
	await newTournament.form.checkItems("mapPickingStyle", ["TO"]);
	await newTournament.pasteMapPool(MapPool.SZ);

	// groups of 4: top 2 advance to the finals directly, 3rd placers get
	// another shot at the last finals spot through the redemption bracket
	await newTournament.renameBracket(0, "Groups");
	await newTournament.setBracketFormat(0, "Round robin");
	await newTournament.addFollowUpBracket({
		name: "Redemption",
		format: "Single elimination",
		placements: [3],
	});
	await newTournament.addFollowUpBracket({
		name: "Finals",
		format: "Single elimination",
		placements: [1, 2],
	});
	await newTournament.connect(1, 2, [1]);

	await newTournament.save();

	await expect(page).toHaveURL(/\/to\/\d+/);
	const tournamentId = Number(page.url().match(/\/to\/(\d+)/)![1]);

	await createTeams(factories, tournamentId, teamSeeds(8));
	await factories.TournamentFactory.playOut(tournamentId, 0);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournamentId);

	await brackets.selectBracket("Groups");
	const groups = await brackets.groupStandingsTeamNames(2);
	const redemptionTeamNames = groups.map((group) => group[2]);

	// the finals can not be started before the redemption bracket has been played out
	await brackets.selectBracket("Finals");
	await expect(brackets.locators.teamsPendingFromSourcesText).toBeVisible();
	await isNotVisible(brackets.locators.finalizeBracketButton);

	await brackets.selectBracket("Redemption");
	await brackets.finalize();

	const redemptionMatchId = Number(
		await brackets.locators.matches.first().getAttribute("data-match-id"),
	);
	const redemptionMatch = await brackets.openMatch(redemptionMatchId);
	await redemptionMatch.openTab("action");
	await redemptionMatch.reportResultForTeam({
		teamName: redemptionTeamNames[0],
		mapsToReport: 3,
	});
	await redemptionMatch.backToBracket();

	await brackets.selectBracket("Finals");
	await isNotVisible(brackets.locators.teamsPendingFromSourcesText);
	await brackets.finalize();

	// the redemption bracket's winner took the last spot in the finals
	await expect(
		brackets.locators.bracketsViewer
			.getByText(redemptionTeamNames[0])
			.first(),
	).toBeVisible();
})

prepares maps (including third place match linking)

Renders/to/:id/to/:id/brackets

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression SOS_BRACKETS map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: SOS_BRACKETS,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Select bracket “Great White”
    await brackets.selectBracket("Great White");
  3. Open prepare maps dialog → prepareDialog
    const prepareDialog = await brackets.openPrepareMapsDialog();
Bracket map list dialog
Act
  1. Set expected teams 8
    await prepareDialog.setExpectedTeams(8);
  2. Confirm
    await prepareDialog.confirm();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Select bracket “Great White”
    await brackets.selectBracket("Great White");
Assert
  1. prepared maps check icon is visible
    await expect(brackets.locators.preparedMapsCheckIcon).toBeVisible();
Act
  1. we did not prepare maps for group stage

    Select bracket “Groups stage”
    await brackets.selectBracket("Groups stage");
Assert
  1. prepared maps check icon is not visible
    await isNotVisible(brackets.locators.preparedMapsCheckIcon);
Act
  1. reuses the maps prepared for Great White

    Select bracket “Hammerhead”
    await brackets.selectBracket("Hammerhead");
Assert
  1. prepared maps check icon is visible
    await expect(brackets.locators.preparedMapsCheckIcon).toBeVisible();
Act
  1. Select bracket “Great White”
    await brackets.selectBracket("Great White");
  2. Open prepare maps dialog → unlinkDialog
    const unlinkDialog = await brackets.openPrepareMapsDialog();
Bracket map list dialog
Act
  1. Unlink finals third place match
    await unlinkDialog.unlinkFinalsThirdPlaceMatch();
  2. Increase map count “last”
    await unlinkDialog.increaseMapCount("last");
  3. Confirm
    await unlinkDialog.confirm();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Select bracket “Great White”
    await brackets.selectBracket("Great White");
  3. Open prepare maps dialog → relinkDialog
    const relinkDialog = await brackets.openPrepareMapsDialog();
Bracket map list dialog
Assert
  1. linking is offered again since the finals and third place match maps now differ

    link finals button is visible
    await expect(relinkDialog.locators.linkFinalsButton).toBeVisible();
Source
test("prepares maps (including third place match linking)", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: SOS_BRACKETS,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	await brackets.selectBracket("Great White");

	const prepareDialog = await brackets.openPrepareMapsDialog();
	await prepareDialog.setExpectedTeams(8);
	await prepareDialog.confirm();

	await brackets.goto(tournament.id);
	await brackets.selectBracket("Great White");

	await expect(brackets.locators.preparedMapsCheckIcon).toBeVisible();

	// we did not prepare maps for group stage
	await brackets.selectBracket("Groups stage");

	await isNotVisible(brackets.locators.preparedMapsCheckIcon);

	// reuses the maps prepared for Great White
	await brackets.selectBracket("Hammerhead");

	await expect(brackets.locators.preparedMapsCheckIcon).toBeVisible();

	await brackets.selectBracket("Great White");

	const unlinkDialog = await brackets.openPrepareMapsDialog();
	await unlinkDialog.unlinkFinalsThirdPlaceMatch();
	await unlinkDialog.increaseMapCount("last");
	await unlinkDialog.confirm();

	await brackets.goto(tournament.id);
	await brackets.selectBracket("Great White");

	const relinkDialog = await brackets.openPrepareMapsDialog();

	// linking is offered again since the finals and third place match maps now differ
	await expect(relinkDialog.locators.linkFinalsButton).toBeVisible();
})

Tournament bracket pick/ban

ban/pick ${pickBan}

Sendouteam one captainteam two captaintournament-bracket-pick-ban.spec.ts:27

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(4) → teams
    const teams = await createTeams(factories, tournament.id, teamSeeds(4));
  3. match 2 of the group has the third team as alpha and the second as bravo

    matchId = 2
    const matchId = 2;
  4. teamOneCaptainId = teams[2].ownerUserId
    const teamOneCaptainId = teams[2].ownerUserId;
  5. teamTwoCaptainId = teams[1].ownerUserId
    const teamTwoCaptainId = teams[1].ownerUserId;
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open finalize dialog → mapListDialog
    const mapListDialog = await brackets.openFinalizeDialog();
Bracket map list dialog
Act
  1. Toggle pick ban
    await mapListDialog.togglePickBan();
  2. Set pick ban pickBan
    await mapListDialog.setPickBan(pickBan);
  3. Confirm
    await mapListDialog.confirm();
  4. If !isCounterpick
    if (!isCounterpick) {
    	for (const captainId of [teamTwoCaptainId, teamOneCaptainId]) {
    		await impersonate(page, captainId);
    		await match.goto({ tournamentId: tournament.id, matchId });
    		await match.openTab("action");
    
    		await match.pickBan();
    	}
    
    	// once both teams banned the ban prompt is gone and the actual map
    	// banner takes over.
    	await expect(match.locators.stageBanner).toBeVisible();
    }
    1. For each captainId of teamTwoCaptainId, teamOneCaptainId
      for (const captainId of [teamTwoCaptainId, teamOneCaptainId]) {
      	await impersonate(page, captainId);
      	await match.goto({ tournamentId: tournament.id, matchId });
      	await match.openTab("action");
      
      	await match.pickBan();
      }
      1. await impersonate(page, captainId);
      2. Open the tournament match page tournament id tournament.id matchId
        await match.goto({ tournamentId: tournament.id, matchId });
      3. Open tab “action”
        await match.openTab("action");
      4. Pick ban
        await match.pickBan();
    2. once both teams banned the ban prompt is gone and the actual map banner takes over.

      stage banner is visible
      await expect(match.locators.stageBanner).toBeVisible();
TOteam one captain
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “action”
    await match.openTab("action");
  3. Report result maps to report 1 winner 2 set ends false
    await match.reportResult({ mapsToReport: 1, winner: 2, setEnds: false });
  4. If isCounterpick
    if (isCounterpick) {
    	await match.pickBan();
    }
    1. Pick ban
      await match.pickBan();
TTteam two captain
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “action”
    await match.openTab("action");
  3. Report result maps to report 1 winner 1 set ends false
    await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
  4. If isCounterpick
    if (isCounterpick) {
    	// team two won the first map, so its own counterpick after losing the
    	// second one is the one the mode repeat rule bites on
    	await expect(match.locators.pickBanModeGroups).toHaveCount(
    		modeGroupsAfterWinning,
    	);
    	await match.pickBan();
    
    	await match.undoLastReport();
    	await expect(match.locators.selectWinnerText).toBeVisible();
    	await match.reportResult({
    		mapsToReport: 1,
    		winner: 1,
    		setEnds: false,
    	});
    	await expect(match.locators.counterpickText).toBeVisible();
    	await match.pickBan("last");
    	await expect(match.locators.selectWinnerText).toBeVisible();
    	await expect(match.score([1, 1])).toBeVisible();
    }
    1. team two won the first map, so its own counterpick after losing the second one is the one the mode repeat rule bites on

      pick ban mode groups count is modeGroupsAfterWinning
      await expect(match.locators.pickBanModeGroups).toHaveCount(
      	modeGroupsAfterWinning,
      );
    2. Pick ban
      await match.pickBan();
    3. Undo last report
      await match.undoLastReport();
    4. select winner text is visible
      await expect(match.locators.selectWinnerText).toBeVisible();
    5. Report result maps to report 1 winner 1 set ends false
      await match.reportResult({
      	mapsToReport: 1,
      	winner: 1,
      	setEnds: false,
      });
    6. counterpick text is visible
      await expect(match.locators.counterpickText).toBeVisible();
    7. Pick ban “last”
      await match.pickBan("last");
    8. select winner text is visible
      await expect(match.locators.selectWinnerText).toBeVisible();
    9. score 1, 1 is visible
      await expect(match.score([1, 1])).toBeVisible();
Source
test(`ban/pick ${pickBan}`, async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
		mapPoolMaps: TO_MAP_POOL,
	});
	const teams = await createTeams(factories, tournament.id, teamSeeds(4));
	// match 2 of the group has the third team as alpha and the second as bravo
	const matchId = 2;
	const teamOneCaptainId = teams[2].ownerUserId;
	const teamTwoCaptainId = teams[1].ownerUserId;

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	const mapListDialog = await brackets.openFinalizeDialog();
	await mapListDialog.togglePickBan();
	await mapListDialog.setPickBan(pickBan);
	await mapListDialog.confirm();

	const match = new TournamentMatchPage(page);

	if (!isCounterpick) {
		for (const captainId of [teamTwoCaptainId, teamOneCaptainId]) {
			await impersonate(page, captainId);
			await match.goto({ tournamentId: tournament.id, matchId });
			await match.openTab("action");

			await match.pickBan();
		}

		// once both teams banned the ban prompt is gone and the actual map
		// banner takes over.
		await expect(match.locators.stageBanner).toBeVisible();
	}

	await impersonate(page, teamOneCaptainId);
	await match.goto({ tournamentId: tournament.id, matchId });

	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, winner: 2, setEnds: false });

	if (isCounterpick) {
		await match.pickBan();
	}

	await impersonate(page, teamTwoCaptainId);
	await match.goto({ tournamentId: tournament.id, matchId });

	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });

	if (isCounterpick) {
		// team two won the first map, so its own counterpick after losing the
		// second one is the one the mode repeat rule bites on
		await expect(match.locators.pickBanModeGroups).toHaveCount(
			modeGroupsAfterWinning,
		);
		await match.pickBan();

		await match.undoLastReport();
		await expect(match.locators.selectWinnerText).toBeVisible();
		await match.reportResult({
			mapsToReport: 1,
			winner: 1,
			setEnds: false,
		});
		await expect(match.locators.counterpickText).toBeVisible();
		await match.pickBan("last");
		await expect(match.locators.selectWinnerText).toBeVisible();
		await expect(match.score([1, 1])).toBeVisible();
	}
})

ban/pick CUSTOM flow

Sendouhigher seed captainlower seed captaintournament-bracket-pick-ban.spec.ts:104

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(4) → teams
    const teams = await createTeams(factories, tournament.id, teamSeeds(4));
  3. match 2 of the group has the third team (lower seed) as alpha and the second team (higher seed) as bravo

    matchId = 2
    const matchId = 2;
  4. higherSeedCaptainId = teams[1].ownerUserId
    const higherSeedCaptainId = teams[1].ownerUserId;
  5. lowerSeedCaptainId = teams[2].ownerUserId
    const lowerSeedCaptainId = teams[2].ownerUserId;
  6. customFlow = { preSet: [ { action: "BAN", side: "HIGHER_SEED" }, { action: "BAN", side: "H…
    const customFlow = {
    	preSet: [
    		{ action: "BAN", side: "HIGHER_SEED" },
    		{ action: "BAN", side: "HIGHER_SEED" },
    		{ action: "BAN", side: "LOWER_SEED" },
    		{ action: "BAN", side: "LOWER_SEED" },
    		{ action: "ROLL" },
    	],
    	postGame: [
    		{ action: "BAN", side: "WINNER" },
    		{ action: "BAN", side: "WINNER" },
    		{ action: "PICK", side: "LOSER" },
    	],
    };
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open finalize dialog → mapListDialog
    const mapListDialog = await brackets.openFinalizeDialog();
Bracket map list dialog
Act
  1. Set pick ban “CUSTOM”
    await mapListDialog.setPickBan("CUSTOM");
Assert
  1. before set text is visible
    await expect(mapListDialog.locators.beforeSetText).toBeVisible();
Act
  1. Confirm with custom flow customFlow
    await mapListDialog.confirmWithCustomFlow(customFlow);
HShigher seed captain
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “action”
    await match.openTab("action");
  3. Pick ban
    await match.pickBan();
Assert
  1. last ban text is visible
    await expect(match.locators.lastBanText).toBeVisible();
Act
  1. Pick ban
    await match.pickBan();
LSlower seed captain
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “action”
    await match.openTab("action");
  3. Pick ban
    await match.pickBan();
Assert
  1. last ban text is visible
    await expect(match.locators.lastBanText).toBeVisible();
Act
  1. Pick ban
    await match.pickBan();
Assert
  1. the roll auto-executes after the last ban

    stage banner is visible
    await expect(match.locators.stageBanner).toBeVisible();
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 1 winner 1 set ends false
    await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
Assert
  1. score 1, 0 is visible
    await expect(match.score([1, 0])).toBeVisible();
  2. PostGame: the winner (alpha, whose captain is still impersonated) bans 2 maps

    ban A map text is visible
    await expect(match.locators.banAMapText).toBeVisible();
Act
  1. Pick ban
    await match.pickBan();
Assert
  1. last ban text is visible
    await expect(match.locators.lastBanText).toBeVisible();
Act
  1. Pick ban
    await match.pickBan();
HShigher seed captain
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “action”
    await match.openTab("action");
Assert
  1. pick A map text is visible
    await expect(match.locators.pickAMapText).toBeVisible();
Act
  1. Pick ban
    await match.pickBan();
Assert
  1. undoing game 1 also deletes the postGame pick/ban events

    stage banner is visible
    await expect(match.locators.stageBanner).toBeVisible();
Act
  1. Undo last report
    await match.undoLastReport();
Assert
  1. score 0, 0 is visible
    await expect(match.score([0, 0])).toBeVisible();
  2. stage banner is visible
    await expect(match.locators.stageBanner).toBeVisible();
Act
  1. the postGame cycle restarts

    Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 1 winner 1 set ends false
    await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
Assert
  1. score 1, 0 is visible
    await expect(match.score([1, 0])).toBeVisible();
  2. ban A map text is visible
    await expect(match.locators.banAMapText).toBeVisible();
Source
test("ban/pick CUSTOM flow", async ({ page, factories }) => {
	test.slow();
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
		mapPoolMaps: TO_MAP_POOL,
	});
	const teams = await createTeams(factories, tournament.id, teamSeeds(4));
	// match 2 of the group has the third team (lower seed) as alpha and the
	// second team (higher seed) as bravo
	const matchId = 2;
	const higherSeedCaptainId = teams[1].ownerUserId;
	const lowerSeedCaptainId = teams[2].ownerUserId;

	const customFlow = {
		preSet: [
			{ action: "BAN", side: "HIGHER_SEED" },
			{ action: "BAN", side: "HIGHER_SEED" },
			{ action: "BAN", side: "LOWER_SEED" },
			{ action: "BAN", side: "LOWER_SEED" },
			{ action: "ROLL" },
		],
		postGame: [
			{ action: "BAN", side: "WINNER" },
			{ action: "BAN", side: "WINNER" },
			{ action: "PICK", side: "LOSER" },
		],
	};

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	const mapListDialog = await brackets.openFinalizeDialog();
	await mapListDialog.setPickBan("CUSTOM");
	await expect(mapListDialog.locators.beforeSetText).toBeVisible();
	await mapListDialog.confirmWithCustomFlow(customFlow);

	const match = new TournamentMatchPage(page);

	// PreSet: higher seed bans 2 maps
	await impersonate(page, higherSeedCaptainId);
	await match.goto({ tournamentId: tournament.id, matchId });
	await match.openTab("action");

	await match.pickBan();

	await expect(match.locators.lastBanText).toBeVisible();
	await match.pickBan();

	// PreSet: lower seed bans 2 maps
	await impersonate(page, lowerSeedCaptainId);
	await match.goto({ tournamentId: tournament.id, matchId });
	await match.openTab("action");

	await match.pickBan();

	await expect(match.locators.lastBanText).toBeVisible();
	await match.pickBan();

	// the roll auto-executes after the last ban
	await expect(match.locators.stageBanner).toBeVisible();
	await match.openTab("action");

	await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
	await expect(match.score([1, 0])).toBeVisible();

	// PostGame: the winner (alpha, whose captain is still impersonated) bans 2 maps
	await expect(match.locators.banAMapText).toBeVisible();
	await match.pickBan();

	await expect(match.locators.lastBanText).toBeVisible();
	await match.pickBan();

	// PostGame: the loser (bravo) picks a map
	await impersonate(page, higherSeedCaptainId);
	await match.goto({ tournamentId: tournament.id, matchId });
	await match.openTab("action");

	await expect(match.locators.pickAMapText).toBeVisible();
	await match.pickBan();

	// undoing game 1 also deletes the postGame pick/ban events
	await expect(match.locators.stageBanner).toBeVisible();
	await match.undoLastReport();

	await expect(match.score([0, 0])).toBeVisible();
	await expect(match.locators.stageBanner).toBeVisible();

	// the postGame cycle restarts
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
	await expect(match.score([1, 0])).toBeVisible();

	await expect(match.locators.banAMapText).toBeVisible();
})

Tournament bracket round robin

starts a round robin bracket with unevenly sized groups (5 teams)

Renders/to/:id/to/:id/brackets

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression RR_TO_SE_WITH_UNDERGROUND map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: RR_TO_SE_WITH_UNDERGROUND,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. 5 checked in teams -> groups of 3 and 2 with different round counts

    Create teams tournament.id, teamSeeds(5)
    await createTeams(factories, tournament.id, teamSeeds(5));
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
Assert
  1. bracket starts without an "Invalid map count" error -> matches are rendered

    first matches is visible
    await expect(brackets.locators.matches.first()).toBeVisible();
Source
test("starts a round robin bracket with unevenly sized groups (5 teams)", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: RR_TO_SE_WITH_UNDERGROUND,
		mapPoolMaps: TO_MAP_POOL,
	});
	// 5 checked in teams -> groups of 3 and 2 with different round counts
	await createTeams(factories, tournament.id, teamSeeds(5));

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	// bracket starts without an "Invalid map count" error -> matches are rendered
	await expect(brackets.locators.matches.first()).toBeVisible();
})

organizer edits a match after it is done

Renders/to/:id/to/:id/brackets/to/:id/info/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, roster size 5, roster size 5
    await createTeams(factories, tournament.id, [
    	{ rosterSize: 5 },
    	{ rosterSize: 5 },
    ]);
SSendou
Tournament page
Act
  1. Open the tournament page tournament.id
    await tournamentPage.goto(tournament.id);
  2. Open brackets → brackets
    const brackets = await tournamentPage.nav.openBrackets();
  3. Finalize
    await brackets.finalize();
  4. Open match 1 → match
    const match = await brackets.openMatch(1);
  5. Open tab “action”
    await match.openTab("action");
  6. Report result maps to report 2
    await match.reportResult({ mapsToReport: 2 });
  7. Open tab “admin”
    await match.openTab("admin");
  8. Click match.editResultButton(0)
    await match.editResultButton(0).click();
  9. Click match.editResultPlayerCheckbox("alpha", 3)
    await match.editResultPlayerCheckbox("alpha", 3).click();
  10. Click match.editResultPlayerCheckbox("alpha", 4)
    await match.editResultPlayerCheckbox("alpha", 4).click();
  11. RR collects KO, which shows the edit went through

    Check match.locators.koCheckbox
    await match.locators.koCheckbox.check();
  12. Save result 0
    await match.saveResult(0);
Assert
  1. match.editResultButton(0) is visible
    await expect(match.editResultButton(0)).toBeVisible();
  2. match.locators.koResultText is visible
    await expect(match.locators.koResultText).toBeVisible();
Source
test("organizer edits a match after it is done", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, [
		{ rosterSize: 5 },
		{ rosterSize: 5 },
	]);

	await impersonate(page);

	const tournamentPage = new TournamentPage(page);
	await tournamentPage.goto(tournament.id);

	const brackets = await tournamentPage.nav.openBrackets();
	await brackets.finalize();

	const match = await brackets.openMatch(1);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 2 });

	await match.openTab("admin");
	await match.editResultButton(0).click();
	await match.editResultPlayerCheckbox("alpha", 3).click();
	await match.editResultPlayerCheckbox("alpha", 4).click();
	// RR collects KO, which shows the edit went through
	await match.locators.koCheckbox.check();
	await match.saveResult(0);

	await expect(match.editResultButton(0)).toBeVisible();
	await expect(match.locators.koResultText).toBeVisible();
})

changes to picked map pool & best of

Renders/calendar/new/to/:id/to/:id/admin/to/:id/admin/to/:id/brackets/to/:id/info/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(2)
    await createTeams(factories, tournament.id, teamSeeds(2));
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Edit event info → eventEdit
    const eventEdit = await admin.editEventInfo();
Calendar new event page
Act
  1. Go to step “maps”
    await eventEdit.goToStep("maps");
  2. Clear map pool
    await eventEdit.clearMapPool();
  3. Paste map pool MapPool.CB
    await eventEdit.pasteMapPool(MapPool.CB);
  4. Save
    await eventEdit.save();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open finalize dialog → mapListDialog
    const mapListDialog = await brackets.openFinalizeDialog();
Bracket map list dialog
Act
  1. Increase map count “first”
    await mapListDialog.increaseMapCount("first");
  2. Confirm
    await mapListDialog.confirm();
Tournament brackets page
Act
  1. Open match 1 → match
    const match = await brackets.openMatch(1);
Tournament match page
Assert
  1. Bo5 of clam blitz: one mode icon + ×5 count text

    mode progress “CB” is visible
    await expect(match.modeProgress("CB")).toBeVisible();
  2. map count text 5 is visible
    await expect(match.mapCountText(5)).toBeVisible();
Source
test("changes to picked map pool & best of", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(2));

	await impersonate(page);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);

	const eventEdit = await admin.editEventInfo();
	await eventEdit.goToStep("maps");
	await eventEdit.clearMapPool();
	await eventEdit.pasteMapPool(MapPool.CB);
	await eventEdit.save();

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	const mapListDialog = await brackets.openFinalizeDialog();
	await mapListDialog.increaseMapCount("first");
	await mapListDialog.confirm();

	const match = await brackets.openMatch(1);
	// Bo5 of clam blitz: one mode icon + ×5 count text
	await expect(match.modeProgress("CB")).toBeVisible();
	await expect(match.mapCountText(5)).toBeVisible();
})

reopens round robin match and changes score

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    });
  2. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
  3. the two passes play rounds 1 (matches 1 & 2) and 2 (matches 3 & 4), so the winner of a match whose participants already played on gets changed

    Start bracket tournament tournament.id
    await factories.TournamentFactory.startBracket(tournament.id);
  4. Play matches tournament tournament.id
    await factories.TournamentFactory.playMatches(tournament.id);
  5. Play matches tournament tournament.id
    await factories.TournamentFactory.playMatches(tournament.id);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open match 2 → match
    const match = await brackets.openMatch(2);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
  2. Reopen
    await match.reopen();
Assert
  1. switching tabs is a defaultShouldRevalidate: false navigation that would abort the in-flight post-reopen revalidation, leaving the match "over"

    reopen match button is not visible
    await isNotVisible(match.locators.reopenMatchButton);
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Undo last report
    await match.undoLastReport();
  3. Report result maps to report 2 winner 2 set ends true
    await match.reportResult({
    	mapsToReport: 2,
    	winner: 2,
    	setEnds: true,
    });
Source
test("reopens round robin match and changes score", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));

	// the two passes play rounds 1 (matches 1 & 2) and 2 (matches 3 & 4), so the
	// winner of a match whose participants already played on gets changed
	await factories.TournamentFactory.startBracket(tournament.id);
	await factories.TournamentFactory.playMatches(tournament.id);
	await factories.TournamentFactory.playMatches(tournament.id);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	const match = await brackets.openMatch(2);
	await match.openTab("admin");
	await match.reopen();
	// switching tabs is a `defaultShouldRevalidate: false` navigation that would
	// abort the in-flight post-reopen revalidation, leaving the match "over"
	await isNotVisible(match.locators.reopenMatchButton);
	await match.openTab("action");
	await match.undoLastReport();
	await match.reportResult({
		mapsToReport: 2,
		winner: 2,
		setEnds: true,
	});
})

reopening round robin match does not lock already-unlocked matches (issue #2690)

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    });
  2. Create teams tournament.id, teamSeeds(4)
    await createTeams(factories, tournament.id, teamSeeds(4));
  3. playing R1 (matches 1 and 2) unlocks R2

    Start bracket tournament tournament.id
    await factories.TournamentFactory.startBracket(tournament.id);
  4. Play matches tournament tournament.id
    await factories.TournamentFactory.playMatches(tournament.id);
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. match 3 is R2: start it without completing it

    Open match 3 → match
    let match = await brackets.openMatch(3);
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 1 set ends false
    await match.reportResult({ mapsToReport: 1, setEnds: false });
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. reopening the R1 match simulates a score misreport correction

    Open match 1 → match
    match = await brackets.openMatch(1);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
  2. Reopen
    await match.reopen();
  3. Back to bracket
    await match.backToBracket();
Tournament brackets page
Act
  1. the R2 match already in progress used to become locked and unplayable

    Open match 3 → match
    match = await brackets.openMatch(3);
Tournament match page
Assert
  1. score 1, 0 is visible
    await expect(match.score([1, 0])).toBeVisible();
Act
  1. Open tab “action”
    await match.openTab("action");
Assert
  1. winner radio 1 is visible
    await expect(match.winnerRadio(1)).toBeVisible();
Source
test("reopening round robin match does not lock already-unlocked matches (issue #2690)", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
	});
	await createTeams(factories, tournament.id, teamSeeds(4));

	// playing R1 (matches 1 and 2) unlocks R2
	await factories.TournamentFactory.startBracket(tournament.id);
	await factories.TournamentFactory.playMatches(tournament.id);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	// match 3 is R2: start it without completing it
	let match = await brackets.openMatch(3);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, setEnds: false });
	await match.backToBracket();

	// reopening the R1 match simulates a score misreport correction
	match = await brackets.openMatch(1);
	await match.openTab("admin");
	await match.reopen();
	await match.backToBracket();

	// the R2 match already in progress used to become locked and unplayable
	match = await brackets.openMatch(3);
	await expect(match.score([1, 0])).toBeVisible();
	await match.openTab("action");
	await expect(match.winnerRadio(1)).toBeVisible();
})

user no screen setting affects tournament match

Renders/settings/to/:id/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL enable no screen toggle true → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    	mapPoolMaps: TO_MAP_POOL,
    	enableNoScreenToggle: true,
    });
  2. Create teams tournament.id, members Sendou, ...teamSeeds(3)
    await createTeams(factories, tournament.id, [
    	{ members: [ADMIN_ID] },
    	...teamSeeds(3),
    ]);
SSendou
Match profile page
Act
  1. Open the match profile page
    await matchProfile.goto();
  2. Check [Accessibility] Avoid Splattercolor Screen
    await matchProfile.form.check("noScreen");
  3. Save
    await matchProfile.save();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. match 1 has Sendou's team in it, match 2 does not

    Open match 1 → ownMatch
    const ownMatch = await brackets.openMatch(1);
Tournament match page
Assert
  1. screen banned is visible
    await expect(ownMatch.locators.screenBanned).toBeVisible();
Act
  1. Back to bracket
    await ownMatch.backToBracket();
Tournament brackets page
Act
  1. Open match 2 → otherMatch
    const otherMatch = await brackets.openMatch(2);
Tournament match page
Assert
  1. screen allowed is visible
    await expect(otherMatch.locators.screenAllowed).toBeVisible();
Source
test("user no screen setting affects tournament match", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
		mapPoolMaps: TO_MAP_POOL,
		enableNoScreenToggle: true,
	});
	await createTeams(factories, tournament.id, [
		{ members: [ADMIN_ID] },
		...teamSeeds(3),
	]);

	await impersonate(page);

	const matchProfile = new MatchProfilePage(page);
	await matchProfile.goto();
	await matchProfile.form.check("noScreen");
	await matchProfile.save();

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	// match 1 has Sendou's team in it, match 2 does not
	const ownMatch = await brackets.openMatch(1);
	await expect(ownMatch.locators.screenBanned).toBeVisible();

	await ownMatch.backToBracket();
	const otherMatch = await brackets.openMatch(2);
	await expect(otherMatch.locators.screenAllowed).toBeVisible();
})

hosts a 'play all' round robin stage

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: ROUND_ROBIN,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(2)
    await createTeams(factories, tournament.id, teamSeeds(2));
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open finalize dialog → mapListDialog
    const mapListDialog = await brackets.openFinalizeDialog();
Bracket map list dialog
Act
  1. Set count type “PLAY_ALL”
    await mapListDialog.setCountType("PLAY_ALL");
  2. Confirm
    await mapListDialog.confirm();
Tournament brackets page
Act
  1. Open match 1 → match
    const match = await brackets.openMatch(1);
Tournament match page
Assert
  1. play all text 3 is visible
    await expect(match.playAllText(3)).toBeVisible();
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 3
    await match.reportResult({ mapsToReport: 3 });
Source
test("hosts a 'play all' round robin stage", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: ROUND_ROBIN,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(2));

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	const mapListDialog = await brackets.openFinalizeDialog();
	await mapListDialog.setCountType("PLAY_ALL");
	await mapListDialog.confirm();

	const match = await brackets.openMatch(1);
	await expect(match.playAllText(3)).toBeVisible();
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 3 });
})

Tournament bracket scroll position

returns to the spot a ${name} bracket was left at

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracketProgression map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. enough teams for the bracket to be scrolled past the first screen

    Create teams tournament.id, teamSeeds(32)
    await createTeams(factories, tournament.id, teamSeeds(32));
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. the elimination bracket's height does not follow the finalize controls above it going away

    Reload
    await brackets.reload();
Chat sidebar
Act
  1. match pages open the chat, which narrows the bracket: open it from the start for one layout throughout

    Open
    await new ChatSidebar(page).open();
  2. If selectGroup
    if (selectGroup) {
    	const otherGroupMatchId = await brackets.lastMatchId();
    	await brackets.openGroup(selectGroup);
    	await isNotVisible(brackets.match(otherGroupMatchId));
    }
    1. Last match id → otherGroupMatchId
      const otherGroupMatchId = await brackets.lastMatchId();
    2. Open group selectGroup
      await brackets.openGroup(selectGroup);
    3. match otherGroupMatchId is not visible
      await isNotVisible(brackets.match(otherGroupMatchId));
Tournament brackets page
Act
  1. Last match id → matchId
    const matchId = await brackets.lastMatchId();
  2. Scroll match near top left matchId
    await brackets.scrollMatchNearTopLeft(matchId);
  3. Match viewport position matchId → leftAt
    const leftAt = await brackets.matchViewportPosition(matchId);
  4. expectAtLeftSpot = () => expect .poll(() => brackets.matchViewportPosition(matchId)) .toEqual(le…
    const expectAtLeftSpot = () =>
    	expect
    		.poll(() => brackets.matchViewportPosition(matchId))
    		.toEqual(leftAt);
  5. Open match matchId → match
    const match = await brackets.openMatch(matchId);
Tournament match page
Act
  1. Back to bracket
    await match.backToBracket();
Assert
  1. at left spot
    await expectAtLeftSpot();
Tournament brackets page
Act
  1. Open match matchId
    await brackets.openMatch(matchId);
Tournament match page
Act
  1. Browser back
    await match.browserBack();
Assert
  1. at left spot
    await expectAtLeftSpot();
Tournament brackets page
Act
  1. opened from elsewhere, the spot the bracket was left at does not show the match

    Scroll to start
    await brackets.scrollToStart();
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
  2. Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. match matchId be in viewport
    await expect(brackets.match(matchId)).toBeInViewport();
Source
test(`returns to the spot a ${name} bracket was left at`, async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression,
		mapPoolMaps: TO_MAP_POOL,
	});
	// enough teams for the bracket to be scrolled past the first screen
	await createTeams(factories, tournament.id, teamSeeds(32));

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();
	// the elimination bracket's height does not follow the finalize controls above it going away
	await brackets.reload();
	// match pages open the chat, which narrows the bracket: open it from the start for one layout throughout
	await new ChatSidebar(page).open();

	if (selectGroup) {
		const otherGroupMatchId = await brackets.lastMatchId();
		await brackets.openGroup(selectGroup);
		await isNotVisible(brackets.match(otherGroupMatchId));
	}

	const matchId = await brackets.lastMatchId();
	await brackets.scrollMatchNearTopLeft(matchId);
	const leftAt = await brackets.matchViewportPosition(matchId);
	const expectAtLeftSpot = () =>
		expect
			.poll(() => brackets.matchViewportPosition(matchId))
			.toEqual(leftAt);

	const match = await brackets.openMatch(matchId);
	await match.backToBracket();
	await expectAtLeftSpot();

	await brackets.openMatch(matchId);
	await match.browserBack();
	await expectAtLeftSpot();

	// opened from elsewhere, the spot the bracket was left at does not show the match
	await brackets.scrollToStart();
	await match.goto({ tournamentId: tournament.id, matchId });
	await match.backToBracket();
	await expect(brackets.match(matchId)).toBeInViewport();
})

Tournament bracket swiss

swiss tournament with bracket advancing/unadvancing & dropping out a team

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/brackets/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression SWISS_TO_TOP_CUT map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: SWISS_TO_TOP_CUT,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(16)
    await createTeams(factories, tournament.id, teamSeeds(16));
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. For each matchId of 1, 2, 3, 4
    for (const matchId of [1, 2, 3, 4]) {
    	const match = await brackets.openMatch(matchId);
    	await match.openTab("action");
    	await match.reportResult({ mapsToReport: 2 });
    	await match.backToBracket();
    }
    1. Open match matchId → match
      const match = await brackets.openMatch(matchId);
    2. Open tab “action”
      await match.openTab("action");
    3. Report result maps to report 2
      await match.reportResult({ mapsToReport: 2 });
    4. Back to bracket
      await match.backToBracket();
Assert
  1. start round button is visible
    await expect(brackets.locators.startRoundButton).toBeVisible();
Act
  1. Open group “B”
    await brackets.openGroup("B");
Assert
  1. start round button is not visible
    await isNotVisible(brackets.locators.startRoundButton);
Act
  1. Open group “A”
    await brackets.openGroup("A");
  2. Start round
    await brackets.startRound();
Assert
  1. match 9 is visible
    await expect(brackets.match(9)).toBeVisible();
Act
  1. Open admin → admin
    const admin = await brackets.nav.openAdmin();
  2. drop out the top seed, playing in group A

    Drop out team 0
    await admin.dropOutTeam(0);
  3. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  4. Reset round
    await brackets.resetRound();
  5. Start round
    await brackets.startRound();
Assert
  1. bye team is visible
    await expect(brackets.locators.byeTeam).toBeVisible();
Source
test("swiss tournament with bracket advancing/unadvancing & dropping out a team", async ({
	page,
	factories,
}) => {
	test.slow();

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: SWISS_TO_TOP_CUT,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(16));

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	for (const matchId of [1, 2, 3, 4]) {
		const match = await brackets.openMatch(matchId);
		await match.openTab("action");
		await match.reportResult({ mapsToReport: 2 });
		await match.backToBracket();
	}

	await expect(brackets.locators.startRoundButton).toBeVisible();
	await brackets.openGroup("B");
	await isNotVisible(brackets.locators.startRoundButton);
	await brackets.openGroup("A");

	await brackets.startRound();
	await expect(brackets.match(9)).toBeVisible();

	const admin = await brackets.nav.openAdmin();
	// drop out the top seed, playing in group A
	await admin.dropOutTeam(0);

	await brackets.goto(tournament.id);

	await brackets.resetRound();
	await brackets.startRound();
	await expect(brackets.locators.byeTeam).toBeVisible();
})

swiss tournament with early advance replaying & giving a bye to a lone team

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression SWISS_EARLY_ADVANCE_TO_TOP_CUT map pool maps TO_MAP_POOL → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    	bracketProgression: SWISS_EARLY_ADVANCE_TO_TOP_CUT,
    	mapPoolMaps: TO_MAP_POOL,
    });
  2. Create teams tournament.id, teamSeeds(3)
    await createTeams(factories, tournament.id, teamSeeds(3));
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
  3. round 1: Team 1 vs. Team 2, Team 3 gets a bye

    Open match 1 → roundOneMatch
    const roundOneMatch = await brackets.openMatch(1);
Tournament match page
Act
  1. Open tab “action”
    await roundOneMatch.openTab("action");
  2. Report result for team team name “Team 1” maps to report 2
    await roundOneMatch.reportResultForTeam({
    	teamName: "Team 1",
    	mapsToReport: 2,
    });
  3. Back to bracket
    await roundOneMatch.backToBracket();
Tournament brackets page
Act
  1. round 2: Team 1 vs. Team 3, Team 2 gets a bye

    Start round
    await brackets.startRound();
  2. Open match 3 → roundTwoMatch
    const roundTwoMatch = await brackets.openMatch(3);
Tournament match page
Act
  1. Open tab “action”
    await roundTwoMatch.openTab("action");
  2. Team 3 reaches the advance threshold and is done with the bracket

    Report result for team team name “Team 3” maps to report 2
    await roundTwoMatch.reportResultForTeam({
    	teamName: "Team 3",
    	mapsToReport: 2,
    });
  3. Back to bracket
    await roundTwoMatch.backToBracket();
Tournament brackets page
Act
  1. round 3: Team 1 & Team 2 are the only ones left so they replay their round 1 match

    Start round
    await brackets.startRound();
Assert
  1. match 5 contains “Team 1”
    await expect(brackets.match(5)).toContainText("Team 1");
  2. match 5 contains “Team 2”
    await expect(brackets.match(5)).toContainText("Team 2");
Act
  1. Open match 5 → roundThreeMatch
    const roundThreeMatch = await brackets.openMatch(5);
Tournament match page
Act
  1. Open tab “action”
    await roundThreeMatch.openTab("action");
  2. Report result for team team name “Team 1” maps to report 2
    await roundThreeMatch.reportResultForTeam({
    	teamName: "Team 1",
    	mapsToReport: 2,
    });
  3. Back to bracket
    await roundThreeMatch.backToBracket();
Tournament brackets page
Act
  1. round 4: Team 2 is alone in the round so they get a bye

    Start round
    await brackets.startRound();
Assert
  1. bye team reads “BYE: Team 3”, “BYE: Team 2”, “BYE: Team 2”
    await expect(brackets.locators.byeTeam).toHaveText([
    	"BYE: Team 3",
    	"BYE: Team 2",
    	"BYE: Team 2",
    ]);
  2. every team has advanced so the last round is never played

    round label 5 is not visible
    await isNotVisible(brackets.roundLabel(5));
  3. start round button is not visible
    await isNotVisible(brackets.locators.startRoundButton);
Act
  1. Select bracket “Top Cut”
    await brackets.selectBracket("Top Cut");
  2. Finalize
    await brackets.finalize();
Assert
  1. match 8 is visible
    await expect(brackets.match(8)).toBeVisible();
Source
test("swiss tournament with early advance replaying & giving a bye to a lone team", async ({
	page,
	factories,
}) => {
	test.slow();

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
		bracketProgression: SWISS_EARLY_ADVANCE_TO_TOP_CUT,
		mapPoolMaps: TO_MAP_POOL,
	});
	await createTeams(factories, tournament.id, teamSeeds(3));

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

	// round 1: Team 1 vs. Team 2, Team 3 gets a bye
	const roundOneMatch = await brackets.openMatch(1);
	await roundOneMatch.openTab("action");
	await roundOneMatch.reportResultForTeam({
		teamName: "Team 1",
		mapsToReport: 2,
	});
	await roundOneMatch.backToBracket();

	// round 2: Team 1 vs. Team 3, Team 2 gets a bye
	await brackets.startRound();
	const roundTwoMatch = await brackets.openMatch(3);
	await roundTwoMatch.openTab("action");
	// Team 3 reaches the advance threshold and is done with the bracket
	await roundTwoMatch.reportResultForTeam({
		teamName: "Team 3",
		mapsToReport: 2,
	});
	await roundTwoMatch.backToBracket();

	// round 3: Team 1 & Team 2 are the only ones left so they replay their round 1 match
	await brackets.startRound();
	await expect(brackets.match(5)).toContainText("Team 1");
	await expect(brackets.match(5)).toContainText("Team 2");
	const roundThreeMatch = await brackets.openMatch(5);
	await roundThreeMatch.openTab("action");
	await roundThreeMatch.reportResultForTeam({
		teamName: "Team 1",
		mapsToReport: 2,
	});
	await roundThreeMatch.backToBracket();

	// round 4: Team 2 is alone in the round so they get a bye
	await brackets.startRound();
	await expect(brackets.locators.byeTeam).toHaveText([
		"BYE: Team 3",
		"BYE: Team 2",
		"BYE: Team 2",
	]);

	// every team has advanced so the last round is never played
	await isNotVisible(brackets.roundLabel(5));
	await isNotVisible(brackets.locators.startRoundButton);

	await brackets.selectBracket("Top Cut");
	await brackets.finalize();
	await expect(brackets.match(8)).toBeVisible();
})

Tournament bracket

sets active roster as regular member

Renders/to/:id/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    });
  2. the team with subs is created second, making it the bravo side of the match

    Create teams tournament.id, , roster size 5 → [, teamWithSub]
    const [, teamWithSub] = await createTeams(factories, tournament.id, [
    	{},
    	{ rosterSize: 5 },
    ]);
  3. Start bracket tournament tournament.id → [match]
    const [match] = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
TWteam with sub.owner
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id match id match.id
    await matchPage.goto({ tournamentId: tournament.id, matchId: match.id });
Assert
  1. active roster needed text is visible
    await expect(matchPage.locators.activeRosterNeededText).toBeVisible();
Act
  1. the roster tab opens in editing mode while the active roster is missing

    Open tab “rosters”
    await matchPage.openTab("rosters");
  2. Click player checkbox “bravo”, 0
    await matchPage.playerCheckbox("bravo", 0).click();
  3. Click player checkbox “bravo”, 1
    await matchPage.playerCheckbox("bravo", 1).click();
  4. Click player checkbox “bravo”, 2
    await matchPage.playerCheckbox("bravo", 2).click();
  5. Click player checkbox “bravo”, 3
    await matchPage.playerCheckbox("bravo", 3).click();
  6. Save active roster “bravo”
    await matchPage.saveActiveRoster("bravo");
  7. Open the tournament match page tournament id tournament.id match id match.id
    await matchPage.goto({ tournamentId: tournament.id, matchId: match.id });
Assert
  1. active roster needed text is not visible
    await isNotVisible(matchPage.locators.activeRosterNeededText);
Act
  1. Open tab “rosters”
    await matchPage.openTab("rosters");
  2. Click edit active roster button “bravo”
    await matchPage.editActiveRosterButton("bravo").click();
  3. Click player checkbox “bravo”, 3
    await matchPage.playerCheckbox("bravo", 3).click();
  4. Click player checkbox “bravo”, 4
    await matchPage.playerCheckbox("bravo", 4).click();
  5. Save active roster “bravo”
    await matchPage.saveActiveRoster("bravo");
Assert
  1. edit active roster button “bravo” is visible
    await expect(matchPage.editActiveRosterButton("bravo")).toBeVisible();
Source
test("sets active roster as regular member", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
	});
	// the team with subs is created second, making it the bravo side of the match
	const [, teamWithSub] = await createTeams(factories, tournament.id, [
		{},
		{ rosterSize: 5 },
	]);
	const [match] = await factories.TournamentFactory.startBracket(
		tournament.id,
	);

	await impersonate(page, teamWithSub.ownerUserId);

	const matchPage = new TournamentMatchPage(page);
	await matchPage.goto({ tournamentId: tournament.id, matchId: match.id });

	await expect(matchPage.locators.activeRosterNeededText).toBeVisible();

	// the roster tab opens in editing mode while the active roster is missing
	await matchPage.openTab("rosters");
	await matchPage.playerCheckbox("bravo", 0).click();
	await matchPage.playerCheckbox("bravo", 1).click();
	await matchPage.playerCheckbox("bravo", 2).click();
	await matchPage.playerCheckbox("bravo", 3).click();
	await matchPage.saveActiveRoster("bravo");

	await matchPage.goto({ tournamentId: tournament.id, matchId: match.id });
	await isNotVisible(matchPage.locators.activeRosterNeededText);

	await matchPage.openTab("rosters");
	await matchPage.editActiveRosterButton("bravo").click();
	await matchPage.playerCheckbox("bravo", 3).click();
	await matchPage.playerCheckbox("bravo", 4).click();
	await matchPage.saveActiveRoster("bravo");

	await expect(matchPage.editActiveRosterButton("bravo")).toBeVisible();
})

only asks for weapons of the maps played

Renders/to/:id/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    });
  2. the team with subs is created second, making it the bravo side of the match

    Create teams tournament.id, , roster size 5 → [, teamWithSub]
    const [, teamWithSub] = await createTeams(factories, tournament.id, [
    	{},
    	{ rosterSize: 5 },
    ]);
  3. Start bracket tournament tournament.id → [match]
    const [match] = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
  4. ROSTER_WITH_VIEWER = [0, 1, 2, 4]
    const ROSTER_WITH_VIEWER = [0, 1, 2, 4];
  5. ROSTER_WITHOUT_VIEWER = [0, 1, 2, 3]
    const ROSTER_WITHOUT_VIEWER = [0, 1, 2, 3];
  6. viewerUserId = teamWithSub.memberUserIds[4]
    const viewerUserId = teamWithSub.memberUserIds[4];
Vviewer
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id match id match.id
    await matchPage.goto({ tournamentId: tournament.id, matchId: match.id });
  2. Map 1: subbed in, reports their weapon like any other player

    Open tab “rosters”
    await matchPage.openTab("rosters");
  3. Set active roster “bravo”, ROSTER_WITH_VIEWER
    await matchPage.setActiveRoster("bravo", ROSTER_WITH_VIEWER);
  4. Open tab “action”
    await matchPage.openTab("action");
  5. Report weapon “Splattershot”
    await matchPage.reportWeapon("Splattershot");
Assert
  1. undo weapon button is visible
    await expect(matchPage.locators.undoWeaponButton).toBeVisible();
Act
  1. Report result maps to report 1 winner 1 set ends false
    await matchPage.reportResult({
    	mapsToReport: 1,
    	winner: 1,
    	setEnds: false,
    });
  2. Map 2: subbed out, so there is no weapon of theirs to report

    Open tab “rosters”
    await matchPage.openTab("rosters");
  3. Set active roster “bravo”, ROSTER_WITHOUT_VIEWER
    await matchPage.setActiveRoster("bravo", ROSTER_WITHOUT_VIEWER);
  4. Open tab “action”
    await matchPage.openTab("action");
  5. Expand weapon reporter
    await matchPage.expandWeaponReporter();
Assert
  1. weapon prompt 2 is not visible
    await isNotVisible(matchPage.weaponPrompt(2));
Act
  1. Report result maps to report 1 winner 2 set ends false
    await matchPage.reportResult({
    	mapsToReport: 1,
    	winner: 2,
    	setEnds: false,
    });
  2. Map 3: subbed back in, asked for the map they are about to play

    Open tab “rosters”
    await matchPage.openTab("rosters");
  3. Set active roster “bravo”, ROSTER_WITH_VIEWER
    await matchPage.setActiveRoster("bravo", ROSTER_WITH_VIEWER);
  4. Open tab “action”
    await matchPage.openTab("action");
  5. Expand weapon reporter
    await matchPage.expandWeaponReporter();
Assert
  1. weapon prompt 3 is visible
    await expect(matchPage.weaponPrompt(3)).toBeVisible();
Act
  1. Undoing map 2 puts it back up for grabs, now with them on the roster

    Undo last report
    await matchPage.undoLastReport();
  2. Expand weapon reporter
    await matchPage.expandWeaponReporter();
Assert
  1. weapon prompt 2 is visible
    await expect(matchPage.weaponPrompt(2)).toBeVisible();
Source
test("only asks for weapons of the maps played", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
	});
	// the team with subs is created second, making it the bravo side of the match
	const [, teamWithSub] = await createTeams(factories, tournament.id, [
		{},
		{ rosterSize: 5 },
	]);
	const [match] = await factories.TournamentFactory.startBracket(
		tournament.id,
	);

	const ROSTER_WITH_VIEWER = [0, 1, 2, 4];
	const ROSTER_WITHOUT_VIEWER = [0, 1, 2, 3];
	const viewerUserId = teamWithSub.memberUserIds[4];

	await impersonate(page, viewerUserId);

	const matchPage = new TournamentMatchPage(page);
	await matchPage.goto({ tournamentId: tournament.id, matchId: match.id });

	// Map 1: subbed in, reports their weapon like any other player
	await matchPage.openTab("rosters");
	await matchPage.setActiveRoster("bravo", ROSTER_WITH_VIEWER);

	await matchPage.openTab("action");
	await matchPage.reportWeapon("Splattershot");
	await expect(matchPage.locators.undoWeaponButton).toBeVisible();
	await matchPage.reportResult({
		mapsToReport: 1,
		winner: 1,
		setEnds: false,
	});

	// Map 2: subbed out, so there is no weapon of theirs to report
	await matchPage.openTab("rosters");
	await matchPage.setActiveRoster("bravo", ROSTER_WITHOUT_VIEWER);

	await matchPage.openTab("action");
	await matchPage.expandWeaponReporter();
	await isNotVisible(matchPage.weaponPrompt(2));

	await matchPage.reportResult({
		mapsToReport: 1,
		winner: 2,
		setEnds: false,
	});

	// Map 3: subbed back in, asked for the map they are about to play
	await matchPage.openTab("rosters");
	await matchPage.setActiveRoster("bravo", ROSTER_WITH_VIEWER);

	await matchPage.openTab("action");
	await matchPage.expandWeaponReporter();
	await expect(matchPage.weaponPrompt(3)).toBeVisible();

	// Undoing map 2 puts it back up for grabs, now with them on the roster
	await matchPage.undoLastReport();
	await matchPage.expandWeaponReporter();
	await expect(matchPage.weaponPrompt(2)).toBeVisible();
})

adds a sub mid tournament

Renders/to/:id/to/:id/brackets/to/:id/join/to/:id/teams/:tid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    });
  2. Create teams tournament.id, teamSeeds(3) → teams
    const teams = await createTeams(factories, tournament.id, teamSeeds(3));
  3. Start bracket tournament tournament.id
    await factories.TournamentFactory.startBracket(tournament.id);
  4. Create user → sub
    const sub = await factories.UserFactory.create();
Tteams[2].owner
Tournament team page
Act
  1. Open the tournament team page tournament.id, teams[2].id
    await teamPage.goto(tournament.id, teams[2].id);
  2. Copy sub invite link → inviteLink
    const inviteLink = await teamPage.copySubInviteLink();
Ssub
Tournament join page
Act
  1. Go to via invite link inviteLink
    await join.gotoViaInviteLink(inviteLink);
  2. Join
    await join.join();
Assert
  1. the page URL is /brackets/
    await expect(page).toHaveURL(/brackets/);
Source
test("adds a sub mid tournament", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
	});
	const teams = await createTeams(factories, tournament.id, teamSeeds(3));
	await factories.TournamentFactory.startBracket(tournament.id);
	const sub = await factories.UserFactory.create();

	// captain of the last seeded team
	await impersonate(page, teams[2].ownerUserId);

	const teamPage = new TournamentTeamPage(page);
	await teamPage.goto(tournament.id, teams[2].id);

	const inviteLink = await teamPage.copySubInviteLink();

	await impersonate(page, sub.id);

	const join = new TournamentJoinPage(page);
	await join.gotoViaInviteLink(inviteLink);
	await join.join();

	await expect(page).toHaveURL(/brackets/);
})

can end set early when past time limit and shows timer on bracket and match page

Renders/to/:id/to/:id/brackets/to/:id/matches/:mid

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: startedTournamentTimes(),
    });
  2. Create teams tournament.id, teamSeeds(2)
    await createTeams(factories, tournament.id, teamSeeds(2));
  3. Start bracket tournament tournament.id → [{ id: matchId }]
    const [{ id: matchId }] = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
SSendou
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open match matchId → match
    let match = await brackets.openMatch(matchId);
  3. Install time new Date()
    await page.clock.install({ time: new Date() });
Tournament match page
Act
  1. Open tab “action”
    await match.openTab("action");
  2. Report result maps to report 1 winner 1 set ends false
    await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });
Assert
  1. match timer is visible
    await expect(match.locators.matchTimer).toBeVisible();
Act
  1. Back to bracket
    await match.backToBracket();
Tournament brackets page
Assert
  1. match matchId is visible
    await expect(brackets.match(matchId)).toBeVisible();
  2. match timer matchId is visible
    await expect(brackets.matchTimer(matchId)).toBeVisible();
Act
  1. past the 26min limit of a Bo3

    Fast forward “30:00”
    await page.clock.fastForward("30:00");
  2. Reload
    await brackets.reload();
  3. Open match matchId → match
    match = await brackets.openMatch(matchId);
Tournament match page
Act
  1. Open tab “admin”
    await match.openTab("admin");
  2. End set with random winner
    await match.endSetWithRandomWinner();
Assert
  1. final banner is visible
    await expect(match.locators.finalBanner).toBeVisible();
Source
test("can end set early when past time limit and shows timer on bracket and match page", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: startedTournamentTimes(),
	});
	await createTeams(factories, tournament.id, teamSeeds(2));
	const [{ id: matchId }] = await factories.TournamentFactory.startBracket(
		tournament.id,
	);

	await impersonate(page);

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	let match = await brackets.openMatch(matchId);

	await page.clock.install({ time: new Date() });

	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, winner: 1, setEnds: false });

	await expect(match.locators.matchTimer).toBeVisible();

	await match.backToBracket();

	await expect(brackets.match(matchId)).toBeVisible();

	await expect(brackets.matchTimer(matchId)).toBeVisible();

	// past the 26min limit of a Bo3
	await page.clock.fastForward("30:00");
	await brackets.reload();

	match = await brackets.openMatch(matchId);

	await match.openTab("admin");
	await match.endSetWithRandomWinner();

	await expect(match.locators.finalBanner).toBeVisible();
})

shows a result reported while the app was suspended

Renders/to/:id/to/:id/brackets

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create in progress match name “Backgrounded Cup” friend N-ZAP → { tournament, matchId }
    const { tournament, matchId } = await createInProgressMatch(factories, {
    	name: "Backgrounded Cup",
    	friendId: NZAP_TEST_ID,
    });
Act
  1. the event stream a suspended app comes back to: still connected as far as the page knows, but no longer subscribed to anything, so the broadcast of the result below never reaches it. Answered rather than aborted: a failed subscription reopens the stream, and that reconnect alone catches the page up

    Route /\/sse\/[^/]+\/topics$/, (route) => route.fulfill({ status: 200 })
    await page.route(/\/sse\/[^/]+\/topics$/, (route) =>
    	route.fulfill({ status: 200 }),
    );
NN-ZAP
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
Assert
  1. match scores matchId reads “0”, “0”
    await expect(brackets.matchScores(matchId)).toHaveText(["0", "0"]);
Act
  1. Open second user browser, workerBaseURL → organizer
    const organizer = await openSecondUser(browser, workerBaseURL);
  2. Try
    try {
    	const matchPage = new TournamentMatchPage(organizer.page);
    	await matchPage.goto({ tournamentId: tournament.id, matchId });
    	await matchPage.openTab("action");
    	await matchPage.reportResult({ mapsToReport: 1, setEnds: false });
    
    	// the page missed it, the same way a phone with its screen off does
    	await expect(brackets.matchScores(matchId)).toHaveText(["0", "0"]);
    
    	await deviceAsleep(page, "10:00");
    
    	await expect(brackets.matchScores(matchId)).toHaveText(["1", "0"], {
    		timeout: 15_000,
    	});
    } finally {
    	await organizer.close();
    }
    1. Open the tournament match page tournament id tournament.id matchId
      await matchPage.goto({ tournamentId: tournament.id, matchId });
    2. Open tab “action”
      await matchPage.openTab("action");
    3. Report result maps to report 1 set ends false
      await matchPage.reportResult({ mapsToReport: 1, setEnds: false });
    4. the page missed it, the same way a phone with its screen off does

      match scores matchId reads “0”, “0”
      await expect(brackets.matchScores(matchId)).toHaveText(["0", "0"]);
    5. Device asleep “10:00”
      await deviceAsleep(page, "10:00");
    6. match scores matchId reads “1”, “0”
      await expect(brackets.matchScores(matchId)).toHaveText(["1", "0"], {
      	timeout: 15_000,
      });
Source
test("shows a result reported while the app was suspended", async ({
	page,
	browser,
	workerBaseURL,
	factories,
}) => {
	test.slow();
	const { tournament, matchId } = await createInProgressMatch(factories, {
		name: "Backgrounded Cup",
		friendId: NZAP_TEST_ID,
	});

	// the event stream a suspended app comes back to: still connected as far as
	// the page knows, but no longer subscribed to anything, so the broadcast of
	// the result below never reaches it. Answered rather than aborted: a failed
	// subscription reopens the stream, and that reconnect alone catches the page up
	await page.route(/\/sse\/[^/]+\/topics$/, (route) =>
		route.fulfill({ status: 200 }),
	);

	await impersonate(page, NZAP_TEST_ID);
	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);

	await expect(brackets.matchScores(matchId)).toHaveText(["0", "0"]);

	const organizer = await openSecondUser(browser, workerBaseURL);
	try {
		const matchPage = new TournamentMatchPage(organizer.page);
		await matchPage.goto({ tournamentId: tournament.id, matchId });
		await matchPage.openTab("action");
		await matchPage.reportResult({ mapsToReport: 1, setEnds: false });

		// the page missed it, the same way a phone with its screen off does
		await expect(brackets.matchScores(matchId)).toHaveText(["0", "0"]);

		await deviceAsleep(page, "10:00");

		await expect(brackets.matchScores(matchId)).toHaveText(["1", "0"], {
			timeout: 15_000,
		});
	} finally {
		await organizer.close();
	}
})