Skip to content

Tournament · E2E tests

3 modules · 24 exports · 4 repositories · 23 e2e tests · 12 routes

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

Tournament A/B divisions

assigns 6A/6B, lists the division on the divisions page, starts bracket, renders 36 matches across 6 rounds and two standings tables

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

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. teamCount = TEAMS_PER_DIVISION * 2
    const teamCount = TEAMS_PER_DIVISION * 2;
  2. Create teamCount * ROSTER_SIZE × user → players
    const players = await factories.UserFactory.createMany(
    	teamCount * ROSTER_SIZE,
    );
  3. Create tournament author N-ZAP start times dateToDatabaseTimestamp(subMinutes(new Date(), 30)) map picking style “AUTO” bracket progression type “round_robin” name “Groups stage” requires check in false settings has ab divisions true teams per group teamCount is league true → tournament
    const tournament = await factories.TournamentFactory.create(
    	{
    		authorId: NZAP_TEST_ID,
    		startTimes: [dateToDatabaseTimestamp(subMinutes(new Date(), 30))],
    		mapPickingStyle: "AUTO",
    		bracketProgression: [
    			{
    				type: "round_robin",
    				name: "Groups stage",
    				requiresCheckIn: false,
    				settings: { hasAbDivisions: true, teamsPerGroup: teamCount },
    			},
    		],
    	},
    	{ isLeague: true },
    );
  4. Repeat for (let i = 0; i < teamCount; i++)
    for (let i = 0; i < teamCount; i++) {
    	await factories.TournamentTeamFactory.create(
    		{
    			tournamentId: tournament.id,
    			memberUserIds: players
    				.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
    				.map((player) => player.id),
    		},
    		{ isCheckedIn: true },
    	);
    }
    1. Create tournament team tournament id tournament.id member user ids players .slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE) .map((player) => playe… is checked in true
      await factories.TournamentTeamFactory.create(
      	{
      		tournamentId: tournament.id,
      		memberUserIds: players
      			.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
      			.map((player) => player.id),
      	},
      	{ isCheckedIn: true },
      );
NN-ZAP
Tournament seeds page
Act
  1. Open the tournament seeds page tournament.id
    await seeds.goto(tournament.id);
  2. Open ab divisions dialog
    await seeds.openAbDivisionsDialog();
Assert
  1. ab division radio groups count is teamCount
    await expect(seeds.locators.abDivisionRadioGroups).toHaveCount(teamCount);
Act
  1. Repeat for (let i = 0; i < TEAMS_PER_DIVISION; i++)
    for (let i = 0; i < TEAMS_PER_DIVISION; i++) {
    	await seeds.assignAbDivision(i, "A");
    }
    1. Assign ab division i, “A”
      await seeds.assignAbDivision(i, "A");
  2. Repeat for (let i = TEAMS_PER_DIVISION; i < teamCount; i++)
    for (let i = TEAMS_PER_DIVISION; i < teamCount; i++) {
    	await seeds.assignAbDivision(i, "B");
    }
    1. Assign ab division i, “B”
      await seeds.assignAbDivision(i, "B");
  3. Save ab divisions
    await seeds.saveAbDivisions();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
Assert
  1. brackets viewer is visible
    await expect(brackets.locators.bracketsViewer).toBeVisible();
  2. matches count is TEAMS_PER_DIVISION * TEAMS_PER_DIVISION
    await expect(brackets.locators.matches).toHaveCount(
    	TEAMS_PER_DIVISION * TEAMS_PER_DIVISION,
    );
  3. Repeat for ( let roundNumber = 1; roundNumber <= TEAMS_PER_DIVISION; roundNumber++ )
    for (
    	let roundNumber = 1;
    	roundNumber <= TEAMS_PER_DIVISION;
    	roundNumber++
    ) {
    	await expect(brackets.roundLabel(roundNumber).first()).toBeVisible();
    }
    1. first round label roundNumber is visible
      await expect(brackets.roundLabel(roundNumber).first()).toBeVisible();
  4. rr standings tables count is 2
    await expect(brackets.locators.rrStandingsTables).toHaveCount(2);
Source
test("assigns 6A/6B, lists the division on the divisions page, starts bracket, renders 36 matches across 6 rounds and two standings tables", async ({
	page,
	factories,
}) => {
	test.slow();

	const teamCount = TEAMS_PER_DIVISION * 2;
	const players = await factories.UserFactory.createMany(
		teamCount * ROSTER_SIZE,
	);
	const tournament = await factories.TournamentFactory.create(
		{
			authorId: NZAP_TEST_ID,
			startTimes: [dateToDatabaseTimestamp(subMinutes(new Date(), 30))],
			mapPickingStyle: "AUTO",
			bracketProgression: [
				{
					type: "round_robin",
					name: "Groups stage",
					requiresCheckIn: false,
					settings: { hasAbDivisions: true, teamsPerGroup: teamCount },
				},
			],
		},
		{ isLeague: true },
	);
	for (let i = 0; i < teamCount; i++) {
		await factories.TournamentTeamFactory.create(
			{
				tournamentId: tournament.id,
				memberUserIds: players
					.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
					.map((player) => player.id),
			},
			{ isCheckedIn: true },
		);
	}

	await impersonate(page, NZAP_TEST_ID);

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

	await seeds.openAbDivisionsDialog();

	await expect(seeds.locators.abDivisionRadioGroups).toHaveCount(teamCount);

	for (let i = 0; i < TEAMS_PER_DIVISION; i++) {
		await seeds.assignAbDivision(i, "A");
	}
	for (let i = TEAMS_PER_DIVISION; i < teamCount; i++) {
		await seeds.assignAbDivision(i, "B");
	}

	await seeds.saveAbDivisions();

	// a one-division league has no divisions page, its brackets page is the one
	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournament.id);
	await brackets.finalize();

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

	await expect(brackets.locators.matches).toHaveCount(
		TEAMS_PER_DIVISION * TEAMS_PER_DIVISION,
	);

	for (
		let roundNumber = 1;
		roundNumber <= TEAMS_PER_DIVISION;
		roundNumber++
	) {
		await expect(brackets.roundLabel(roundNumber).first()).toBeVisible();
	}

	await expect(brackets.locators.rrStandingsTables).toHaveCount(2);
})

Invitational tournament

team can't register on their own, the TO adds them instead

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/registration/:tid?/to/:id/register/to/:id/teams

Setup
Arrange
  1. Create invitational → tournament
    const tournament = await createInvitational(factories);
  2. Create user discord name “Captain Carla” → captain
    const captain = await factories.UserFactory.create({
    	discordName: "Captain Carla",
    });
Ccaptain
Tournament register page
Act
  1. Open the tournament register page tournament.id
    await register.goto(tournament.id);
Assert
  1. registration closed alert is visible
    await expect(register.locators.registrationClosedAlert).toBeVisible();
  2. register tab count is 0
    await expect(register.nav.locators.registerTab).toHaveCount(0);
NN-ZAP
Tournament admin registration page
Act
  1. Go to new tournament.id
    await registration.gotoNew(tournament.id);
Assert
  1. add heading is visible
    await expect(registration.locators.addHeading).toBeVisible();
Act
  1. Fill Team name with “Invited Squad”
    await registration.form.fill("pickUpName", "Invited Squad");
  2. Select player “Captain Carla”
    await registration.selectPlayer("Captain Carla");
  3. Select captain captain.id
    await registration.selectCaptain(captain.id);
  4. Save
    await registration.save();
Tournament admin page
Assert
  1. team name “Invited Squad” is visible
    await expect(admin.teamName("Invited Squad")).toBeVisible();
Ccaptain
Tournament teams page
Act
  1. Open the tournament teams page tournament.id
    await teams.goto(tournament.id);
Assert
  1. team named “Invited Squad” is visible
    await expect(teams.teamNamed("Invited Squad")).toBeVisible();
  2. member named “Captain Carla” is visible
    await expect(teams.memberNamed("Captain Carla")).toBeVisible();
Tournament register page
Assert
  1. as the captain of an invitational team they can now manage the registration

    register tab is visible
    await expect(register.nav.locators.registerTab).toBeVisible();
Source
test("team can't register on their own, the TO adds them instead", async ({
	page,
	factories,
}) => {
	const tournament = await createInvitational(factories);
	const captain = await factories.UserFactory.create({
		discordName: "Captain Carla",
	});

	await impersonate(page, captain.id);

	const register = new TournamentRegisterPage(page);
	await register.goto(tournament.id);
	await expect(register.locators.registrationClosedAlert).toBeVisible();
	await expect(register.nav.locators.registerTab).toHaveCount(0);

	await impersonate(page, NZAP_TEST_ID);

	const registration = new TournamentAdminRegistrationPage(page);
	await registration.gotoNew(tournament.id);
	await expect(registration.locators.addHeading).toBeVisible();

	await registration.form.fill("pickUpName", "Invited Squad");
	await registration.selectPlayer("Captain Carla");
	await registration.selectCaptain(captain.id);
	await registration.save();

	const admin = new TournamentAdminPage(page);
	await expect(admin.teamName("Invited Squad")).toBeVisible();

	await impersonate(page, captain.id);

	const teams = new TournamentTeamsPage(page);
	await teams.goto(tournament.id);
	await expect(teams.teamNamed("Invited Squad")).toBeVisible();
	await expect(teams.memberNamed("Captain Carla")).toBeVisible();

	// as the captain of an invitational team they can now manage the registration
	await expect(register.nav.locators.registerTab).toBeVisible();
})

member added by the TO can't leave the team

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/registration/:tid?/to/:id/register

