import { ADMIN_ID } from "~/features/admin/admin-constants"; import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types"; import type { Factories } from "./helpers/factories"; import { expect, impersonate, isNotVisible, test } from "./helpers/playwright"; import { WeaponBuildsPage } from "./pages/builds/weapon-builds-page"; import { CalendarPage } from "./pages/calendar/calendar-page"; import { FaqPage } from "./pages/info/faq-page"; import { SELECTED_MAP_CLASS } from "./pages/settings/map-mode-preferences-field"; import { MatchProfilePage } from "./pages/settings/match-profile-page"; import { SettingsPage } from "./pages/settings/settings-page"; import { TournamentBracketsPage } from "./pages/tournament/tournament-brackets-page"; import { TournamentResultsPage } from "./pages/tournament/tournament-results-page"; const LUNA_BLASTER_ID = 200; // deliberately scrambled so the sorted and unsorted renders differ const UNSORTED_ABILITIES: BuildAbilitiesTuple = [ ["ISM", "SSU", "ISM", "SSU"], ["SSU", "ISM", "SSU", "ISM"], ["RSU", "QR", "QR", "QR"], ]; test.describe("Settings", () => { test("updates 'disableBuildAbilitySorting'", async ({ page, factories }) => { await factories.BuildFactory.create({ ownerId: ADMIN_ID, weaponSplIds: [LUNA_BLASTER_ID], abilities: UNSORTED_ABILITIES, }); await impersonate(page); const weaponBuilds = new WeaponBuildsPage(page); await weaponBuilds.goto("luna-blaster"); const oldContents = await weaponBuilds.buildCard(0).root.innerHTML(); const settings = new SettingsPage(page); await settings.disableBuildAbilitySorting(); await weaponBuilds.goto("luna-blaster"); const newContents = await weaponBuilds.buildCard(0).root.innerHTML(); expect(newContents).not.toBe(oldContents); }); test("updates clock format preference", async ({ page, factories }) => { // the clock header only renders above a day's events await factories.CalendarEventFactory.create({ authorId: ADMIN_ID }); await impersonate(page); const calendar = new CalendarPage(page); await calendar.goto(); const clockTime = calendar.locators.clockHeaderTimes.first(); const initialTime = await clockTime.textContent(); expect(initialTime).toMatch(/AM|PM/); const settings = new SettingsPage(page); await settings.setClockFormat("24h"); await calendar.goto(); const newTime = await clockTime.textContent(); expect(newTime).not.toMatch(/AM|PM/); expect(newTime).not.toBe(initialTime); expect(newTime).toContain(":"); }); test("switches language to Japanese, persists it across pages and restores English", async ({ page, }) => { const settings = new SettingsPage(page); const faq = new FaqPage(page); await settings.goto("locale"); await expect(settings.locators.pageHeading).toHaveText("Settings"); await settings.selectLanguage("日本語"); await expect(settings.locators.pageHeading).toHaveText("設定"); // full page load without the lng param -> the language comes from the cookie; // the faq namespace is registered via the route handle await faq.goto(); await expect(faq.question("プラスサーバーとはなんですか?")).toBeVisible(); await settings.goto("locale"); await expect(settings.locators.pageHeading).toHaveText("設定"); await settings.selectLanguage("English"); await expect(settings.locators.pageHeading).toHaveText("Settings"); await faq.goto(); await expect(faq.question("What is the Plus Server?")).toBeVisible(); }); test("persists sound settings and dark/light theme across reload", async ({ page, }) => { await impersonate(page); const settings = new SettingsPage(page); await settings.goto("sounds"); const likeSound = settings.soundCheckbox("Group invitation received"); await expect(likeSound).toBeChecked(); await likeSound.click(); await expect(likeSound).not.toBeChecked(); await settings.setSoundVolume("37"); // default resolves to light (auto + light color scheme) so dark is a real change await settings.goto("theme"); await settings.setTheme("Dark"); await expect(settings.locators.htmlRoot).toHaveClass(/dark/); await settings.reload(); await expect(settings.locators.htmlRoot).toHaveClass(/dark/); await settings.goto("sounds"); await expect(likeSound).not.toBeChecked(); await expect(settings.locators.volumeSlider).toHaveValue("37"); await settings.goto("theme"); await settings.setTheme("Light"); await expect(settings.locators.htmlRoot).toHaveClass(/light/); await expect(settings.locators.htmlRoot).not.toHaveClass(/dark/); }); }); test.describe("Match profile map preferences", () => { test("retains map selection when toggling a mode prefer -> avoid -> prefer", async ({ page, }) => { await impersonate(page); const matchProfile = new MatchProfilePage(page); await matchProfile.goto(); await matchProfile.maps.setModePreference("SZ", "Prefer"); await matchProfile.maps.selectModeTab("SZ"); await matchProfile.maps.mapButton("SZ", 1).click(); await expect(matchProfile.maps.mapButton("SZ", 1)).toHaveClass( SELECTED_MAP_CLASS, ); // avoiding hides the picker, but the selection should be remembered await matchProfile.maps.setModePreference("SZ", "Avoid"); await isNotVisible(matchProfile.maps.mapButton("SZ", 1)); await matchProfile.maps.setModePreference("SZ", "Prefer"); await matchProfile.maps.selectModeTab("SZ"); await expect(matchProfile.maps.mapButton("SZ", 1)).toHaveClass( SELECTED_MAP_CLASS, ); }); test("can save 'zones only' after a now-avoided mode previously had a map pool", async ({ page, }) => { await impersonate(page); const matchProfile = new MatchProfilePage(page); await matchProfile.goto(); // stage 2 is not banned in TC await matchProfile.maps.setModePreference("SZ", "Prefer"); await matchProfile.maps.selectModeTab("SZ"); await matchProfile.maps.mapButton("SZ", 1).click(); await matchProfile.maps.setModePreference("TC", "Prefer"); await matchProfile.maps.selectModeTab("TC"); await matchProfile.maps.mapButton("TC", 2).click(); await matchProfile.save(); await matchProfile.maps.setModePreference("TW", "Avoid"); await matchProfile.maps.setModePreference("TC", "Avoid"); await matchProfile.maps.setModePreference("RM", "Avoid"); await matchProfile.maps.setModePreference("CB", "Avoid"); await matchProfile.save(); // the previously saved TC pool must not resurface as an invalid "pool for an avoided mode" await matchProfile.goto(); await matchProfile.save(); await isNotVisible(matchProfile.locators.avoidedModePoolError); }); }); test.describe("Spoiler-free mode", () => { test("censors bracket and reveals on click", async ({ page, factories }) => { const tournament = await createFinalizedTournament(factories); await impersonate(page); const settings = new SettingsPage(page); await settings.enableSpoilerFreeMode(); const brackets = new TournamentBracketsPage(page); await brackets.goto(tournament.id); await expect(brackets.locators.showResultsButton).toBeVisible(); await expect(brackets.locators.censoredTeamNames.first()).toBeVisible(); await brackets.locators.showResultsButton.click(); await expect(brackets.locators.hideResultsButton).toBeVisible(); await isNotVisible(brackets.locators.censoredTeamNames); // the sessionStorage reveal carries over to the results page const results = new TournamentResultsPage(page); await results.goto(tournament.id); await expect(results.locators.resultTeamNames.first()).toBeVisible(); }); test("results page is censored and can be revealed", async ({ page, factories, }) => { const tournament = await createFinalizedTournament(factories); await impersonate(page); const settings = new SettingsPage(page); await settings.enableSpoilerFreeMode(); const results = new TournamentResultsPage(page); await results.goto(tournament.id); await expect(results.locators.showResultsButton).toBeVisible(); await isNotVisible(results.locators.resultTeamNames); await results.locators.showResultsButton.click(); await expect(results.locators.resultTeamNames.first()).toBeVisible(); }); }); const TEAM_COUNT = 4; const TEAM_SIZE = 4; async function createFinalizedTournament(factories: Factories) { const users = await factories.UserFactory.createMany(TEAM_COUNT * TEAM_SIZE); const teamRosters = Array.from({ length: TEAM_COUNT }, (_, teamIndex) => users .slice(teamIndex * TEAM_SIZE, (teamIndex + 1) * TEAM_SIZE) .map((user) => user.id), ); return factories.TournamentFactory.createPlayed( { authorId: ADMIN_ID }, { teamRosters, playedOut: "all" }, ); }