Skip to content

Team · E2E tests

0 modules · 0 exports · 1 repository · 14 e2e tests · 7 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

New team creation

creates new team

Renders//t/:customUrl/t/:customUrl/t/new

NN-ZAP
Act
  1. Go to “/”
    await navigate({ page, url: "/" });
Anything adder
Act
  1. Add “team”
    await new AnythingAdder(page).add("team");
Assert
  1. the page URL is /t\/new/
    await expect(page).toHaveURL(/t\/new/);
New team page
Act
  1. Fill Name with “Chimera”
    await newTeam.form.fill("name", "Chimera");
  2. Submit
    await newTeam.form.submit();
Assert
  1. the page URL is /chimera/
    await expect(page).toHaveURL(/chimera/);
Source
test("creates new team", async ({ page }) => {
	await impersonate(page, NZAP_TEST_ID);
	await navigate({ page, url: "/" });

	await new AnythingAdder(page).add("team");

	await expect(page).toHaveURL(/t\/new/);

	const newTeam = new NewTeamPage(page);
	await newTeam.form.fill("name", "Chimera");
	await newTeam.form.submit();

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

Team page

edit team info

Renders/t/:customUrl/t/:customUrl/t/:customUrl/edit

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID],
    });
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open edit → teamEdit
    const teamEdit = await team.openEdit();
Team edit page
Act
  1. Fill Name with “Better Alliance Rogue”
    await teamEdit.form.fill("name", "Better Alliance Rogue");
  2. Fill Team Bluesky with “BetterAllianceRogue”
    await teamEdit.form.fill("bsky", "BetterAllianceRogue");
  3. Fill Bio with “shorter bio”
    await teamEdit.form.fill("bio", "shorter bio");
  4. Submit
    await teamEdit.form.submit();
Assert
  1. the page URL is /better-alliance-rogue/
    await expect(page).toHaveURL(/better-alliance-rogue/);
Team page
Assert
  1. bio reads “shorter bio”
    await expect(team.locators.bio).toHaveText("shorter bio");
  2. bsky link has “href” = “https://bsky.app/profile/BetterAllianceRogue”
    await expect(team.locators.bskyLink).toHaveAttribute(
    	"href",
    	"https://bsky.app/profile/BetterAllianceRogue",
    );
Source
test("edit team info", async ({ page, factories }) => {
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID],
	});

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const teamEdit = await team.openEdit();
	await teamEdit.form.fill("name", "Better Alliance Rogue");
	await teamEdit.form.fill("bsky", "BetterAllianceRogue");
	await teamEdit.form.fill("bio", "shorter bio");
	await teamEdit.form.submit();

	await expect(page).toHaveURL(/better-alliance-rogue/);
	await expect(team.locators.bio).toHaveText("shorter bio");
	await expect(team.locators.bskyLink).toHaveAttribute(
		"href",
		"https://bsky.app/profile/BetterAllianceRogue",
	);
})

edits and removes team map preferences

Renders/t/:customUrl/t/:customUrl/edit

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID],
    });
SSendou
Team edit page
Act
  1. Open the team edit page customUrl
    await teamEdit.goto(customUrl);
Assert
  1. a team without preferences has nothing to remove

    remove map preferences button is not visible
    await isNotVisible(teamEdit.locators.removeMapPreferencesButton);
Act
  1. Set mode preference “SZ”, “Prefer”
    await teamEdit.maps.setModePreference("SZ", "Prefer");
  2. Set mode preference “TW”, “Avoid”
    await teamEdit.maps.setModePreference("TW", "Avoid");
  3. Select mode tab “SZ”
    await teamEdit.maps.selectModeTab("SZ");
  4. Click map button “SZ”, 1
    await teamEdit.maps.mapButton("SZ", 1).click();
  5. Save map preferences
    await teamEdit.saveMapPreferences();
  6. reload so the form shows what was actually persisted

    Open the team edit page customUrl
    await teamEdit.goto(customUrl);
Assert
  1. preference radio “SZ”, “Prefer” is checked
    await expect(teamEdit.maps.preferenceRadio("SZ", "Prefer")).toBeChecked();
  2. preference radio “TW”, “Avoid” is checked
    await expect(teamEdit.maps.preferenceRadio("TW", "Avoid")).toBeChecked();
Act
  1. Select mode tab “SZ”
    await teamEdit.maps.selectModeTab("SZ");
Assert
  1. map button “SZ”, 1 has class SELECTED_MAP_CLASS
    await expect(teamEdit.maps.mapButton("SZ", 1)).toHaveClass(
    	SELECTED_MAP_CLASS,
    );
Act
  1. Remove map preferences
    await teamEdit.removeMapPreferences();
Assert
  1. remove map preferences button is not visible
    await isNotVisible(teamEdit.locators.removeMapPreferencesButton);
  2. the form re-syncs in place, so a following save can't restore what was removed

    preference radio “SZ”, “Neutral” is checked
    await expect(teamEdit.maps.preferenceRadio("SZ", "Neutral")).toBeChecked();
  3. preference radio “TW”, “Neutral” is checked
    await expect(teamEdit.maps.preferenceRadio("TW", "Neutral")).toBeChecked();
Act
  1. Select mode tab “SZ”
    await teamEdit.maps.selectModeTab("SZ");
Assert
  1. map button “SZ”, 1 does not have class SELECTED_MAP_CLASS
    await expect(teamEdit.maps.mapButton("SZ", 1)).not.toHaveClass(
    	SELECTED_MAP_CLASS,
    );
Act
  1. Open the team edit page customUrl
    await teamEdit.goto(customUrl);
