mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-30 15:53:59 -05:00
associations, org, team
This commit is contained in:
parent
a522239521
commit
fe3f8cdeba
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
313
e2e/org.spec.ts
313
e2e/org.spec.ts
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
59
e2e/pages/associations/associations-page.ts
Normal file
59
e2e/pages/associations/associations-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
23
e2e/pages/associations/new-association-page.ts
Normal file
23
e2e/pages/associations/new-association-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
32
e2e/pages/calendar/calendar-new-event-page.ts
Normal file
32
e2e/pages/calendar/calendar-new-event-page.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
32
e2e/pages/layout/anything-adder.ts
Normal file
32
e2e/pages/layout/anything-adder.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
23
e2e/pages/org/new-organization-page.ts
Normal file
23
e2e/pages/org/new-organization-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
38
e2e/pages/org/organization-edit-page.ts
Normal file
38
e2e/pages/org/organization-edit-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
96
e2e/pages/org/organization-page.ts
Normal file
96
e2e/pages/org/organization-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
25
e2e/pages/scrims/scrims-page.ts
Normal file
25
e2e/pages/scrims/scrims-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
21
e2e/pages/team/join-team-page.ts
Normal file
21
e2e/pages/team/join-team-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
19
e2e/pages/team/new-team-page.ts
Normal file
19
e2e/pages/team/new-team-page.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
21
e2e/pages/team/team-edit-page.ts
Normal file
21
e2e/pages/team/team-edit-page.ts
Normal 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) });
|
||||
}
|
||||
}
|
||||
92
e2e/pages/team/team-page.ts
Normal file
92
e2e/pages/team/team-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
88
e2e/pages/team/team-roster-page.ts
Normal file
88
e2e/pages/team/team-roster-page.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
25
e2e/pages/tournament/tournament-admin-page.ts
Normal file
25
e2e/pages/tournament/tournament-admin-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
27
e2e/pages/tournament/tournament-nav.ts
Normal file
27
e2e/pages/tournament/tournament-nav.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
e2e/pages/user/user-page.ts
Normal file
26
e2e/pages/user/user-page.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
351
e2e/team.spec.ts
351
e2e/team.spec.ts
|
|
@ -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)],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user