associations, org, team

This commit is contained in:
Kalle 2026-07-29 15:08:01 +03:00
parent a522239521
commit fe3f8cdeba
24 changed files with 1033 additions and 392 deletions

View File

@ -29,5 +29,11 @@ export async function pinUserId(userId: number, pinnedId: number) {
db,
);
// the search index is kept in sync by triggers that don't watch `id`, so its
// entry would keep pointing at the id the user just moved off of
await sql`insert into "UserSearch"("UserSearch") values ('rebuild')`.execute(
db,
);
return pinnedId;
}

View File

@ -1,76 +1,66 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { associationsPage, scrimsPage } from "~/utils/urls";
import {
expect,
impersonate,
isNotVisible,
navigate,
submit,
test,
} from "./helpers/playwright";
import { AssociationsPage } from "./pages/associations/associations-page";
import { NewAssociationPage } from "./pages/associations/new-association-page";
import { AnythingAdder } from "./pages/layout/anything-adder";
import { ScrimsPage } from "./pages/scrims/scrims-page";
test.describe("Associations", () => {
test("creates a new association", async ({ page }) => {
// await seed(page);
await impersonate(page, NZAP_TEST_ID);
await navigate({
page,
url: "/",
});
await navigate({ page, url: "/" });
await page.getByTestId("anything-adder-menu-button").first().click();
await page.getByTestId("menu-item-association").click();
await new AnythingAdder(page).add("association");
await page.getByLabel("Name").fill("My Association");
await submit(page);
const newAssociation = new NewAssociationPage(page);
await newAssociation.form.fill("name", "My Association");
await newAssociation.save();
await expect(
page.getByRole("heading").filter({ hasText: "My Association" }),
).toBeVisible();
const associations = new AssociationsPage(page);
await expect(associations.heading("My Association")).toBeVisible();
});
test("deletes an association", async ({ page }) => {
// await seed(page);
test("deletes an association", async ({ page, factories }) => {
await factories.AssociationFactory.create({ userId: ADMIN_ID });
await factories.AssociationFactory.create({ userId: ADMIN_ID });
await impersonate(page, ADMIN_ID);
await navigate({
page,
url: scrimsPage(),
});
await page.getByRole("link", { name: "Associations" }).click();
await expect(page.getByTestId("delete-association")).toHaveCount(2);
const scrims = new ScrimsPage(page);
await scrims.goto();
await page.getByTestId("delete-association").first().click();
await submit(page, "confirm-button");
const associations = await scrims.openAssociations();
await expect(page.getByTestId("delete-association")).toHaveCount(1);
await expect(associations.locators.deleteButtons).toHaveCount(2);
await associations.deleteFirst();
await expect(associations.locators.deleteButtons).toHaveCount(1);
});
test("joins and leaves an association", async ({ page }) => {
// await seed(page);
await impersonate(page, ADMIN_ID);
await navigate({
page,
url: associationsPage(),
});
test("joins and leaves an association", async ({ page, factories }) => {
await factories.AssociationFactory.create({ userId: ADMIN_ID });
const inviteLink = await page
.getByLabel("Share link to add members")
.first()
.inputValue();
await impersonate(page, ADMIN_ID);
const associations = new AssociationsPage(page);
await associations.goto();
const inviteCode = await associations.inviteCode();
await impersonate(page, NZAP_TEST_ID);
await navigate({
page,
url: inviteLink.replace("https://sendou.ink", "http://localhost:6173"),
});
await associations.gotoInvite(inviteCode);
await associations.join();
await submit(page);
await associations.leave();
await page.getByTestId("leave-team-button").click();
await submit(page, "confirm-button");
await isNotVisible(page.getByTestId("leave-team-button"));
await isNotVisible(associations.locators.leaveButton);
});
});

View File

@ -31,11 +31,16 @@ export async function loadFactories(parallelIndex: number) {
return {
backdate: (await import("~/db/seed/core/backdate")).backdate,
AssociationFactory: await import("~/db/seed/factories/AssociationFactory"),
BuildFactory: await import("~/db/seed/factories/BuildFactory"),
FriendshipFactory: await import("~/db/seed/factories/FriendshipFactory"),
SQGroupFactory: await import("~/db/seed/factories/SQGroupFactory"),
SQMatchFactory: await import("~/db/seed/factories/SQMatchFactory"),
TeamFactory: await import("~/db/seed/factories/TeamFactory"),
TournamentFactory: await import("~/db/seed/factories/TournamentFactory"),
TournamentOrganizationFactory: await import(
"~/db/seed/factories/TournamentOrganizationFactory"
),
TournamentTeamFactory: await import(
"~/db/seed/factories/TournamentTeamFactory"
),

View File

@ -114,13 +114,16 @@ export async function selectUser({
userName,
labelName,
exact = false,
within,
}: {
page: Page;
userName: string;
labelName: string;
exact?: boolean;
/** Scopes the combobox lookup, for pages carrying the label on more than one element. */
within?: Locator;
}) {
const comboboxButton = page.getByLabel(labelName, { exact });
const comboboxButton = (within ?? page).getByLabel(labelName, { exact });
const searchInput = page.getByTestId("user-search-input");
const option = page.getByTestId("user-search-item").first();

View File

@ -1,253 +1,208 @@
import { NZAP_TEST_ID, ORG_ADMIN_TEST_ID } from "~/db/seed/constants";
import { addHours } from "date-fns";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
banUserActionSchema,
newOrganizationSchema,
updateIsEstablishedSchema,
} from "~/features/tournament-organization/tournament-organization-schemas";
import {
TOURNAMENT_NEW_PAGE,
tournamentOrganizationPage,
tournamentPage,
} from "~/utils/urls";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import {
expect,
impersonate,
isNotVisible,
navigate,
submit,
test,
waitForPOSTResponse,
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
import { CalendarNewEventPage } from "./pages/calendar/calendar-new-event-page";
import { AnythingAdder } from "./pages/layout/anything-adder";
import { NewOrganizationPage } from "./pages/org/new-organization-page";
import { OrganizationPage } from "./pages/org/organization-page";
import { TournamentPage } from "./pages/tournament/tournament-page";
const url = tournamentOrganizationPage({
organizationSlug: "sendouink",
});
const ORGANIZATION_NAME = "Sendou.ink";
const TOURNAMENT_NAME = "PICNIC #2";
const PLAYER_NAME = "Banned Bob";
test.describe("Tournament Organization", () => {
test("can create a new organization", async ({ page }) => {
// await seed(page);
await impersonate(page);
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 page.getByTestId("anything-adder-menu-button").first().click();
await page.getByTestId("menu-item-organization").click();
await new AnythingAdder(page).add("organization");
const form = createFormHelpers(page, newOrganizationSchema);
await form.fill("name", "Test Organization");
await submit(page);
const newOrganization = new NewOrganizationPage(page);
await newOrganization.form.fill("name", "Test Organization");
await newOrganization.save();
await expect(page.getByTestId("edit-org-button")).toBeVisible();
const organization = new OrganizationPage(page);
await expect(organization.locators.editButton).toBeVisible();
});
test("user can be promoted to admin gaining org controls and can edit tournaments", async ({
page,
factories,
}) => {
// await seed(page);
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 editButtonLocator = page.getByTestId("edit-org-button");
const organization = new OrganizationPage(page);
// 1. As a regular user, verify edit controls are not visible
await impersonate(page, NZAP_TEST_ID);
await navigate({
page,
url,
});
await isNotVisible(editButtonLocator);
await organization.goto(org.slug);
await isNotVisible(organization.locators.editButton);
// 2. As admin, promote user to admin
await impersonate(page, ADMIN_ID);
await navigate({ page, url });
await editButtonLocator.click();
// Add member as admin - find the specific member fieldset containing N-ZAP
// The array field creates numbered fieldsets (#1, #2, #3) for each member
// Find the role select that belongs to the same fieldset as N-ZAP user select
const nzapFieldset = page.locator('fieldset:has(button:has-text("N-ZAP"))');
await nzapFieldset
.getByLabel("Role", { exact: true })
.selectOption("ADMIN");
await submit(page);
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 navigate({ page, url });
await page.getByRole("tab", { name: "Admin" }).click();
const isEstablishedForm = createFormHelpers(
page,
updateIsEstablishedSchema,
);
await waitForPOSTResponse(page, () =>
isEstablishedForm.check("isEstablished"),
);
await organization.goto(org.slug);
await organization.establish();
// 3. As the promoted user, verify edit controls are visible and page can be accessed
await impersonate(page, NZAP_TEST_ID);
await navigate({
page,
url,
});
await editButtonLocator.click();
await expect(
page.getByText("Editing tournament organization"),
).toBeVisible();
await organization.goto(org.slug);
await organization.openEdit();
await expect(organizationEdit.locators.title).toBeVisible();
// 4. As the promoted user, verify they can edit tournaments
await navigate({
page,
url: tournamentPage(1),
});
const tournamentPage = new TournamentPage(page);
await tournamentPage.goto(tournament.id);
await page.getByTestId("admin-tab").click();
await page.getByTestId("edit-event-info-button").click();
await expect(page.getByLabel(/^Name *\*?$/)).toHaveValue("PICNIC #2");
const tournamentAdmin = await tournamentPage.nav.openAdmin();
const calendarEvent = await tournamentAdmin.editEventInfo();
await expect(calendarEvent.locators.nameInput).toHaveValue(TOURNAMENT_NAME);
});
test("banned player cannot join a tournament of that organization", async ({
page,
factories,
}) => {
// await seed(page, "REG_OPEN");
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))],
});
// 1. As admin, ban NZAP user from the organization
const organization = new OrganizationPage(page);
const tournamentPage = new TournamentPage(page);
// 1. As admin, ban the player from the organization
await impersonate(page, ADMIN_ID);
await navigate({ page, url });
await organization.goto(org.slug);
await organization.openBannedUsers();
const bannedUsersTab = page.getByTestId("banned-users-tab");
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();
// Go to banned users section and add NZAP to ban list
await bannedUsersTab.click();
await page.getByText("New ban", { exact: true }).click();
// Wait for the dialog to be visible
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
// Find and click the UserSearch combobox (React Aria Select renders as combobox)
const userSearchCombobox = dialog.getByRole("button").filter({
has: page.locator('[class*="selectValue"]'),
});
await userSearchCombobox.click();
// Wait for the popover to open and fill search
await expect(page.getByTestId("user-search-input")).toBeVisible();
await page.getByTestId("user-search-input").fill("N-ZAP");
await expect(page.getByTestId("user-search-item").first()).toBeVisible();
await page.keyboard.press("Enter");
// Fill the note and set expiration date
const banForm = createFormHelpers(page, banUserActionSchema);
await banForm.fill("privateNote", "Test reason");
// Set a future expiration date to avoid validation issues
const futureDate = new Date();
futureDate.setMonth(futureDate.getMonth() + 1);
await banForm.setDateTime("expiresAt", futureDate);
await submit(page);
// The added ban should be visible in the table
await expect(page.getByRole("table")).toContainText("Test reason");
// 2. As the banned user, try to join a tournament
await impersonate(page, NZAP_TEST_ID);
await navigate({
page,
url: tournamentPage(1),
});
// Try to create a team
await page.getByTestId("register-cta").click();
// Fill in team details
await page.getByLabel("Pick-up name").fill("Banned Team");
await waitForPOSTResponse(page, () =>
page.getByTestId("save-team-button").click(),
await expect(organization.locators.bannedUsersTable).toContainText(
"Test reason",
);
// Verify the team was not created (Fill roster only appears after successful registration)
await expect(page.getByText("Fill roster")).not.toBeVisible();
// 2. As the banned user, try to join a tournament
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();
// 3. As admin, remove the ban
await impersonate(page, ADMIN_ID);
await navigate({ page, url });
await bannedUsersTab.click();
await page.getByRole("button", { name: "Unban" }).click();
await submit(page, "confirm-button");
await organization.goto(org.slug);
await organization.openBannedUsers();
await organization.unban();
// 4. As the unbanned user, verify they can now join a tournament
await impersonate(page, NZAP_TEST_ID);
await navigate({
page,
url: tournamentPage(1),
});
await page.getByTestId("register-cta").click();
await impersonate(page, player.id);
await tournamentPage.goto(tournament.id);
const registerAgain = await tournamentPage.register();
// Try to create a team again
await expect(page.getByText(/Teams \(\d+\)/)).toBeVisible();
const teamsTab = tournamentPage.nav.locators.teamsTab;
await expect(teamsTab).toContainText("Teams (0)");
const teamCountBefore = await page.getByText(/Teams \(\d+\)/).textContent();
await registerAgain.form.fill("pickUpName", "Unbanned Team");
await registerAgain.form.submit();
await page.getByLabel("Pick-up name").fill("Unbanned Team");
await page.getByTestId("save-team-button").click();
const countBefore = Number(teamCountBefore?.match(/\d+/)?.[0] ?? 0);
await expect(page.getByText(`Teams (${countBefore + 1})`)).toBeVisible();
await expect(teamsTab).toContainText("Teams (1)");
// 5. As admin, ban user again but with permanent ban this time
await impersonate(page, ADMIN_ID);
await navigate({ page, url });
await bannedUsersTab.click();
await page.getByText("New ban", { exact: true }).click();
const dialog2 = page.getByRole("dialog");
await expect(dialog2).toBeVisible();
const userSearchCombobox2 = dialog2.getByRole("button").filter({
has: page.locator('[class*="selectValue"]'),
});
await userSearchCombobox2.click();
await expect(page.getByTestId("user-search-input")).toBeVisible();
await page.getByTestId("user-search-input").fill("N-ZAP");
await expect(page.getByTestId("user-search-item").first()).toBeVisible();
await page.keyboard.press("Enter");
const banForm2 = createFormHelpers(page, banUserActionSchema);
await banForm2.fill("privateNote", "Does not expire");
// Don't set expiresAt - leave empty for permanent ban
await submit(page);
// Verify "Permanent" appears in the table
await expect(page.getByRole("table")).toContainText("Permanent");
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",
);
});
test("allows member of established org to create tournament", async ({
page,
factories,
}) => {
// await seed(page);
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 impersonate(page, ORG_ADMIN_TEST_ID);
await navigate({
page,
url: TOURNAMENT_NEW_PAGE,
});
await expect(
page.getByText("No permissions to add tournaments"),
newTournament.locators.noTournamentPermissionsAlert,
).toBeVisible();
await expect(page.getByText("New tournament")).not.toBeVisible();
await expect(newTournament.locators.newTournamentHeading).not.toBeVisible();
await impersonate(page, ADMIN_ID);
await navigate({
page,
url: "/org/sendouink",
});
const organization = new OrganizationPage(page);
await organization.goto(org.slug);
await organization.establish();
await page.getByRole("tab", { name: "Admin" }).click();
const isEstablishedForm = createFormHelpers(
page,
updateIsEstablishedSchema,
);
await waitForPOSTResponse(page, () =>
isEstablishedForm.check("isEstablished"),
);
await impersonate(page, ORG_ADMIN_TEST_ID);
await navigate({
page,
url: TOURNAMENT_NEW_PAGE,
});
await impersonate(page, orgAdmin.id);
await newTournament.gotoNewTournament();
await expect(
page.getByText("No permissions to add tournaments"),
newTournament.locators.noTournamentPermissionsAlert,
).not.toBeVisible();
await expect(page.getByText("New tournament")).toBeVisible();
await expect(newTournament.locators.newTournamentHeading).toBeVisible();
});
});

View File

@ -0,0 +1,59 @@
import type { Page } from "@playwright/test";
import invariant from "~/utils/invariant";
import { associationsPage } from "~/utils/urls";
import {
modalClickConfirmButton,
navigate,
submit,
} from "../../helpers/playwright";
export class AssociationsPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
deleteButtons: page.getByTestId("delete-association"),
leaveButton: page.getByTestId("leave-team-button"),
inviteLinkInputs: page.getByLabel("Share link to add members"),
};
}
async goto() {
await navigate({ page: this.page, url: associationsPage() });
}
async gotoInvite(inviteCode: string) {
await navigate({ page: this.page, url: associationsPage(inviteCode) });
}
heading(name: string) {
return this.page.getByRole("heading").filter({ hasText: name });
}
/** The share link is shown as the whole production url, only its code is of use here. */
async inviteCode() {
const inviteLink = await this.locators.inviteLinkInputs
.first()
.inputValue();
const inviteCode = new URL(inviteLink).searchParams.get("inviteCode");
invariant(inviteCode, `No invite code in the share link: ${inviteLink}`);
return inviteCode;
}
async join() {
await submit(this.page);
}
async deleteFirst() {
await this.locators.deleteButtons.first().click();
await modalClickConfirmButton(this.page);
}
async leave() {
await this.locators.leaveButton.click();
await modalClickConfirmButton(this.page);
}
}