Assert
  1. preference radio “SZ”, “Neutral” is checked
    await expect(teamEdit.maps.preferenceRadio("SZ", "Neutral")).toBeChecked();
  2. remove map preferences button is not visible
    await isNotVisible(teamEdit.locators.removeMapPreferencesButton);
Source
test("edits and removes team map preferences", async ({
	page,
	factories,
}) => {
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID],
	});

	await impersonate(page, ADMIN_ID);

	const teamEdit = new TeamEditPage(page);
	await teamEdit.goto(customUrl);

	// a team without preferences has nothing to remove
	await isNotVisible(teamEdit.locators.removeMapPreferencesButton);

	await teamEdit.maps.setModePreference("SZ", "Prefer");
	await teamEdit.maps.setModePreference("TW", "Avoid");
	await teamEdit.maps.selectModeTab("SZ");
	await teamEdit.maps.mapButton("SZ", 1).click();
	await teamEdit.saveMapPreferences();

	// reload so the form shows what was actually persisted
	await teamEdit.goto(customUrl);
	await expect(teamEdit.maps.preferenceRadio("SZ", "Prefer")).toBeChecked();
	await expect(teamEdit.maps.preferenceRadio("TW", "Avoid")).toBeChecked();
	await teamEdit.maps.selectModeTab("SZ");
	await expect(teamEdit.maps.mapButton("SZ", 1)).toHaveClass(
		SELECTED_MAP_CLASS,
	);

	await teamEdit.removeMapPreferences();
	await isNotVisible(teamEdit.locators.removeMapPreferencesButton);

	// the form re-syncs in place, so a following save can't restore what was removed
	await expect(teamEdit.maps.preferenceRadio("SZ", "Neutral")).toBeChecked();
	await expect(teamEdit.maps.preferenceRadio("TW", "Neutral")).toBeChecked();
	await teamEdit.maps.selectModeTab("SZ");
	await expect(teamEdit.maps.mapButton("SZ", 1)).not.toHaveClass(
		SELECTED_MAP_CLASS,
	);

	await teamEdit.goto(customUrl);
	await expect(teamEdit.maps.preferenceRadio("SZ", "Neutral")).toBeChecked();
	await isNotVisible(teamEdit.locators.removeMapPreferencesButton);
})

kicks a member & changes a role

Renders/t/:customUrl/t/:customUrl/t/:customUrl/roster

Setup
Arrange
  1. Create full team → { customUrl }
    const { customUrl } = await createFullTeam(factories);
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
Assert
  1. owner badge Sendou is visible
    await expect(team.ownerBadge(ADMIN_ID)).toBeVisible();
Act
  1. Open manage roster → roster
    const roster = await team.openManageRoster();
Team roster page
Act
  1. Select role “SUPPORT”
    await roster.memberRow(0).selectRole("SUPPORT");
  2. Member row ROSTER_SIZE - 1 → lastMember
    const lastMember = roster.memberRow(ROSTER_SIZE - 1);
Member row
Assert
  1. row is visible
    await expect(lastMember.locators.row).toBeVisible();
Act
  1. Remove
    await lastMember.remove();
Assert
  1. row is not visible
    await isNotVisible(lastMember.locators.row);
Team roster page
Act
  1. Save
    await roster.save();
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
Assert
  1. member role 0 reads “Support”
    await expect(team.memberRole(0)).toHaveText("Support");
Source
test("kicks a member & changes a role", async ({ page, factories }) => {
	const { customUrl } = await createFullTeam(factories);

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	await expect(team.ownerBadge(ADMIN_ID)).toBeVisible();

	const roster = await team.openManageRoster();
	await roster.memberRow(0).selectRole("SUPPORT");

	const lastMember = roster.memberRow(ROSTER_SIZE - 1);
	await expect(lastMember.locators.row).toBeVisible();
	await lastMember.remove();
	await isNotVisible(lastMember.locators.row);

	await roster.save();

	await team.goto(customUrl);

	await expect(team.memberRole(0)).toHaveText("Support");
})

sets a custom role for a member

Renders/t/:customUrl/t/:customUrl/t/:customUrl/roster

Setup
Arrange
  1. Create full team → { customUrl }
    const { customUrl } = await createFullTeam(factories);
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open manage roster → roster
    const roster = await team.openManageRoster();
Team roster page
Act
  1. Set custom role “Strategist”, “OTHER”
    await roster.memberRow(1).setCustomRole("Strategist", "OTHER");
  2. Save
    await roster.save();
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. custom role is classified as "OTHER" so it lives under the "Other" tab

    Click other roles tab
    await team.locators.otherRolesTab.click();
Assert
  1. custom role “Strategist” is visible
    await expect(team.customRole("Strategist")).toBeVisible();
Source
test("sets a custom role for a member", async ({ page, factories }) => {
	const { customUrl } = await createFullTeam(factories);

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const roster = await team.openManageRoster();
	await roster.memberRow(1).setCustomRole("Strategist", "OTHER");
	await roster.save();

	await team.goto(customUrl);

	// custom role is classified as "OTHER" so it lives under the "Other" tab
	await team.locators.otherRolesTab.click();
	await expect(team.customRole("Strategist")).toBeVisible();
})

reorders members via move buttons

Renders/t/:customUrl/t/:customUrl/t/:customUrl/roster

Setup
Arrange
  1. Create full team → { customUrl }
    const { customUrl } = await createFullTeam(factories);
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open manage roster → roster
    const roster = await team.openManageRoster();
Team roster page
Act
  1. Member row 0 → firstRow
    const firstRow = roster.memberRow(0);
  2. Member row 1 → secondRow
    const secondRow = roster.memberRow(1);
  3. Member row ROSTER_SIZE - 1 → lastRow
    const lastRow = roster.memberRow(ROSTER_SIZE - 1);