Setup
Arrange
  1. Create invitational → tournament
    const tournament = await createInvitational(factories);
  2. Create user discord name “Captain Carla” → captain
    const captain = await factories.UserFactory.create({
    	discordName: "Captain Carla",
    });
  3. Create user discord name “Member Mia” → member
    const member = await factories.UserFactory.create({
    	discordName: "Member Mia",
    });
NN-ZAP
Tournament admin registration page
Act
  1. Go to new tournament.id
    await registration.gotoNew(tournament.id);
Assert
  1. add heading is visible
    await expect(registration.locators.addHeading).toBeVisible();
Act
  1. Fill Team name with “Invited Squad”
    await registration.form.fill("pickUpName", "Invited Squad");
  2. Select player “Captain Carla”
    await registration.selectPlayer("Captain Carla");
  3. Add member “Member Mia”
    await registration.addMember("Member Mia");
  4. Select captain captain.id
    await registration.selectCaptain(captain.id);
  5. Save
    await registration.save();
Tournament admin page
Assert
  1. team name “Invited Squad” is visible
    await expect(admin.teamName("Invited Squad")).toBeVisible();
Mmember
Tournament register page
Act
  1. Open the tournament register page tournament.id
    await register.goto(tournament.id);
  2. Click leave team button
    await register.locators.leaveTeamButton.click();
Assert
  1. organizer added leave explanation is visible
    await expect(
    	register.locators.organizerAddedLeaveExplanation,
    ).toBeVisible();
Ccaptain
Tournament register page
Act
  1. Open the tournament register page tournament.id
    await register.goto(tournament.id);
Assert
  1. member 2 is visible
    await expect(register.member(2)).toBeVisible();
  2. delete member button count is 0
    await expect(register.locators.deleteMemberButton).toHaveCount(0);
  3. unregister button count is 0
    await expect(register.locators.unregisterButton).toHaveCount(0);
Source
test("member added by the TO can't leave the team", async ({
	page,
	factories,
}) => {
	const tournament = await createInvitational(factories);
	const captain = await factories.UserFactory.create({
		discordName: "Captain Carla",
	});
	const member = await factories.UserFactory.create({
		discordName: "Member Mia",
	});

	await impersonate(page, NZAP_TEST_ID);

	const registration = new TournamentAdminRegistrationPage(page);
	await registration.gotoNew(tournament.id);
	await expect(registration.locators.addHeading).toBeVisible();

	await registration.form.fill("pickUpName", "Invited Squad");
	await registration.selectPlayer("Captain Carla");
	await registration.addMember("Member Mia");
	await registration.selectCaptain(captain.id);
	await registration.save();

	const admin = new TournamentAdminPage(page);
	await expect(admin.teamName("Invited Squad")).toBeVisible();

	await impersonate(page, member.id);

	const register = new TournamentRegisterPage(page);
	await register.goto(tournament.id);
	await register.locators.leaveTeamButton.click();
	await expect(
		register.locators.organizerAddedLeaveExplanation,
	).toBeVisible();

	// nor can the captain kick them, the organizer owns the roster
	await impersonate(page, captain.id);
	await register.goto(tournament.id);
	await expect(register.member(2)).toBeVisible();
	await expect(register.locators.deleteMemberButton).toHaveCount(0);
	await expect(register.locators.unregisterButton).toHaveCount(0);
})

Tournament league

teams agree on a time, decline a reschedule, the organizer overrides it and the set gets played

N-ZAPopponent team.ownerSendoutournament-league.spec.ts:50

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

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create league set → { tournament, opponentTeam, matchId }
    const { tournament, opponentTeam, matchId } =
    	await createLeagueSet(factories);
  2. nzapTime = addHours(addDays(new Date(), 2), 1)
    const nzapTime = addHours(addDays(new Date(), 2), 1);
  3. opponentTime = addHours(addDays(new Date(), 3), 1)
    const opponentTime = addHours(addDays(new Date(), 3), 1);
NN-ZAP
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await matchPage.goto({ tournamentId: tournament.id, matchId });
Assert
  1. unscheduled banner is visible
    await expect(matchPage.locators.unscheduledBanner).toBeVisible();
  2. stage banner is not visible
    await isNotVisible(matchPage.locators.stageBanner);
Act
  1. Propose time nzapTime
    await matchPage.proposeTime(nzapTime);
Assert
  1. own candidates › element “candidate-time” count is 1
    await expect(
    	matchPage.locators.ownCandidates.getByTestId("candidate-time"),
    ).toHaveCount(1);
  2. only the other team can pick a candidate

    pick candidate buttons is not visible
    await isNotVisible(matchPage.locators.pickCandidateButtons);
OTopponent team.owner
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await matchPage.goto({ tournamentId: tournament.id, matchId });
Assert
  1. opponent candidates › element “candidate-time” count is 1
    await expect(
    	matchPage.locators.opponentCandidates.getByTestId("candidate-time"),
    ).toHaveCount(1);
Act
  1. Propose time opponentTime
    await matchPage.proposeTime(opponentTime);
Assert
  1. candidate times count is 2
    await expect(matchPage.locators.candidateTimes).toHaveCount(2);
Act
  1. Pick candidate
    await matchPage.pickCandidate();
Assert
  1. agreed time is visible
    await expect(matchPage.locators.agreedTime).toBeVisible();
  2. candidate times count is 0
    await expect(matchPage.locators.candidateTimes).toHaveCount(0);
  3. agreed and playable: the stage banner is back and the set can be reported

    stage banner is visible
    await expect(matchPage.locators.stageBanner).toBeVisible();
NN-ZAP
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await matchPage.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “schedule”
    await matchPage.openTab("schedule");
  3. Propose time addHours(nzapTime, 2)
    await matchPage.proposeTime(addHours(nzapTime, 2));
Assert
  1. candidate times count is 1
    await expect(matchPage.locators.candidateTimes).toHaveCount(1);
OTopponent team.owner
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await matchPage.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “schedule”
    await matchPage.openTab("schedule");
Assert
  1. reject reschedule button is visible
    await expect(matchPage.locators.rejectRescheduleButton).toBeVisible();
Act
  1. Click reject reschedule button
    await matchPage.locators.rejectRescheduleButton.click();
Assert
  1. candidate times count is 0
    await expect(matchPage.locators.candidateTimes).toHaveCount(0);
  2. agreed time is visible
    await expect(matchPage.locators.agreedTime).toBeVisible();
SSendou
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await matchPage.goto({ tournamentId: tournament.id, matchId });
  2. Open tab “admin”
    await matchPage.openTab("admin");
  3. Organizer set time addHours(opponentTime, 3)
    await matchPage.organizerSetTime(addHours(opponentTime, 3));
  4. Open tab “schedule”
    await matchPage.openTab("schedule");
Assert
  1. set by organizer text is visible
    await expect(matchPage.locators.setByOrganizerText).toBeVisible();
  2. propose times button is not visible
    await isNotVisible(matchPage.locators.proposeTimesButton);
Act
  1. the scheduled set shows up on the matches page, and gets played like any other

    Go to tournamentMatchesPage(tournament.id)
    await navigate({ page, url: tournamentMatchesPage(tournament.id) });
Assert
  1. element “scheduled-sets” is visible
    await expect(page.getByTestId("scheduled-sets")).toBeVisible();
  2. element `set-row-${matchId}` is visible
    await expect(page.getByTestId(`set-row-${matchId}`)).toBeVisible();
NN-ZAP
Tournament match page
Act
  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 2
    await matchPage.reportResult({ mapsToReport: 2 });
Assert
  1. final banner is visible
    await expect(matchPage.locators.finalBanner).toBeVisible();
Act
  1. Go to tournamentMatchesPage(tournament.id)
    await navigate({ page, url: tournamentMatchesPage(tournament.id) });
  2. Click element “matches-tab-past”
    await page.getByTestId("matches-tab-past").click();
Assert
  1. element “past-sets” › element `set-row-${matchId}` is visible
    await expect(
    	page.getByTestId("past-sets").getByTestId(`set-row-${matchId}`),
    ).toBeVisible();