View File

@ -0,0 +1,23 @@
import type { Page } from "@playwright/test";
import { createNewAssociationSchema } from "~/features/associations/associations-schemas";
import { newAssociationsPage } from "~/utils/urls";
import { navigate, submit } from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
export class NewAssociationPage {
private readonly page: Page;
readonly form;
constructor(page: Page) {
this.page = page;
this.form = createFormHelpers(page, createNewAssociationSchema);
}
async goto() {
await navigate({ page: this.page, url: newAssociationsPage() });
}
async save() {
await submit(this.page);
}
}

View File

@ -0,0 +1,32 @@
import type { Page } from "@playwright/test";
import { calendarNewBaseSchema } from "~/features/calendar/calendar-new-schemas";
import { CALENDAR_NEW_PAGE, TOURNAMENT_NEW_PAGE } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
/** `/calendar/new`, also used for adding tournaments and editing existing events. */
export class CalendarNewEventPage {
private readonly page: Page;
readonly form;
readonly locators;
constructor(page: Page) {
this.page = page;
this.form = createFormHelpers(page, calendarNewBaseSchema);
this.locators = {
nameInput: page.getByLabel(/^Name *\*?$/),
newTournamentHeading: page.getByText("New tournament"),
noTournamentPermissionsAlert: page.getByText(
"No permissions to add tournaments",
),
};
}
async goto() {
await navigate({ page: this.page, url: CALENDAR_NEW_PAGE });
}
async gotoNewTournament() {
await navigate({ page: this.page, url: TOURNAMENT_NEW_PAGE });
}
}