Member row
Act
  1. Inner text → firstName
    const firstName = await firstRow.locators.username.innerText();
  2. Inner text → secondName
    const secondName = await secondRow.locators.username.innerText();
Assert
  1. firstName is not secondName
    expect(firstName).not.toBe(secondName);
  2. the first member can't move up and the last can't move down

    move up button is disabled
    await expect(firstRow.locators.moveUpButton).toBeDisabled();
  3. move down button is disabled
    await expect(lastRow.locators.moveDownButton).toBeDisabled();
Act
  1. Move down
    await firstRow.moveDown();
Assert
  1. username reads secondName
    await expect(firstRow.locators.username).toHaveText(secondName);
  2. username reads firstName
    await expect(secondRow.locators.username).toHaveText(firstName);
Team roster page
Act
  1. Save
    await roster.save();
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open manage roster
    await team.openManageRoster();
Member row
Assert
  1. username reads secondName
    await expect(firstRow.locators.username).toHaveText(secondName);
Source
test("reorders members via move buttons", async ({ page, factories }) => {
	const { customUrl } = await createFullTeam(factories);

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const roster = await team.openManageRoster();
	const firstRow = roster.memberRow(0);
	const secondRow = roster.memberRow(1);
	const lastRow = roster.memberRow(ROSTER_SIZE - 1);

	const firstName = await firstRow.locators.username.innerText();
	const secondName = await secondRow.locators.username.innerText();
	expect(firstName).not.toBe(secondName);

	// the first member can't move up and the last can't move down
	await expect(firstRow.locators.moveUpButton).toBeDisabled();
	await expect(lastRow.locators.moveDownButton).toBeDisabled();

	await firstRow.moveDown();

	await expect(firstRow.locators.username).toHaveText(secondName);
	await expect(secondRow.locators.username).toHaveText(firstName);

	await roster.save();

	await team.goto(customUrl);
	await team.openManageRoster();

	await expect(firstRow.locators.username).toHaveText(secondName);
})

shows a finalized tournament placement on the results page

Renders/t/:customUrl/t/:customUrl/t/:customUrl/results

Setup
Arrange
  1. Create ROSTER_SIZE - 1 × user → teammates
    const teammates = await factories.UserFactory.createMany(ROSTER_SIZE - 1);
  2. memberUserIds = [ ADMIN_ID, ...teammates.map((teammate) => teammate.id), ]
    const memberUserIds = [
    	ADMIN_ID,
    	...teammates.map((teammate) => teammate.id),
    ];
  3. Create team name “Alliance Rogue” memberUserIds → { id: teamId, customUrl }
    const { id: teamId, customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds,
    });
  4. Create tournament author Sendou name “In The Zone 30” → tournament
    const tournament = await factories.TournamentFactory.create({
    	authorId: ADMIN_ID,
    	name: TOURNAMENT_NAME,
    });
  5. Create tournament team tournament id tournament.id memberUserIds team name “Alliance Rogue” prefers not to host 0 teamId is checked in true → linkedTournamentTeam
    const linkedTournamentTeam = await factories.TournamentTeamFactory.create(
    	{
    		tournamentId: tournament.id,
    		memberUserIds,
    		team: { name: TEAM_NAME, prefersNotToHost: 0, teamId },
    	},
    	{ isCheckedIn: true },
    );
  6. Create 4 × user → opponents
    const opponents = await factories.UserFactory.createMany(ROSTER_SIZE);
  7. Create tournament team tournament id tournament.id member user ids opponents.map((opponent) => opponent.id) is checked in true
    await factories.TournamentTeamFactory.create(
    	{
    		tournamentId: tournament.id,
    		memberUserIds: opponents.map((opponent) => opponent.id),
    	},
    	{ isCheckedIn: true },
    );
  8. Play out tournament tournament.id “all” → matches
    const matches = await factories.TournamentFactory.playOut(
    	tournament.id,
    	"all",
    );
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open results → results
    const results = await team.openResults();
Assert
  1. the page URL is /\/results/
    await expect(page).toHaveURL(/\/results/);
  2. wonTheFinal = matches.some( (match) => match.winnerTeamId === linkedTournamentTeam.id, )
    const wonTheFinal = matches.some(
    	(match) => match.winnerTeamId === linkedTournamentTeam.id,
    );
Team results page
Assert
  1. result row “In The Zone 30” contains “/ 2”
    await expect(results.resultRow(TOURNAMENT_NAME)).toContainText("/ 2");
  2. placement “In The Zone 30”, wonTheFinal ? "1st" : "2nd" is visible
    await expect(
    	results.placement(TOURNAMENT_NAME, wonTheFinal ? "1st" : "2nd"),
    ).toBeVisible();
Source
test("shows a finalized tournament placement on the results page", async ({
	page,
	factories,
}) => {
	const teammates = await factories.UserFactory.createMany(ROSTER_SIZE - 1);
	const memberUserIds = [
		ADMIN_ID,
		...teammates.map((teammate) => teammate.id),
	];
	const { id: teamId, customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds,
	});

	const tournament = await factories.TournamentFactory.create({
		authorId: ADMIN_ID,
		name: TOURNAMENT_NAME,
	});
	const linkedTournamentTeam = await factories.TournamentTeamFactory.create(
		{
			tournamentId: tournament.id,
			memberUserIds,
			team: { name: TEAM_NAME, prefersNotToHost: 0, teamId },
		},
		{ isCheckedIn: true },
	);
	const opponents = await factories.UserFactory.createMany(ROSTER_SIZE);
	await factories.TournamentTeamFactory.create(
		{
			tournamentId: tournament.id,
			memberUserIds: opponents.map((opponent) => opponent.id),
		},
		{ isCheckedIn: true },
	);
	const matches = await factories.TournamentFactory.playOut(
		tournament.id,
		"all",
	);

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const results = await team.openResults();
	await expect(page).toHaveURL(/\/results/);

	const wonTheFinal = matches.some(
		(match) => match.winnerTeamId === linkedTournamentTeam.id,
	);
	await expect(results.resultRow(TOURNAMENT_NAME)).toContainText("/ 2");
	await expect(
		results.placement(TOURNAMENT_NAME, wonTheFinal ? "1st" : "2nd"),
	).toBeVisible();
})