Source
test("teams agree on a time, decline a reschedule, the organizer overrides it and the set gets played", async ({
	page,
	factories,
}) => {
	test.slow();

	const { tournament, opponentTeam, matchId } =
		await createLeagueSet(factories);
	const matchPage = new TournamentMatchPage(page);
	const nzapTime = addHours(addDays(new Date(), 2), 1);
	const opponentTime = addHours(addDays(new Date(), 3), 1);

	// N-ZAP proposes a time; the set has no time yet so the schedule tab opens by itself
	await impersonate(page, NZAP_TEST_ID);
	await matchPage.goto({ tournamentId: tournament.id, matchId });
	await expect(matchPage.locators.unscheduledBanner).toBeVisible();
	await isNotVisible(matchPage.locators.stageBanner);
	await matchPage.proposeTime(nzapTime);
	await expect(
		matchPage.locators.ownCandidates.getByTestId("candidate-time"),
	).toHaveCount(1);
	// only the other team can pick a candidate
	await isNotVisible(matchPage.locators.pickCandidateButtons);

	// the opponent answers with a time of their own, then picks N-ZAP's after all
	await impersonate(page, opponentTeam.ownerUserId);
	await matchPage.goto({ tournamentId: tournament.id, matchId });
	await expect(
		matchPage.locators.opponentCandidates.getByTestId("candidate-time"),
	).toHaveCount(1);
	await matchPage.proposeTime(opponentTime);
	await expect(matchPage.locators.candidateTimes).toHaveCount(2);
	await matchPage.pickCandidate();
	await expect(matchPage.locators.agreedTime).toBeVisible();
	await expect(matchPage.locators.candidateTimes).toHaveCount(0);
	// agreed and playable: the stage banner is back and the set can be reported
	await expect(matchPage.locators.stageBanner).toBeVisible();

	// N-ZAP asks for another time as a favor, the opponent keeps the agreed one
	await impersonate(page, NZAP_TEST_ID);
	await matchPage.goto({ tournamentId: tournament.id, matchId });
	await matchPage.openTab("schedule");
	await matchPage.proposeTime(addHours(nzapTime, 2));
	await expect(matchPage.locators.candidateTimes).toHaveCount(1);

	await impersonate(page, opponentTeam.ownerUserId);
	await matchPage.goto({ tournamentId: tournament.id, matchId });
	await matchPage.openTab("schedule");
	await expect(matchPage.locators.rejectRescheduleButton).toBeVisible();
	await matchPage.locators.rejectRescheduleButton.click();
	await expect(matchPage.locators.candidateTimes).toHaveCount(0);
	await expect(matchPage.locators.agreedTime).toBeVisible();

	// the organizer's time is final and closes the board
	await impersonate(page, ADMIN_ID);
	await matchPage.goto({ tournamentId: tournament.id, matchId });
	await matchPage.openTab("admin");
	await matchPage.organizerSetTime(addHours(opponentTime, 3));
	await matchPage.openTab("schedule");
	await expect(matchPage.locators.setByOrganizerText).toBeVisible();
	await isNotVisible(matchPage.locators.proposeTimesButton);

	// the scheduled set shows up on the matches page, and gets played like any other
	await navigate({ page, url: tournamentMatchesPage(tournament.id) });
	await expect(page.getByTestId("scheduled-sets")).toBeVisible();
	await expect(page.getByTestId(`set-row-${matchId}`)).toBeVisible();

	await impersonate(page, NZAP_TEST_ID);
	await matchPage.goto({ tournamentId: tournament.id, matchId });
	await matchPage.openTab("action");
	await matchPage.reportResult({ mapsToReport: 2 });
	await expect(matchPage.locators.finalBanner).toBeVisible();

	await navigate({ page, url: tournamentMatchesPage(tournament.id) });
	await page.getByTestId("matches-tab-past").click();
	await expect(
		page.getByTestId("past-sets").getByTestId(`set-row-${matchId}`),
	).toBeVisible();
})

a set can't be played before the teams agree on a time or the round is playable

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

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ROUND_ROBIN map pool maps TO_MAP_POOL is league true → tournament
    const tournament = await factories.TournamentFactory.create(
    	{
    		authorId: ADMIN_ID,
    		startTimes: startedTournamentTimes(),
    		bracketProgression: ROUND_ROBIN,
    		mapPoolMaps: TO_MAP_POOL,
    	},
    	{ isLeague: true },
    );
  2. Create teams tournament.id, members N-ZAP,
    await createTeams(factories, tournament.id, [
    	{ members: [NZAP_TEST_ID] },
    	{},
    ]);
  3. playable in a week: the board opens a day before that

    Start bracket tournament tournament.id is playable at () => dateToDatabaseTimestamp(addDays(new Date(), 7)) → [match]
    const [match] = await factories.TournamentFactory.startBracket(
    	tournament.id,
    	{
    		isPlayableAt: () => dateToDatabaseTimestamp(addDays(new Date(), 7)),
    	},
    );
NN-ZAP
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. element “league-not-open-banner” is visible
    await expect(page.getByTestId("league-not-open-banner")).toBeVisible();
  2. schedule tab is not visible
    await isNotVisible(matchPage.locators.scheduleTab);
  3. stage banner is not visible
    await isNotVisible(matchPage.locators.stageBanner);
Source
test("a set can't be played before the teams agree on a time or the round is playable", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create(
		{
			authorId: ADMIN_ID,
			startTimes: startedTournamentTimes(),
			bracketProgression: ROUND_ROBIN,
			mapPoolMaps: TO_MAP_POOL,
		},
		{ isLeague: true },
	);
	await createTeams(factories, tournament.id, [
		{ members: [NZAP_TEST_ID] },
		{},
	]);
	// playable in a week: the board opens a day before that
	const [match] = await factories.TournamentFactory.startBracket(
		tournament.id,
		{
			isPlayableAt: () => dateToDatabaseTimestamp(addDays(new Date(), 7)),
		},
	);

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

	await expect(page.getByTestId("league-not-open-banner")).toBeVisible();
	await isNotVisible(matchPage.locators.scheduleTab);
	await isNotVisible(matchPage.locators.stageBanner);
})

a league of several divisions lists them on the divisions page

Renders/to/:id/to/:id/divisions

Setup
Arrange
  1. Create tournament author Sendou start times startedTournamentTimes() bracket progression ...ROUND_ROBIN[0] name “Division 1”, ...ROUND_ROBIN[0] name “Division 2” map pool maps TO_MAP_POOL is league true → tournament
    const tournament = await factories.TournamentFactory.create(
    	{
    		authorId: ADMIN_ID,
    		startTimes: startedTournamentTimes(),
    		bracketProgression: [
    			{ ...ROUND_ROBIN[0], name: "Division 1" },
    			{ ...ROUND_ROBIN[0], name: "Division 2" },
    		],
    		mapPoolMaps: TO_MAP_POOL,
    	},
    	{ isLeague: true },
    );
  2. Create teams tournament.id, , , ,
    await createTeams(factories, tournament.id, [{}, {}, {}, {}]);
SSendou
Tournament divisions page
Act
  1. Open the tournament divisions page tournament.id
    await divisions.goto(tournament.id);
Assert
  1. division links count is 2
    await expect(divisions.locators.divisionLinks).toHaveCount(2);
Source
test("a league of several divisions lists them on the divisions page", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create(
		{
			authorId: ADMIN_ID,
			startTimes: startedTournamentTimes(),
			bracketProgression: [
				{ ...ROUND_ROBIN[0], name: "Division 1" },
				{ ...ROUND_ROBIN[0], name: "Division 2" },
			],
			mapPoolMaps: TO_MAP_POOL,
		},
		{ isLeague: true },
	);
	await createTeams(factories, tournament.id, [{}, {}, {}, {}]);

	await impersonate(page, ADMIN_ID);
	const divisions = new TournamentDivisionsPage(page);
	await divisions.goto(tournament.id);

	await expect(divisions.locators.divisionLinks).toHaveCount(2);
})

Tournament staff

gives and takes away staff role

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/staff

Setup
Arrange
  1. Create tournament author Sendou start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    });
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Open staff → staff
    const staff = await admin.openStaff();
Tournament admin staff page
Assert
  1. the tournament author is always shown as an organizer (info only)

    author row is visible
    await expect(staff.locators.authorRow).toBeVisible();
Act
  1. Add staffer “N-ZAP”
    await staff.addStaffer("N-ZAP");
Assert
  1. staff row “N-ZAP” is visible
    await expect(staff.staffRow("N-ZAP")).toBeVisible();
Act
  1. Remove staffer
    await staff.removeStaffer();
Assert
  1. staff row “N-ZAP” is not visible
    await isNotVisible(staff.staffRow("N-ZAP"));
Source
test("gives and takes away staff role", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
	});

	await impersonate(page, ADMIN_ID);

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

	// the tournament author is always shown as an organizer (info only)
	await expect(staff.locators.authorRow).toBeVisible();

	await staff.addStaffer("N-ZAP");

	await expect(staff.staffRow("N-ZAP")).toBeVisible();

	await staff.removeStaffer();

	await isNotVisible(staff.staffRow("N-ZAP"));
})

gives organizer role which allows another user to TO

Renders/to/:id/to/:id/admin/to/:id/admin/to/:id/admin/staff/to/:id/info

Setup
Arrange
  1. Create tournament author Sendou start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    });
NN-ZAP
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. no access yet, so redirected to info

    Wait for URL “**/info”
    await page.waitForURL("**/info");
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Open staff → staff
    const staff = await admin.openStaff();
Tournament admin staff page
Act
  1. Add staffer “N-ZAP”, “ORGANIZER”
    await staff.addStaffer("N-ZAP", "ORGANIZER");
NN-ZAP
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
Assert
  1. an organizer gets admin page access

    admin tab “Teams” is visible
    await expect(admin.adminTab("Teams")).toBeVisible();
  2. but an organizer has no perms to manage staff

    admin tab “Staff” is not visible
    await isNotVisible(admin.adminTab("Staff"));
Source
test("gives organizer role which allows another user to TO", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
	});

	await impersonate(page, NZAP_TEST_ID);

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

	// no access yet, so redirected to info
	await page.waitForURL("**/info");

	await impersonate(page, ADMIN_ID);
	await admin.goto(tournament.id);
	const staff = await admin.openStaff();
	await staff.addStaffer("N-ZAP", "ORGANIZER");

	await impersonate(page, NZAP_TEST_ID);
	await admin.goto(tournament.id);

	// an organizer gets admin page access
	await expect(admin.adminTab("Teams")).toBeVisible();
	// but an organizer has no perms to manage staff
	await isNotVisible(admin.adminTab("Staff"));
})

gives staff role which allows another user to see limited info

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