View File

@ -0,0 +1,32 @@
import type { Page } from "@playwright/test";
type AddableItem =
| "art"
| "association"
| "builds"
| "calendarEvent"
| "lfgPost"
| "organization"
| "plus"
| "scrimPost"
| "team"
| "tournament"
| "vods";
/** The "Add new…" menu of the site layout, available on every page. */
export class AnythingAdder {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
menuButton: page.getByTestId("anything-adder-menu-button").first(),
};
}
async add(item: AddableItem) {
await this.locators.menuButton.click();
await this.page.getByTestId(`menu-item-${item}`).click();
}
}

View File

@ -0,0 +1,23 @@
import type { Page } from "@playwright/test";
import { newOrganizationSchema } from "~/features/tournament-organization/tournament-organization-schemas";
import { ORGANIZATION_NEW_PAGE } from "~/utils/urls";
import { navigate, submit } from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
export class NewOrganizationPage {
private readonly page: Page;
readonly form;
constructor(page: Page) {
this.page = page;
this.form = createFormHelpers(page, newOrganizationSchema);
}
async goto() {
await navigate({ page: this.page, url: ORGANIZATION_NEW_PAGE });
}
async save() {
await submit(this.page);
}
}

View File

@ -0,0 +1,38 @@
import type { Page } from "@playwright/test";
import type { TournamentOrganizationRole } from "~/features/tournament-organization/tournament-organization-constants";
import { tournamentOrganizationEditPage } from "~/utils/urls";
import { navigate, submit } from "../../helpers/playwright";
export class OrganizationEditPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
title: page.getByText("Editing tournament organization"),
};
}
async goto(organizationSlug: string) {
await navigate({
page: this.page,
url: tournamentOrganizationEditPage(organizationSlug),
});
}
/** The members array field renders one fieldset per member, identified by the selected user. */
memberFieldset(username: string) {
return this.page.locator(`fieldset:has(button:has-text("${username}"))`);
}
async setMemberRole(username: string, role: TournamentOrganizationRole) {
await this.memberFieldset(username)
.getByLabel("Role", { exact: true })
.selectOption(role);
}
async save() {
await submit(this.page);
}
}