deletes team

Renders//t/:customUrl/t/:customUrl

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID],
    });
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open actions menu
    await team.openActionsMenu();
  3. Delete
    await team.delete();
Assert
  1. the page URL is not new RegExp(customUrl)
    await expect(page).not.toHaveURL(new RegExp(customUrl));
Source
test("deletes team", async ({ page, factories }) => {
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID],
	});

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	await team.openActionsMenu();
	await team.delete();

	await expect(page).not.toHaveURL(new RegExp(customUrl));
})

resets invite code, joins team, leaves, rejoins

SendouN-ZAPteam.spec.ts:279

Renders/t/:customUrl/t/:customUrl/t/:customUrl/join/t/:customUrl/roster

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID],
    });
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open manage roster → roster
    const roster = await team.openManageRoster();
Team roster page
Act
  1. Invite link → oldInviteLink
    const oldInviteLink = await roster.inviteLink();
  2. Reset invite link
    await roster.resetInviteLink();
Assert
  1. invite link does not read oldInviteLink
    await expect(roster.locators.inviteLink).not.toHaveText(oldInviteLink);
Act
  1. Invite link → newInviteLink
    const newInviteLink = await roster.inviteLink();
NN-ZAP
Join team page
Act
  1. Open the join team page newInviteLink
    await join.goto(newInviteLink);
  2. Join
    await join.join();
Team page
Act
  1. Open actions menu
    await team.openActionsMenu();
  2. Leave
    await team.leave();
Join team page
Act
  1. Open the join team page newInviteLink
    await join.goto(newInviteLink);
  2. Join
    await join.join();
Team page
Act
  1. Open actions menu
    await team.openActionsMenu();
Assert
  1. leave team button is visible
    await expect(team.locators.leaveTeamButton).toBeVisible();
Source
test("resets invite code, joins team, leaves, rejoins", async ({
	page,
	factories,
}) => {
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID],
	});

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const roster = await team.openManageRoster();
	const oldInviteLink = await roster.inviteLink();

	await roster.resetInviteLink();

	await expect(roster.locators.inviteLink).not.toHaveText(oldInviteLink);
	const newInviteLink = await roster.inviteLink();

	await impersonate(page, NZAP_TEST_ID);

	const join = new JoinTeamPage(page);
	await join.goto(newInviteLink);
	await join.join();

	await team.openActionsMenu();
	await team.leave();

	await join.goto(newInviteLink);
	await join.join();

	await team.openActionsMenu();
	await expect(team.locators.leaveTeamButton).toBeVisible();
})

joins a secondary team, makes main team & leaves making the seconary team the main one

secondary team ownerSendouteam.spec.ts:317

Renders/t/:customUrl/t/:customUrl/t/:customUrl/join/t/:customUrl/roster/u/:identifier/u/:identifier

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou
    await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID],
    });
  2. Create user → secondaryTeamOwner
    const secondaryTeamOwner = await factories.UserFactory.create();
  3. Create team name “Team Olive” member user ids secondaryTeamOwner.id → { customUrl: secondaryCustomUrl }
    const { customUrl: secondaryCustomUrl } =
    	await factories.TeamFactory.create({
    		name: SECONDARY_TEAM_NAME,
    		memberUserIds: [secondaryTeamOwner.id],
    	});
STsecondary team owner
Team page
Act
  1. Open the team page secondaryCustomUrl
    await secondaryTeam.goto(secondaryCustomUrl);
  2. Open manage roster → roster
    const roster = await secondaryTeam.openManageRoster();
Team roster page
Act
  1. Invite link → inviteLink
    const inviteLink = await roster.inviteLink();
SSendou
Join team page
Act
  1. Open the join team page inviteLink
    await join.goto(inviteLink);
  2. Join
    await join.join();
Team page
Act
  1. Open actions menu
    await secondaryTeam.openActionsMenu();
  2. Make main team
    await secondaryTeam.makeMainTeam();
User page
Act
  1. Open the user page Sendou
    await user.goto(ADMIN_ID);
Assert
  1. team links count is 2
    await expect(user.teamLinks()).toHaveCount(2);
  2. first team links does not contain “Alliance Rogue”
    await expect(user.teamLinks().first()).not.toContainText(TEAM_NAME);
Act
  1. Open main team → mainTeam
    const mainTeam = await user.openMainTeam();
Team page
Act
  1. Open actions menu
    await mainTeam.openActionsMenu();
Assert
  1. main team indicator is visible
    await expect(mainTeam.locators.mainTeamIndicator).toBeVisible();
Act
  1. Leave
    await mainTeam.leave();
User page
Act
  1. Open the user page Sendou
    await user.goto(ADMIN_ID);
Assert
  1. team links count is 1
    await expect(user.teamLinks()).toHaveCount(1);
  2. first team links contains “Alliance Rogue”
    await expect(user.teamLinks().first()).toContainText(TEAM_NAME);
