Skip to content

Calendar · E2E tests

3 modules · 31 exports · 1 repository · 11 e2e tests · 6 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

Calendar

applies filters and operates hidden events toggle

Renders/calendar

Setup
Arrange
  1. all of them at the same time so they share one clock header and its toggle

    startTimes = [databaseTimestampNow()]
    const startTimes = [databaseTimestampNow()];
  2. Repeat for (let i = 0; i < SENDOU_INK_TOURNAMENTS_COUNT; i++)
    for (let i = 0; i < SENDOU_INK_TOURNAMENTS_COUNT; i++) {
    	await factories.TournamentFactory.create({
    		authorId: ADMIN_ID,
    		startTimes,
    	});
    }
    1. Create tournament author Sendou startTimes
      await factories.TournamentFactory.create({
      	authorId: ADMIN_ID,
      	startTimes,
      });
  3. Repeat for (let i = 0; i < EXTERNAL_EVENTS_COUNT; i++)
    for (let i = 0; i < EXTERNAL_EVENTS_COUNT; i++) {
    	await factories.CalendarEventFactory.create({
    		authorId: ADMIN_ID,
    		startTimes,
    	});
    }
    1. Create calendar event author Sendou startTimes
      await factories.CalendarEventFactory.create({
      	authorId: ADMIN_ID,
      	startTimes,
      });
NN-ZAP
Calendar page
Act
  1. Open the calendar page
    await calendar.goto();
  2. Toggle event type filter “isSendou”
    await calendar.toggleEventTypeFilter("isSendou");
Assert
  1. tournament cards count is 3
    await expect(calendar.locators.tournamentCards).toHaveCount(
    	SENDOU_INK_TOURNAMENTS_COUNT,
    );
Act
  1. Reload
    await calendar.reload();
Assert
  1. remembers selection via search params

    tournament cards count is 3
    await expect(calendar.locators.tournamentCards).toHaveCount(
    	SENDOU_INK_TOURNAMENTS_COUNT,
    );
Act
  1. Toggle hidden events
    await calendar.toggleHiddenEvents();
Assert
  1. tournament cards count is SENDOU_INK_TOURNAMENTS_COUNT + EXTERNAL_EVENTS_COUNT
    await expect(calendar.locators.tournamentCards).toHaveCount(
    	SENDOU_INK_TOURNAMENTS_COUNT + EXTERNAL_EVENTS_COUNT,
    );
Act
  1. Toggle hidden events
    await calendar.toggleHiddenEvents();
Assert
  1. tournament cards count is 3
    await expect(calendar.locators.tournamentCards).toHaveCount(
    	SENDOU_INK_TOURNAMENTS_COUNT,
    );
Source
test("applies filters and operates hidden events toggle", async ({
	page,
	factories,
}) => {
	// all of them at the same time so they share one clock header and its toggle
	const startTimes = [databaseTimestampNow()];
	for (let i = 0; i < SENDOU_INK_TOURNAMENTS_COUNT; i++) {
		await factories.TournamentFactory.create({
			authorId: ADMIN_ID,
			startTimes,
		});
	}
	for (let i = 0; i < EXTERNAL_EVENTS_COUNT; i++) {
		await factories.CalendarEventFactory.create({
			authorId: ADMIN_ID,
			startTimes,
		});
	}

	await impersonate(page, NZAP_TEST_ID);

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

	await calendar.toggleEventTypeFilter("isSendou");

	await expect(calendar.locators.tournamentCards).toHaveCount(
		SENDOU_INK_TOURNAMENTS_COUNT,
	);

	await calendar.reload();

	// remembers selection via search params
	await expect(calendar.locators.tournamentCards).toHaveCount(
		SENDOU_INK_TOURNAMENTS_COUNT,
	);

	await calendar.toggleHiddenEvents();

	await expect(calendar.locators.tournamentCards).toHaveCount(
		SENDOU_INK_TOURNAMENTS_COUNT + EXTERNAL_EVENTS_COUNT,
	);

	await calendar.toggleHiddenEvents();

	await expect(calendar.locators.tournamentCards).toHaveCount(
		SENDOU_INK_TOURNAMENTS_COUNT,
	);
})

sets default filters

Renders/calendar

Setup
Arrange
  1. not hosted on sendou.ink, so the ranked filter hides it

    Create calendar event author Sendou
    await factories.CalendarEventFactory.create({ authorId: ADMIN_ID });
NN-ZAP
Calendar page
Act
  1. Open the calendar page
    await calendar.goto();
Assert
  1. hidden events buttons is not visible
    await isNotVisible(calendar.locators.hiddenEventsButtons);
Act
  1. Toggle event type filter “isRanked”
    await calendar.toggleEventTypeFilter("isRanked");
  2. Save filters as default
    await calendar.saveFiltersAsDefault();
Assert
  1. first hidden events buttons is visible
    await expect(calendar.locators.hiddenEventsButtons.first()).toBeVisible();
Act
  1. Open the calendar page
    await calendar.goto();
