Fix E2E tests

This commit is contained in:
Kalle 2026-05-02 15:57:43 +03:00
parent a54030ce8d
commit daa099a120
62 changed files with 520 additions and 498 deletions

View File

@ -82,10 +82,10 @@ pnpm exec playwright show-trace test-results/<test-folder>/trace.zip
## Test pattern reference
Every test follows this pattern — use these imports from `~/utils/playwright`, NOT raw Playwright APIs:
Every test follows this pattern — use these imports from `./helpers/playwright`, NOT raw Playwright APIs:
```typescript
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import { expect, impersonate, navigate, seed, test } from "./helpers/playwright";
test.describe("Feature", () => {
test("does something", async ({ page }) => {
@ -104,7 +104,7 @@ Key rules:
- Use `seed(page, variation?)` to reset the database. Available variations: DEFAULT, NO_TOURNAMENT_TEAMS, REG_OPEN, SMALL_SOS, NZAP_IN_TEAM, NO_SCRIMS, NO_SQ_GROUPS
- Use `impersonate(page, userId?)` to authenticate. Default is admin (ADMIN_ID)
- Avoid `page.waitForTimeout` — use assertions or `waitFor` patterns instead
- Import `test` from `~/utils/playwright` (not from `@playwright/test`) — it includes worker port fixtures
- Import `test` from `./helpers/playwright` (not from `@playwright/test`) — it includes worker port fixtures
## Environment variables

View File

@ -28,6 +28,7 @@ export interface SendouButtonProps
shape?: "circle" | "square";
icon?: JSX.Element;
children?: React.ReactNode;
testId?: string;
}
export function SendouButton({
@ -37,10 +38,12 @@ export function SendouButton({
shape,
className,
icon,
testId,
...rest
}: SendouButtonProps) {
return (
<ReactAriaButton
data-testid={testId}
{...rest}
className={buttonClassName({ className, variant, size, shape })}
>

View File

@ -148,6 +148,7 @@ export function MatchActionPickBanTab({
if (!selected) return;
onSubmit?.({ type, map: selected });
}}
testId="pick-ban-submit-button"
>
{t("common:actions.submit")}
</SendouButton>
@ -294,6 +295,7 @@ function StageTile({
}}
onClick={onSelect}
disabled={disabled}
data-testid="pick-ban-button"
/>
{isSelected ? (
type === "PICK" ? (
@ -353,6 +355,7 @@ function ModeTile({
})}
onClick={onSelect}
disabled={disabled}
data-testid="pick-ban-button"
>
<ModeImage mode={option.mode!} size={48} />
</button>

View File

@ -134,6 +134,7 @@ export function MatchActionTab({
isOwnTeam={teams[0].id === ownTeamId}
hideLabel={ownTeamId == null}
className={styles.alpha}
testId="winner-radio-1"
/>
<StageImage
stageId={stageId}
@ -150,6 +151,7 @@ export function MatchActionTab({
isOwnTeam={teams[1].id === ownTeamId}
hideLabel={ownTeamId == null}
className={clsx(styles.bravo)}
testId="winner-radio-2"
/>
</RadioGroup>
@ -160,6 +162,7 @@ export function MatchActionTab({
type="checkbox"
checked={isKo}
onChange={(e) => setIsKo(e.target.checked)}
data-testid="ko-checkbox"
/>
{t("q:match.action.ko")}
</label>
@ -178,6 +181,7 @@ export function MatchActionTab({
}
}}
className={styles.submit}
testId="report-score-button"
>
{t("common:actions.submit")}
</SendouButton>
@ -248,6 +252,7 @@ function SetEndingConfirmation({
variant="primary"
isDisabled={isSubmitting}
onPress={onConfirm}
testId="confirm-set-end-button"
>
{t("common:actions.confirm")}
</SendouButton>
@ -264,11 +269,13 @@ function TeamRadioOption({
isOwnTeam,
hideLabel,
className,
testId,
}: {
team: ActionTabTeam;
isOwnTeam: boolean;
hideLabel?: boolean;
className?: string;
testId?: string;
}) {
const { t } = useTranslation(["q"]);
@ -279,6 +286,7 @@ function TeamRadioOption({
value={String(team.id)}
aria-label={team.name}
className={clsx(styles.teamRadioContainer, className)}
data-testid={testId}
>
{({ isSelected, isFocusVisible }) => (
<span

View File

@ -37,6 +37,7 @@ export function MatchBanner({
style={{
"--stage-img": `url(${stageBannerImageUrl(stageId)})`,
}}
data-testid="stage-banner"
>
<div className={clsx(styles.map, styles.thickText)}>
<ModeImage mode={mode} size={24} />
@ -73,6 +74,7 @@ interface IconBannerProps {
subtitle?: string;
screenLegal?: boolean;
topRight?: React.ReactNode;
testId?: string;
}
export function IconBanner({
@ -81,9 +83,10 @@ export function IconBanner({
subtitle,
screenLegal,
topRight,
testId,
}: IconBannerProps) {
return (
<div className={styles.iconBanner}>
<div className={styles.iconBanner} data-testid={testId}>
{icon}
<div className={styles.iconBannerHeader}>{header}</div>
{subtitle ? (
@ -109,7 +112,11 @@ function ScreenNotice({ screenLegal }: { screenLegal: boolean }) {
return (
<SendouPopover
trigger={
<SendouButton variant="minimal" className={styles.notice}>
<SendouButton
variant="minimal"
className={styles.notice}
testId={screenLegal ? "screen-allowed" : "screen-banned"}
>
<Icon
size={imgSize}
className={screenLegal ? styles.legalIcon : styles.illegalIcon}

View File

@ -37,7 +37,10 @@ function ModeProgress({ games }: Pick<MatchBannerBottomRowProps, "games">) {
if (allSameMode) {
return (
<div className={styles.modeProgress}>
<div className={styles.mode}>
<div
className={styles.mode}
data-testid={`mode-progress-${knownModes[0]}`}
>
<ModeImage mode={knownModes[0]} size={16} />
</div>
<div className={styles.modeCount}>×{games.length}</div>
@ -49,11 +52,19 @@ function ModeProgress({ games }: Pick<MatchBannerBottomRowProps, "games">) {
<div className={styles.modeProgress}>
{games.map((game, i) =>
game.mode ? (
<div key={i} className={styles.mode}>
<div
key={i}
className={styles.mode}
data-testid={`mode-progress-${game.mode}`}
>
<ModeImage mode={game.mode} size={16} />
</div>
) : (
<div key={i} className={clsx(styles.mode, styles.modePlaceholder)}>
<div
key={i}
className={clsx(styles.mode, styles.modePlaceholder)}
data-testid="mode-progress-banned"
>
<MousePointerClick size={16} />
</div>
),

View File

@ -33,7 +33,10 @@ function Score({ score }: { score: MatchBannerTopRowProps["score"] }) {
<div>
{score.alpha}-{score.bravo}
</div>
<div className={styles.sub}>
<div
className={styles.sub}
data-testid={score.isFinal ? "match-final" : undefined}
>
{score.isFinal
? t("q:match.banner.final")
: score.bestOf
@ -73,7 +76,7 @@ function Timer({
: minuteFormatter.format(minutes);
return (
<div className={styles.values}>
<div className={styles.values} data-testid="match-timer">
<time dateTime={dateTime(time.currentMinutes)} className={styles.sub}>
{displayValue(time.currentMinutes)}
</time>

View File

@ -86,7 +86,11 @@ export function MatchJoinTab({
<InfoWithHeader header={t("q:match.hostedBy")} value={hostedBy} />
) : null}
<InfoWithHeader header={t("q:match.pool")} value={pool} />
<InfoWithHeader header={t("q:match.password.short")} value={pass} />
<InfoWithHeader
header={t("q:match.password.short")}
value={pass}
testId="room-pass"
/>
</div>
</div>
</div>
@ -122,11 +126,21 @@ function StaleRoomPrompt({
);
}
function InfoWithHeader({ header, value }: { header: string; value: string }) {
function InfoWithHeader({
header,
value,
testId,
}: {
header: string;
value: string;
testId?: string;
}) {
return (
<div>
<div className={styles.infoHeader}>{header}</div>
<div className={styles.infoValue}>{value}</div>
<div className={styles.infoValue} data-testid={testId}>
{value}
</div>
</div>
);
}

View File

@ -138,13 +138,14 @@ function TeamRoster({
{team.members.length > 0 ? (
<ul className={styles.rosterMembers}>
{isEditing
? team.members.map((member) => (
? team.members.map((member, index) => (
<li key={member.id}>
<label className="stack horizontal sm items-center cursor-pointer">
<input
type="checkbox"
checked={selectedMemberIds.includes(member.id)}
onChange={() => handleToggleMember(member.id)}
data-testid={`player-checkbox-${side}-${index}`}
/>
<Avatar user={member} size="xxs" />
<span>{member.username}</span>
@ -188,6 +189,7 @@ function TeamRoster({
isSubmitting || selectedMemberIds.length !== minMembersPerTeam
}
onPress={handleSubmit}
testId={`save-active-roster-button-${side}`}
>
{t("common:actions.submit")}
</SendouButton>
@ -210,6 +212,7 @@ function TeamRoster({
setSelectedMemberIds(activeMembers.map((m) => m.id));
setIsEditing(true);
}}
testId={`edit-active-roster-button-${side}`}
>
{t("common:actions.edit")}
</SendouButton>

View File

@ -143,6 +143,7 @@ export function TournamentMatchActionTab({
{ method: "post" },
);
}}
testId="undo-score-button"
>
{t("q:match.undoReport")}
</SendouButton>

View File

@ -512,7 +512,7 @@ function EditReportedScoreForm({
<fieldset key={team.id} className={styles.teamFieldset}>
<legend>{team.name}</legend>
<div className="stack sm">
{team.members.map((member) => {
{team.members.map((member, memberIdx) => {
const checked = checkedPlayers[teamIdx as 0 | 1].includes(
member.userId,
);
@ -527,6 +527,7 @@ function EditReportedScoreForm({
onChange={() =>
togglePlayer(teamIdx as 0 | 1, member.userId)
}
data-testid={`edit-result-player-checkbox-${teamIdx === 0 ? "alpha" : "bravo"}-${memberIdx}`}
/>
<span>{member.username}</span>
</label>

View File

@ -83,6 +83,7 @@ export function TournamentMatchBanner({
teams: teamsMissingActiveRoster.join(" & "),
})}
screenLegal={screenLegal}
testId="active-roster-needed-text"
/>
) : data.matchIsOver ? (
<MultiMatchBanner

View File

@ -41,7 +41,16 @@ export function TournamentMatchTabs({
turnOfResult,
isPickBanStep,
} = useMatch();
if (!teamOne || !teamTwo) return null;
// Preview matches (participants TBD) only render the admin tab so organizers
// can pre-cast or pre-prepare; everything else needs both teams.
if (!teamOne || !teamTwo) {
return tabs.includes(TAB_KEYS.ADMIN) ? (
<MatchTabs tabs={[TAB_KEYS.ADMIN]}>
<TournamentMatchAdminTab data={data} />
</MatchTabs>
) : null;
}
const opponentOneId = teamOne.id;
const opponentTwoId = teamTwo.id;

View File

@ -207,7 +207,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const isParticipant = match.players.some((p) => p.id === user?.id);
const canJoin =
!matchIsOver &&
isParticipant &&
(isParticipant || tournament.isOrganizerOrStreamer(user)) &&
!isLeagueRoundLocked(tournament, match.roundId);
const [roomLinks, anyUserPrefersNoSplatnet] = canJoin

View File

@ -15,6 +15,7 @@ export const handle: SendouRouteHandle = {
i18n: ["q"],
};
// xxx: check page when both teams are not resolved yet
export default function TournamentMatchPage() {
const data = useLoaderData<typeof loader>();

View File

@ -574,7 +574,7 @@ Run `pnpm run i18n:sync` after adding English translations to initialize other l
Use `createFormHelpers` for type-safe form interactions:
```ts
import { createFormHelpers } from "~/utils/playwright-form";
import { createFormHelpers } from "./helpers/playwright-form";
import { myFormSchema } from "~/features/my/my-schemas";
test("fills and submits form", async ({ page }) => {
@ -628,7 +628,7 @@ import {
selectStage,
selectWeapon,
selectUser,
} from "~/utils/playwright";
} from "./helpers/playwright";
```
## Complete Example
@ -717,9 +717,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
### E2E Test (`feature.spec.ts`)
```ts
import { createFormHelpers } from "~/utils/playwright-form";
import { createFormHelpers } from "./helpers/playwright-form";
import { createItemSchema } from "~/features/item/feature-schemas";
import { test, navigate, impersonate, seed } from "~/utils/playwright";
import { test, navigate, impersonate, seed } from "./helpers/playwright";
test("creates new item", async ({ page }) => {
await seed(page);

View File

@ -1,3 +1,4 @@
import { ANALYZER_URL } from "~/utils/urls";
import {
expect,
impersonate,
@ -6,8 +7,7 @@ import {
seed,
selectWeapon,
test,
} from "~/utils/playwright";
import { ANALYZER_URL } from "~/utils/urls";
} from "./helpers/playwright";
test.describe("Build Analyzer", () => {
test("analyzes a build and links to new build page with same abilities", async ({

View File

@ -1,8 +1,14 @@
import type { Page } from "@playwright/test";
import { ORG_ADMIN_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import { tournamentTeamPage } from "~/utils/urls";
import {
expect,
impersonate,
navigate,
seed,
test,
} from "./helpers/playwright";
const ITZ_TOURNAMENT_ID = 2;
const ITZ_TEAM_ID = 101;

View File

@ -1,7 +1,13 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
test,
} from "./helpers/playwright";
const __dirname = path.dirname(fileURLToPath(import.meta.url));

View File

@ -1,5 +1,6 @@
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,
@ -8,8 +9,7 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import { associationsPage, scrimsPage } from "~/utils/urls";
} from "./helpers/playwright";
test.describe("Associations", () => {
test("creates a new association", async ({ page }) => {

View File

@ -1,3 +1,5 @@
import { badgePage } from "~/utils/urls";
import { NZAP_TEST_ID } from "../app/db/seed/constants";
import {
expect,
impersonate,
@ -5,9 +7,7 @@ import {
seed,
selectUser,
test,
} from "~/utils/playwright";
import { badgePage } from "~/utils/urls";
import { NZAP_TEST_ID } from "../app/db/seed/constants";
} from "./helpers/playwright";
test.describe("Badges", () => {
test("adds a badge owner sending a notification", async ({ page }) => {

View File

@ -1,6 +1,7 @@
import type { Page } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { ADMIN_PAGE, SUSPENDED_PAGE } from "~/utils/urls";
import {
expect,
impersonate,
@ -8,8 +9,7 @@ import {
seed,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { ADMIN_PAGE, SUSPENDED_PAGE } from "~/utils/urls";
} from "./helpers/playwright";
async function banUser(
page: Page,

View File

@ -4,9 +4,15 @@ import type { GearType } from "~/db/tables";
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
import { newBuildBaseSchema } from "~/features/user-page/user-page-schemas";
import invariant from "~/utils/invariant";
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import { BUILDS_PAGE, userBuildsPage, userNewBuildPage } from "~/utils/urls";
import {
expect,
impersonate,
navigate,
seed,
test,
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
test.describe("Builds", () => {
test("adds a build", async ({ page }) => {

View File

@ -1,4 +1,5 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { calendarPage } from "~/utils/urls";
import {
expect,
expectIsHydrated,
@ -7,8 +8,7 @@ import {
navigate,
seed,
test,
} from "~/utils/playwright";
import { calendarPage } from "~/utils/urls";
} from "./helpers/playwright";
const SENDOU_INK_TOURNAMENTS_COUNT = 6;

View File

@ -1,5 +1,5 @@
import { expect, navigate, test } from "~/utils/playwright";
import { COMP_ANALYZER_URL } from "~/utils/urls";
import { expect, navigate, test } from "./helpers/playwright";
test.describe("Composition Analyzer", () => {
test("weapon selection, removal, and URL persistence", async ({ page }) => {

View File

@ -1,5 +1,11 @@
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import { EVENTS_PAGE } from "~/utils/urls";
import {
expect,
impersonate,
navigate,
seed,
test,
} from "./helpers/playwright";
test.describe("Events", () => {
test("filters between tabs and navigates to an event", async ({ page }) => {

View File

@ -1,4 +1,5 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { FRIENDS_PAGE } from "~/utils/urls";
import {
expect,
impersonate,
@ -8,8 +9,7 @@ import {
submit,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { FRIENDS_PAGE } from "~/utils/urls";
} from "./helpers/playwright";
test.describe("Friends", () => {
test("send friend request, accept it, then delete friend", async ({

View File

@ -1,4 +1,10 @@
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
test,
} from "./helpers/playwright";
test.describe("Global search", () => {
test("searches for users and organizations", async ({ page }) => {

View File

@ -1,7 +1,7 @@
import { type ChildProcess, execSync, spawn } from "node:child_process";
import fs from "node:fs";
import type { FullConfig } from "@playwright/test";
import { E2E_BASE_PORT } from "~/utils/playwright";
import { E2E_BASE_PORT } from "./helpers/playwright";
const WORKER_COUNT = Number(process.env.E2E_WORKERS) || 4;
const DEBUG = process.env.E2E_DEBUG === "true";

View File

@ -7,7 +7,7 @@ import {
import dotenv from "dotenv";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import type { SeedVariation } from "~/features/api-private/routes/seed";
import { tournamentBracketsPage } from "./urls";
import { tournamentBracketsPage } from "~/utils/urls";
dotenv.config();
export const E2E_BASE_PORT = Number(process.env.PORT || 5173) + 500;

View File

@ -0,0 +1,108 @@
import type { Page } from "@playwright/test";
import { expect, submit, waitForPOSTResponse } from "./playwright";
/**
* Helpers for interacting with the tournament match page in e2e tests.
*
* The match page splits its UI into URL-driven tabs (rosters/action/admin/etc.)
* these helpers handle the navigation so individual tests can stay focused on
* the assertion they care about.
*/
type Side = 1 | 2;
export const navigateToMatch = async (page: Page, matchId: number) => {
await expect(async () => {
await page.locator(`[data-match-id="${matchId}"]`).click();
await expect(page.getByTestId("back-to-bracket-button")).toBeVisible();
}).toPass();
};
export const backToBracket = async (page: Page) => {
await expect(async () => {
await page.getByTestId("back-to-bracket-button").click();
await expect(page.getByTestId("brackets-viewer")).toBeVisible();
}).toPass();
};
export const expectScore = (page: Page, score: [number, number]) =>
expect(page.getByText(score.join("-")).first()).toBeVisible();
const TAB_LABELS = {
action: "Action",
admin: "Admin",
result: "Result",
rosters: "Rosters",
join: "Join",
} as const;
export const goToTab = async (
page: Page,
tab: "action" | "admin" | "result" | "rosters" | "join",
) => {
// When teams have more members than the minimum, the action tab is hidden
// until each team's active roster is locked in via the rosters tab. Auto-set
// any roster that's still in default-editing mode so callers can stay focused
// on the flow they actually care about.
if (tab === "action") {
await ensureActiveRostersSet(page);
}
await page.getByRole("tab", { name: TAB_LABELS[tab] }).click();
};
const ensureActiveRostersSet = async (page: Page) => {
const sides = ["alpha", "bravo"] as const;
// If the action tab is already there, no rosters need setting.
if ((await page.getByRole("tab", { name: TAB_LABELS.action }).count()) > 0) {
return;
}
// Editing inputs only render on the rosters tab — switch there.
await page.getByRole("tab", { name: TAB_LABELS.rosters }).click();
// Wait for the rosters panel to be ready before probing for editing UI.
await expect(page.getByRole("tabpanel", { name: "Rosters" })).toBeVisible();
for (const side of sides) {
const submitButton = page.getByTestId(`save-active-roster-button-${side}`);
if ((await submitButton.count()) === 0) continue;
// Default-editing renders all members unchecked; pick the first 4.
for (let i = 0; i < 4; i++) {
const checkbox = page.getByTestId(`player-checkbox-${side}-${i}`);
if (!(await checkbox.isChecked())) await checkbox.click();
}
await submit(page, `save-active-roster-button-${side}`);
}
};
/**
* Sweeps `mapsToReport` maps in a row, all won by `winner`. By default the
* last map ends the set (the typical case full Bo3/Bo5 sweep), and the
* helper goes through the confirmation screen for that map. Pass
* `setEnds: false` when reporting a partial set (e.g. only 1 of a Bo3).
*/
export const reportResult = async (
page: Page,
{
mapsToReport,
winner = 1,
setEnds = true,
}: { mapsToReport: number; winner?: Side; setEnds?: boolean },
) => {
for (let i = 0; i < mapsToReport; i++) {
const isFinal = setEnds && i === mapsToReport - 1;
await page.getByTestId(`winner-radio-${winner}`).click();
if (isFinal) {
await page.getByTestId("report-score-button").click();
await submit(page, "confirm-set-end-button");
} else {
await submit(page, "report-score-button");
}
}
};
export const undoLastReport = (page: Page) =>
waitForPOSTResponse(page, async () => {
await page.getByTestId("undo-score-button").click();
});

View File

@ -1,3 +1,4 @@
import { LFG_PAGE } from "~/utils/urls";
import {
expect,
impersonate,
@ -5,8 +6,7 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import { LFG_PAGE } from "~/utils/urls";
} from "./helpers/playwright";
test.describe("LFG", () => {
test("adds a new lfg post", async ({ page }) => {

View File

@ -1,4 +1,10 @@
import { expect, impersonate, navigate, seed, test } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
test,
} from "./helpers/playwright";
test.describe("Navigation", () => {
test("desktop navigation", async ({ page }) => {

View File

@ -1,5 +1,5 @@
import { expect, navigate, selectWeapon, test } from "~/utils/playwright";
import { OBJECT_DAMAGE_CALCULATOR_URL } from "~/utils/urls";
import { expect, navigate, selectWeapon, test } from "./helpers/playwright";
test.describe("Object Damage Calculator", () => {
test.beforeEach(async ({ page }) => {

View File

@ -5,6 +5,11 @@ import {
newOrganizationSchema,
updateIsEstablishedSchema,
} from "~/features/tournament-organization/tournament-organization-schemas";
import {
TOURNAMENT_NEW_PAGE,
tournamentOrganizationPage,
tournamentPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -14,13 +19,8 @@ import {
submit,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import {
TOURNAMENT_NEW_PAGE,
tournamentOrganizationPage,
tournamentPage,
} from "~/utils/urls";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
const url = tournamentOrganizationPage({
organizationSlug: "sendouink",

View File

@ -1,6 +1,7 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { scrimsNewFormSchema } from "~/features/scrims/scrims-schemas";
import { newScrimPostPage, scrimsPage } from "~/utils/urls";
import {
expect,
impersonate,
@ -9,9 +10,8 @@ import {
selectUser,
submit,
test,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import { newScrimPostPage, scrimsPage } from "~/utils/urls";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
test.describe("Scrims", () => {
test("creates a new scrim & deletes it", async ({ page }) => {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,11 @@
import type { Page } from "@playwright/test";
import { NZAP_TEST_ID, STAFF_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
SENDOUQ_LOOKING_PAGE,
SENDOUQ_PAGE,
sendouQMatchPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -9,12 +14,9 @@ import {
selectWeapon,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import {
SENDOUQ_LOOKING_PAGE,
SENDOUQ_PAGE,
sendouQMatchPage,
} from "~/utils/urls";
} from "./helpers/playwright";
// xxx: fix failing tests
/**
* Tests for the SendouQ match page (`/q/match/$id`).

View File

@ -1,5 +1,11 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
SENDOUQ_LOOKING_PAGE,
SENDOUQ_PAGE,
SENDOUQ_PREPARING_PAGE,
sendouQInviteLink,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -7,13 +13,7 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import {
SENDOUQ_LOOKING_PAGE,
SENDOUQ_PAGE,
SENDOUQ_PREPARING_PAGE,
sendouQInviteLink,
} from "~/utils/urls";
} from "./helpers/playwright";
test.describe("SendouQ", () => {
test("Group preparation flow - add friends and users via invite link", async ({

View File

@ -4,6 +4,12 @@ import {
disableBuildAbilitySortingSchema,
spoilerFreeModeSchema,
} from "~/features/settings/settings-schemas";
import {
CALENDAR_PAGE,
SETTINGS_PAGE,
tournamentBracketsPage,
tournamentResultsPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -12,14 +18,8 @@ import {
seed,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import {
CALENDAR_PAGE,
SETTINGS_PAGE,
tournamentBracketsPage,
tournamentResultsPage,
} from "~/utils/urls";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
test.describe("Settings", () => {
test("updates 'disableBuildAbilitySorting'", async ({ page }) => {

View File

@ -1,6 +1,7 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID, ADMIN_ID } from "~/features/admin/admin-constants";
import { createTeamSchema } from "~/features/team/team-schemas";
import { editTeamPage, teamPage, userPage } from "~/utils/urls";
import {
expect,
impersonate,
@ -10,9 +11,8 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import { editTeamPage, teamPage, userPage } from "~/utils/urls";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
test.describe("New team creation", () => {
test("creates new team", async ({ page }) => {

View File

@ -1,6 +1,6 @@
import type { Locator, Page } from "@playwright/test";
import { expect, navigate, test } from "~/utils/playwright";
import { TIER_LIST_MAKER_URL } from "~/utils/urls";
import { expect, navigate, test } from "./helpers/playwright";
test.describe("Tier List Maker", () => {
test("toggles work, items can be dragged, and state persists after reload", async ({

View File

@ -1,5 +1,5 @@
import { expect, navigate, seed, test } from "~/utils/playwright";
import { topSearchPage, userPage } from "~/utils/urls";
import { expect, navigate, seed, test } from "./helpers/playwright";
test.describe("Top search", () => {
test("views different x rank placements", async ({ page }) => {

View File

@ -1,3 +1,4 @@
import { tournamentBracketsPage } from "~/utils/urls";
import {
expect,
impersonate,
@ -5,8 +6,7 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import { tournamentBracketsPage } from "~/utils/urls";
} from "./helpers/playwright";
const AB_RR_TOURNAMENT_ID = 8;
const TEAMS_PER_DIVISION = 6;

View File

@ -1,7 +1,16 @@
import type { Page } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
import { updateNoScreenSchema } from "~/features/settings/settings-schemas";
import {
NOTIFICATIONS_URL,
SETTINGS_PAGE,
tournamentAdminPage,
tournamentBracketsPage,
tournamentMatchPage,
tournamentPage,
tournamentTeamsPage,
userResultsPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -13,137 +22,16 @@ import {
submit,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
import {
NOTIFICATIONS_URL,
SETTINGS_PAGE,
tournamentAdminPage,
tournamentBracketsPage,
tournamentMatchPage,
tournamentPage,
tournamentTeamsPage,
userResultsPage,
} from "~/utils/urls";
const navigateToMatch = async (page: Page, matchId: number) => {
await expect(async () => {
await page.locator(`[data-match-id="${matchId}"]`).click();
await expect(page.getByTestId("match-header")).toBeVisible();
}).toPass();
};
const reportResult = async ({
page,
amountOfMapsToReport,
winner = 1,
points,
}: {
page: Page;
amountOfMapsToReport: 1 | 2 | 3 | 4;
winner?: 1 | 2;
points?: [number, number];
}) => {
const confirmCheckbox = page.getByTestId("end-confirmation");
const fillPointsInput = async () => {
if (!points) return;
await page.getByTestId("points-input-1").fill(String(points[0]));
await page.getByTestId("points-input-2").fill(String(points[1]));
};
await page.getByTestId("actions-tab").click();
// Auto-detect and set rosters for teams with 5+ players
// Check if first team needs roster selection (checkbox exists and is not disabled)
const firstTeamCheckbox = page.getByTestId("player-checkbox-0").first();
if (
(await firstTeamCheckbox.count()) > 0 &&
!(await firstTeamCheckbox.isDisabled())
) {
await page.getByTestId("player-checkbox-0").first().click();
await page.getByTestId("player-checkbox-1").first().click();
await page.getByTestId("player-checkbox-2").first().click();
await page.getByTestId("player-checkbox-3").first().click();
await submit(page, "save-active-roster-button-0");
// update went through
await expect(page.getByTestId("player-checkbox-0").first()).toBeDisabled();
}
// Check if second team needs roster selection
const lastTeamCheckbox = page.getByTestId("player-checkbox-0").last();
if (
(await lastTeamCheckbox.count()) > 0 &&
!(await lastTeamCheckbox.isDisabled())
) {
await page.getByTestId("player-checkbox-0").last().click();
await page.getByTestId("player-checkbox-1").last().click();
await page.getByTestId("player-checkbox-2").last().click();
await page.getByTestId("player-checkbox-3").last().click();
await submit(page, "save-active-roster-button-1");
}
await fillPointsInput();
await page.getByTestId(`winner-radio-${winner}`).click();
await submit(page, "report-score-button");
await expect(page.getByText(winner === 1 ? "1-0" : "0-1")).toBeVisible();
if (amountOfMapsToReport >= 2) {
await page.getByTestId(`winner-radio-${winner}`).click();
await fillPointsInput();
if (amountOfMapsToReport === 2) {
await confirmCheckbox.click();
await submit(page, "report-score-button");
await expect(page.getByTestId("report-timestamp")).toBeVisible();
} else {
await submit(page, "report-score-button");
}
}
if (amountOfMapsToReport === 3) {
await expect(page.getByText("2-0")).toBeVisible();
await page.getByTestId(`winner-radio-${winner}`).click();
await fillPointsInput();
await confirmCheckbox.click();
await submit(page, "report-score-button");
await expect(page.getByTestId("report-timestamp")).toBeVisible();
}
if (amountOfMapsToReport === 4) {
await expect(page.getByText("2-0")).toBeVisible();
await page.getByTestId(`winner-radio-${winner}`).click();
await fillPointsInput();
await submit(page, "report-score-button");
await expect(page.getByText("3-0")).toBeVisible();
await page.getByTestId(`winner-radio-${winner}`).click();
await confirmCheckbox.click();
await submit(page, "report-score-button");
await expect(page.getByTestId("report-timestamp")).toBeVisible();
}
};
const backToBracket = async (page: Page) => {
await expect(async () => {
await page.getByTestId("back-to-bracket-button").click();
await expect(page.getByTestId("brackets-viewer")).toBeVisible();
}).toPass();
};
const expectScore = (page: Page, score: [number, number]) =>
expect(page.getByText(score.join("-"))).toBeVisible();
backToBracket,
expectScore,
goToTab,
navigateToMatch,
reportResult,
undoLastReport,
} from "./helpers/tournament-match";
test.describe("Tournament bracket", () => {
test("sets active roster as regular member", async ({ page }) => {
@ -161,35 +49,32 @@ test.describe("Tournament bracket", () => {
await expect(page.getByTestId("active-roster-needed-text")).toBeVisible();
await page.getByTestId("actions-tab").click();
// Team 10 has 5 players; select first 4 for active roster
// Team 10 is team 2 (second team in the match), so use last()
await page.getByTestId("player-checkbox-0").last().click();
await page.getByTestId("player-checkbox-1").last().click();
await page.getByTestId("player-checkbox-2").last().click();
await page.getByTestId("player-checkbox-3").last().click();
await submit(page, "save-active-roster-button-1");
// Team 10 (5 players) is opponentTwo in match 2 → bravo side.
// The roster tab opens in editing mode by default when active roster is missing.
await goToTab(page, "rosters");
await page.getByTestId("player-checkbox-bravo-0").click();
await page.getByTestId("player-checkbox-bravo-1").click();
await page.getByTestId("player-checkbox-bravo-2").click();
await page.getByTestId("player-checkbox-bravo-3").click();
await submit(page, "save-active-roster-button-bravo");
// did it persist?
await navigate({
page,
url: tournamentMatchPage({ tournamentId, matchId }),
});
// Only team 10 needed to set roster (team 9 has 4 players)
await isNotVisible(page.getByTestId("active-roster-needed-text"));
await page.getByTestId("actions-tab").click();
await page.getByTestId("edit-active-roster-button").click();
await page.getByTestId("player-checkbox-3").last().click();
await page.getByTestId("player-checkbox-4").last().click();
await submit(page, "save-active-roster-button-1");
await goToTab(page, "rosters");
await page.getByTestId("edit-active-roster-button-bravo").click();
// Swap player 3 out for player 4
await page.getByTestId("player-checkbox-bravo-3").click();
await page.getByTestId("player-checkbox-bravo-4").click();
await submit(page, "save-active-roster-button-bravo");
await expect(page.getByTestId("edit-active-roster-button")).toBeVisible();
await expect(
page.getByTestId("player-checkbox-3").last(),
).not.toBeChecked();
page.getByTestId("edit-active-roster-button-bravo"),
).toBeVisible();
});
// 1) Report winner of N-ZAP's first match
@ -212,7 +97,8 @@ test.describe("Tournament bracket", () => {
// 1)
await navigateToMatch(page, 5);
await reportResult({ page, amountOfMapsToReport: 2 });
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
// 2)
@ -222,30 +108,32 @@ test.describe("Tournament bracket", () => {
url: tournamentBracketsPage({ tournamentId }),
});
await navigateToMatch(page, 6);
await reportResult({ page, amountOfMapsToReport: 2 });
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
// 3)
await navigateToMatch(page, 18);
await reportResult({
page,
amountOfMapsToReport: 1,
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, setEnds: false });
await backToBracket(page);
// 4)
await navigateToMatch(page, 5);
await goToTab(page, "admin");
await isNotVisible(page.getByTestId("reopen-match-button"));
await backToBracket(page);
// 5)
await navigateToMatch(page, 18);
await submit(page, "undo-score-button");
await goToTab(page, "action");
await undoLastReport(page);
await expectScore(page, [0, 0]);
await backToBracket(page);
// 6)
await navigateToMatch(page, 5);
await goToTab(page, "admin");
await submit(page, "reopen-match-button");
await expectScore(page, [1, 0]);
@ -256,13 +144,10 @@ test.describe("Tournament bracket", () => {
url: tournamentBracketsPage({ tournamentId }),
});
await navigateToMatch(page, 5);
await submit(page, "undo-score-button");
await goToTab(page, "action");
await undoLastReport(page);
await expectScore(page, [0, 0]);
await reportResult({
page,
amountOfMapsToReport: 2,
winner: 2,
});
await reportResult(page, { mapsToReport: 2, winner: 2 });
await backToBracket(page);
await expect(
page.locator("[data-round-id='5'] [data-participant-id='102']"),
@ -335,11 +220,9 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="1"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
});
await navigateToMatch(page, 1);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await page.getByTestId("finalize-tournament-button").click();
@ -408,11 +291,8 @@ test.describe("Tournament bracket", () => {
for (const id of [2, 4, 6, 7, 8, 9, 10, 11, 12]) {
await navigateToMatch(page, id);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
}
@ -447,10 +327,8 @@ test.describe("Tournament bracket", () => {
await submit(page, "confirm-finalize-bracket-button");
await navigateToMatch(page, 13);
await reportResult({
page,
amountOfMapsToReport: 3,
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 3 });
await navigate({
page,
@ -460,10 +338,8 @@ test.describe("Tournament bracket", () => {
await submit(page, "confirm-finalize-bracket-button");
for (const matchId of [14, 15, 16, 17]) {
await navigateToMatch(page, matchId);
await reportResult({
page,
amountOfMapsToReport: 3,
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 3 });
await backToBracket(page);
}
@ -471,8 +347,10 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("assign-badges-later-switch").click();
await submit(page, "confirm-button");
// not possible to reopen finals match anymore
// after finalizing the tournament, the admin tab disappears so the
// reopen action is no longer reachable
await navigateToMatch(page, 14);
await isNotVisible(page.getByRole("tab", { name: "Admin" }));
await isNotVisible(page.getByTestId("reopen-match-button"));
await backToBracket(page);
});
@ -508,23 +386,18 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="1"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await navigateToMatch(page, 1);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await page.getByRole("tab", { name: "Great White" }).click();
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="2"]').click();
await reportResult({
page,
amountOfMapsToReport: 3,
});
await navigateToMatch(page, 2);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 3 });
await backToBracket(page);
await page.getByTestId("finalize-tournament-button").click();
@ -573,12 +446,9 @@ test.describe("Tournament bracket", () => {
await submit(page, "confirm-finalize-bracket-button");
for (const matchId of [1, 2, 3, 4, 5, 6]) {
await page.locator(`[data-match-id="${matchId}"]`).click();
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await navigateToMatch(page, matchId);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
}
@ -591,7 +461,7 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="7"]').click();
await navigateToMatch(page, 7);
await expect(page.getByTestId("back-to-bracket-button")).toBeVisible();
await page.getByTestId("admin-tab").click();
@ -696,25 +566,22 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="2"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await navigateToMatch(page, 2);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await page.getByTestId("actions-tab").click();
await page.getByTestId("revise-button").click();
await page.getByTestId("player-checkbox-3").first().click();
await page.getByTestId("player-checkbox-4").first().click();
await page.getByTestId("points-input-1").fill("99");
await submit(page, "save-revise-button");
await goToTab(page, "admin");
await page.getByTestId("edit-result-0-button").click();
// Swap player 3 out for player 4 on the alpha (winner) team
await page.getByTestId("edit-result-player-checkbox-alpha-3").click();
await page.getByTestId("edit-result-player-checkbox-alpha-4").click();
// Toggle KO so we can verify the edit went through (RR collects KO).
await page.getByLabel("KO").check();
await submit(page, "save-result-0-button");
await expect(page.getByTestId("revise-button")).toBeVisible();
await expect(
page.getByTestId("player-checkbox-3").first(),
).not.toBeChecked();
await expect(page.getByText("99p")).toBeVisible();
// Edit returns to read-only view, now showing the KO label
await expect(page.getByTestId("edit-result-0-button")).toBeVisible();
await expect(page.getByText(/\(KO\)/).first()).toBeVisible();
});
test("changes to picked map pool & best of", async ({ page }) => {
@ -740,8 +607,10 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("increase-map-count-button").first().click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="1"]').click();
await expect(page.getByTestId("mode-progress-CB")).toHaveCount(5);
await navigateToMatch(page, 1);
// Bo5 of clam blitz: one mode icon + ×5 count text
await expect(page.getByTestId("mode-progress-CB")).toBeVisible();
await expect(page.getByText("×5")).toBeVisible();
});
test("reopens round robin match and changes score", async ({ page }) => {
@ -761,47 +630,36 @@ test.describe("Tournament bracket", () => {
// needs also to be completed so 9 unlocks
await navigateToMatch(page, 7);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
// set situation where match A is completed and its participants also completed their follow up matches B & C
// and then we go back and change the winner of A
await navigateToMatch(page, 8);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await navigateToMatch(page, 9);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await navigateToMatch(page, 10);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await navigateToMatch(page, 8);
await goToTab(page, "admin");
await submit(page, "reopen-match-button");
await submit(page, "undo-score-button");
await reportResult({
page,
amountOfMapsToReport: 2,
points: [0, 100],
await goToTab(page, "action");
await undoLastReport(page);
await reportResult(page, {
mapsToReport: 2,
winner: 2,
setEnds: true,
});
});
@ -827,41 +685,33 @@ test.describe("Tournament bracket", () => {
// Complete R1 matches in group B (matches 7 and 8) to unlock R2 matches
await navigateToMatch(page, 7);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await navigateToMatch(page, 8);
await reportResult({
page,
amountOfMapsToReport: 2,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
// Match 9 is R2 in group B - should now be unlocked since R1 is complete
// Start it but don't complete it
await navigateToMatch(page, 9);
await reportResult({
page,
amountOfMapsToReport: 1,
points: [100, 0],
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, setEnds: false });
await backToBracket(page);
// Reopen match 7 (R1 match) - simulating a score misreport correction
await navigateToMatch(page, 7);
await goToTab(page, "admin");
await submit(page, "reopen-match-button");
await backToBracket(page);
// Verify the R2 match that was already in progress is still playable
// Before the fix, this would become locked and unplayable
await navigateToMatch(page, 9);
await expect(page.getByText("1-0")).toBeVisible();
await page.getByTestId("actions-tab").click();
await expectScore(page, [1, 0]);
await goToTab(page, "action");
await expect(page.getByTestId("winner-radio-1")).toBeVisible();
});
@ -898,32 +748,35 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="1"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
});
await navigateToMatch(page, 1);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await page.locator('[data-match-id="3"]').click();
await navigateToMatch(page, 3);
await goToTab(page, "admin");
// Picking a chip auto-submits the cast channel; lock the match afterwards.
await waitForPOSTResponse(page, async () => {
await page.locator('label[for$="-test"]').click();
});
await submit(page, "cast-info-submit-button");
await backToBracket(page);
await page.locator('[data-match-id="2"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
});
await navigateToMatch(page, 2);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
await expect(page.getByText("🔒 CAST")).toBeVisible();
await page.locator('[data-match-id="3"]').click();
await expect(page.getByText("Match locked to be casted")).toBeVisible();
await navigateToMatch(page, 3);
await goToTab(page, "admin");
// Lock state is signalled by the toggle being "Unlock" instead of "Lock"
await expect(page.getByRole("button", { name: "Unlock" })).toBeVisible();
await submit(page, "cast-info-submit-button");
await expect(page.getByTestId("stage-banner")).toBeVisible();
await page.getByTestId("cast-info-select").selectOption("test");
await submit(page, "cast-info-submit-button");
// Cast channel "test" persists across unlock; the bracket badge flips
// from 🔒 CAST to 🔴 LIVE once the match is unlocked and ongoing.
await backToBracket(page);
await expect(page.getByText("🔴 LIVE")).toBeVisible();
});
@ -943,11 +796,9 @@ test.describe("Tournament bracket", () => {
await submit(page, "confirm-finalize-bracket-button");
await isNotVisible(page.locator('[data-match-id="1"]'));
await page.locator('[data-match-id="2"]').click();
await reportResult({
page,
amountOfMapsToReport: 2,
});
await navigateToMatch(page, 2);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await page.getByTestId("admin-tab").click();
await page
@ -988,11 +839,11 @@ test.describe("Tournament bracket", () => {
await page.getByTestId("finalize-bracket-button").click();
await submit(page, "confirm-finalize-bracket-button");
await page.locator('[data-match-id="1"]').click();
await navigateToMatch(page, 1);
await expect(page.getByTestId("screen-banned")).toBeVisible();
await backToBracket(page);
await page.locator('[data-match-id="2"]').click();
await navigateToMatch(page, 2);
await expect(page.getByTestId("screen-allowed")).toBeVisible();
});
@ -1015,12 +866,8 @@ test.describe("Tournament bracket", () => {
await navigateToMatch(page, 1);
await expect(page.getByText("Play all 3")).toBeVisible();
await reportResult({
page,
amountOfMapsToReport: 3,
points: [100, 0],
winner: 1,
});
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 3 });
});
test("swiss tournament with bracket advancing/unadvancing & dropping out a team", async ({
@ -1043,11 +890,9 @@ test.describe("Tournament bracket", () => {
// report all group A round 1 scores
for (const id of [1, 2, 3, 4]) {
await page.locator(`[data-match-id="${id}"]`).click();
await reportResult({
page,
amountOfMapsToReport: 2,
});
await navigateToMatch(page, id);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
}
@ -1171,13 +1016,15 @@ test.describe("Tournament bracket", () => {
page,
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("actions-tab").click();
await goToTab(page, "action");
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
}
await expect(page.getByTestId("mode-progress-banned")).toHaveCount(2);
// once both teams banned the ban prompt is gone and the actual map
// banner takes over.
await expect(page.getByTestId("stage-banner")).toBeVisible();
}
await impersonate(page, teamOneCaptainId);
@ -1187,14 +1034,12 @@ test.describe("Tournament bracket", () => {
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("actions-tab").click();
await page.getByTestId("winner-radio-2").click();
await page.getByTestId("points-input-2").fill("100");
await submit(page, "report-score-button");
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, winner: 2, setEnds: false });
if (pickBan === "COUNTERPICK") {
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
}
await impersonate(page, teamTwoCaptainId);
@ -1204,24 +1049,22 @@ test.describe("Tournament bracket", () => {
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("actions-tab").click();
await page.getByTestId("winner-radio-1").click();
await page.getByTestId("points-input-1").fill("100");
await submit(page, "report-score-button");
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, winner: 1, setEnds: false });
if (pickBan === "COUNTERPICK") {
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
await submit(page, "undo-score-button");
await expect(
page.getByText("Please select the winner of this map"),
).toBeVisible();
await page.getByTestId("winner-radio-1").click();
await page.getByTestId("points-input-1").fill("100");
await submit(page, "report-score-button");
await undoLastReport(page);
await expect(page.getByText("Select the winner")).toBeVisible();
await reportResult(page, {
mapsToReport: 1,
winner: 1,
setEnds: false,
});
await page.getByTestId("pick-ban-button").last().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
await expect(
page.getByText("Counterpick", { exact: true }),
).toBeVisible();
@ -1241,7 +1084,8 @@ test.describe("Tournament bracket", () => {
await page.clock.install({ time: new Date() });
await reportResult({ page, amountOfMapsToReport: 1, winner: 1 });
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, winner: 1, setEnds: false });
await expect(page.getByTestId("match-timer")).toBeVisible();
@ -1260,12 +1104,13 @@ test.describe("Tournament bracket", () => {
await navigateToMatch(page, matchId);
await page.getByText("End Set").click();
await goToTab(page, "admin");
await page.getByRole("button", { name: "End set" }).click();
await page.getByRole("radio", { name: /Random/ }).check();
await submit(page, "end-set-button");
// Verify match ended early
await expect(page.getByText("Match ended early")).toBeVisible();
// Match is now finalized (no longer ongoing) → "Final" appears in banner
await expect(page.getByTestId("match-final")).toBeVisible();
});
test("dropping team out ends ongoing match early and auto-forfeits losers bracket match", async ({
@ -1277,7 +1122,8 @@ test.describe("Tournament bracket", () => {
// 1) Report partial score on match 5 (winners bracket)
await navigateToMatch(page, 5);
await reportResult({ page, amountOfMapsToReport: 1, winner: 1 });
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, winner: 1, setEnds: false });
await backToBracket(page);
// 2) Drop team 102 (one of the teams in match 5) via admin
@ -1289,18 +1135,18 @@ test.describe("Tournament bracket", () => {
await page.getByLabel("Team", { exact: true }).selectOption("102");
await submit(page);
// 3) Verify the ongoing match ended early
// 3) Verify the ongoing match ended early (no longer ongoing → "Final")
await navigate({
page,
url: tournamentMatchPage({ tournamentId, matchId: 5 }),
});
await expect(page.getByText("Match ended early")).toBeVisible();
await expect(page.getByText("dropped out of the tournament")).toBeVisible();
await expect(page.getByTestId("match-final")).toBeVisible();
await backToBracket(page);
// 4) Complete the adjacent match (match 6) so its loser goes to losers bracket
await navigateToMatch(page, 6);
await reportResult({ page, amountOfMapsToReport: 2 });
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 2 });
await backToBracket(page);
// 5) The losers bracket match (match 18) should now have teams:
@ -1308,8 +1154,7 @@ test.describe("Tournament bracket", () => {
// - Loser of match 6
// It should have ended early since team 102 is dropped
await navigateToMatch(page, 18);
await expect(page.getByText("Match ended early")).toBeVisible();
await expect(page.getByText("dropped out of the tournament")).toBeVisible();
await expect(page.getByTestId("match-final")).toBeVisible();
});
test("ban/pick CUSTOM flow", async ({ page }) => {
@ -1378,14 +1223,14 @@ test.describe("Tournament bracket", () => {
page,
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("actions-tab").click();
await goToTab(page, "action");
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
await expect(page.getByText(/Ban a map \(2\/2\)/)).toBeVisible();
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
// 3) PreSet: Lower seed bans 2 maps
await impersonate(page, lowerSeedCaptainId);
@ -1393,32 +1238,30 @@ test.describe("Tournament bracket", () => {
page,
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("actions-tab").click();
await goToTab(page, "action");
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
await expect(page.getByText(/Ban a map \(2\/2\)/)).toBeVisible();
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
// 4) Roll auto-executed after last ban; report game 1 score
await expect(page.getByTestId("stage-banner")).toBeVisible();
await page.getByTestId("actions-tab").click();
await goToTab(page, "action");
await page.getByTestId("winner-radio-1").click();
await page.getByTestId("points-input-1").fill("100");
await submit(page, "report-score-button");
await reportResult(page, { mapsToReport: 1, winner: 1, setEnds: false });
await expectScore(page, [1, 0]);
// 5) PostGame: Winner (team 1, captain 33) bans 2 maps
await expect(page.getByText(/Ban a map/)).toBeVisible();
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
await expect(page.getByText(/Ban a map \(2\/2\)/)).toBeVisible();
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
// PostGame: Loser (team 2, captain 29) picks a map
await impersonate(page, higherSeedCaptainId);
@ -1426,24 +1269,22 @@ test.describe("Tournament bracket", () => {
page,
url: tournamentMatchPage({ tournamentId, matchId }),
});
await page.getByTestId("actions-tab").click();
await goToTab(page, "action");
await expect(page.getByText(/Pick a map/)).toBeVisible();
await page.getByTestId("pick-ban-button").first().click();
await submit(page);
await submit(page, "pick-ban-submit-button");
// 6) Undo game 1 score — also deletes postGame pick/ban events
await expect(page.getByTestId("stage-banner")).toBeVisible();
await submit(page, "undo-score-button");
await undoLastReport(page);
await expectScore(page, [0, 0]);
await expect(page.getByTestId("stage-banner")).toBeVisible();
// 7) Re-report game 1 and verify postGame cycle restarts
await page.getByTestId("actions-tab").click();
await page.getByTestId("winner-radio-1").click();
await page.getByTestId("points-input-1").fill("100");
await submit(page, "report-score-button");
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, winner: 1, setEnds: false });
await expectScore(page, [1, 0]);
await expect(page.getByText(/Ban a map/)).toBeVisible();

View File

@ -1,5 +1,10 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
tournamentAdminPage,
tournamentBracketsPage,
tournamentMatchPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -11,12 +16,7 @@ import {
startBracket,
submit,
test,
} from "~/utils/playwright";
import {
tournamentAdminPage,
tournamentBracketsPage,
tournamentMatchPage,
} from "~/utils/urls";
} from "./helpers/playwright";
const TOURNAMENT_ID = 2;

View File

@ -1,4 +1,8 @@
import type { Page } from "@playwright/test";
import {
tournamentAdminPage,
tournamentBracketsPage,
tournamentStreamsPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -7,49 +11,13 @@ import {
startBracket,
submit,
test,
} from "~/utils/playwright";
} from "./helpers/playwright";
import {
tournamentAdminPage,
tournamentBracketsPage,
tournamentStreamsPage,
} from "~/utils/urls";
const navigateToMatch = async (page: Page, matchId: number) => {
await expect(async () => {
await page.locator(`[data-match-id="${matchId}"]`).click();
await expect(page.getByTestId("match-header")).toBeVisible();
}).toPass();
};
const selectRosterIfNeeded = async (page: Page, teamIndex: 0 | 1) => {
const position = teamIndex === 0 ? "first" : "last";
const checkbox = page.getByTestId("player-checkbox-0")[position]();
if ((await checkbox.count()) > 0 && !(await checkbox.isDisabled())) {
await page.getByTestId("player-checkbox-0")[position]().click();
await page.getByTestId("player-checkbox-1")[position]().click();
await page.getByTestId("player-checkbox-2")[position]().click();
await page.getByTestId("player-checkbox-3")[position]().click();
await submit(page, `save-active-roster-button-${teamIndex}`);
await expect(
page.getByTestId("player-checkbox-0")[position](),
).toBeDisabled();
}
};
const reportPartialScore = async (page: Page) => {
await page.getByTestId("actions-tab").click();
await selectRosterIfNeeded(page, 0);
await selectRosterIfNeeded(page, 1);
await page.getByTestId("winner-radio-1").click();
await submit(page, "report-score-button");
await expect(page.getByText("1-0")).toBeVisible();
};
const backToBracket = async (page: Page) => {
await page.getByTestId("back-to-bracket-button").click();
await expect(page.getByTestId("brackets-viewer")).toBeVisible();
};
backToBracket,
goToTab,
navigateToMatch,
reportResult,
} from "./helpers/tournament-match";
test.describe("Tournament streams", () => {
test("can set cast twitch accounts in admin", async ({ page }) => {
@ -95,7 +63,8 @@ test.describe("Tournament streams", () => {
await navigateToMatch(page, matchId);
// Report partial score to set startedAt (match becomes "in progress")
await reportPartialScore(page);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, setEnds: false });
await backToBracket(page);
// The LIVE button should be visible since team 102 members are streaming
@ -158,11 +127,12 @@ test.describe("Tournament streams", () => {
// Navigate to match and start it
await navigateToMatch(page, matchId);
await reportPartialScore(page);
await goToTab(page, "action");
await reportResult(page, { mapsToReport: 1, setEnds: false });
// Set match as casted
await page.getByTestId("cast-info-select").selectOption("test_cast_stream");
await submit(page, "cast-info-submit-button");
// Set match as casted via chip radio
await goToTab(page, "admin");
await page.locator('label[for$="-test_cast_stream"]').click();
await backToBracket(page);
// Verify LIVE button appears (multiple may exist from player streams)

View File

@ -1,3 +1,4 @@
import { calendarPage, tournamentBracketsPage } from "~/utils/urls";
import {
expect,
impersonate,
@ -5,8 +6,7 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import { calendarPage, tournamentBracketsPage } from "~/utils/urls";
} from "./helpers/playwright";
test.describe("Tournament tiers", () => {
test("shows tentative tier before bracket starts and confirmed tier after", async ({

View File

@ -1,6 +1,11 @@
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import type { StageId } from "~/modules/in-game-lists/types";
import {
tournamentBracketsPage,
tournamentPage,
tournamentTeamsPage,
} from "~/utils/urls";
import {
expect,
impersonate,
@ -9,12 +14,7 @@ import {
seed,
submit,
test,
} from "~/utils/playwright";
import {
tournamentBracketsPage,
tournamentPage,
tournamentTeamsPage,
} from "~/utils/urls";
} from "./helpers/playwright";
// TODO: restore operates admin controls after single fetch tested in prod

View File

@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
import { userEditProfileBaseSchema } from "~/features/user-page/user-page-schemas";
import { userEditProfilePage, userPage } from "~/utils/urls";
import {
expect,
impersonate,
@ -11,9 +12,8 @@ import {
submit,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import { userEditProfilePage, userPage } from "~/utils/urls";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
const goToEditPage = (page: Page) =>
page.getByText("Edit", { exact: true }).click();

View File

@ -1,4 +1,5 @@
import { vodFormBaseSchema } from "~/features/vods/vods-schemas";
import { newVodPage, VODS_PAGE, vodVideoPage } from "~/utils/urls";
import {
expect,
impersonate,
@ -10,9 +11,8 @@ import {
selectWeapon,
submit,
test,
} from "~/utils/playwright";
import { createFormHelpers } from "~/utils/playwright-form";
import { newVodPage, VODS_PAGE, vodVideoPage } from "~/utils/urls";
} from "./helpers/playwright";
import { createFormHelpers } from "./helpers/playwright-form";
const VIDEO_DATE = new Date(2024, 4, 15, 12, 0); // May 15, 2024 at 12:00