Source
test("joins a secondary team, makes main team & leaves making the seconary team the main one", async ({
	page,
	factories,
}) => {
	await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID],
	});
	const secondaryTeamOwner = await factories.UserFactory.create();
	const { customUrl: secondaryCustomUrl } =
		await factories.TeamFactory.create({
			name: SECONDARY_TEAM_NAME,
			memberUserIds: [secondaryTeamOwner.id],
		});

	await impersonate(page, secondaryTeamOwner.id);

	const secondaryTeam = new TeamPage(page);
	await secondaryTeam.goto(secondaryCustomUrl);

	const roster = await secondaryTeam.openManageRoster();
	const inviteLink = await roster.inviteLink();

	await impersonate(page, ADMIN_ID);

	const join = new JoinTeamPage(page);
	await join.goto(inviteLink);
	await join.join();

	await secondaryTeam.openActionsMenu();
	await secondaryTeam.makeMainTeam();

	const user = new UserPage(page);
	await user.goto(ADMIN_ID);

	await expect(user.teamLinks()).toHaveCount(2);
	await expect(user.teamLinks().first()).not.toContainText(TEAM_NAME);

	const mainTeam = await user.openMainTeam();

	await mainTeam.openActionsMenu();
	await expect(mainTeam.locators.mainTeamIndicator).toBeVisible();
	await mainTeam.leave();

	await user.goto(ADMIN_ID);

	await expect(user.teamLinks()).toHaveCount(1);
	await expect(user.teamLinks().first()).toContainText(TEAM_NAME);
})

makes another user editor, who can edit the page & becomes owner after the original leaves

SendouN-ZAPteam.spec.ts:367

Renders/t/:customUrl/t/:customUrl/t/:customUrl/edit/t/:customUrl/roster

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou, N-ZAP → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID, NZAP_TEST_ID],
    });
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open manage roster → roster
    const roster = await team.openManageRoster();
Team roster page
Act
  1. the owner has no editor toggle, so the first one belongs to N-ZAP

    Make editor 0
    await roster.makeEditor(0);
  2. Save
    await roster.save();
NN-ZAP
Team edit page
Act
  1. Open the team edit page customUrl
    await teamEdit.goto(customUrl);
  2. Fill Bio with “from editor”
    await teamEdit.form.fill("bio", "from editor");
  3. Submit
    await teamEdit.form.submit();
Assert
  1. the page URL is new RegExp(customUrl)
    await expect(page).toHaveURL(new RegExp(customUrl));
Team page
Assert
  1. bio reads “from editor”
    await expect(team.locators.bio).toHaveText("from editor");
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open actions menu
    await team.openActionsMenu();
  3. Start leaving
    await team.startLeaving();
Assert
  1. confirm dialog contains “New owner will be N-ZAP”
    await expect(team.locators.confirmDialog).toContainText(
    	"New owner will be N-ZAP",
    );
Act
  1. Confirm leaving
    await team.confirmLeaving();
Assert
  1. owner badge N-ZAP is visible
    await expect(team.ownerBadge(NZAP_TEST_ID)).toBeVisible();
  2. actions menu button is not visible
    await isNotVisible(team.locators.actionsMenuButton);
Source
test("makes another user editor, who can edit the page & becomes owner after the original leaves", async ({
	page,
	factories,
}) => {
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID, NZAP_TEST_ID],
	});

	await impersonate(page, ADMIN_ID);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const roster = await team.openManageRoster();
	// the owner has no editor toggle, so the first one belongs to N-ZAP
	await roster.makeEditor(0);
	await roster.save();

	await impersonate(page, NZAP_TEST_ID);

	const teamEdit = new TeamEditPage(page);
	await teamEdit.goto(customUrl);
	await teamEdit.form.fill("bio", "from editor");
	await teamEdit.form.submit();

	await expect(page).toHaveURL(new RegExp(customUrl));
	await expect(team.locators.bio).toHaveText("from editor");

	await impersonate(page, ADMIN_ID);
	await team.goto(customUrl);

	await team.openActionsMenu();
	await team.startLeaving();
	await expect(team.locators.confirmDialog).toContainText(
		"New owner will be N-ZAP",
	);
	await team.confirmLeaving();

	await expect(team.ownerBadge(NZAP_TEST_ID)).toBeVisible();
	await isNotVisible(team.locators.actionsMenuButton);
})

Team schedule

member sees the heatmap, the grid states and playable windows

Renders/t/:customUrl/t/:customUrl/t/:customUrl/schedule

Setup
Arrange
  1. Create user discord name “Schedules-Later” → noScheduleMember
    const noScheduleMember = await factories.UserFactory.create({
    	discordName: "Schedules-Later",
    });
  2. Create team name “Alliance Rogue” member user ids Sendou, N-ZAP, noScheduleMember.id → { id: teamId, customUrl }
    const { id: teamId, customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID, NZAP_TEST_ID, noScheduleMember.id],
    });
  3. { startsAt } = weekRange()
    const { startsAt } = weekRange();
  4. Create availability week user Sendou week starts at startsAt timezone MACHINE_TIMEZONE slots daySlot(WEDNESDAY, "18:00", "22:00"), daySlot(THURSDAY, "00:30", "02:00") day notes date weekDates()[WEDNESDAY] text “Leaving early”
    await factories.AvailabilityWeekFactory.create({
    	userId: ADMIN_ID,
    	weekStartsAt: startsAt,
    	timezone: MACHINE_TIMEZONE,
    	// the small-hours slot guards day bucketing: on machines off UTC it
    	// falls on another UTC day, so it moves columns if the server ignores
    	// the viewer's timezone
    	slots: [
    		daySlot(WEDNESDAY, "18:00", "22:00"),
    		daySlot(THURSDAY, "00:30", "02:00"),
    	],
    	dayNotes: [{ date: weekDates()[WEDNESDAY], text: "Leaving early" }],
    });
  5. Create availability week user N-ZAP week starts at startsAt timezone MACHINE_TIMEZONE slots daySlot(WEDNESDAY, "19:00", "23:00")
    await factories.AvailabilityWeekFactory.create({
    	userId: NZAP_TEST_ID,
    	weekStartsAt: startsAt,
    	timezone: MACHINE_TIMEZONE,
    	slots: [daySlot(WEDNESDAY, "19:00", "23:00")],
    });
  6. a commitment late in the shared Wednesday evening: renders as a busy block beside the reported time in the grid and trims the heatmap's effective availability without removing the window

    Create team event teamId author Sendou name “VoD review” ...daySlot(WEDNESDAY, "22:00", "23:30")
    await factories.TeamEventFactory.create({
    	teamId,
    	authorId: ADMIN_ID,
    	name: "VoD review",
    	...daySlot(WEDNESDAY, "22:00", "23:30"),
    });