Setup
Arrange
  1. Create tournament author Sendou start times dateToDatabaseTimestamp(subMinutes(new Date(), 30)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(subMinutes(new Date(), 30))],
    });
  2. Create ROSTER_SIZE * 2 × user → players
    const players = await factories.UserFactory.createMany(ROSTER_SIZE * 2);
  3. For each roster of players.slice(0, ROSTER_SIZE), players.slice(ROSTER_SIZE)
    for (const roster of [
    	players.slice(0, ROSTER_SIZE),
    	players.slice(ROSTER_SIZE),
    ]) {
    	await factories.TournamentTeamFactory.create(
    		{
    			tournamentId: tournament.id,
    			memberUserIds: roster.map((user) => user.id),
    		},
    		{ isCheckedIn: true },
    	);
    }
    1. Create tournament team tournament id tournament.id member user ids roster.map((user) => user.id) is checked in true
      await factories.TournamentTeamFactory.create(
      	{
      		tournamentId: tournament.id,
      		memberUserIds: roster.map((user) => user.id),
      	},
      	{ isCheckedIn: true },
      );
  4. Start bracket tournament tournament.id → matches
    const matches = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
  5. matchId = matches[0].id
    const matchId = matches[0].id;
NN-ZAP
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
Assert
  1. room pass is not visible
    await isNotVisible(match.locators.roomPass);
SSendou
Tournament admin page
Act
  1. Open the tournament admin page tournament.id
    await admin.goto(tournament.id);
  2. Open staff → staff
    const staff = await admin.openStaff();
Tournament admin staff page
Act
  1. Add staffer “N-ZAP”, “STREAMER”
    await staff.addStaffer("N-ZAP", "STREAMER");
Assert
  1. staff row “N-ZAP” contains “streamer”
    await expect(staff.staffRow("N-ZAP")).toContainText("streamer");
NN-ZAP
Tournament match page
Act
  1. Open the tournament match page tournament id tournament.id matchId
    await match.goto({ tournamentId: tournament.id, matchId });
Assert
  1. room pass is visible
    await expect(match.locators.roomPass).toBeVisible();
Source
test("gives staff role which allows another user to see limited info", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(subMinutes(new Date(), 30))],
	});
	const players = await factories.UserFactory.createMany(ROSTER_SIZE * 2);
	for (const roster of [
		players.slice(0, ROSTER_SIZE),
		players.slice(ROSTER_SIZE),
	]) {
		await factories.TournamentTeamFactory.create(
			{
				tournamentId: tournament.id,
				memberUserIds: roster.map((user) => user.id),
			},
			{ isCheckedIn: true },
		);
	}
	const matches = await factories.TournamentFactory.startBracket(
		tournament.id,
	);
	const matchId = matches[0].id;

	await impersonate(page, NZAP_TEST_ID);

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

	await isNotVisible(match.locators.roomPass);

	await impersonate(page, ADMIN_ID);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);
	const staff = await admin.openStaff();
	await staff.addStaffer("N-ZAP", "STREAMER");

	await expect(staff.staffRow("N-ZAP")).toContainText("streamer");

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

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

Tournament streams

can set cast twitch accounts in admin

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

Setup
Arrange
  1. Create tournament author N-ZAP start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: NZAP_TEST_ID,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    });
NN-ZAP
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. Fill account 0, “test_cast_stream”
    await stream.fillAccount(0, CAST_ACCOUNT);
  2. Add account field
    await stream.addAccountField();
  3. Fill account 1, “another_cast”
    await stream.fillAccount(1, "another_cast");
  4. Save
    await stream.save();
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
Tournament admin stream page
Act
  1. Open the tournament admin stream page tournament.id
    await stream.goto(tournament.id);
Assert
  1. account input 0 has value “test_cast_stream”
    await expect(stream.accountInput(0)).toHaveValue(CAST_ACCOUNT);
  2. account input 1 has value “another_cast”
    await expect(stream.accountInput(1)).toHaveValue("another_cast");
Source
test("can set cast twitch accounts in admin", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: NZAP_TEST_ID,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
	});

	await impersonate(page, NZAP_TEST_ID);

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

	await stream.fillAccount(0, CAST_ACCOUNT);
	await stream.addAccountField();
	await stream.fillAccount(1, "another_cast");
	await stream.save();

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

	await expect(stream.accountInput(0)).toHaveValue(CAST_ACCOUNT);
	await expect(stream.accountInput(1)).toHaveValue("another_cast");
})

can view streams on bracket popover when match is in progress

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

Setup
Arrange
  1. Create startable tournament → { tournament, players }
    const { tournament, players } = await createStartableTournament(factories);
  2. Replace all live stream user id players[0].id viewer count 150
    await factories.LiveStreamFactory.replaceAll([
    	{ userId: players[0].id, viewerCount: STREAM_VIEWER_COUNT },
    ]);
  3. Start bracket tournament tournament.id → matches
    const matches = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
NN-ZAP
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open match matches[0].id → match
    const match = await brackets.openMatch(matches[0].id);
Tournament match page
Act
  1. a partial score sets startedAt, making the match in progress

    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
Assert
  1. a participant of an in progress match is streaming

    first live badges is visible
    await expect(brackets.locators.liveBadges.first()).toBeVisible();
Act
  1. Click first live badges
    await brackets.locators.liveBadges.first().click();
Assert
  1. stream popover is visible
    await expect(brackets.locators.streamPopover).toBeVisible();
  2. first stream popover streams is visible
    await expect(brackets.locators.streamPopoverStreams.first()).toBeVisible();
Source
test("can view streams on bracket popover when match is in progress", async ({
	page,
	factories,
}) => {
	const { tournament, players } = await createStartableTournament(factories);
	await factories.LiveStreamFactory.replaceAll([
		{ userId: players[0].id, viewerCount: STREAM_VIEWER_COUNT },
	]);
	const matches = await factories.TournamentFactory.startBracket(
		tournament.id,
	);

	await impersonate(page, NZAP_TEST_ID);

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

	const match = await brackets.openMatch(matches[0].id);
	// a partial score sets startedAt, making the match in progress
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, setEnds: false });
	await match.backToBracket();

	// a participant of an in progress match is streaming
	await expect(brackets.locators.liveBadges.first()).toBeVisible();

	await brackets.locators.liveBadges.first().click();

	await expect(brackets.locators.streamPopover).toBeVisible();
	await expect(brackets.locators.streamPopoverStreams.first()).toBeVisible();
})

can view streams on streams page

Renders/to/:id/to/:id/streams

Setup
Arrange
  1. Create startable tournament → { tournament, players }
    const { tournament, players } = await createStartableTournament(factories);
  2. Replace all live stream user id players[0].id viewer count 150
    await factories.LiveStreamFactory.replaceAll([
    	{ userId: players[0].id, viewerCount: STREAM_VIEWER_COUNT },
    ]);
  3. Start bracket tournament tournament.id
    await factories.TournamentFactory.startBracket(tournament.id);
LOLogged out
Tournament streams page
Act
  1. Open the tournament streams page tournament.id
    await streams.goto(tournament.id);
Assert
  1. first streams is visible
    await expect(streams.locators.streams.first()).toBeVisible();
  2. first viewer count 150 is visible
    await expect(
    	streams.viewerCount(STREAM_VIEWER_COUNT).first(),
    ).toBeVisible();
Source
test("can view streams on streams page", async ({ page, factories }) => {
	const { tournament, players } = await createStartableTournament(factories);
	await factories.LiveStreamFactory.replaceAll([
		{ userId: players[0].id, viewerCount: STREAM_VIEWER_COUNT },
	]);
	await factories.TournamentFactory.startBracket(tournament.id);

	const streams = new TournamentStreamsPage(page);
	await streams.goto(tournament.id);

	await expect(streams.locators.streams.first()).toBeVisible();

	await expect(
		streams.viewerCount(STREAM_VIEWER_COUNT).first(),
	).toBeVisible();
})

Renders/to/:id/to/:id/streams

Setup
Arrange
  1. Create startable tournament → { tournament, players }
    const { tournament, players } = await createStartableTournament(factories);
  2. Replace all live stream user id players[0].id account “both_platforms” viewer count 100, user id players[0].id platform “YOUTUBE” account “UCbothPlatforms000000000” viewer count 50
    await factories.LiveStreamFactory.replaceAll([
    	{
    		userId: players[0].id,
    		account: "both_platforms",
    		viewerCount: 100,
    	},
    	{
    		userId: players[0].id,
    		platform: "YOUTUBE",
    		account: "UCbothPlatforms000000000",
    		viewerCount: 50,
    	},
    ]);
  3. Start bracket tournament tournament.id
    await factories.TournamentFactory.startBracket(tournament.id);
LOLogged out
Tournament streams page
Act
  1. Open the tournament streams page tournament.id
    await streams.goto(tournament.id);
Assert
  1. streams count is 1
    await expect(streams.locators.streams).toHaveCount(1);
  2. platform link “Twitch” has “href” = LiveStream.url({ platform: "TWITCH", account: "both_platforms" })
    await expect(streams.platformLink("Twitch")).toHaveAttribute(
    	"href",
    	LiveStream.url({ platform: "TWITCH", account: "both_platforms" }),
    );
  3. platform link “YouTube” has “href” = LiveStream.url({ platform: "YOUTUBE", account: "UCbothPlatforms000000000", })
    await expect(streams.platformLink("YouTube")).toHaveAttribute(
    	"href",
    	LiveStream.url({
    		platform: "YOUTUBE",
    		account: "UCbothPlatforms000000000",
    	}),
    );
  4. viewer count 150 is visible
    await expect(streams.viewerCount(150)).toBeVisible();
Act
  1. Click help button
    await streams.locators.helpButton.click();
Assert
  1. help dialog is visible
    await expect(streams.locators.helpDialog).toBeVisible();
