Files
sendou.ink/e2e/admin.spec.ts
2026-09-09 20:21:49 +03:00

198 lines
6.6 KiB
TypeScript

import path from "node:path";
import { fileURLToPath } from "node:url";
import { addHours, addYears } from "date-fns";
import { NZAP_TEST_DISCORD_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { SPLATOON_3_XP_BADGE_VALUES } from "~/features/badges/badges-constants";
import {
expect,
impersonate,
isNotVisible,
navigate,
test,
} from "./helpers/playwright";
import { AdminActionsPage } from "./pages/admin/admin-actions-page";
import { AdminStreamsPage } from "./pages/admin/admin-streams-page";
import { ApiPage } from "./pages/api/api-page";
import { NewArtPage } from "./pages/art/new-art-page";
import { TopRightButtons } from "./pages/layout/top-right-buttons";
import { NewOrganizationPage } from "./pages/org/new-organization-page";
import { UserPage } from "./pages/user/user-page";
import { NewVodPage } from "./pages/vods/new-vod-page";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const TEST_IMAGE_PATH = path.join(__dirname, "fixtures/test-image.png");
const ROLE_TARGET = {
discordId: "223456789012345678",
discordName: "RoleTarget",
};
const KEEPER = { discordId: "323456789012345678", discordName: "KeeperUser" };
const LEAVER = { discordId: "423456789012345678", discordName: "LeaverUser" };
const FRIEND_CODE = "0123-4567-8901";
// the wiped database makes the placement's player the first SplatoonPlayer row
const LINKED_PLAYER_ID = 1;
test.describe("Admin panel", () => {
test("grants roles, friend code and API access to a user", async ({
page,
factories,
}) => {
const target = await factories.UserFactory.create({
...ROLE_TARGET,
friendCode: null,
});
const api = new ApiPage(page);
await impersonate(page, target.id);
await api.goto();
await expect(api.locators.noAccessMessage).toBeVisible();
await impersonate(page, ADMIN_ID);
const adminActions = new AdminActionsPage(page);
await adminActions.goto();
await adminActions.updateFriendCode(ROLE_TARGET.discordName, FRIEND_CODE);
await adminActions.giveArtist(ROLE_TARGET.discordName);
await adminActions.giveVideoAdder(ROLE_TARGET.discordName);
await adminActions.giveApiAccess(ROLE_TARGET.discordName);
await adminActions.openFriendCodeLookUp();
await adminActions.searchFriendCode(FRIEND_CODE);
await expect(
adminActions.foundUserLink(ROLE_TARGET.discordName),
).toBeVisible();
await impersonate(page, target.id);
const newArt = new NewArtPage(page);
await newArt.goto();
await expect(newArt.locators.descriptionInput).toBeVisible();
const newVod = new NewVodPage(page);
await newVod.goto();
await expect(newVod.locators.addMatchButton).toBeVisible();
await api.goto();
await isNotVisible(api.locators.noAccessMessage);
const token = await api.generateToken("read");
expect(token.length).toBeGreaterThan(0);
});
test("grants tournament organizer role and patron status to a user", async ({
page,
factories,
}) => {
const target = await factories.UserFactory.create(ROLE_TARGET);
const newOrganization = new NewOrganizationPage(page);
const topRightButtons = new TopRightButtons(page);
await impersonate(page, target.id);
await newOrganization.goto();
await expect(newOrganization.locators.noPermissionsAlert).toBeVisible();
await navigate({ page, url: "/" });
await expect(topRightButtons.locators.supportLink).toBeVisible();
await impersonate(page, ADMIN_ID);
const adminActions = new AdminActionsPage(page);
await adminActions.goto();
await adminActions.giveTournamentOrganizer(ROLE_TARGET.discordName);
// tier one so the tournament organizer grant above stays the only source of that role
await adminActions.forcePatron(ROLE_TARGET.discordName, {
tier: "Support",
expiresAt: addYears(new Date(), 1),
});
await impersonate(page, target.id);
await newOrganization.goto();
await expect(newOrganization.locators.heading).toBeVisible();
await navigate({ page, url: "/" });
await isNotVisible(topRightButtons.locators.supportLink);
});
test("links a player, refreshes plus tiers and migrates an account", async ({
page,
factories,
}) => {
// linking a player syncs XP badges, which requires every XP badge to exist
for (const value of SPLATOON_3_XP_BADGE_VALUES) {
await factories.BadgeFactory.create({ code: String(value) });
}
await factories.XRankPlacementFactory.create({
playerSplId: "e2e-unlinked-player",
name: "PlacedPlayer",
});
const keeper = await factories.UserFactory.create({ ...KEEPER });
await factories.UserFactory.create({ ...LEAVER });
await factories.PlusVoteFactory.create({
authorId: ADMIN_ID,
votedId: keeper.id,
});
await impersonate(page, ADMIN_ID);
const adminActions = new AdminActionsPage(page);
await adminActions.goto();
await adminActions.linkPlayer("N-ZAP", LINKED_PLAYER_ID);
await adminActions.refreshPlusTiers();
await adminActions.migrateUser({
oldUserName: KEEPER.discordName,
newUserName: LEAVER.discordName,
});
await expect(
await adminActions.userSearchSuggestion(KEEPER.discordName),
).toContainText("+1");
const userPage = new UserPage(page);
await userPage.goto(NZAP_TEST_DISCORD_ID);
await expect(userPage.widget("x-rank-peaks")).toBeVisible();
const playerPage = await userPage.openPlacements();
await expect(playerPage.locators.heading).toBeVisible();
// the old user kept their account, now reached via the new user's Discord id
await userPage.goto(LEAVER.discordId);
await expect(userPage.usernameHeading(KEEPER.discordName)).toBeVisible();
});
test("keeps the scroll position when an action shows a toast", async ({
page,
factories,
}) => {
await factories.UserFactory.create({ ...ROLE_TARGET, friendCode: null });
await impersonate(page, ADMIN_ID);
const adminActions = new AdminActionsPage(page);
await adminActions.goto();
// the form is far down the page, so filling it scrolls there
await adminActions.updateFriendCode(ROLE_TARGET.discordName, FRIEND_CODE);
expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(0);
});
test("adds and deletes an external stream", async ({ page }) => {
await impersonate(page, ADMIN_ID);
const streams = new AdminStreamsPage(page);
await streams.goto();
await expect(streams.locators.addStreamHeading).toBeVisible();
await expect(streams.locators.noStreams).toBeVisible();
await streams.createStream({
name: "E2E Stream",
url: "https://www.twitch.tv/sendou",
startTime: addHours(new Date(), 1),
logoPath: TEST_IMAGE_PATH,
});
await expect(streams.streamLink("E2E Stream")).toBeVisible();
await isNotVisible(streams.locators.noStreams);
await streams.deleteStream("E2E Stream");
await expect(streams.locators.noStreams).toBeVisible();
});
});