SSendou
Team page
Act
  1. Open the team page customUrl
    await team.goto(customUrl);
  2. Open schedule → schedule
    const schedule = await team.openSchedule();
Team schedule page
Assert
  1. heatmap is the default view: two share Wed 19-22, one is also free Wed 18-19 and Thu 1-2

    heatmap is visible
    await expect(schedule.locators.heatmap).toBeVisible();
  2. heatmap cells 2 count is 3
    await expect(schedule.heatmapCells(2)).toHaveCount(3);
  3. heatmap cells 1 count is 2
    await expect(schedule.heatmapCells(1)).toHaveCount(2);
  4. the count shade must actually paint: an equal-specificity base background once blanked the whole grid

    heatmap cell background 2 is not schedule.heatmapCellBackground(0)
    expect(await schedule.heatmapCellBackground(2)).not.toBe(
    	await schedule.heatmapCellBackground(0),
    );
  5. heatmap unreported contains “Schedules-Later”
    await expect(schedule.locators.heatmapUnreported).toContainText(
    	"Schedules-Later",
    );
  6. day dot 2 is visible
    await expect(schedule.dayDot(WEDNESDAY)).toBeVisible();
Act
  1. hovering a block names who is free then and who has no schedule

    Hover first heatmap cells 2
    await schedule.heatmapCells(2).first().hover();
Assert
  1. heatmap tooltip contains “2/3”
    await expect(schedule.locators.heatmapTooltip).toContainText("2/3");
  2. heatmap tooltip contains “N-ZAP”
    await expect(schedule.locators.heatmapTooltip).toContainText("N-ZAP");
  3. heatmap tooltip contains “Schedules-Later”
    await expect(schedule.locators.heatmapTooltip).toContainText(
    	"Schedules-Later",
    );
Act
  1. dropping the member without a schedule from the count clears the nudge about them

    Click member chip noScheduleMember.id
    await schedule.memberChip(noScheduleMember.id).click();
Assert
  1. heatmap unreported is not visible
    await isNotVisible(schedule.locators.heatmapUnreported);
Act
  1. Click member chip noScheduleMember.id
    await schedule.memberChip(noScheduleMember.id).click();
  2. Click grid view tab
    await schedule.locators.gridViewTab.click();
Assert
  1. grid is visible
    await expect(schedule.locators.grid).toBeVisible();
  2. cell range Sendou, 2 is visible
    await expect(schedule.cellRange(ADMIN_ID, WEDNESDAY)).toBeVisible();
  3. cell range Sendou, 3 is visible
    await expect(schedule.cellRange(ADMIN_ID, THURSDAY)).toBeVisible();
  4. cell Sendou, 0 reads “—”
    await expect(schedule.cell(ADMIN_ID, 0)).toHaveText("—");
  5. cell noScheduleMember.id, 0 reads “?”
    await expect(schedule.cell(noScheduleMember.id, 0)).toHaveText("?");
  6. the grid shows the reported 19-23 in full, the commitment named beside it with its own time rather than cut out of it

    cell range N-ZAP, 2 contains “11:00”
    await expect(schedule.cellRange(NZAP_TEST_ID, WEDNESDAY)).toContainText(
    	"11:00",
    );
  7. cell busy N-ZAP, 2 contains “VoD review”
    await expect(schedule.cellBusy(NZAP_TEST_ID, WEDNESDAY)).toContainText(
    	"VoD review",
    );
  8. cell busy N-ZAP, 2 contains “10:00”
    await expect(schedule.cellBusy(NZAP_TEST_ID, WEDNESDAY)).toContainText(
    	"10:00",
    );
  9. notes contains “Leaving early”
    await expect(schedule.locators.notes).toContainText("Leaving early");
  10. two members share Wed 19-22 while the third has no schedule, so the only playable window is the one-short tier

    windows reads /Wed/
    await expect(schedule.locators.windows).toHaveText(/Wed/);
  11. day dot 2 is visible
    await expect(schedule.dayDot(WEDNESDAY)).toBeVisible();
  12. day dot 0 is not visible
    await isNotVisible(schedule.dayDot(0));
  13. team events contains “VoD review”
    await expect(schedule.locators.teamEvents).toContainText("VoD review");