Source
test("streamer live on both platforms shows as one card with a link per platform", async ({
	page,
	factories,
}) => {
	const { tournament, players } = await createStartableTournament(factories);
	await factories.LiveStreamFactory.replaceAll([
		{
			userId: players[0].id,
			account: "both_platforms",
			viewerCount: 100,
		},
		{
			userId: players[0].id,
			platform: "YOUTUBE",
			account: "UCbothPlatforms000000000",
			viewerCount: 50,
		},
	]);
	await factories.TournamentFactory.startBracket(tournament.id);

	const streams = new TournamentStreamsPage(page);
	await streams.goto(tournament.id);

	await expect(streams.locators.streams).toHaveCount(1);
	await expect(streams.platformLink("Twitch")).toHaveAttribute(
		"href",
		LiveStream.url({ platform: "TWITCH", account: "both_platforms" }),
	);
	await expect(streams.platformLink("YouTube")).toHaveAttribute(
		"href",
		LiveStream.url({
			platform: "YOUTUBE",
			account: "UCbothPlatforms000000000",
		}),
	);
	await expect(streams.viewerCount(150)).toBeVisible();

	await streams.locators.helpButton.click();
	await expect(streams.locators.helpDialog).toBeVisible();
})

cast stream shows on bracket when match is set as casted

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

Setup
Arrange
  1. Create startable tournament → { tournament }
    const { tournament } = await createStartableTournament(factories);
  2. Replace all live stream user id null account “test_cast_stream”
    await factories.LiveStreamFactory.replaceAll([
    	{ userId: null, account: CAST_ACCOUNT },
    ]);
NN-ZAP
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. Fill account 0, “test_cast_stream”
    await stream.fillAccount(0, CAST_ACCOUNT);
  2. Save
    await stream.save();
Arrange
  1. Start bracket tournament tournament.id → matches
    const matches = await factories.TournamentFactory.startBracket(
    	tournament.id,
    );
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Open match matches[0].id → match
    const match = await brackets.openMatch(matches[0].id);
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. Open tab “admin”
    await match.openTab("admin");
  4. Set casted by “test_cast_stream”
    await match.setCastedBy(CAST_ACCOUNT);
  5. 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("cast stream shows on bracket when match is set as casted", async ({
	page,
	factories,
}) => {
	const { tournament } = await createStartableTournament(factories);
	await factories.LiveStreamFactory.replaceAll([
		{ userId: null, account: CAST_ACCOUNT },
	]);

	await impersonate(page, NZAP_TEST_ID);

	const admin = new TournamentAdminPage(page);
	await admin.goto(tournament.id);
	const stream = await admin.openStream();
	await stream.fillAccount(0, CAST_ACCOUNT);
	await stream.save();

	const matches = await factories.TournamentFactory.startBracket(
		tournament.id,
	);

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

	const match = await brackets.openMatch(matches[0].id);
	await match.openTab("action");
	await match.reportResult({ mapsToReport: 1, setEnds: false });

	await match.openTab("admin");
	await match.setCastedBy(CAST_ACCOUNT);
	await match.backToBracket();

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

Tournament tiers

shows tentative tier before bracket starts and confirmed tier after

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

LOLogged out
Act
  1. Slow
    test.slow();
Arrange
  1. Create TEAM_COUNT * ROSTER_SIZE × user → players
    const players = await factories.UserFactory.createMany(
    	TEAM_COUNT * ROSTER_SIZE,
    );
  2. rosters = Array.from({ length: TEAM_COUNT }, (_, i) => players .slice(i * ROSTER_SIZE, …
    const rosters = Array.from({ length: TEAM_COUNT }, (_, i) =>
    	players
    		.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
    		.map((player) => player.id),
    );
  3. Create tournament organization owner N-ZAP series name “PICNIC” description null show leaderboard false → org
    const org = await factories.TournamentOrganizationFactory.create(
    	{ ownerId: NZAP_TEST_ID },
    	{
    		series: [{ name: "PICNIC", description: null, showLeaderboard: false }],
    	},
    );
  4. a finalized earlier edition: its tier seeds the series' tier history and playing it out gives every player the seeding skill the confirmed tier of the next edition is calculated from

    Create played tournament name “PICNIC 1” author N-ZAP organization id org.id start times dateToDatabaseTimestamp(subHours(new Date(), 5)) tags null is ranked false team rosters rosters tier 5 played out “all”
    await factories.TournamentFactory.createPlayed(
    	{
    		name: "PICNIC 1",
    		authorId: NZAP_TEST_ID,
    		organizationId: org.id,
    		startTimes: [dateToDatabaseTimestamp(subHours(new Date(), 5))],
    		tags: null,
    		isRanked: false,
    	},
    	{ teamRosters: rosters, tier: HISTORY_TIER, playedOut: "all" },
    );
  5. Create tournament name “PICNIC 2” author N-ZAP organization id org.id start times dateToDatabaseTimestamp(subMinutes(new Date(), 30)) tags null map picking style “AUTO” team pick modes mode “SZ” count 6 pool “SENDOUQ” → tournament
    const tournament = await factories.TournamentFactory.create({
    	name: "PICNIC 2",
    	authorId: NZAP_TEST_ID,
    	organizationId: org.id,
    	startTimes: [dateToDatabaseTimestamp(subMinutes(new Date(), 30))],
    	tags: null,
    	mapPickingStyle: "AUTO",
    	teamPick: { modes: [{ mode: "SZ", count: 6 }], pool: "SENDOUQ" },
    });
  6. For each roster of rosters
    for (const roster of rosters) {
    	await factories.TournamentTeamFactory.create(
    		{ tournamentId: tournament.id, memberUserIds: roster },
    		{ isCheckedIn: true },
    	);
    }
    1. Create tournament team tournament id tournament.id member user ids roster is checked in true
      await factories.TournamentTeamFactory.create(
      	{ tournamentId: tournament.id, memberUserIds: roster },
      	{ isCheckedIn: true },
      );
Calendar page
Act
  1. Open the calendar page
    await calendar.goto();
Assert
  1. tentative tier pill “PICNIC 2” is visible
    await expect(calendar.tentativeTierPill("PICNIC 2")).toBeVisible();
NN-ZAP
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
  2. Finalize
    await brackets.finalize();
Calendar page
Act
  1. Open the calendar page
    await calendar.goto();
Assert
  1. confirmed tier pill “PICNIC 2” is visible
    await expect(calendar.confirmedTierPill("PICNIC 2")).toBeVisible();
Source
test("shows tentative tier before bracket starts and confirmed tier after", async ({
	page,
	factories,
}) => {
	test.slow();

	const players = await factories.UserFactory.createMany(
		TEAM_COUNT * ROSTER_SIZE,
	);
	const rosters = Array.from({ length: TEAM_COUNT }, (_, i) =>
		players
			.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
			.map((player) => player.id),
	);

	const org = await factories.TournamentOrganizationFactory.create(
		{ ownerId: NZAP_TEST_ID },
		{
			series: [{ name: "PICNIC", description: null, showLeaderboard: false }],
		},
	);

	// a finalized earlier edition: its tier seeds the series' tier history and
	// playing it out gives every player the seeding skill the confirmed tier
	// of the next edition is calculated from
	await factories.TournamentFactory.createPlayed(
		{
			name: "PICNIC 1",
			authorId: NZAP_TEST_ID,
			organizationId: org.id,
			startTimes: [dateToDatabaseTimestamp(subHours(new Date(), 5))],
			tags: null,
			isRanked: false,
		},
		{ teamRosters: rosters, tier: HISTORY_TIER, playedOut: "all" },
	);

	const tournament = await factories.TournamentFactory.create({
		name: "PICNIC 2",
		authorId: NZAP_TEST_ID,
		organizationId: org.id,
		startTimes: [dateToDatabaseTimestamp(subMinutes(new Date(), 30))],
		tags: null,
		mapPickingStyle: "AUTO",
		teamPick: { modes: [{ mode: "SZ", count: 6 }], pool: "SENDOUQ" },
	});
	for (const roster of rosters) {
		await factories.TournamentTeamFactory.create(
			{ tournamentId: tournament.id, memberUserIds: roster },
			{ isCheckedIn: true },
		);
	}

	const calendar = new CalendarPage(page);
	await calendar.goto();

	await expect(calendar.tentativeTierPill("PICNIC 2")).toBeVisible();

	await impersonate(page, NZAP_TEST_ID);

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

	await calendar.goto();

	await expect(calendar.confirmedTierPill("PICNIC 2")).toBeVisible();
})

Tournament

registers for tournament

captainfriends[0]tournament.spec.ts:33

Renders//to/:id/to/:id/info/to/:id/register

Setup
Arrange
  1. Create 4 × user → [captain, ...friends]
    const [captain, ...friends] =
    	await factories.UserFactory.createMany(ROSTER_SIZE);
  2. For each friend of friends
    for (const friend of friends) {
    	await factories.FriendshipFactory.create({
    		userOneId: captain.id,
    		userTwoId: friend.id,
    	});
    }
    1. Create friendship user one id captain.id user two id friend.id
      await factories.FriendshipFactory.create({
      	userOneId: captain.id,
      	userTwoId: friend.id,
      });
  3. Create tournament author Sendou start times dateToDatabaseTimestamp(addHours(new Date(), 2)) map picking style “AUTO” → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    	// teams pick their own counterpick maps
    	mapPickingStyle: "AUTO",
    });
Ccaptain
Tournament page
Act
  1. Open the tournament page tournament.id
    await tournamentPage.goto(tournament.id);
  2. Register → register
    const register = await tournamentPage.register();
Tournament register page
Act
  1. Fill Pick-up name with “Chimera”
    await register.form.fill("pickUpName", TEAM_NAME);
  2. Submit
    await register.form.submit();
Assert
  1. member 1 is visible
    await expect(register.member(1)).toBeVisible();
Act
  1. Repeat for (let memberNumber = 2; memberNumber <= ROSTER_SIZE; memberNumber++)
    for (let memberNumber = 2; memberNumber <= ROSTER_SIZE; memberNumber++) {
    	await register.addPlayer();
    	await expect(register.member(memberNumber)).toBeVisible();
    }
    1. Add player
      await register.addPlayer();
    2. member memberNumber is visible
      await expect(register.member(memberNumber)).toBeVisible();
  2. Pick counterpick maps
    await register.pickCounterpickMaps();
  3. Save counterpick maps
    await register.saveCounterpickMaps();
Assert
  1. step checkmark 3 is visible
    await expect(register.stepCheckmark(3)).toBeVisible();
Ffriends[0]
Act
  1. Go to “/”
    await navigate({ page, url: "/" });
Notification popover
Act
  1. Open
    await notifications.open();
Assert
  1. notification `Added to a team (${TEAM_NAME})` is visible
    await expect(
    	notifications.notification(`Added to a team (${TEAM_NAME})`),
    ).toBeVisible();
Source
test("registers for tournament", async ({ page, factories }) => {
	const [captain, ...friends] =
		await factories.UserFactory.createMany(ROSTER_SIZE);
	for (const friend of friends) {
		await factories.FriendshipFactory.create({
			userOneId: captain.id,
			userTwoId: friend.id,
		});
	}

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
		// teams pick their own counterpick maps
		mapPickingStyle: "AUTO",
	});

	await impersonate(page, captain.id);

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

	const register = await tournamentPage.register();

	await register.form.fill("pickUpName", TEAM_NAME);
	await register.form.submit();

	await expect(register.member(1)).toBeVisible();

	for (let memberNumber = 2; memberNumber <= ROSTER_SIZE; memberNumber++) {
		await register.addPlayer();
		await expect(register.member(memberNumber)).toBeVisible();
	}

	await register.pickCounterpickMaps();
	await register.saveCounterpickMaps();

	await expect(register.stepCheckmark(3)).toBeVisible();

	// adding to the roster notified the added member
	await impersonate(page, friends[0].id);
	await navigate({ page, url: "/" });

	const notifications = new NotificationPopover(page);
	await notifications.open();

	await expect(
		notifications.notification(`Added to a team (${TEAM_NAME})`),
	).toBeVisible();
})