Assert
  1. remembers selection via user preferences

    first hidden events buttons is visible
    await expect(calendar.locators.hiddenEventsButtons.first()).toBeVisible();
Act
  1. Remove event type filter
    await calendar.removeEventTypeFilter();
Assert
  1. removing the filter sticks instead of falling back to the saved default

    hidden events buttons is not visible
    await isNotVisible(calendar.locators.hiddenEventsButtons);
Act
  1. Reload
    await calendar.reload();
Assert
  1. hidden events buttons is not visible
    await isNotVisible(calendar.locators.hiddenEventsButtons);
Source
test("sets default filters", async ({ page, factories }) => {
	// not hosted on sendou.ink, so the ranked filter hides it
	await factories.CalendarEventFactory.create({ authorId: ADMIN_ID });

	await impersonate(page, NZAP_TEST_ID);

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

	await isNotVisible(calendar.locators.hiddenEventsButtons);

	await calendar.toggleEventTypeFilter("isRanked");
	await calendar.saveFiltersAsDefault();

	await expect(calendar.locators.hiddenEventsButtons.first()).toBeVisible();

	await calendar.goto();

	// remembers selection via user preferences
	await expect(calendar.locators.hiddenEventsButtons.first()).toBeVisible();

	await calendar.removeEventTypeFilter();

	// removing the filter sticks instead of falling back to the saved default
	await isNotVisible(calendar.locators.hiddenEventsButtons);

	await calendar.reload();

	await isNotVisible(calendar.locators.hiddenEventsButtons);
})

navigates view more buttons

Renders/calendar

LOLogged out
Calendar page
Act
  1. Open the calendar page
    await calendar.goto();
  2. Navigate previous
    await calendar.navigatePrevious();
Assert
  1. today header is not visible
    await isNotVisible(calendar.locators.todayHeader);
Act
  1. Navigate next
    await calendar.navigateNext();
Assert
  1. today header is visible
    await expect(calendar.locators.todayHeader).toBeVisible();
Source
test("navigates view more buttons", async ({ page }) => {
	const calendar = new CalendarPage(page);
	await calendar.goto();

	await calendar.navigatePrevious();

	await isNotVisible(calendar.locators.todayHeader);

	await calendar.navigateNext();

	await expect(calendar.locators.todayHeader).toBeVisible();
})

renders clock header times in the browser locale