Source
test("member sees the heatmap, the grid states and playable windows", async ({
	page,
	factories,
}) => {
	const noScheduleMember = await factories.UserFactory.create({
		discordName: "Schedules-Later",
	});
	const { id: teamId, customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID, NZAP_TEST_ID, noScheduleMember.id],
	});

	const { startsAt } = weekRange();
	await factories.AvailabilityWeekFactory.create({
		userId: ADMIN_ID,
		weekStartsAt: startsAt,
		timezone: MACHINE_TIMEZONE,
		// the small-hours slot guards day bucketing: on machines off UTC it
		// falls on another UTC day, so it moves columns if the server ignores
		// the viewer's timezone
		slots: [
			daySlot(WEDNESDAY, "18:00", "22:00"),
			daySlot(THURSDAY, "00:30", "02:00"),
		],
		dayNotes: [{ date: weekDates()[WEDNESDAY], text: "Leaving early" }],
	});
	await factories.AvailabilityWeekFactory.create({
		userId: NZAP_TEST_ID,
		weekStartsAt: startsAt,
		timezone: MACHINE_TIMEZONE,
		slots: [daySlot(WEDNESDAY, "19:00", "23:00")],
	});
	// a commitment late in the shared Wednesday evening: renders as a busy
	// block beside the reported time in the grid and trims the heatmap's
	// effective availability without removing the window
	await factories.TeamEventFactory.create({
		teamId,
		authorId: ADMIN_ID,
		name: "VoD review",
		...daySlot(WEDNESDAY, "22:00", "23:30"),
	});

	await impersonate(page, ADMIN_ID);
	await setTimezoneCookie(page);

	const team = new TeamPage(page);
	await team.goto(customUrl);

	const schedule = await team.openSchedule();

	// heatmap is the default view: two share Wed 19-22, one is also free Wed 18-19 and Thu 1-2
	await expect(schedule.locators.heatmap).toBeVisible();
	await expect(schedule.heatmapCells(2)).toHaveCount(3);
	await expect(schedule.heatmapCells(1)).toHaveCount(2);
	// the count shade must actually paint: an equal-specificity base background once blanked the whole grid
	expect(await schedule.heatmapCellBackground(2)).not.toBe(
		await schedule.heatmapCellBackground(0),
	);
	await expect(schedule.locators.heatmapUnreported).toContainText(
		"Schedules-Later",
	);
	await expect(schedule.dayDot(WEDNESDAY)).toBeVisible();

	// hovering a block names who is free then and who has no schedule
	await schedule.heatmapCells(2).first().hover();
	await expect(schedule.locators.heatmapTooltip).toContainText("2/3");
	await expect(schedule.locators.heatmapTooltip).toContainText("N-ZAP");
	await expect(schedule.locators.heatmapTooltip).toContainText(
		"Schedules-Later",
	);

	// dropping the member without a schedule from the count clears the nudge about them
	await schedule.memberChip(noScheduleMember.id).click();
	await isNotVisible(schedule.locators.heatmapUnreported);
	await schedule.memberChip(noScheduleMember.id).click();

	await schedule.locators.gridViewTab.click();
	await expect(schedule.locators.grid).toBeVisible();

	await expect(schedule.cellRange(ADMIN_ID, WEDNESDAY)).toBeVisible();
	await expect(schedule.cellRange(ADMIN_ID, THURSDAY)).toBeVisible();
	await expect(schedule.cell(ADMIN_ID, 0)).toHaveText("—");
	await expect(schedule.cell(noScheduleMember.id, 0)).toHaveText("?");
	// the grid shows the reported 19-23 in full, the commitment named beside
	// it with its own time rather than cut out of it
	await expect(schedule.cellRange(NZAP_TEST_ID, WEDNESDAY)).toContainText(
		"11:00",
	);
	await expect(schedule.cellBusy(NZAP_TEST_ID, WEDNESDAY)).toContainText(
		"VoD review",
	);
	await expect(schedule.cellBusy(NZAP_TEST_ID, WEDNESDAY)).toContainText(
		"10:00",
	);
	await expect(schedule.locators.notes).toContainText("Leaving early");

	// two members share Wed 19-22 while the third has no schedule, so the
	// only playable window is the one-short tier
	await expect(schedule.locators.windows).toHaveText(/Wed/);
	await expect(schedule.dayDot(WEDNESDAY)).toBeVisible();
	await isNotVisible(schedule.dayDot(0));

	await expect(schedule.locators.teamEvents).toContainText("VoD review");
})

hides the schedule from non-members, a friend of a member included

Renders/t/:customUrl/t/:customUrl/schedule

Setup
Arrange
  1. Create user → friend
    const friend = await factories.UserFactory.create();
  2. Create team name “Alliance Rogue” member user ids Sendou → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID],
    });
  3. Create friendship user one Sendou user two id friend.id
    await factories.FriendshipFactory.create({
    	userOneId: ADMIN_ID,
    	userTwoId: friend.id,
    });
  4. Create availability week user Sendou week starts at weekRange().startsAt timezone MACHINE_TIMEZONE slots daySlot(WEDNESDAY, "18:00", "22:00")
    await factories.AvailabilityWeekFactory.create({
    	userId: ADMIN_ID,
    	weekStartsAt: weekRange().startsAt,
    	timezone: MACHINE_TIMEZONE,
    	slots: [daySlot(WEDNESDAY, "18:00", "22:00")],
    });
Ffriend
Team schedule page
Act
  1. Open the team schedule page customUrl
    await schedule.goto(customUrl);
Assert
  1. hidden message is visible
    await expect(schedule.locators.hiddenMessage).toBeVisible();
  2. grid is not visible
    await isNotVisible(schedule.locators.grid);