shows the estimated end time next to the start time

Renders/to/:id/to/:id/info

Setup
Arrange
  1. startsAt = dateToDatabaseTimestamp(addHours(new Date(), 2))
    const startsAt = dateToDatabaseTimestamp(addHours(new Date(), 2));
  2. Create tournament author Sendou start times startsAt → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [startsAt],
    });
LOLogged out
Tournament page
Act
  1. Open the tournament page tournament.id
    await tournamentPage.goto(tournament.id);
Assert
  1. a lone single elimination bracket is the estimator's two hour case

    estimated end has “datetime” = databaseTimestampToDate(startsAt + 2 * HOUR_SECONDS).toISOString()
    await expect(tournamentPage.locators.estimatedEnd).toHaveAttribute(
    	"datetime",
    	databaseTimestampToDate(startsAt + 2 * HOUR_SECONDS).toISOString(),
    );
Source
test("shows the estimated end time next to the start time", async ({
	page,
	factories,
}) => {
	const startsAt = dateToDatabaseTimestamp(addHours(new Date(), 2));
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [startsAt],
	});

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

	// a lone single elimination bracket is the estimator's two hour case
	await expect(tournamentPage.locators.estimatedEnd).toHaveAttribute(
		"datetime",
		databaseTimestampToDate(startsAt + 2 * HOUR_SECONDS).toISOString(),
	);
})

quick adds all of the team's players at once

Renders/to/:id/to/:id/info/to/:id/register

Setup
Arrange
  1. Create 4 × user → [captain, slayer, support, coach]
    const [captain, slayer, support, coach] =
    	await factories.UserFactory.createMany(4);
  2. Create team member user ids captain.id, slayer.id, support.id, coach.id roles slayer.id “SLAYER” support.id “SUPPORT” coach.id “COACH” → team
    const team = await factories.TeamFactory.create(
    	{ memberUserIds: [captain.id, slayer.id, support.id, coach.id] },
    	{
    		roles: {
    			[slayer.id]: "SLAYER",
    			[support.id]: "SUPPORT",
    			[coach.id]: "COACH",
    		},
    	},
    );
  3. Create tournament author Sendou start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    });
Ccaptain
Tournament page
Act
  1. Open the tournament page tournament.id
    await tournamentPage.goto(tournament.id);
  2. Register → register
    const register = await tournamentPage.register();
Tournament register page
Act
  1. Fill Pick-up name with “Chimera”
    await register.form.fill("pickUpName", TEAM_NAME);
  2. Submit
    await register.form.submit();
Assert
  1. member 1 is visible
    await expect(register.member(1)).toBeVisible();
Act
  1. teammates are offered in the quick add, grouped under the team

    Open quick add
    await register.openQuickAdd();
Assert
  1. availability row slayer.id is visible
    await expect(register.availabilityRow(slayer.id)).toBeVisible();
  2. availability row coach.id is visible
    await expect(register.availabilityRow(coach.id)).toBeVisible();
Act
  1. Press Escape
    await page.keyboard.press("Escape");
  2. Add all team players team.id
    await register.addAllTeamPlayers(team.id);
Assert
  1. member 2 is visible
    await expect(register.member(2)).toBeVisible();
  2. member 3 is visible
    await expect(register.member(3)).toBeVisible();
  3. the coach is not part of the competitive lineup

    member 4 is not visible
    await isNotVisible(register.member(4));
Source
test("quick adds all of the team's players at once", async ({
	page,
	factories,
}) => {
	const [captain, slayer, support, coach] =
		await factories.UserFactory.createMany(4);
	const team = await factories.TeamFactory.create(
		{ memberUserIds: [captain.id, slayer.id, support.id, coach.id] },
		{
			roles: {
				[slayer.id]: "SLAYER",
				[support.id]: "SUPPORT",
				[coach.id]: "COACH",
			},
		},
	);

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
	});

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

	const register = await tournamentPage.register();
	await register.form.fill("pickUpName", TEAM_NAME);
	await register.form.submit();
	await expect(register.member(1)).toBeVisible();

	// teammates are offered in the quick add, grouped under the team
	await register.openQuickAdd();
	await expect(register.availabilityRow(slayer.id)).toBeVisible();
	await expect(register.availabilityRow(coach.id)).toBeVisible();
	await page.keyboard.press("Escape");

	await register.addAllTeamPlayers(team.id);

	await expect(register.member(2)).toBeVisible();
	await expect(register.member(3)).toBeVisible();
	// the coach is not part of the competitive lineup
	await isNotVisible(register.member(4));
})

shows the roster's availability for the event window

Renders/to/:id/to/:id/register