View File

@ -0,0 +1,96 @@
import type { Page } from "@playwright/test";
import {
banUserActionSchema,
updateIsEstablishedSchema,
} from "~/features/tournament-organization/tournament-organization-schemas";
import { tournamentOrganizationPage } from "~/utils/urls";
import {
modalClickConfirmButton,
navigate,
selectUser,
submit,
waitForPOSTResponse,
} from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
import { OrganizationEditPage } from "./organization-edit-page";
export class OrganizationPage {
private readonly page: Page;
private readonly isEstablishedForm;
readonly locators;
constructor(page: Page) {
this.page = page;
this.isEstablishedForm = createFormHelpers(page, updateIsEstablishedSchema);
this.locators = {
editButton: page.getByTestId("edit-org-button"),
bannedUsersTab: page.getByTestId("banned-users-tab"),
adminTab: page.getByRole("tab", { name: "Admin" }),
newBanButton: page.getByRole("button", { name: "New ban" }),
unbanButton: page.getByRole("button", { name: "Unban" }),
bannedUsersTable: page.getByRole("table"),
};
}
async goto(organizationSlug: string) {
await navigate({
page: this.page,
url: tournamentOrganizationPage({ organizationSlug }),
});
}
async openEdit() {
await this.locators.editButton.click();
return new OrganizationEditPage(this.page);
}
/** Established organizations can add tournaments and their admins can edit them. */
async establish() {
await this.locators.adminTab.click();
await waitForPOSTResponse(this.page, () =>
this.isEstablishedForm.check("isEstablished"),
);
}
async openBannedUsers() {
await this.locators.bannedUsersTab.click();
}
async openBanModal() {
await this.locators.newBanButton.click();
return new BanUserDialog(this.page);
}
async unban() {
await this.locators.unbanButton.click();
await modalClickConfirmButton(this.page);
}
}
class BanUserDialog {
private readonly page: Page;
readonly form;
readonly locators;
constructor(page: Page) {
this.page = page;
this.form = createFormHelpers(page, banUserActionSchema);
this.locators = {
dialog: page.getByRole("dialog"),
};
}
/** Scoped to the dialog: the tab panel behind it carries the label too. */
async selectUser(userName: string) {
await selectUser({
page: this.page,
userName,
labelName: "Player",
within: this.locators.dialog,
});
}
async save() {
await submit(this.page);
}
}

View File

@ -0,0 +1,25 @@
import type { Page } from "@playwright/test";
import { scrimsPage } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { AssociationsPage } from "../associations/associations-page";
export class ScrimsPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
associationsLink: page.getByRole("link", { name: "Associations" }),
};
}
async goto() {
await navigate({ page: this.page, url: scrimsPage() });
}
async openAssociations() {
await this.locators.associationsLink.click();
return new AssociationsPage(this.page);
}
}