Source
test("hides the schedule from non-members, a friend of a member included", async ({
	page,
	factories,
}) => {
	const friend = await factories.UserFactory.create();
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID],
	});
	await factories.FriendshipFactory.create({
		userOneId: ADMIN_ID,
		userTwoId: friend.id,
	});
	await factories.AvailabilityWeekFactory.create({
		userId: ADMIN_ID,
		weekStartsAt: weekRange().startsAt,
		timezone: MACHINE_TIMEZONE,
		slots: [daySlot(WEDNESDAY, "18:00", "22:00")],
	});

	await impersonate(page, friend.id);

	const schedule = new TeamSchedulePage(page);
	await schedule.goto(customUrl);
	await expect(schedule.locators.hiddenMessage).toBeVisible();
	await isNotVisible(schedule.locators.grid);
})

owner adds, edits and deletes a team event, a regular member only sees it

SendouN-ZAPteam.spec.ts:555

Renders/t/:customUrl/t/:customUrl/schedule

Setup
Arrange
  1. Create team name “Alliance Rogue” member user ids Sendou, N-ZAP → { customUrl }
    const { customUrl } = await factories.TeamFactory.create({
    	name: TEAM_NAME,
    	memberUserIds: [ADMIN_ID, NZAP_TEST_ID],
    });
SSendou
Team schedule page
Act
  1. Open the team schedule page customUrl
    await schedule.goto(customUrl);
  2. Click add event button
    await schedule.locators.addEventButton.click();
  3. Fill name with “VoD review vs. FTWin”
    await form.fill("name", "VoD review vs. FTWin");
  4. Set starts at to nextWeekTime(WEDNESDAY, "20:00")
    await form.setDateTime("startsAt", nextWeekTime(WEDNESDAY, "20:00"));
  5. Select “90” for duration
    await form.select("duration", "90");
  6. Submit
    await form.submit();
  7. Click next week toggle
    await schedule.locators.nextWeekToggle.click();
Assert
  1. team events contains “VoD review vs. FTWin”
    await expect(schedule.locators.teamEvents).toContainText(
    	"VoD review vs. FTWin",
    );
Act
  1. Click element /edit-team-event/
    await page.getByTestId(/edit-team-event/).click();
  2. Fill name with “Strategy meeting”
    await editForm.fill("name", "Strategy meeting");
  3. Check items participants “SELECTED”
    await editForm.checkItems("participants", ["SELECTED"]);
  4. Click field “N-ZAP”, exact true
    await page.getByLabel("N-ZAP", { exact: true }).click();
  5. Submit
    await editForm.submit();
Assert
  1. team events contains “Strategy meeting”
    await expect(schedule.locators.teamEvents).toContainText(
    	"Strategy meeting",
    );
  2. team events contains “N-ZAP”
    await expect(schedule.locators.teamEvents).toContainText("N-ZAP");
NN-ZAP
Team schedule page
Act
  1. Open the team schedule page customUrl
    await schedule.goto(customUrl);
  2. Click next week toggle
    await schedule.locators.nextWeekToggle.click();
Assert
  1. team events is visible
    await expect(schedule.locators.teamEvents).toBeVisible();
  2. add event button is not visible
    await isNotVisible(schedule.locators.addEventButton);
  3. element /delete-team-event/ is not visible
    await isNotVisible(page.getByTestId(/delete-team-event/));
SSendou
Team schedule page
Act
  1. Open the team schedule page customUrl
    await schedule.goto(customUrl);
  2. Click next week toggle
    await schedule.locators.nextWeekToggle.click();
  3. Click element /delete-team-event/
    await page.getByTestId(/delete-team-event/).click();
  4. Click element “confirm-button”
    await page.getByTestId("confirm-button").click();
Assert
  1. team events is not visible
    await isNotVisible(schedule.locators.teamEvents);
Source
test("owner adds, edits and deletes a team event, a regular member only sees it", async ({
	page,
	factories,
}) => {
	const { customUrl } = await factories.TeamFactory.create({
		name: TEAM_NAME,
		memberUserIds: [ADMIN_ID, NZAP_TEST_ID],
	});

	await impersonate(page, ADMIN_ID);
	await setTimezoneCookie(page);

	const schedule = new TeamSchedulePage(page);
	await schedule.goto(customUrl);

	await schedule.locators.addEventButton.click();
	const form = createFormHelpers(page, addTeamEventSchema);
	await form.fill("name", "VoD review vs. FTWin");
	await form.setDateTime("startsAt", nextWeekTime(WEDNESDAY, "20:00"));
	await form.select("duration", "90");
	await form.submit();

	await schedule.locators.nextWeekToggle.click();
	await expect(schedule.locators.teamEvents).toContainText(
		"VoD review vs. FTWin",
	);

	await page.getByTestId(/edit-team-event/).click();
	const editForm = createFormHelpers(page, editTeamEventSchema);
	await editForm.fill("name", "Strategy meeting");
	await editForm.checkItems("participants", ["SELECTED"]);
	await page.getByLabel("N-ZAP", { exact: true }).click();
	await editForm.submit();

	await expect(schedule.locators.teamEvents).toContainText(
		"Strategy meeting",
	);
	await expect(schedule.locators.teamEvents).toContainText("N-ZAP");

	await impersonate(page, NZAP_TEST_ID);
	await schedule.goto(customUrl);
	await schedule.locators.nextWeekToggle.click();
	await expect(schedule.locators.teamEvents).toBeVisible();
	await isNotVisible(schedule.locators.addEventButton);
	await isNotVisible(page.getByTestId(/delete-team-event/));

	await impersonate(page, ADMIN_ID);
	await schedule.goto(customUrl);
	await schedule.locators.nextWeekToggle.click();
	await page.getByTestId(/delete-team-event/).click();
	await page.getByTestId("confirm-button").click();
	await isNotVisible(schedule.locators.teamEvents);
})