Setup
Arrange
  1. Create 5 × user → [captain, partialMember, unknownMember, stranger, friend]
    const [captain, partialMember, unknownMember, stranger, friend] =
    	await factories.UserFactory.createMany(5);
  2. Create team member user ids captain.id, partialMember.id, unknownMember.id
    await factories.TeamFactory.create({
    	memberUserIds: [captain.id, partialMember.id, unknownMember.id],
    });
  3. Create friendship user one id captain.id user two id friend.id
    await factories.FriendshipFactory.create({
    	userOneId: captain.id,
    	userTwoId: friend.id,
    });
  4. startsAt = addHours(new Date(), 2)
    const startsAt = addHours(new Date(), 2);
  5. Create tournament author Sendou start times dateToDatabaseTimestamp(startsAt) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(startsAt)],
    });
  6. Create tournament team tournament id tournament.id member user ids captain.id, partialMember.id, unknownMember.id, stranger.id
    await factories.TournamentTeamFactory.create({
    	tournamentId: tournament.id,
    	memberUserIds: [
    		captain.id,
    		partialMember.id,
    		unknownMember.id,
    		stranger.id,
    	],
    });
  7. { startsAt: weekStartsAt } = Availability.weekRange( startsAt, MACHINE_TIMEZONE, )
    const { startsAt: weekStartsAt } = Availability.weekRange(
    	startsAt,
    	MACHINE_TIMEZONE,
    );
  8. coveringSlot = { startsAt: dateToDatabaseTimestamp(startsAt), endsAt: dateToDatabaseTimestam…
    const coveringSlot = {
    	startsAt: dateToDatabaseTimestamp(startsAt),
    	endsAt: dateToDatabaseTimestamp(addHours(startsAt, 5)),
    };
  9. For each userId of captain.id, friend.id
    for (const userId of [captain.id, friend.id]) {
    	await factories.AvailabilityWeekFactory.create({
    		userId,
    		weekStartsAt,
    		timezone: MACHINE_TIMEZONE,
    		slots: [coveringSlot],
    	});
    }
    1. Create availability week userId weekStartsAt timezone MACHINE_TIMEZONE slots coveringSlot
      await factories.AvailabilityWeekFactory.create({
      	userId,
      	weekStartsAt,
      	timezone: MACHINE_TIMEZONE,
      	slots: [coveringSlot],
      });
  10. Create availability week user id partialMember.id weekStartsAt timezone MACHINE_TIMEZONE slots starts at dateToDatabaseTimestamp(addHours(startsAt, 1)) ends at coveringSlot.endsAt
    await factories.AvailabilityWeekFactory.create({
    	userId: partialMember.id,
    	weekStartsAt,
    	timezone: MACHINE_TIMEZONE,
    	slots: [
    		{
    			startsAt: dateToDatabaseTimestamp(addHours(startsAt, 1)),
    			endsAt: coveringSlot.endsAt,
    		},
    	],
    });
Ccaptain
Tournament register page
Act
  1. Open the tournament register page tournament.id
    await register.goto(tournament.id);
Assert
  1. row = (userId: number) => register.availabilityRow(userId)
    const row = (userId: number) => register.availabilityRow(userId);
  2. row(captain.id) has “data-status” = “available”
    await expect(row(captain.id)).toHaveAttribute("data-status", "available");
  3. row(partialMember.id) has “data-status” = “partial”
    await expect(row(partialMember.id)).toHaveAttribute(
    	"data-status",
    	"partial",
    );
  4. row(unknownMember.id) has “data-status” = “unknown”
    await expect(row(unknownMember.id)).toHaveAttribute(
    	"data-status",
    	"unknown",
    );
  5. on the tournament roster without being a teammate or a friend, so their schedule is not the viewer's to see

    row(stranger.id) has “data-status” = “hidden”
    await expect(row(stranger.id)).toHaveAttribute("data-status", "hidden");
Act
  1. the friend with an overlapping submitted range is offered in quick add

    Open quick add
    await register.openQuickAdd();
Assert
  1. row(friend.id) has “data-status” = “available”
    await expect(row(friend.id)).toHaveAttribute("data-status", "available");
Source
test("shows the roster's availability for the event window", async ({
	page,
	factories,
}) => {
	const [captain, partialMember, unknownMember, stranger, friend] =
		await factories.UserFactory.createMany(5);
	await factories.TeamFactory.create({
		memberUserIds: [captain.id, partialMember.id, unknownMember.id],
	});
	await factories.FriendshipFactory.create({
		userOneId: captain.id,
		userTwoId: friend.id,
	});

	const startsAt = addHours(new Date(), 2);
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(startsAt)],
	});
	await factories.TournamentTeamFactory.create({
		tournamentId: tournament.id,
		memberUserIds: [
			captain.id,
			partialMember.id,
			unknownMember.id,
			stranger.id,
		],
	});

	const { startsAt: weekStartsAt } = Availability.weekRange(
		startsAt,
		MACHINE_TIMEZONE,
	);
	const coveringSlot = {
		startsAt: dateToDatabaseTimestamp(startsAt),
		endsAt: dateToDatabaseTimestamp(addHours(startsAt, 5)),
	};
	for (const userId of [captain.id, friend.id]) {
		await factories.AvailabilityWeekFactory.create({
			userId,
			weekStartsAt,
			timezone: MACHINE_TIMEZONE,
			slots: [coveringSlot],
		});
	}
	await factories.AvailabilityWeekFactory.create({
		userId: partialMember.id,
		weekStartsAt,
		timezone: MACHINE_TIMEZONE,
		slots: [
			{
				startsAt: dateToDatabaseTimestamp(addHours(startsAt, 1)),
				endsAt: coveringSlot.endsAt,
			},
		],
	});

	await impersonate(page, captain.id);
	await setTimezoneCookie(page);
	const register = new TournamentRegisterPage(page);
	await register.goto(tournament.id);

	const row = (userId: number) => register.availabilityRow(userId);
	await expect(row(captain.id)).toHaveAttribute("data-status", "available");
	await expect(row(partialMember.id)).toHaveAttribute(
		"data-status",
		"partial",
	);
	await expect(row(unknownMember.id)).toHaveAttribute(
		"data-status",
		"unknown",
	);
	// on the tournament roster without being a teammate or a friend, so
	// their schedule is not the viewer's to see
	await expect(row(stranger.id)).toHaveAttribute("data-status", "hidden");
	// the friend with an overlapping submitted range is offered in quick add
	await register.openQuickAdd();
	await expect(row(friend.id)).toHaveAttribute("data-status", "available");
})

registers a two player roster for a 2v2 tournament that takes no third member

Renders/to/:id/to/:id/brackets/to/:id/info/to/:id/register

Setup
Arrange
  1. Create 3 × user → [captain, ...friends]
    const [captain, ...friends] = await factories.UserFactory.createMany(3);
  2. For each friend of friends
    for (const friend of friends) {
    	await factories.FriendshipFactory.create({
    		userOneId: captain.id,
    		userTwoId: friend.id,
    	});
    }
    1. Create friendship user one id captain.id user two id friend.id
      await factories.FriendshipFactory.create({
      	userOneId: captain.id,
      	userTwoId: friend.id,
      });
  3. Create tournament author Sendou start times dateToDatabaseTimestamp(addMinutes(new Date(), 30)) min members per team 2 → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	// check-in opens an hour before the tournament starts
    	startTimes: [dateToDatabaseTimestamp(addMinutes(new Date(), 30))],
    	minMembersPerTeam: 2,
    });
  4. the bracket preview needs an opponent to have anything to show

    Create 2 × user → opponents
    const opponents = await factories.UserFactory.createMany(2);
  5. Create tournament team tournament id tournament.id team pickUpTeam("Opponent") member user ids opponents.map((user) => user.id) is checked in true
    await factories.TournamentTeamFactory.create(
    	{
    		tournamentId: tournament.id,
    		team: pickUpTeam("Opponent"),
    		memberUserIds: opponents.map((user) => user.id),
    	},
    	{ isCheckedIn: true },
    );
Ccaptain
Tournament page
Act
  1. Open the tournament page tournament.id
    await tournamentPage.goto(tournament.id);
  2. Register → register
    const register = await tournamentPage.register();
Tournament register page
Act
  1. Fill Pick-up name with “Chimera”
    await register.form.fill("pickUpName", TEAM_NAME);
  2. Submit
    await register.form.submit();
Assert
  1. member 1 is visible
    await expect(register.member(1)).toBeVisible();
  2. no subs footer “2v2” is visible
    await expect(register.noSubsFooter("2v2")).toBeVisible();
Act
  1. Add player
    await register.addPlayer();
Assert
  1. member 2 is visible
    await expect(register.member(2)).toBeVisible();
  2. a 2v2 roster is full at two: no room for the second friend or anyone coming in through the invite link

    add player button is not visible
    await isNotVisible(register.locators.addPlayerButton);
  3. copy invite link button is not visible
    await isNotVisible(register.locators.copyInviteLinkButton);
Act
  1. Check in
    await register.checkIn();
  2. Open brackets → brackets
    const brackets = await register.openBrackets();
Assert
  1. brackets.teamName(TEAM_NAME).first() is visible
    await expect(brackets.teamName(TEAM_NAME).first()).toBeVisible();
Source
test("registers a two player roster for a 2v2 tournament that takes no third member", async ({
	page,
	factories,
}) => {
	const [captain, ...friends] = await factories.UserFactory.createMany(3);
	for (const friend of friends) {
		await factories.FriendshipFactory.create({
			userOneId: captain.id,
			userTwoId: friend.id,
		});
	}

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		// check-in opens an hour before the tournament starts
		startTimes: [dateToDatabaseTimestamp(addMinutes(new Date(), 30))],
		minMembersPerTeam: 2,
	});
	// the bracket preview needs an opponent to have anything to show
	const opponents = await factories.UserFactory.createMany(2);
	await factories.TournamentTeamFactory.create(
		{
			tournamentId: tournament.id,
			team: pickUpTeam("Opponent"),
			memberUserIds: opponents.map((user) => user.id),
		},
		{ isCheckedIn: true },
	);

	await impersonate(page, captain.id);

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

	const register = await tournamentPage.register();

	await register.form.fill("pickUpName", TEAM_NAME);
	await register.form.submit();

	await expect(register.member(1)).toBeVisible();
	await expect(register.noSubsFooter("2v2")).toBeVisible();

	await register.addPlayer();
	await expect(register.member(2)).toBeVisible();

	// a 2v2 roster is full at two: no room for the second friend or anyone
	// coming in through the invite link
	await isNotVisible(register.locators.addPlayerButton);
	await isNotVisible(register.locators.copyInviteLinkButton);

	await register.checkIn();

	const brackets = await register.openBrackets();
	await expect(brackets.teamName(TEAM_NAME).first()).toBeVisible();
})

checks in and appears on the bracket

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

Setup
Arrange
  1. Create tournament author Sendou start times dateToDatabaseTimestamp(addMinutes(new Date(), 30)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	// check-in opens an hour before the tournament starts
    	startTimes: [dateToDatabaseTimestamp(addMinutes(new Date(), 30))],
    });
  2. Create 4 × user → roster
    const roster = await factories.UserFactory.createMany(ROSTER_SIZE);
  3. Create tournament team tournament id tournament.id team pickUpTeam(TEAM_NAME) member user ids roster.map((user) => user.id)
    await factories.TournamentTeamFactory.create({
    	tournamentId: tournament.id,
    	team: pickUpTeam(TEAM_NAME),
    	memberUserIds: roster.map((user) => user.id),
    });
  4. Create notification notification type “TO_CHECK_IN_OPENED” meta tournament id tournament.id tournament name “In The Zone” users roster.map((user) => ({ userId: user.id }))
    await factories.NotificationFactory.create({
    	notification: {
    		type: "TO_CHECK_IN_OPENED",
    		meta: { tournamentId: tournament.id, tournamentName: "In The Zone" },
    	},
    	users: roster.map((user) => ({ userId: user.id })),
    });
  5. Create 2 × user → opponents
    const opponents = await factories.UserFactory.createMany(2);
  6. For each [i, opponent] of opponents.entries()
    for (const [i, opponent] of opponents.entries()) {
    	await factories.TournamentTeamFactory.create(
    		{
    			tournamentId: tournament.id,
    			team: pickUpTeam(`Opponent ${i + 1}`),
    			memberUserIds: [opponent.id],
    		},
    		{ isCheckedIn: true },
    	);
    }
    1. Create tournament team tournament id tournament.id team pickUpTeam(`Opponent ${i + 1}`) member user ids opponent.id is checked in true
      await factories.TournamentTeamFactory.create(
      	{
      		tournamentId: tournament.id,
      		team: pickUpTeam(`Opponent ${i + 1}`),
      		memberUserIds: [opponent.id],
      	},
      	{ isCheckedIn: true },
      );