View File

@ -0,0 +1,21 @@
import type { Page } from "@playwright/test";
import { navigate, submit } from "../../helpers/playwright";
import { TeamPage } from "./team-page";
export class JoinTeamPage {
private readonly page: Page;
constructor(page: Page) {
this.page = page;
}
/** Takes the whole invite link as shown on the roster page. */
async goto(inviteLink: string) {
await navigate({ page: this.page, url: inviteLink });
}
async join() {
await submit(this.page);
return new TeamPage(this.page);
}
}

View File

@ -0,0 +1,19 @@
import type { Page } from "@playwright/test";
import { createTeamSchema } from "~/features/team/team-schemas";
import { NEW_TEAM_PAGE } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
export class NewTeamPage {
private readonly page: Page;
readonly form;
constructor(page: Page) {
this.page = page;
this.form = createFormHelpers(page, createTeamSchema);
}
async goto() {
await navigate({ page: this.page, url: NEW_TEAM_PAGE });
}
}

View File

@ -0,0 +1,21 @@
import type { Page } from "@playwright/test";
import { editTeamFormSchema } from "~/features/team/team-schemas";
import { editTeamPage } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
export class TeamEditPage {
private readonly page: Page;
readonly form;
constructor(page: Page) {
this.page = page;
this.form = createFormHelpers(page, editTeamFormSchema, {
submitTestId: "edit-team-submit-button",
});
}
async goto(customUrl: string) {
await navigate({ page: this.page, url: editTeamPage(customUrl) });
}
}

View File

@ -0,0 +1,92 @@
import type { Page } from "@playwright/test";
import { teamPage } from "~/utils/urls";
import {
modalClickConfirmButton,
navigate,
waitForPOSTResponse,
} from "../../helpers/playwright";
import { TeamEditPage } from "./team-edit-page";
import { TeamRosterPage } from "./team-roster-page";
export class TeamPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
bio: page.getByTestId("team-bio"),
bskyLink: page.getByTestId("bsky-link").first(),
manageRosterButton: page.getByTestId("manage-roster-button"),
editTeamButton: page.getByTestId("edit-team-button"),
actionsMenuButton: page.getByTestId("team-actions-menu-button"),
mainTeamIndicator: page.getByTestId("main-team-indicator"),
makeMainTeamButton: page.getByTestId("make-main-team-button"),
leaveTeamButton: page.getByTestId("leave-team-button"),
deleteTeamButton: page.getByTestId("delete-team-button"),
otherRolesTab: page.getByRole("tab", { name: /Other/ }),
confirmDialog: page.getByRole("dialog"),
};
}
async goto(customUrl: string) {
await navigate({ page: this.page, url: teamPage(customUrl) });
}
memberRole(nth: number) {
return this.page.getByTestId(`member-row-role-${nth}`);
}
ownerBadge(userId: number) {
return this.page.getByTestId(`member-owner-${userId}`);
}
customRole(name: string) {
return this.page.getByText(name).first();
}
async openManageRoster() {
await this.locators.manageRosterButton.click();
return new TeamRosterPage(this.page);
}
async openEdit() {
await this.locators.editTeamButton.click();
return new TeamEditPage(this.page);
}
async openActionsMenu() {
await this.locators.actionsMenuButton.click();
}
/** Requires the actions menu to be open. */
async makeMainTeam() {
await waitForPOSTResponse(this.page, () =>
this.locators.makeMainTeamButton.click(),
);
}
/**
* Opens the leave confirmation dialog, which names the new owner if the
* leaver owns the team. Requires the actions menu to be open.
*/
async startLeaving() {
await this.locators.leaveTeamButton.click();
}
async confirmLeaving() {
await modalClickConfirmButton(this.page);
}
/** Requires the actions menu to be open. */
async leave() {
await this.startLeaving();
await this.confirmLeaving();
}
/** Requires the actions menu to be open. */
async delete() {
await this.locators.deleteTeamButton.click();
await modalClickConfirmButton(this.page);
}
}

View File

@ -0,0 +1,88 @@
import type { Page } from "@playwright/test";
import type {
MemberRole,
MemberRoleType,
} from "~/features/team/team-constants";
import type { CUSTOM_ROLE_VALUE } from "~/features/team/team-schemas";
import { manageTeamRosterPage } from "~/utils/urls";
import { navigate, submit } from "../../helpers/playwright";
export class TeamRosterPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
inviteLink: page.getByTestId("invite-link"),
editorToggles: page.getByLabel("Editor"),
};
}
async goto(customUrl: string) {
await navigate({ page: this.page, url: manageTeamRosterPage(customUrl) });
}
memberRow(nth: number) {
return new MemberRow(this.page, nth);
}
inviteLink() {
return this.locators.inviteLink.innerText();
}
async resetInviteLink() {
await submit(this.page, "reset-invite-link-button");
}
/** The owner and the user themselves can't be made editors, so they have no toggle. */
async makeEditor(nth: number) {
await this.locators.editorToggles.nth(nth).click({ force: true });
}
async save() {
await submit(this.page);
}
}
class MemberRow {
private readonly row;
/** The array field's wrapper, holding the reorder & remove buttons. */
private readonly fieldset;
readonly locators;
constructor(page: Page, index: number) {
this.row = page.getByTestId(`member-row-${index}`);
this.fieldset = page.locator(
`fieldset:has([data-testid='member-row-${index}'])`,
);
this.locators = {
row: this.row,
username: page.getByTestId(`member-row-username-${index}`),
roleSelect: this.row.locator("select").first(),
customRoleInput: this.row.getByRole("textbox"),
roleTypeSelect: this.row.locator("select").nth(1),
removeButton: this.fieldset.getByRole("button", { name: "Remove item" }),
moveUpButton: this.fieldset.getByRole("button", { name: "Move up" }),
moveDownButton: this.fieldset.getByRole("button", { name: "Move down" }),
};
}
async selectRole(role: MemberRole | typeof CUSTOM_ROLE_VALUE) {
await this.locators.roleSelect.selectOption(role);
}
async setCustomRole(name: string, roleType: MemberRoleType) {
await this.selectRole("CUSTOM");
await this.locators.customRoleInput.fill(name);
await this.locators.roleTypeSelect.selectOption(roleType);
}
async remove() {
await this.locators.removeButton.click();
}
async moveDown() {
await this.locators.moveDownButton.click();
}
}

