Tournament Organization
can create a new organization
Renders//org/new/org/:slug
Arrange
Create user null roles “TOURNAMENT_ORGANIZER” → organizer
const organizer = await factories.UserFactory.create(null, {
roles: ["TOURNAMENT_ORGANIZER"],
});
Act
Go to “/”
await navigate({ page, url: "/" });
Anything adder Act
Add “organization”
await new AnythingAdder(page).add("organization");
New organization page Act
Fill name with “Test Organization”
await newOrganization.form.fill("name", "Test Organization");Save
await newOrganization.save();
Organization page Assert
edit button is visible
await expect(organization.locators.editButton).toBeVisible();
Source
test("can create a new organization", async ({ page, factories }) => {
const organizer = await factories.UserFactory.create(null, {
roles: ["TOURNAMENT_ORGANIZER"],
});
await impersonate(page, organizer.id);
await navigate({ page, url: "/" });
await new AnythingAdder(page).add("organization");
const newOrganization = new NewOrganizationPage(page);
await newOrganization.form.fill("name", "Test Organization");
await newOrganization.save();
const organization = new OrganizationPage(page);
await expect(organization.locators.editButton).toBeVisible();
})Renders/calendar/new/org/:slug/org/:slug/edit/to/:id/to/:id/admin/to/:id/admin/to/:id/info
Arrange
Create tournament organization owner Sendou name “Sendou.ink” members user N-ZAP role “MEMBER” → org
const org = await factories.TournamentOrganizationFactory.create(
{ ownerId: ADMIN_ID, name: ORGANIZATION_NAME },
{ members: [{ userId: NZAP_TEST_ID, role: "MEMBER" }] },
);Create tournament author Sendou name “PICNIC #2” organization id org.id start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
const tournament = await factories.TournamentFactory.create({
authorId: ADMIN_ID,
name: TOURNAMENT_NAME,
organizationId: org.id,
startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
});
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Assert
edit button is not visible
await isNotVisible(organization.locators.editButton);
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Open edit → organizationEdit
const organizationEdit = await organization.openEdit();
Organization edit page Act
Set member role “N-ZAP”, “ADMIN”
await organizationEdit.setMemberRole("N-ZAP", "ADMIN");Save
await organizationEdit.save();
Organization page Act
Establish the organization so its admins can edit tournament event info
Open the organization page org.slug
await organization.goto(org.slug);
Establish
await organization.establish();
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Open edit
await organization.openEdit();
Organization edit page Assert
title is visible
await expect(organizationEdit.locators.title).toBeVisible();
Tournament page Act
Open the tournament page tournament.id
await tournamentPage.goto(tournament.id);
Open admin → tournamentAdmin
const tournamentAdmin = await tournamentPage.nav.openAdmin();
Edit event info → calendarEvent
const calendarEvent = await tournamentAdmin.editEventInfo();
Assert
calendarEvent.locators.nameInput has value “PICNIC #2”
await expect(calendarEvent.locators.nameInput).toHaveValue(TOURNAMENT_NAME);
Source
test("user can be promoted to admin gaining org controls and can edit tournaments", async ({
page,
factories,
}) => {
const org = await factories.TournamentOrganizationFactory.create(
{ ownerId: ADMIN_ID, name: ORGANIZATION_NAME },
{ members: [{ userId: NZAP_TEST_ID, role: "MEMBER" }] },
);
const tournament = await factories.TournamentFactory.create({
authorId: ADMIN_ID,
name: TOURNAMENT_NAME,
organizationId: org.id,
startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
});
const organization = new OrganizationPage(page);
await impersonate(page, NZAP_TEST_ID);
await organization.goto(org.slug);
await isNotVisible(organization.locators.editButton);
await impersonate(page, ADMIN_ID);
await organization.goto(org.slug);
const organizationEdit = await organization.openEdit();
await organizationEdit.setMemberRole("N-ZAP", "ADMIN");
await organizationEdit.save();
// Establish the organization so its admins can edit tournament event info
await organization.goto(org.slug);
await organization.establish();
await impersonate(page, NZAP_TEST_ID);
await organization.goto(org.slug);
await organization.openEdit();
await expect(organizationEdit.locators.title).toBeVisible();
const tournamentPage = new TournamentPage(page);
await tournamentPage.goto(tournament.id);
const tournamentAdmin = await tournamentPage.nav.openAdmin();
const calendarEvent = await tournamentAdmin.editEventInfo();
await expect(calendarEvent.locators.nameInput).toHaveValue(TOURNAMENT_NAME);
})banned player cannot join a tournament of that organization
Renders/org/:slug/to/:id/to/:id/info/to/:id/register
Act
a long flow: runs past two thirds of the default timeout under load
Slow
test.slow();
Arrange
Create user discord name “Banned Bob” → player
const player = await factories.UserFactory.create({
discordName: PLAYER_NAME,
});Create tournament organization owner Sendou name “Sendou.ink” → org
const org = await factories.TournamentOrganizationFactory.create({
ownerId: ADMIN_ID,
name: ORGANIZATION_NAME,
});Create tournament author Sendou name “PICNIC #2” organization id org.id start times dateToDatabaseTimestamp(addHours(new Date(), 2)) → tournament
const tournament = await factories.TournamentFactory.create({
authorId: ADMIN_ID,
name: TOURNAMENT_NAME,
organizationId: org.id,
startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
});
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Open banned users
await organization.openBannedUsers();
Open ban modal → banModal
const banModal = await organization.openBanModal();
Ban user dialog Assert
dialog is visible
await expect(banModal.locators.dialog).toBeVisible();
Act
Select user “Banned Bob”
await banModal.selectUser(PLAYER_NAME);
Fill Private note with “Test reason”
await banModal.form.fill("privateNote", "Test reason");a future expiration date to avoid validation issues
Set Ban expiration date to addHours(new Date(), 24)
await banModal.form.setDateTime("expiresAt", addHours(new Date(), 24));Save
await banModal.save();
Organization page Assert
banned users table contains “Test reason”
await expect(organization.locators.bannedUsersTable).toContainText(
"Test reason",
);
Tournament page Act
Open the tournament page tournament.id
await tournamentPage.goto(tournament.id);
Register → register
const register = await tournamentPage.register();
Tournament register page Act
Fill Pick-up name with “Banned Team”
await register.form.fill("pickUpName", "Banned Team");Submit
await waitForPOSTResponse(page, () => register.form.submit());
Assert
"Fill roster" only appears after a successful registration
fill roster heading is not visible
await expect(register.locators.fillRosterHeading).not.toBeVisible();
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Open banned users
await organization.openBannedUsers();
Unban
await organization.unban();
Tournament page Act
Open the tournament page tournament.id
await tournamentPage.goto(tournament.id);
Register → registerAgain
const registerAgain = await tournamentPage.register();
Assert
teamsTab = teams tab
const teamsTab = tournamentPage.nav.locators.teamsTab;
teamsTab contains “Teams (0)”
await expect(teamsTab).toContainText("Teams (0)");
Tournament register page Act
Fill Pick-up name with “Unbanned Team”
await registerAgain.form.fill("pickUpName", "Unbanned Team");Submit
await registerAgain.form.submit();
Assert
teamsTab contains “Teams (1)”
await expect(teamsTab).toContainText("Teams (1)");
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Open banned users
await organization.openBannedUsers();
Open ban modal → permanentBanModal
const permanentBanModal = await organization.openBanModal();
Ban user dialog Assert
dialog is visible
await expect(permanentBanModal.locators.dialog).toBeVisible();
Act
Select user “Banned Bob”
await permanentBanModal.selectUser(PLAYER_NAME);
Fill Private note with “Does not expire”
await permanentBanModal.form.fill("privateNote", "Does not expire");expiresAt left empty makes the ban permanent
Save
await permanentBanModal.save();
Organization page Assert
banned users table contains “Permanent”
await expect(organization.locators.bannedUsersTable).toContainText(
"Permanent",
);
Source
test("banned player cannot join a tournament of that organization", async ({
page,
factories,
}) => {
// a long flow: runs past two thirds of the default timeout under load
test.slow();
const player = await factories.UserFactory.create({
discordName: PLAYER_NAME,
});
const org = await factories.TournamentOrganizationFactory.create({
ownerId: ADMIN_ID,
name: ORGANIZATION_NAME,
});
const tournament = await factories.TournamentFactory.create({
authorId: ADMIN_ID,
name: TOURNAMENT_NAME,
organizationId: org.id,
startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
});
const organization = new OrganizationPage(page);
const tournamentPage = new TournamentPage(page);
await impersonate(page, ADMIN_ID);
await organization.goto(org.slug);
await organization.openBannedUsers();
const banModal = await organization.openBanModal();
await expect(banModal.locators.dialog).toBeVisible();
await banModal.selectUser(PLAYER_NAME);
await banModal.form.fill("privateNote", "Test reason");
// a future expiration date to avoid validation issues
await banModal.form.setDateTime("expiresAt", addHours(new Date(), 24));
await banModal.save();
await expect(organization.locators.bannedUsersTable).toContainText(
"Test reason",
);
await impersonate(page, player.id);
await tournamentPage.goto(tournament.id);
const register = await tournamentPage.register();
await register.form.fill("pickUpName", "Banned Team");
await waitForPOSTResponse(page, () => register.form.submit());
// "Fill roster" only appears after a successful registration
await expect(register.locators.fillRosterHeading).not.toBeVisible();
await impersonate(page, ADMIN_ID);
await organization.goto(org.slug);
await organization.openBannedUsers();
await organization.unban();
await impersonate(page, player.id);
await tournamentPage.goto(tournament.id);
const registerAgain = await tournamentPage.register();
const teamsTab = tournamentPage.nav.locators.teamsTab;
await expect(teamsTab).toContainText("Teams (0)");
await registerAgain.form.fill("pickUpName", "Unbanned Team");
await registerAgain.form.submit();
await expect(teamsTab).toContainText("Teams (1)");
await impersonate(page, ADMIN_ID);
await organization.goto(org.slug);
await organization.openBannedUsers();
const permanentBanModal = await organization.openBanModal();
await expect(permanentBanModal.locators.dialog).toBeVisible();
await permanentBanModal.selectUser(PLAYER_NAME);
await permanentBanModal.form.fill("privateNote", "Does not expire");
// expiresAt left empty makes the ban permanent
await permanentBanModal.save();
await expect(organization.locators.bannedUsersTable).toContainText(
"Permanent",
);
})shows org admin the stats counting a played tournament's participants
Renders/org/:slug/org/:slug/stats
Arrange
PLAYER_COUNT = 8
const PLAYER_COUNT = 8;
Create tournament organization owner Sendou name “Sendou.ink” → org
const org = await factories.TournamentOrganizationFactory.create({
ownerId: ADMIN_ID,
name: ORGANIZATION_NAME,
});Create PLAYER_COUNT - 1 × user → players
const players = await factories.UserFactory.createMany(PLAYER_COUNT - 1);
playerIds = [ADMIN_ID, ...players.map((player) => player.id)]
const playerIds = [ADMIN_ID, ...players.map((player) => player.id)];
teamRosters = [playerIds.slice(0, 4), playerIds.slice(4, 8)]
const teamRosters = [playerIds.slice(0, 4), playerIds.slice(4, 8)];
only full months before the current one count towards the stats
lastMonth = subMonths(new Date(), 1)
const lastMonth = subMonths(new Date(), 1);
Create played tournament author Sendou name “PICNIC #2” organization id org.id start times dateToDatabaseTimestamp(lastMonth) teamRosters
await factories.TournamentFactory.createPlayed(
{
authorId: ADMIN_ID,
name: TOURNAMENT_NAME,
organizationId: org.id,
startTimes: [dateToDatabaseTimestamp(lastMonth)],
},
{ teamRosters },
);
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Assert
stats button is not visible
await isNotVisible(organization.locators.statsButton);
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Open stats → stats
const stats = await organization.openStats();
Assert
expectedAverage = ( PLAYER_COUNT / ESTABLISHED_ORG.MONTHS_CONSIDERED ).toFixed(1)
const expectedAverage = (
PLAYER_COUNT / ESTABLISHED_ORG.MONTHS_CONSIDERED
).toFixed(1);
Organization stats page Assert
established status contains expectedAverage
await expect(stats.locators.establishedStatus).toContainText(
expectedAverage,
);
established status contains `/ ${ESTABLISHED_ORG.GAIN_THRESHOLD}`
await expect(stats.locators.establishedStatus).toContainText(
`/ ${ESTABLISHED_ORG.GAIN_THRESHOLD}`,
);month row lastMonth has “aria-valuenow” = String(PLAYER_COUNT)
await expect(stats.monthRow(lastMonth)).toHaveAttribute(
"aria-valuenow",
String(PLAYER_COUNT),
);
Source
test("shows org admin the stats counting a played tournament's participants", async ({
page,
factories,
}) => {
const PLAYER_COUNT = 8;
const org = await factories.TournamentOrganizationFactory.create({
ownerId: ADMIN_ID,
name: ORGANIZATION_NAME,
});
const players = await factories.UserFactory.createMany(PLAYER_COUNT - 1);
const playerIds = [ADMIN_ID, ...players.map((player) => player.id)];
const teamRosters = [playerIds.slice(0, 4), playerIds.slice(4, 8)];
// only full months before the current one count towards the stats
const lastMonth = subMonths(new Date(), 1);
await factories.TournamentFactory.createPlayed(
{
authorId: ADMIN_ID,
name: TOURNAMENT_NAME,
organizationId: org.id,
startTimes: [dateToDatabaseTimestamp(lastMonth)],
},
{ teamRosters },
);
const organization = new OrganizationPage(page);
// a non-member sees no stats button
await impersonate(page, NZAP_TEST_ID);
await organization.goto(org.slug);
await isNotVisible(organization.locators.statsButton);
await impersonate(page, ADMIN_ID);
await organization.goto(org.slug);
const stats = await organization.openStats();
const expectedAverage = (
PLAYER_COUNT / ESTABLISHED_ORG.MONTHS_CONSIDERED
).toFixed(1);
await expect(stats.locators.establishedStatus).toContainText(
expectedAverage,
);
await expect(stats.locators.establishedStatus).toContainText(
`/ ${ESTABLISHED_ORG.GAIN_THRESHOLD}`,
);
await expect(stats.monthRow(lastMonth)).toHaveAttribute(
"aria-valuenow",
String(PLAYER_COUNT),
);
})allows member of established org to create tournament
Renders/calendar/new/org/:slug
Arrange
Create user → orgAdmin
const orgAdmin = await factories.UserFactory.create();
Create tournament organization owner Sendou name “Sendou.ink” members user id orgAdmin.id role “ADMIN” → org
const org = await factories.TournamentOrganizationFactory.create(
{ ownerId: ADMIN_ID, name: ORGANIZATION_NAME },
{ members: [{ userId: orgAdmin.id, role: "ADMIN" }] },
);
Calendar new event page Act
Go to new tournament
await newTournament.gotoNewTournament();
Assert
no tournament permissions alert is visible
await expect(
newTournament.locators.noTournamentPermissionsAlert,
).toBeVisible();
new tournament heading is not visible
await expect(newTournament.locators.newTournamentHeading).not.toBeVisible();
Organization page Act
Open the organization page org.slug
await organization.goto(org.slug);
Establish
await organization.establish();
Calendar new event page Act
Go to new tournament
await newTournament.gotoNewTournament();
Assert
no tournament permissions alert is not visible
await expect(
newTournament.locators.noTournamentPermissionsAlert,
).not.toBeVisible();
new tournament heading is visible
await expect(newTournament.locators.newTournamentHeading).toBeVisible();
Source
test("allows member of established org to create tournament", async ({
page,
factories,
}) => {
const orgAdmin = await factories.UserFactory.create();
const org = await factories.TournamentOrganizationFactory.create(
{ ownerId: ADMIN_ID, name: ORGANIZATION_NAME },
{ members: [{ userId: orgAdmin.id, role: "ADMIN" }] },
);
const newTournament = new CalendarNewEventPage(page);
await impersonate(page, orgAdmin.id);
await newTournament.gotoNewTournament();
await expect(
newTournament.locators.noTournamentPermissionsAlert,
).toBeVisible();
await expect(newTournament.locators.newTournamentHeading).not.toBeVisible();
await impersonate(page, ADMIN_ID);
const organization = new OrganizationPage(page);
await organization.goto(org.slug);
await organization.establish();
await impersonate(page, orgAdmin.id);
await newTournament.gotoNewTournament();
await expect(
newTournament.locators.noTournamentPermissionsAlert,
).not.toBeVisible();
await expect(newTournament.locators.newTournamentHeading).toBeVisible();
})AnythingAdder.adde2e/pages/layout/anything-adder.ts:28
No JSDoc
async add(item: AddableItem) {
await this.locators.menuButton.click();
await this.page.getByTestId(`menu-item-${item}`).click();
}newOrganizationSchema.nameapp/features/tournament-organization/tournament-organization-schemas.ts:34
No JSDoc
name: orgNameField
NewOrganizationPage.savee2e/pages/org/new-organization-page.ts:25
No JSDoc
async save() {
await submit(this.page);
}OrganizationPage.locators.editButtone2e/pages/org/organization-page.ts:27
No JSDoc
page.getByTestId("edit-org-button")AnythingAddere2e/pages/layout/anything-adder.ts:17
NewOrganizationPagee2e/pages/org/new-organization-page.ts:7
No JSDoc
OrganizationPagee2e/pages/org/organization-page.ts:18
No JSDoc
OrganizationPage.gotoe2e/pages/org/organization-page.ts:37
No JSDoc
async goto(organizationSlug: string) {
await navigate({
page: this.page,
url: tournamentOrganizationPage({ organizationSlug }),
});
}isNotVisiblee2e/helpers/playwright.ts:745
No JSDoc
export function isNotVisible(locator: Locator) {
return expect(locator).toHaveCount(0);
}OrganizationPage.openEdite2e/pages/org/organization-page.ts:44
No JSDoc
async openEdit() {
await this.locators.editButton.click();
return new OrganizationEditPage(this.page);
}OrganizationEditPage.setMemberRolee2e/pages/org/organization-edit-page.ts:29
No JSDoc
async setMemberRole(username: string, role: TournamentOrganizationRole) {
await this.memberFieldset(username)
.getByLabel("Role", { exact: true })
.selectOption(role);
}OrganizationEditPage.savee2e/pages/org/organization-edit-page.ts:35
No JSDoc
async save() {
await submit(this.page);
}OrganizationPage.establishe2e/pages/org/organization-page.ts:55
async establish() {
await this.locators.adminTab.click();
await waitForPOSTResponse(this.page, () =>
this.isEstablishedForm.check("isEstablished"),
);
}OrganizationEditPage.locators.titlee2e/pages/org/organization-edit-page.ts:13
No JSDoc
page.getByText("Editing tournament organization")TournamentPage.gotoe2e/pages/tournament/tournament-page.ts:21
No JSDoc
async goto(tournamentId: number) {
await navigate({ page: this.page, url: tournamentPage(tournamentId) });
}OrganizationEditPagee2e/pages/org/organization-edit-page.ts:6
No JSDoc
TournamentPagee2e/pages/tournament/tournament-page.ts:7
No JSDoc
OrganizationPage.openBannedUserse2e/pages/org/organization-page.ts:62
No JSDoc
async openBannedUsers() {
await this.locators.bannedUsersTab.click();
}OrganizationPage.openBanModale2e/pages/org/organization-page.ts:66
No JSDoc
async openBanModal() {
await this.locators.newBanButton.click();
return new BanUserDialog(this.page);
}BanUserDialog.locators.dialoge2e/pages/org/organization-page.ts:86
No JSDoc
page.getByRole("dialog")BanUserDialog.selectUsere2e/pages/org/organization-page.ts:91
async selectUser(userName: string) {
await selectUser({
page: this.page,
userName,
labelName: "Player",
within: this.locators.dialog,
});
}banUserActionSchema.privateNoteapp/features/tournament-organization/tournament-organization-schemas.ts:111
privateNote: textAreaOptional({
label: "labels.banUserNote",
bottomText: "bottomTexts.banUserNoteHelp",
maxLength: TOURNAMENT_ORGANIZATION.BAN_REASON_MAX_LENGTH,
})banUserActionSchema.expiresAtapp/features/tournament-organization/tournament-organization-schemas.ts:116
expiresAt: datetimeOptional({
label: "labels.banUserExpiresAt",
bottomText: "bottomTexts.banUserExpiresAtHelp",
min: () => new Date(),
minMessage: "errors.dateInPast",
})BanUserDialog.savee2e/pages/org/organization-page.ts:100
No JSDoc
async save() {
await submit(this.page);
}OrganizationPage.locators.bannedUsersTablee2e/pages/org/organization-page.ts:33
No JSDoc
page.getByRole("table")TournamentPage.registere2e/pages/tournament/tournament-page.ts:33
No JSDoc
async register() {
await this.locators.registerCta.click();
return new TournamentRegisterPage(this.page);
}registerTeamFormSchema.pickUpNameapp/features/tournament/tournament-register-schemas.ts:17
pickUpName: textFieldOptional({
label: "labels.regPickUpName",
maxLength: TOURNAMENT.TEAM_NAME_MAX_LENGTH,
})TournamentRegisterPage.locators.fillRosterHeadinge2e/pages/tournament/tournament-register-page.ts:30
No JSDoc
page.getByText("Fill roster")OrganizationPage.unbane2e/pages/org/organization-page.ts:71
No JSDoc
async unban() {
await this.locators.unbanButton.click();
await modalClickConfirmButton(this.page);
}BanUserDialoge2e/pages/org/organization-page.ts:77
No JSDoc
TournamentRegisterPagee2e/pages/tournament/tournament-register-page.ts:17
No JSDoc
OrganizationPage.locators.statsButtone2e/pages/org/organization-page.ts:28
No JSDoc
page.getByTestId("org-stats-button")OrganizationPage.openStatse2e/pages/org/organization-page.ts:49
No JSDoc
async openStats() {
await this.locators.statsButton.click();
return new OrganizationStatsPage(this.page);
}OrganizationStatsPage.locators.establishedStatuse2e/pages/org/organization-stats-page.ts:11
No JSDoc
page.getByRole("progressbar", {
name: "Established status",
})OrganizationStatsPage.monthRowe2e/pages/org/organization-stats-page.ts:18
monthRow(month: Date) {
return this.page.getByRole("progressbar", {
name: format(month, "MMM yyyy"),
});
}OrganizationStatsPagee2e/pages/org/organization-stats-page.ts:4
No JSDoc
CalendarNewEventPage.gotoNewTournamente2e/pages/calendar/calendar-new-event-page.ts:70
No JSDoc
async gotoNewTournament() {
await navigate({ page: this.page, url: TOURNAMENT_NEW_PAGE });
}CalendarNewEventPage.locators.noTournamentPermissionsAlerte2e/pages/calendar/calendar-new-event-page.ts:35
No JSDoc
page.getByText(
"No permissions to add tournaments",
)
CalendarNewEventPage.locators.newTournamentHeadinge2e/pages/calendar/calendar-new-event-page.ts:34
No JSDoc
page.getByText("New tournament")CalendarNewEventPagee2e/pages/calendar/calendar-new-event-page.ts:24