Rroster[0]
Tournament brackets page
Act
  1. Open the tournament brackets page tournament.id
    await brackets.goto(tournament.id);
Assert
  1. team name “Chimera” is not visible
    await isNotVisible(brackets.teamName(TEAM_NAME));
Tournament register page
Act
  1. Open the tournament register page tournament.id
    await register.goto(tournament.id);
Notification popover
Assert
  1. bell dot is visible
    await expect(notifications.locators.bellDot).toBeVisible();
Tournament register page
Act
  1. Check in
    await register.checkIn();
Notification popover
Assert
  1. checking in resolved the check-in notification without the bell having been opened

    bell dot is hidden
    await expect(notifications.locators.bellDot).toBeHidden();
Tournament register page
Act
  1. Open brackets → bracketsAfterCheckIn
    const bracketsAfterCheckIn = await register.openBrackets();
Assert
  1. bracketsAfterCheckIn.locators.bracketsViewer is visible
    await expect(bracketsAfterCheckIn.locators.bracketsViewer).toBeVisible();
  2. bracketsAfterCheckIn.teamName(TEAM_NAME).first() is visible
    await expect(
    	bracketsAfterCheckIn.teamName(TEAM_NAME).first(),
    ).toBeVisible();
Source
test("checks in and appears on the bracket", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		// check-in opens an hour before the tournament starts
		startTimes: [dateToDatabaseTimestamp(addMinutes(new Date(), 30))],
	});

	const roster = await factories.UserFactory.createMany(ROSTER_SIZE);
	await factories.TournamentTeamFactory.create({
		tournamentId: tournament.id,
		team: pickUpTeam(TEAM_NAME),
		memberUserIds: roster.map((user) => user.id),
	});
	await factories.NotificationFactory.create({
		notification: {
			type: "TO_CHECK_IN_OPENED",
			meta: { tournamentId: tournament.id, tournamentName: "In The Zone" },
		},
		users: roster.map((user) => ({ userId: user.id })),
	});

	const opponents = await factories.UserFactory.createMany(2);
	for (const [i, opponent] of opponents.entries()) {
		await factories.TournamentTeamFactory.create(
			{
				tournamentId: tournament.id,
				team: pickUpTeam(`Opponent ${i + 1}`),
				memberUserIds: [opponent.id],
			},
			{ isCheckedIn: true },
		);
	}

	await impersonate(page, roster[0].id);

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

	await isNotVisible(brackets.teamName(TEAM_NAME));

	const register = new TournamentRegisterPage(page);
	await register.goto(tournament.id);

	const notifications = new NotificationPopover(page);
	await expect(notifications.locators.bellDot).toBeVisible();

	await register.checkIn();

	// checking in resolved the check-in notification without the bell
	// having been opened
	await expect(notifications.locators.bellDot).toBeHidden();

	const bracketsAfterCheckIn = await register.openBrackets();

	await expect(bracketsAfterCheckIn.locators.bracketsViewer).toBeVisible();
	await expect(
		bracketsAfterCheckIn.teamName(TEAM_NAME).first(),
	).toBeVisible();
})

adjusts seeds

Renders/to/:id/to/:id/admin/to/:id/admin/seeds/to/:id/teams

Setup
Arrange
  1. Create tournament author Sendou start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    });
  2. Create 8 × user → captains
    const captains = await factories.UserFactory.createMany(SEEDED_TEAM_COUNT);
  3. teams: Awaited< ReturnType<typeof factories.TournamentTeamFactory.create> >[] = []
    const teams: Awaited<
    	ReturnType<typeof factories.TournamentTeamFactory.create>
    >[] = [];
LOLogged out
Act
  1. For each [i, captain] of captains.entries()
    for (const [i, captain] of captains.entries()) {
    	teams.push(
    		await factories.TournamentTeamFactory.create({
    			tournamentId: tournament.id,
    			team: pickUpTeam(teamNameForSeed(i + 1)),
    			memberUserIds: [captain.id],
    		}),
    	);
    }
    1. Push factories.TournamentTeamFactory.create({ tournamentId: tournament.id, team: p…
      teams.push(
      	await factories.TournamentTeamFactory.create({
      		tournamentId: tournament.id,
      		team: pickUpTeam(teamNameForSeed(i + 1)),
      		memberUserIds: [captain.id],
      	}),
      );
SSendou
Tournament seeds page
Act
  1. Open the tournament seeds page tournament.id
    await seeds.goto(tournament.id);
  2. Drag team down teams[0].id
    await seeds.dragTeamDown(teams[0].id);
  3. Save
    await seeds.save();
Tournament teams page
Act
  1. Open the tournament teams page tournament.id
    await teamsPage.goto(tournament.id);
Assert
  1. first team names does not read teamNameForSeed(1)
    await expect(teamsPage.locators.teamNames.first()).not.toHaveText(
    	teamNameForSeed(1),
    );
Source
test("adjusts seeds", async ({ page, factories }) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
	});

	const captains = await factories.UserFactory.createMany(SEEDED_TEAM_COUNT);
	const teams: Awaited<
		ReturnType<typeof factories.TournamentTeamFactory.create>
	>[] = [];
	for (const [i, captain] of captains.entries()) {
		teams.push(
			await factories.TournamentTeamFactory.create({
				tournamentId: tournament.id,
				team: pickUpTeam(teamNameForSeed(i + 1)),
				memberUserIds: [captain.id],
			}),
		);
	}

	await impersonate(page);

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

	await seeds.dragTeamDown(teams[0].id);
	await seeds.save();

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

	await expect(teamsPage.locators.teamNames.first()).not.toHaveText(
		teamNameForSeed(1),
	);
})

hides a draft tournament from non-organizers, including its loaders

Renders/

Setup
Arrange
  1. Create tournament author Sendou is draft true start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	isDraft: true,
    	startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
    });
  2. Create user → captain
    const captain = await factories.UserFactory.create();
  3. Create tournament team tournament id tournament.id team pickUpTeam(TEAM_NAME) member user ids captain.id → team
    const team = await factories.TournamentTeamFactory.create({
    	tournamentId: tournament.id,
    	team: pickUpTeam(TEAM_NAME),
    	memberUserIds: [captain.id],
    });
LOLogged out
Act
  1. the draft's own pages never render, so flush the factory writes elsewhere

    Go to “/”
    await navigate({ page, url: "/" });
  2. For each view of ...TOURNAMENT_TEAM_VIEWS, `teams/${team.id}`
    for (const view of [...TOURNAMENT_TEAM_VIEWS, `teams/${team.id}`]) {
    	const url = `/to/${tournament.id}/${view}`;
    
    	const pageResponse = await page.request.fetch(url);
    	expect(pageResponse.status(), `${url} page`).toBe(404);
    
    	// each view's loader is also reachable on its own via single fetch
    	const dataResponse = await page.request.fetch(`${url}.data`);
    	expect(await dataResponse.text(), `${url}.data`).not.toContain(TEAM_NAME);
    }
    1. url = `/to/${tournament.id}/${view}`
      const url = `/to/${tournament.id}/${view}`;
    2. Fetch url → pageResponse
      const pageResponse = await page.request.fetch(url);
    3. pageResponse.status() is 404
      expect(pageResponse.status(), `${url} page`).toBe(404);
    4. each view's loader is also reachable on its own via single fetch

      Fetch `${url}.data` → dataResponse
      const dataResponse = await page.request.fetch(`${url}.data`);
    5. dataResponse.text() does not contain “Chimera”
      expect(await dataResponse.text(), `${url}.data`).not.toContain(TEAM_NAME);
  3. Fetch `/to/${tournament.id}/teams.data?_routes=features/tournament/routes/to.$id.te… → scopedToTeamsLoader
    const scopedToTeamsLoader = await page.request.fetch(
    	`/to/${tournament.id}/teams.data?_routes=features/tournament/routes/to.$id.teams`,
    );
Assert
  1. scopedToTeamsLoader.text() does not contain “Chimera”
    expect(await scopedToTeamsLoader.text()).not.toContain(TEAM_NAME);
Source
test("hides a draft tournament from non-organizers, including its loaders", async ({
	page,
	factories,
}) => {
	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		isDraft: true,
		startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
	});
	const captain = await factories.UserFactory.create();
	const team = await factories.TournamentTeamFactory.create({
		tournamentId: tournament.id,
		team: pickUpTeam(TEAM_NAME),
		memberUserIds: [captain.id],
	});

	// the draft's own pages never render, so flush the factory writes elsewhere
	await navigate({ page, url: "/" });

	for (const view of [...TOURNAMENT_TEAM_VIEWS, `teams/${team.id}`]) {
		const url = `/to/${tournament.id}/${view}`;

		const pageResponse = await page.request.fetch(url);
		expect(pageResponse.status(), `${url} page`).toBe(404);

		// each view's loader is also reachable on its own via single fetch
		const dataResponse = await page.request.fetch(`${url}.data`);
		expect(await dataResponse.text(), `${url}.data`).not.toContain(TEAM_NAME);
	}

	const scopedToTeamsLoader = await page.request.fetch(
		`/to/${tournament.id}/teams.data?_routes=features/tournament/routes/to.$id.teams`,
	);
	expect(await scopedToTeamsLoader.text()).not.toContain(TEAM_NAME);
})