View File

@ -0,0 +1,25 @@
import type { Page } from "@playwright/test";
import { tournamentAdminPage } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { CalendarNewEventPage } from "../calendar/calendar-new-event-page";
export class TournamentAdminPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
editEventInfoButton: page.getByTestId("edit-event-info-button"),
};
}
async goto(tournamentId: number) {
await navigate({ page: this.page, url: tournamentAdminPage(tournamentId) });
}
async editEventInfo() {
await this.locators.editEventInfoButton.click();
return new CalendarNewEventPage(this.page);
}
}

View File

@ -0,0 +1,27 @@
import type { Page } from "@playwright/test";
import { clickNavTab } from "../../helpers/playwright";
import { TournamentAdminPage } from "./tournament-admin-page";
import { TournamentBracketsPage } from "./tournament-brackets-page";
/** Tabs shared by every tournament page, some of which collapse into a "More" menu. */
export class TournamentNav {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
teamsTab: page.locator('[data-testid="teams-tab"]:visible'),
};
}
async openBrackets() {
await clickNavTab(this.page, "brackets-tab");
return new TournamentBracketsPage(this.page);
}
async openAdmin() {
await clickNavTab(this.page, "admin-tab");
return new TournamentAdminPage(this.page);
}
}

View File

@ -1,14 +1,17 @@
import type { Page } from "@playwright/test";
import { tournamentPage } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { TournamentNav } from "./tournament-nav";
import { TournamentRegisterPage } from "./tournament-register-page";
export class TournamentPage {
private readonly page: Page;
readonly nav;
readonly locators;
constructor(page: Page) {
this.page = page;
this.nav = new TournamentNav(page);
this.locators = {
registerCta: page.getByTestId("register-cta"),
};

View File

@ -5,22 +5,28 @@ import { registerTeamFormSchema } from "~/features/tournament/tournament-registe
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { tournamentRegisterPage } from "~/utils/urls";
import { clickNavTab, navigate, submit } from "../../helpers/playwright";
import { navigate, submit } from "../../helpers/playwright";
import { createFormHelpers } from "../../helpers/playwright-form";
import { TournamentBracketsPage } from "./tournament-brackets-page";
import { TournamentNav } from "./tournament-nav";
/** Stage the counterpick picking starts from, leaving the lowest ids to tiebreakers. */
const FIRST_COUNTERPICK_STAGE_ID = 5;
export class TournamentRegisterPage {
private readonly page: Page;
readonly nav;
readonly form;
readonly locators;
constructor(page: Page) {
this.page = page;
this.nav = new TournamentNav(page);
this.form = createFormHelpers(page, registerTeamFormSchema, {
submitTestId: "save-team-button",
});
this.locators = {
fillRosterHeading: page.getByText("Fill roster"),
};
}
async goto(tournamentId: number) {
@ -70,8 +76,7 @@ export class TournamentRegisterPage {
return submit(this.page, "check-in-button");
}
async openBrackets() {
await clickNavTab(this.page, "brackets-tab");
return new TournamentBracketsPage(this.page);
openBrackets() {
return this.nav.openBrackets();
}
}

View File

@ -0,0 +1,26 @@
import type { Page } from "@playwright/test";
import { userPage } from "~/utils/urls";
import { navigate } from "../../helpers/playwright";
import { TeamPage } from "../team/team-page";
export class UserPage {
private readonly page: Page;
readonly locators;
constructor(page: Page) {
this.page = page;
this.locators = {
mainTeamLink: page.getByTestId("main-team-link"),
secondaryTeamsTrigger: page.getByTestId("secondary-team-trigger"),
};
}
async goto(discordId: string) {
await navigate({ page: this.page, url: userPage({ discordId }) });
}
async openMainTeam() {
await this.locators.mainTeamLink.click();
return new TeamPage(this.page);
}
}

View File

@ -1,274 +1,301 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID, ADMIN_ID } from "~/features/admin/admin-constants";
import {
createTeamSchema,
editTeamFormSchema,
} from "~/features/team/team-schemas";
import { editTeamPage, teamPage, userPage } from "~/utils/urls";
import type { Factories } from "./helpers/factories";
import {
expect,
impersonate,
isNotVisible,
modalClickConfirmButton,
navigate,
submit,
test,
waitForPOSTResponse,
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
import { AnythingAdder } from "./pages/layout/anything-adder";
import { JoinTeamPage } from "./pages/team/join-team-page";
import { NewTeamPage } from "./pages/team/new-team-page";
import { TeamEditPage } from "./pages/team/team-edit-page";
import { TeamPage } from "./pages/team/team-page";
import { UserPage } from "./pages/user/user-page";
const TEAM_NAME = "Alliance Rogue";
const SECONDARY_TEAM_NAME = "Team Olive";
const ROSTER_SIZE = 4;
test.describe("New team creation", () => {
test("creates new team", async ({ page }) => {
// await seed(page);
await impersonate(page, NZAP_TEST_ID);
await navigate({ page, url: "/" });
await page.getByTestId("anything-adder-menu-button").first().click();
await page.getByTestId("menu-item-team").click();
await new AnythingAdder(page).add("team");
await expect(page).toHaveURL(/t\/new/);
const form = createFormHelpers(page, createTeamSchema);
await form.fill("name", "Chimera");
await form.submit();
const newTeam = new NewTeamPage(page);
await newTeam.form.fill("name", "Chimera");
await newTeam.form.submit();
await expect(page).toHaveURL(/chimera/);
});
});
test.describe("Team page", () => {
test("edit team info", async ({ page }) => {
// await seed(page);
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("edit-team-button").click();
const form = createFormHelpers(page, editTeamFormSchema, {
submitTestId: "edit-team-submit-button",
test("edit team info", async ({ page, factories }) => {
const { customUrl } = await factories.TeamFactory.create({
name: TEAM_NAME,
memberUserIds: [ADMIN_ID],
});
await form.fill("name", "Better Alliance Rogue");
await form.fill("bsky", "BetterAllianceRogue");
await form.fill("bio", "shorter bio");
await impersonate(page, ADMIN_ID);
await form.submit();
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 page.getByText("shorter bio").isVisible();
await expect(page.getByTestId("bsky-link").first()).toHaveAttribute(
await expect(team.locators.bio).toHaveText("shorter bio");
await expect(team.locators.bskyLink).toHaveAttribute(
"href",
"https://bsky.app/profile/BetterAllianceRogue",
);
});
test("kicks a member & changes a role", async ({ page }) => {
// await seed(page);
test("kicks a member & changes a role", async ({ page, factories }) => {
const { customUrl } = await createFullTeam(factories);
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
const team = new TeamPage(page);
await team.goto(customUrl);
// Owner is Sendou
await expect(page.getByTestId(`member-owner-${ADMIN_ID}`)).toBeVisible();
await expect(team.ownerBadge(ADMIN_ID)).toBeVisible();
await page.getByTestId("manage-roster-button").click();
const roster = await team.openManageRoster();
await roster.memberRow(0).selectRole("SUPPORT");
await page
.getByTestId("member-row-0")
.locator("select")
.selectOption("SUPPORT");
const lastMember = roster.memberRow(ROSTER_SIZE - 1);
await expect(lastMember.locators.row).toBeVisible();
await lastMember.remove();
await isNotVisible(lastMember.locators.row);
await expect(page.getByTestId("member-row-3")).toBeVisible();
await page
.locator("fieldset:has([data-testid='member-row-3'])")
.getByRole("button", { name: "Remove item" })
.click();
await isNotVisible(page.getByTestId("member-row-3"));
await roster.save();
await submit(page);
await team.goto(customUrl);
await navigate({ page, url: teamPage("alliance-rogue") });
await expect(page.getByTestId("member-row-role-0")).toHaveText("Support");
await expect(team.memberRole(0)).toHaveText("Support");
});
test("sets a custom role for a member", async ({ page }) => {
// await seed(page);
test("sets a custom role for a member", async ({ page, factories }) => {
const { customUrl } = await createFullTeam(factories);
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("manage-roster-button").click();
const team = new TeamPage(page);
await team.goto(customUrl);
const memberRow = page.getByTestId("member-row-1");
await memberRow.locator("select").first().selectOption("CUSTOM");
await memberRow.getByRole("textbox").fill("Strategist");
await memberRow.locator("select").nth(1).selectOption("OTHER");
const roster = await team.openManageRoster();
await roster.memberRow(1).setCustomRole("Strategist", "OTHER");
await roster.save();
await submit(page);
await navigate({ page, url: teamPage("alliance-rogue") });
await team.goto(customUrl);
// custom role is classified as "OTHER" so it lives under the "Other" tab
await page.getByRole("tab", { name: /Other/ }).click();
await expect(page.getByText("Strategist").first()).toBeVisible();
await team.locators.otherRolesTab.click();
await expect(team.customRole("Strategist")).toBeVisible();
});
test("reorders members via move buttons", async ({ page }) => {
// await seed(page);
test("reorders members via move buttons", async ({ page, factories }) => {
const { customUrl } = await createFullTeam(factories);
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("manage-roster-button").click();
const team = new TeamPage(page);
await team.goto(customUrl);
const firstName = await page
.getByTestId("member-row-username-0")
.innerText();
const secondName = await page
.getByTestId("member-row-username-1")
.innerText();
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);
const firstRow = page.locator("fieldset:has([data-testid='member-row-0'])");
const lastRow = page.locator("fieldset:has([data-testid='member-row-3'])");
// the first member can't move up and the last can't move down
await expect(
firstRow.getByRole("button", { name: "Move up" }),
).toBeDisabled();
await expect(
lastRow.getByRole("button", { name: "Move down" }),
).toBeDisabled();
await expect(firstRow.locators.moveUpButton).toBeDisabled();
await expect(lastRow.locators.moveDownButton).toBeDisabled();
// move the first member down one slot
await firstRow.getByRole("button", { name: "Move down" }).click();
await firstRow.moveDown();
await expect(page.getByTestId("member-row-username-0")).toHaveText(
secondName,
);
await expect(page.getByTestId("member-row-username-1")).toHaveText(
firstName,
);
await expect(firstRow.locators.username).toHaveText(secondName);
await expect(secondRow.locators.username).toHaveText(firstName);
await submit(page);
await roster.save();
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("manage-roster-button").click();
await team.goto(customUrl);
await team.openManageRoster();
// the new order is persisted
await expect(page.getByTestId("member-row-username-0")).toHaveText(
secondName,
);
await expect(firstRow.locators.username).toHaveText(secondName);
});
test("deletes team", async ({ page }) => {
// await seed(page);
test("deletes team", async ({ page, factories }) => {
const { customUrl } = await factories.TeamFactory.create({
name: TEAM_NAME,
memberUserIds: [ADMIN_ID],
});
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("team-actions-menu-button").click();
await page.getByTestId("delete-team-button").click();
await modalClickConfirmButton(page);
const team = new TeamPage(page);
await team.goto(customUrl);
await expect(page).not.toHaveURL(/alliance-rogue/);
await team.openActionsMenu();
await team.delete();
await expect(page).not.toHaveURL(new RegExp(customUrl));
});
test("resets invite code, joins team, leaves, rejoins", async ({ page }) => {
// await seed(page);
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);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("manage-roster-button").click();
const team = new TeamPage(page);
await team.goto(customUrl);
const oldInviteLink = await page.getByTestId("invite-link").innerText();
const roster = await team.openManageRoster();
const oldInviteLink = await roster.inviteLink();
await submit(page, "reset-invite-link-button");
await roster.resetInviteLink();
await expect(page.getByTestId("invite-link")).not.toHaveText(oldInviteLink);
const newInviteLink = await page.getByTestId("invite-link").innerText();
await expect(roster.locators.inviteLink).not.toHaveText(oldInviteLink);
const newInviteLink = await roster.inviteLink();
await impersonate(page, NZAP_TEST_ID);
await navigate({ page, url: newInviteLink });
await submit(page);
const join = new JoinTeamPage(page);
await join.goto(newInviteLink);
await join.join();
await page.getByTestId("team-actions-menu-button").click();
await page.getByTestId("leave-team-button").click();
await modalClickConfirmButton(page);
await team.openActionsMenu();
await team.leave();
await navigate({ page, url: newInviteLink });
await submit(page);
await join.goto(newInviteLink);
await join.join();
await page.getByTestId("team-actions-menu-button").click();
await expect(page.getByTestId("leave-team-button")).toBeVisible();
await team.openActionsMenu();
await expect(team.locators.leaveTeamButton).toBeVisible();
});
test("joins a secondary team, makes main team & leaves making the seconary team the main one", async ({
page,
factories,
}) => {
// await seed(page);
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("team-olive") });
await page.getByTestId("manage-roster-button").click();
const inviteLink = await page.getByTestId("invite-link").innerText();
await navigate({ page, url: inviteLink });
await submit(page);
await page.getByTestId("team-actions-menu-button").click();
await waitForPOSTResponse(page, async () => {
await page.getByTestId("make-main-team-button").click();
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 navigate({ page, url: userPage({ discordId: ADMIN_DISCORD_ID }) });
await impersonate(page, ADMIN_ID);
await expect(page.getByTestId("secondary-team-trigger")).toBeVisible();
await isNotVisible(page.getByText("Alliance Rogue"));
const secondaryTeam = new TeamPage(page);
await secondaryTeam.goto(secondaryCustomUrl);
await page.getByTestId("main-team-link").click();
const roster = await secondaryTeam.openManageRoster();
const inviteLink = await roster.inviteLink();
await page.getByTestId("team-actions-menu-button").click();
await expect(page.getByTestId("main-team-indicator")).toBeVisible();
await page.getByTestId("leave-team-button").click();
await modalClickConfirmButton(page);
const join = new JoinTeamPage(page);
await join.goto(inviteLink);
await join.join();
await navigate({ page, url: userPage({ discordId: ADMIN_DISCORD_ID }) });
await secondaryTeam.openActionsMenu();
await secondaryTeam.makeMainTeam();
await isNotVisible(page.getByTestId("secondary-team-trigger"));
await expect(page.getByText("Alliance Rogue")).toBeVisible();
const user = new UserPage(page);
await user.goto(ADMIN_DISCORD_ID);
await expect(user.locators.secondaryTeamsTrigger).toBeVisible();
await expect(user.locators.mainTeamLink).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_DISCORD_ID);
await isNotVisible(user.locators.secondaryTeamsTrigger);
await expect(user.locators.mainTeamLink).toContainText(TEAM_NAME);
});
test("makes another user editor, who can edit the page & becomes owner after the original leaves", async ({
page,
factories,
}) => {
// await seed(page, "NZAP_IN_TEAM");
const { customUrl } = await factories.TeamFactory.create({
name: TEAM_NAME,
memberUserIds: [ADMIN_ID, NZAP_TEST_ID],
});
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("manage-roster-button").click();
const team = new TeamPage(page);
await team.goto(customUrl);
await page.getByLabel("Editor").first().click({ force: true });
await submit(page);
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);
await navigate({ page, url: editTeamPage("alliance-rogue") });
const editorForm = createFormHelpers(page, editTeamFormSchema, {
submitTestId: "edit-team-submit-button",
});
await editorForm.fill("bio", "from editor");
await editorForm.submit();
const teamEdit = new TeamEditPage(page);
await teamEdit.goto(customUrl);
await teamEdit.form.fill("bio", "from editor");
await teamEdit.form.submit();
await expect(page).toHaveURL(/alliance-rogue/);
await page.getByText("from editor").isVisible();
await expect(page).toHaveURL(new RegExp(customUrl));
await expect(team.locators.bio).toHaveText("from editor");
await impersonate(page, ADMIN_ID);
await navigate({ page, url: teamPage("alliance-rogue") });
await page.getByTestId("team-actions-menu-button").click();
await page.getByTestId("leave-team-button").click();
await page.getByText("New owner will be N-ZAP").isVisible();
await modalClickConfirmButton(page);
await team.goto(customUrl);
await page.getByTestId("team-actions-menu-button").click();
await isNotVisible(page.getByTestId("leave-team-button"));
await team.openActionsMenu();
await team.startLeaving();
await expect(team.locators.confirmDialog).toContainText(
"New owner will be N-ZAP",
);
await team.confirmLeaving();
await team.openActionsMenu();
await isNotVisible(team.locators.leaveTeamButton);
});
});
/** A team of the admin and three others, the admin its owner and first member. */
async function createFullTeam(factories: Factories) {
const members = await factories.UserFactory.createMany(ROSTER_SIZE - 1);
return factories.TeamFactory.create({
name: TEAM_NAME,
memberUserIds: [ADMIN_ID, ...members.map((member) => member.id)],
});
}