Setup
Arrange
  1. Create calendar event author Sendou
    await factories.CalendarEventFactory.create({ authorId: ADMIN_ID });
  2. openWith = async (locale: string) => { const context = await browser.newContext({ locale…
    const openWith = async (locale: string) => {
    	const context = await browser.newContext({
    		locale,
    		baseURL: workerBaseURL,
    	});
    	const page = await context.newPage();
    	return { context, page };
    };
LOLogged out
Act
  1. Open with “en-CA” → ca
    const ca = await openWith("en-CA");
  2. Open with “en-GB” → gb
    const gb = await openWith("en-GB");
  3. Try
    try {
    	const caCalendar = new CalendarPage(ca.page);
    	const gbCalendar = new CalendarPage(gb.page);
    
    	await caCalendar.goto();
    	await gbCalendar.goto();
    
    	const caTime = await caCalendar.locators.clockHeaderTimes
    		.first()
    		.textContent();
    	const gbTime = await gbCalendar.locators.clockHeaderTimes
    		.first()
    		.textContent();
    
    	expect(caTime).toMatch(/AM|PM|a\.m\.|p\.m\./i);
    	expect(gbTime).not.toMatch(/AM|PM|a\.m\.|p\.m\./i);
    	expect(caTime).not.toBe(gbTime);
    } finally {
    	await ca.context.close();
    	await gb.context.close();
    }
    1. Open the calendar page
      await caCalendar.goto();
    2. Open the calendar page
      await gbCalendar.goto();
    3. Text content → caTime
      const caTime = await caCalendar.locators.clockHeaderTimes
      	.first()
      	.textContent();
    4. Text content → gbTime
      const gbTime = await gbCalendar.locators.clockHeaderTimes
      	.first()
      	.textContent();
    5. caTime match /AM|PM|a\.m\.|p\.m\./i
      expect(caTime).toMatch(/AM|PM|a\.m\.|p\.m\./i);
    6. gbTime not match /AM|PM|a\.m\.|p\.m\./i
      expect(gbTime).not.toMatch(/AM|PM|a\.m\.|p\.m\./i);
    7. caTime is not gbTime
      expect(caTime).not.toBe(gbTime);
Source
test("renders clock header times in the browser locale", async ({
	browser,
	workerBaseURL,
	factories,
}) => {
	await factories.CalendarEventFactory.create({ authorId: ADMIN_ID });

	const openWith = async (locale: string) => {
		const context = await browser.newContext({
			locale,
			baseURL: workerBaseURL,
		});
		const page = await context.newPage();
		return { context, page };
	};

	const ca = await openWith("en-CA");
	const gb = await openWith("en-GB");

	try {
		const caCalendar = new CalendarPage(ca.page);
		const gbCalendar = new CalendarPage(gb.page);

		await caCalendar.goto();
		await gbCalendar.goto();

		const caTime = await caCalendar.locators.clockHeaderTimes
			.first()
			.textContent();
		const gbTime = await gbCalendar.locators.clockHeaderTimes
			.first()
			.textContent();

		expect(caTime).toMatch(/AM|PM|a\.m\.|p\.m\./i);
		expect(gbTime).not.toMatch(/AM|PM|a\.m\.|p\.m\./i);
		expect(caTime).not.toBe(gbTime);
	} finally {
		await ca.context.close();
		await gb.context.close();
	}
})

creates, edits and deletes a calendar event

Renders/calendar/calendar/:id/calendar/new

SSendou
Calendar new event page
Act
  1. Open the calendar new event page
    await newEvent.goto();
  2. Fill Name with “Test Calendar Event”
    await newEvent.form.fill("name", "Test Calendar Event");
  3. Set first date new Date(2027, 0, 15, 17, 0)
    await newEvent.setFirstDate(new Date(2027, 0, 15, 17, 0));
  4. Fill Bracket URL with “https://sendou.ink/test-bracket”
    await newEvent.form.fill("bracketUrl", "https://sendou.ink/test-bracket");
  5. Submit
    await newEvent.form.submit();
Assert
  1. the page URL is /\/calendar\/\d+/
    await expect(page).toHaveURL(/\/calendar\/\d+/);
Act
  1. eventId = Number(page.url().match(/\/calendar\/(\d+)/)?.[1])
    const eventId = Number(page.url().match(/\/calendar\/(\d+)/)?.[1]);
  2. editedDate = new Date(2027, 0, 20, 18, 0)
    const editedDate = new Date(2027, 0, 20, 18, 0);
Calendar event page
Act
  1. Open edit → editEvent
    const editEvent = await calendarEvent.openEdit();
Calendar new event page
Act
  1. Fill Name with “Renamed Calendar Event”
    await editEvent.form.fill("name", "Renamed Calendar Event");
  2. Set first date editedDate
    await editEvent.setFirstDate(editedDate);
  3. Save
    await editEvent.save();
Assert
  1. the page URL is calendarEventPage(eventId)
    await expect(page).toHaveURL(calendarEventPage(eventId));
Calendar event page
Assert
  1. start time editedDate is visible
    await expect(calendarEvent.startTime(editedDate)).toBeVisible();
Act
  1. editedDateWeek = { day: 20, month: 0, year: 2027 }
    const editedDateWeek = { day: 20, month: 0, year: 2027 };
Calendar page
Act
  1. Open the calendar page editedDateWeek
    await calendar.goto(editedDateWeek);
Assert
  1. tournament card “Renamed Calendar Event” is visible
    await expect(
    	calendar.tournamentCard("Renamed Calendar Event"),
    ).toBeVisible();
Calendar event page
Act
  1. Open the calendar event page eventId
    await calendarEvent.goto(eventId);
  2. Delete
    await calendarEvent.delete();
Assert
  1. the page URL is /\/calendar$/
    await expect(page).toHaveURL(/\/calendar$/);
Calendar page
Act
  1. Open the calendar page editedDateWeek
    await calendar.goto(editedDateWeek);
Assert
  1. tournament card “Renamed Calendar Event” is not visible
    await isNotVisible(calendar.tournamentCard("Renamed Calendar Event"));
Source
test("creates, edits and deletes a calendar event", async ({ page }) => {
	await impersonate(page);

	const newEvent = new CalendarNewEventPage(page);
	await newEvent.goto();

	await newEvent.form.fill("name", "Test Calendar Event");
	await newEvent.setFirstDate(new Date(2027, 0, 15, 17, 0));
	await newEvent.form.fill("bracketUrl", "https://sendou.ink/test-bracket");

	await newEvent.form.submit();

	await expect(page).toHaveURL(/\/calendar\/\d+/);
	const eventId = Number(page.url().match(/\/calendar\/(\d+)/)?.[1]);

	const calendarEvent = new CalendarEventPage(page);
	const editedDate = new Date(2027, 0, 20, 18, 0);

	const editEvent = await calendarEvent.openEdit();
	await editEvent.form.fill("name", "Renamed Calendar Event");
	await editEvent.setFirstDate(editedDate);
	await editEvent.save();

	await expect(page).toHaveURL(calendarEventPage(eventId));
	await expect(calendarEvent.startTime(editedDate)).toBeVisible();

	const calendar = new CalendarPage(page);
	const editedDateWeek = { day: 20, month: 0, year: 2027 };
	await calendar.goto(editedDateWeek);

	await expect(
		calendar.tournamentCard("Renamed Calendar Event"),
	).toBeVisible();

	await calendarEvent.goto(eventId);
	await calendarEvent.delete();

	await expect(page).toHaveURL(/\/calendar$/);

	await calendar.goto(editedDateWeek);

	await isNotVisible(calendar.tournamentCard("Renamed Calendar Event"));
})

creates a new tournament with a map pool and follow-up bracket

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

Setup
Arrange
  1. tournaments can only be added by an organizer, unlike calendar events

    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. startTime = new Date(2027, 0, 15, 17, 0)
    const startTime = new Date(2027, 0, 15, 17, 0);
  3. mapPool = [ { stage: "Scorch Gorge", mode: "Splat Zones" }, { stage: "Eeltail Alley", m…
    const mapPool = [
    	{ stage: "Scorch Gorge", mode: "Splat Zones" },
    	{ stage: "Eeltail Alley", mode: "Tower Control" },
    	{ stage: "Hagglefish Market", mode: "Rainmaker" },
    ];
  4. Fill Name with “Test Tournament”
    await newTournament.form.fill("name", "Test Tournament");
  5. Fill Description with “An automated test tournament”
    await newTournament.form.fill(
    	"description",
    	"An automated test tournament",
    );
  6. Set first date startTime
    await newTournament.setFirstDate(startTime);
  7. Fill Discord server invite URL with “test-invite”
    await newTournament.form.fill("discordInviteCode", "test-invite");
  8. flip a tournament setting away from its default

    Go to step “teams”
    await newTournament.goToStep("teams");
  9. Check Require in-game names
    await newTournament.form.check("requireInGameNames");
  10. "Organizer picked" allows an arbitrary map pool, unlike the validated team pick pools

    Go to step “maps”
    await newTournament.goToStep("maps");
  11. Check items map picking style “TO”
    await newTournament.form.checkItems("mapPickingStyle", ["TO"]);
  12. Pick map pool mapPool
    await newTournament.pickMapPool(mapPool);
  13. Add follow up bracket name “Underground bracket” format “Single elimination” placements -1
    await newTournament.addFollowUpBracket({
    	name: "Underground bracket",
    	format: "Single elimination",
    	placements: [-1],
    });
  14. Save
    await newTournament.save();
Assert
  1. the page URL is /\/to\/\d+/
    await expect(page).toHaveURL(/\/to\/\d+/);
Act
  1. tournamentId = Number(page.url().match(/\/to\/(\d+)/)?.[1])
    const tournamentId = Number(page.url().match(/\/to\/(\d+)/)?.[1]);
Tournament info page
Act
  1. Open the tournament info page tournamentId
    await tournamentInfo.goto(tournamentId);
Assert
  1. first start time startTime is visible
    await expect(tournamentInfo.startTime(startTime).first()).toBeVisible();
Tournament brackets page
Act
  1. Open the tournament brackets page tournamentId
    await brackets.goto(tournamentId);
Assert
  1. bracket tab “Underground” is visible
    await expect(brackets.bracketTab("Underground")).toBeVisible();
Tournament rules page
Act
  1. Open the tournament rules page tournamentId
    await rules.goto(tournamentId);
Assert
  1. For each { stage, mode } of mapPool
    for (const { stage, mode } of mapPool) {
    	await expect(rules.stageName(stage)).toBeVisible();
    	await expect(rules.modeImage(mode).first()).toBeVisible();
    }
    1. stage name stage is visible
      await expect(rules.stageName(stage)).toBeVisible();
    2. first mode image mode is visible
      await expect(rules.modeImage(mode).first()).toBeVisible();
Source
test("creates a new tournament with a map pool and follow-up bracket", async ({
	page,
	factories,
}) => {
	// tournaments can only be added by an organizer, unlike calendar events
	const organizer = await factories.UserFactory.create(null, {
		roles: ["TOURNAMENT_ORGANIZER"],
	});

	await impersonate(page, organizer.id);

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

	const startTime = new Date(2027, 0, 15, 17, 0);
	const mapPool = [
		{ stage: "Scorch Gorge", mode: "Splat Zones" },
		{ stage: "Eeltail Alley", mode: "Tower Control" },
		{ stage: "Hagglefish Market", mode: "Rainmaker" },
	];

	await newTournament.form.fill("name", "Test Tournament");
	await newTournament.form.fill(
		"description",
		"An automated test tournament",
	);
	await newTournament.setFirstDate(startTime);
	await newTournament.form.fill("discordInviteCode", "test-invite");

	// flip a tournament setting away from its default
	await newTournament.goToStep("teams");
	await newTournament.form.check("requireInGameNames");

	// "Organizer picked" allows an arbitrary map pool, unlike the validated team pick pools
	await newTournament.goToStep("maps");
	await newTournament.form.checkItems("mapPickingStyle", ["TO"]);
	await newTournament.pickMapPool(mapPool);

	await newTournament.addFollowUpBracket({
		name: "Underground bracket",
		format: "Single elimination",
		placements: [-1],
	});

	await newTournament.save();

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

	const tournamentInfo = new TournamentInfoPage(page);
	await tournamentInfo.goto(tournamentId);
	await expect(tournamentInfo.startTime(startTime).first()).toBeVisible();

	const brackets = new TournamentBracketsPage(page);
	await brackets.goto(tournamentId);
	await expect(brackets.bracketTab("Underground")).toBeVisible();

	// the TO map pool round-trips onto the rules page
	const rules = new TournamentRulesPage(page);
	await rules.goto(tournamentId);
	for (const { stage, mode } of mapPool) {
		await expect(rules.stageName(stage)).toBeVisible();
		await expect(rules.modeImage(mode).first()).toBeVisible();
	}
})

browser back returns to the previous step of the tournament form

Renders/calendar/new

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 “Test Tournament”
    await newTournament.form.fill("name", "Test Tournament");
  3. Fill Discord server invite URL with “test-invite”
    await newTournament.form.fill("discordInviteCode", "test-invite");
  4. Go to step “teams”
    await newTournament.goToStep("teams");
Assert
  1. step button “teams” has “aria-current” = “step”
    await expect(newTournament.stepButton("teams")).toHaveAttribute(
    	"aria-current",
    	"step",
    );
  2. the page URL is /step=teams/
    await expect(page).toHaveURL(/step=teams/);
Act
  1. Go back
    await page.goBack();
Assert
  1. step button “basics” has “aria-current” = “step”
    await expect(newTournament.stepButton("basics")).toHaveAttribute(
    	"aria-current",
    	"step",
    );
  2. name input has value “Test Tournament”
    await expect(newTournament.locators.nameInput).toHaveValue(
    	"Test Tournament",
    );
  3. discard changes button is not visible
    await isNotVisible(newTournament.locators.discardChangesButton);
Source
test("browser back returns to the previous step of the tournament form", 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", "Test Tournament");
	await newTournament.form.fill("discordInviteCode", "test-invite");
	await newTournament.goToStep("teams");
	await expect(newTournament.stepButton("teams")).toHaveAttribute(
		"aria-current",
		"step",
	);
	await expect(page).toHaveURL(/step=teams/);

	await page.goBack();

	await expect(newTournament.stepButton("basics")).toHaveAttribute(
		"aria-current",
		"step",
	);
	await expect(newTournament.locators.nameInput).toHaveValue(
		"Test Tournament",
	);
	await isNotVisible(newTournament.locators.discardChangesButton);
})

creates a team picked tournament with a custom map pool

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

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 “Team Pick Tournament”
    await newTournament.form.fill("name", "Team Pick 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 “AUTO”
    await newTournament.form.checkItems("mapPickingStyle", ["AUTO"]);
  6. Set team pick modes “Splat Zones”, “Tower Control”
    await newTournament.setTeamPickModes(["Splat Zones", "Tower Control"]);
  7. Fill team pick count input “Splat Zones” with “2”
    await newTournament.teamPickCountInput("Splat Zones").fill("2");
  8. Fill team pick count input “Tower Control” with “2”
    await newTournament.teamPickCountInput("Tower Control").fill("2");
  9. Check items team pick pool “CUSTOM”
    await newTournament.form.checkItems("teamPickPool", ["CUSTOM"]);
  10. stages = ["Scorch Gorge", "Eeltail Alley", "Hagglefish Market"]
    const stages = ["Scorch Gorge", "Eeltail Alley", "Hagglefish Market"];
  11. customPool = stages.flatMap((stage) => [ { stage, mode: "Splat Zones" }, { stage, mode: "T…
    const customPool = stages.flatMap((stage) => [
    	{ stage, mode: "Splat Zones" },
    	{ stage, mode: "Tower Control" },
    ]);
  12. every mode needs one stage more than the teams pick in it

    Pick map pool customPool.filter((map) => map.stage !== "Hagglefish Market")
    await newTournament.pickMapPool(
    	customPool.filter((map) => map.stage !== "Hagglefish Market"),
    );
Assert
  1. team pick pool status “SZ needs at least 3 stages (has 2)” is visible
    await expect(
    	newTournament.teamPickPoolStatus("SZ needs at least 3 stages (has 2)"),
    ).toBeVisible();
Act
  1. Pick map pool customPool.filter((map) => map.stage === "Hagglefish Market")
    await newTournament.pickMapPool(
    	customPool.filter((map) => map.stage === "Hagglefish Market"),
    );
Assert
  1. team pick pool status “The map pool has enough stages in every mode” is visible
    await expect(
    	newTournament.teamPickPoolStatus(
    		"The map pool has enough stages in every mode",
    	),
    ).toBeVisible();
Act
  1. Save
    await newTournament.save();
Assert
  1. the page URL is /\/to\/\d+/
    await expect(page).toHaveURL(/\/to\/\d+/);
Act
  1. tournamentId = Number(page.url().match(/\/to\/(\d+)/)?.[1])
    const tournamentId = Number(page.url().match(/\/to\/(\d+)/)?.[1]);
Tournament rules page
Act
  1. Open the tournament rules page tournamentId
    await rules.goto(tournamentId);
Assert
  1. rule “each team picking 2× SZ, 2× TC” is visible
    await expect(rules.rule("each team picking 2× SZ, 2× TC")).toBeVisible();
  2. rule “Maps are limited to the map pool above” is visible
    await expect(
    	rules.rule("Maps are limited to the map pool above"),
    ).toBeVisible();
  3. For each stage of stages
    for (const stage of stages) {
    	await expect(rules.stageName(stage)).toBeVisible();
    }
    1. stage name stage is visible
      await expect(rules.stageName(stage)).toBeVisible();
Source
test("creates a team picked tournament with a custom map pool", 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", "Team Pick Tournament");
	await newTournament.setFirstDate(new Date(2027, 0, 15, 17, 0));

	await newTournament.goToStep("maps");
	await newTournament.form.checkItems("mapPickingStyle", ["AUTO"]);
	await newTournament.setTeamPickModes(["Splat Zones", "Tower Control"]);
	await newTournament.teamPickCountInput("Splat Zones").fill("2");
	await newTournament.teamPickCountInput("Tower Control").fill("2");
	await newTournament.form.checkItems("teamPickPool", ["CUSTOM"]);

	const stages = ["Scorch Gorge", "Eeltail Alley", "Hagglefish Market"];
	const customPool = stages.flatMap((stage) => [
		{ stage, mode: "Splat Zones" },
		{ stage, mode: "Tower Control" },
	]);

	// every mode needs one stage more than the teams pick in it
	await newTournament.pickMapPool(
		customPool.filter((map) => map.stage !== "Hagglefish Market"),
	);
	await expect(
		newTournament.teamPickPoolStatus("SZ needs at least 3 stages (has 2)"),
	).toBeVisible();

	await newTournament.pickMapPool(
		customPool.filter((map) => map.stage === "Hagglefish Market"),
	);
	await expect(
		newTournament.teamPickPoolStatus(
			"The map pool has enough stages in every mode",
		),
	).toBeVisible();

	await newTournament.save();

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

	// the picked modes, counts and custom pool round-trip onto the rules page
	const rules = new TournamentRulesPage(page);
	await rules.goto(tournamentId);
	await expect(rules.rule("each team picking 2× SZ, 2× TC")).toBeVisible();
	await expect(
		rules.rule("Maps are limited to the map pool above"),
	).toBeVisible();
	for (const stage of stages) {
		await expect(rules.stageName(stage)).toBeVisible();
	}
})

copies a tournament keeping its follow-up bracket the same time after the start

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"],
    });
  2. copiedStartTime = startOfHour(subDays(new Date(), 7))
    const copiedStartTime = startOfHour(subDays(new Date(), 7));
  3. Create tournament author id organizer.id start times dateToDatabaseTimestamp(copiedStartTime) map picking style “AUTO” bracket progression name “Groups” type “round_robin” requires check in false settings teams per group 4, name “Final Stage” type “single_elimination” requires check in true start time dateToDatabaseTimestamp(addDays(copiedStartTime, 1)) settings sources bracket idx 0 placements 1, 2 → copied
    const copied = await factories.TournamentFactory.create({
    	authorId: organizer.id,
    	startTimes: [dateToDatabaseTimestamp(copiedStartTime)],
    	mapPickingStyle: "AUTO",
    	bracketProgression: [
    		{
    			name: "Groups",
    			type: "round_robin",
    			requiresCheckIn: false,
    			settings: { teamsPerGroup: 4 },
    		},
    		{
    			name: "Final Stage",
    			type: "single_elimination",
    			requiresCheckIn: true,
    			startTime: dateToDatabaseTimestamp(addDays(copiedStartTime, 1)),
    			settings: {},
    			sources: [{ bracketIdx: 0, placements: [1, 2] }],
    		},
    	],
    });
Oorganizer
Calendar new event page
Act
  1. Go to new tournament
    await newTournament.gotoNewTournament();
  2. Copy tournament copied.eventId
    await newTournament.copyTournament(copied.eventId);
  3. Fill Name with “Copied Tournament”
    await newTournament.form.fill("name", "Copied Tournament");
  4. Tournament start time → startTime
    const startTime = await newTournament.tournamentStartTime();
  5. followUpStartTime = addDays(startTime, 1)
    const followUpStartTime = addDays(startTime, 1);
  6. starting on another day than the tournament, the card shows the date too

    Go to step “format”
    await newTournament.goToStep("format");
Assert
  1. bracket card “Final Stage” contains new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", }).format(…
    await expect(newTournament.bracketCard("Final Stage")).toContainText(
    	new Intl.DateTimeFormat("en-US", {
    		month: "short",
    		day: "numeric",
    	}).format(followUpStartTime),
    );
Act
  1. Select bracket 1
    await newTournament.selectBracket(1);
Assert
  1. bracket start time input has value datetimeLocalValue(followUpStartTime)
    await expect(newTournament.locators.bracketStartTimeInput).toHaveValue(
    	datetimeLocalValue(followUpStartTime),
    );
Act
  1. Go to step “basics”
    await newTournament.goToStep("basics");
  2. Set first date addDays(startTime, 2)
    await newTournament.setFirstDate(addDays(startTime, 2));
  3. Go to step “prizes”
    await newTournament.goToStep("prizes");
Assert
  1. bracket start before tournament error is visible
    await expect(
    	newTournament.locators.bracketStartBeforeTournamentError,
    ).toBeVisible();
Act
  1. Fill bracket start time input with datetimeLocalValue(addDays(startTime, 3))
    await newTournament.locators.bracketStartTimeInput.fill(
    	datetimeLocalValue(addDays(startTime, 3)),
    );
  2. Save
    await newTournament.save();
Assert
  1. the page URL is /\/to\/\d+/
    await expect(page).toHaveURL(/\/to\/\d+/);
Source
test("copies a tournament keeping its follow-up bracket the same time after the start", async ({
	page,
	factories,
}) => {
	const organizer = await factories.UserFactory.create(null, {
		roles: ["TOURNAMENT_ORGANIZER"],
	});
	const copiedStartTime = startOfHour(subDays(new Date(), 7));
	const copied = await factories.TournamentFactory.create({
		authorId: organizer.id,
		startTimes: [dateToDatabaseTimestamp(copiedStartTime)],
		mapPickingStyle: "AUTO",
		bracketProgression: [
			{
				name: "Groups",
				type: "round_robin",
				requiresCheckIn: false,
				settings: { teamsPerGroup: 4 },
			},
			{
				name: "Final Stage",
				type: "single_elimination",
				requiresCheckIn: true,
				startTime: dateToDatabaseTimestamp(addDays(copiedStartTime, 1)),
				settings: {},
				sources: [{ bracketIdx: 0, placements: [1, 2] }],
			},
		],
	});

	await impersonate(page, organizer.id);

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

	await newTournament.form.fill("name", "Copied Tournament");
	const startTime = await newTournament.tournamentStartTime();
	const followUpStartTime = addDays(startTime, 1);

	// starting on another day than the tournament, the card shows the date too
	await newTournament.goToStep("format");
	await expect(newTournament.bracketCard("Final Stage")).toContainText(
		new Intl.DateTimeFormat("en-US", {
			month: "short",
			day: "numeric",
		}).format(followUpStartTime),
	);

	await newTournament.selectBracket(1);
	await expect(newTournament.locators.bracketStartTimeInput).toHaveValue(
		datetimeLocalValue(followUpStartTime),
	);

	await newTournament.goToStep("basics");
	await newTournament.setFirstDate(addDays(startTime, 2));
	await newTournament.goToStep("prizes");

	await expect(
		newTournament.locators.bracketStartBeforeTournamentError,
	).toBeVisible();

	await newTournament.locators.bracketStartTimeInput.fill(
		datetimeLocalValue(addDays(startTime, 3)),
	);
	await newTournament.save();

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

copies a tournament keeping registration open until the same time before the new start

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

Setup
Arrange
  1. Create user null roles “TOURNAMENT_ORGANIZER” → organizer
    const organizer = await factories.UserFactory.create(null, {
    	roles: ["TOURNAMENT_ORGANIZER"],
    });
  2. copiedStartTime = startOfHour(subDays(new Date(), 7))
    const copiedStartTime = startOfHour(subDays(new Date(), 7));
  3. Create tournament author id organizer.id start times dateToDatabaseTimestamp(copiedStartTime) map picking style “AUTO” reg closes at dateToDatabaseTimestamp(subHours(copiedStartTime, 1)) → copied
    const copied = await factories.TournamentFactory.create({
    	authorId: organizer.id,
    	startTimes: [dateToDatabaseTimestamp(copiedStartTime)],
    	mapPickingStyle: "AUTO",
    	regClosesAt: dateToDatabaseTimestamp(subHours(copiedStartTime, 1)),
    });
Oorganizer
Calendar new event page
Act
  1. Go to new tournament
    await newTournament.gotoNewTournament();
  2. Copy tournament copied.eventId
    await newTournament.copyTournament(copied.eventId);
  3. Fill Name with “Copied Tournament”
    await newTournament.form.fill("name", "Copied Tournament");
  4. Tournament start time → startTime
    const startTime = await newTournament.tournamentStartTime();
  5. Set first date addDays(startTime, 7)
    await newTournament.setFirstDate(addDays(startTime, 7));
  6. Save
    await newTournament.save();
Assert
  1. the page URL is /\/to\/\d+/
    await expect(page).toHaveURL(/\/to\/\d+/);
Act
  1. tournamentId = Number(page.url().match(/\/to\/(\d+)/)?.[1])
    const tournamentId = Number(page.url().match(/\/to\/(\d+)/)?.[1]);
Tournament register page
Act
  1. Open the tournament register page tournamentId
    await register.goto(tournamentId);
Assert
  1. save team button is visible
    await expect(register.locators.saveTeamButton).toBeVisible();
  2. registration closed alert is not visible
    await isNotVisible(register.locators.registrationClosedAlert);
Source
test("copies a tournament keeping registration open until the same time before the new start", async ({
	page,
	factories,
}) => {
	const organizer = await factories.UserFactory.create(null, {
		roles: ["TOURNAMENT_ORGANIZER"],
	});
	const copiedStartTime = startOfHour(subDays(new Date(), 7));
	const copied = await factories.TournamentFactory.create({
		authorId: organizer.id,
		startTimes: [dateToDatabaseTimestamp(copiedStartTime)],
		mapPickingStyle: "AUTO",
		regClosesAt: dateToDatabaseTimestamp(subHours(copiedStartTime, 1)),
	});

	await impersonate(page, organizer.id);

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

	await newTournament.form.fill("name", "Copied Tournament");
	const startTime = await newTournament.tournamentStartTime();
	await newTournament.setFirstDate(addDays(startTime, 7));
	await newTournament.save();

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

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

	await expect(register.locators.saveTeamButton).toBeVisible();
	await isNotVisible(register.locators.registrationClosedAlert);
})

reports winners of a past event

Renders/calendar/:id/calendar/:id/report-winners

Setup
Arrange
  1. Create calendar event author Sendou start times dateToDatabaseTimestamp(subDays(new Date(), 1)) → event
    const event = await factories.CalendarEventFactory.create({
    	authorId: ADMIN_ID,
    	startTimes: [dateToDatabaseTimestamp(subDays(new Date(), 1))],
    });
SSendou
Report winners page
Act
  1. Open the report winners page event.id
    await reportWinners.goto(event.id);
  2. Fill participant count input with “50”
    await reportWinners.locators.participantCountInput.fill("50");
  3. Fill team name input with “Team Olive”
    await reportWinners.locators.teamNameInput.fill("Team Olive");
  4. Fill placing input with “1”
    await reportWinners.locators.placingInput.fill("1");
  5. a team without any players can't be reported

    Click submit button
    await reportWinners.locators.submitButton.click();
Assert
  1. empty team error is visible
    await expect(reportWinners.locators.emptyTeamError).toBeVisible();
Act
  1. Select player 1, “N-ZAP”
    await reportWinners.selectPlayer(1, "N-ZAP");
  2. Fill player as text 2, “Player Without Account”
    await reportWinners.fillPlayerAsText(2, "Player Without Account");
  3. Submit
    await reportWinners.submit();
Assert
  1. the page URL is calendarEventPage(event.id)
    await expect(page).toHaveURL(calendarEventPage(event.id));
Calendar event page
Assert
  1. result row “Team Olive” contains “Player Without Account”
    await expect(calendarEvent.resultRow("Team Olive")).toContainText(
    	"Player Without Account",
    );
Report winners page
Act
  1. reported results are loaded back into the form for editing

    Open the report winners page event.id
    await reportWinners.goto(event.id);
Assert
  1. participant count input has value “50”
    await expect(reportWinners.locators.participantCountInput).toHaveValue(
    	"50",
    );
  2. team name input has value “Team Olive”
    await expect(reportWinners.locators.teamNameInput).toHaveValue(
    	"Team Olive",
    );
  3. placing input has value “1”
    await expect(reportWinners.locators.placingInput).toHaveValue("1");
  4. player 1 contains “N-ZAP”
    await expect(reportWinners.player(1)).toContainText("N-ZAP");
  5. player 2 has value “Player Without Account”
    await expect(reportWinners.player(2)).toHaveValue("Player Without Account");
Source
test("reports winners of a past event", async ({ page, factories }) => {
	const event = await factories.CalendarEventFactory.create({
		authorId: ADMIN_ID,
		startTimes: [dateToDatabaseTimestamp(subDays(new Date(), 1))],
	});

	await impersonate(page);

	const reportWinners = new ReportWinnersPage(page);
	await reportWinners.goto(event.id);

	await reportWinners.locators.participantCountInput.fill("50");
	await reportWinners.locators.teamNameInput.fill("Team Olive");
	await reportWinners.locators.placingInput.fill("1");

	// a team without any players can't be reported
	await reportWinners.locators.submitButton.click();
	await expect(reportWinners.locators.emptyTeamError).toBeVisible();

	await reportWinners.selectPlayer(1, "N-ZAP");
	await reportWinners.fillPlayerAsText(2, "Player Without Account");

	await reportWinners.submit();

	await expect(page).toHaveURL(calendarEventPage(event.id));

	const calendarEvent = new CalendarEventPage(page);
	await expect(calendarEvent.resultRow("Team Olive")).toContainText(
		"Player Without Account",
	);

	// reported results are loaded back into the form for editing
	await reportWinners.goto(event.id);

	await expect(reportWinners.locators.participantCountInput).toHaveValue(
		"50",
	);
	await expect(reportWinners.locators.teamNameInput).toHaveValue(
		"Team Olive",
	);
	await expect(reportWinners.locators.placingInput).toHaveValue("1");
	await expect(reportWinners.player(1)).toContainText("N-ZAP");
	await expect(reportWinners.player(2)).toHaveValue("Player Without Account");
})