diff --git a/app/features/admin/routes/admin.tsx b/app/features/admin/routes/admin.tsx index d915d9b97..375c1f061 100644 --- a/app/features/admin/routes/admin.tsx +++ b/app/features/admin/routes/admin.tsx @@ -55,7 +55,6 @@ export const meta: MetaFunction = (args) => { }); }; -// xxx: should seed be just in the dropdown menu? export default function AdminPage() { const isStaff = useHasRole("STAFF"); diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 3ab01d414..61264aa8e 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -108,6 +108,7 @@ function isBuildFresh(): boolean { try { const marker = JSON.parse(fs.readFileSync(BUILD_MARKER_FILE, "utf8")); if (marker.siteDomain !== `http://localhost:${E2E_BASE_PORT}`) return false; + if (marker.skalopWsUrl !== "") return false; } catch { return false; } @@ -153,11 +154,20 @@ async function globalSetup(config: FullConfig) { ...process.env, VITE_E2E_TEST_RUN: "true", VITE_SITE_DOMAIN: `http://localhost:${E2E_BASE_PORT}`, + // Skalop is disconnected in e2e: all workers sharing one instance + // cross-talk (identical seeded row ids -> colliding room names -> + // spurious revalidations). When e2e tests for chat etc. are added + // this needs an actual solution: one skalop (or stub) per worker + // with a runtime-derived ws URL, since this build is shared. + VITE_SKALOP_WS_URL: "", }, }); fs.writeFileSync( BUILD_MARKER_FILE, - JSON.stringify({ siteDomain: `http://localhost:${E2E_BASE_PORT}` }), + JSON.stringify({ + siteDomain: `http://localhost:${E2E_BASE_PORT}`, + skalopWsUrl: "", + }), ); } @@ -197,6 +207,9 @@ async function globalSetup(config: FullConfig) { STORAGE_SECRET: "minio-password", STORAGE_REGION: "us-east-1", STORAGE_BUCKET: "sendou", + // no system messages to a shared skalop instance (see build env above) + SKALOP_SYSTEM_MESSAGE_URL: "", + SKALOP_TOKEN: "", }, detached: false, }); diff --git a/e2e/helpers/playwright.ts b/e2e/helpers/playwright.ts index 9b3a90df8..a762e83a0 100644 --- a/e2e/helpers/playwright.ts +++ b/e2e/helpers/playwright.ts @@ -285,9 +285,9 @@ export async function waitForPOSTResponse(page: Page, cb: () => Promise) { // React Aria buttons fire their handler on press end. Occasionally a click // registers the press start (the button goes `:active`) but the press never - // completes into a submit, so no POST fires. The match page revalidating on - // a websocket message mid-click is one way this happens. Re-issue the action - // when the expected POST doesn't arrive within the per-attempt window. + // completes into a submit, so no POST fires — e.g. when a re-render lands + // mid-press. Re-issue the action when the expected POST doesn't arrive + // within the per-attempt window. for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { const responsePromise = page.waitForResponse( (res) => res.request().method() === "POST", @@ -317,7 +317,6 @@ export function modalClickConfirmButton(page: Page) { * first when the tab has collapsed into it on the current viewport. Retried as a * whole because the nav can re-collapse between the visibility check and the click. */ -// xxx: timeouts? export async function clickNavTab(page: Page, testId: string) { const visibleTab = page.locator(`[data-testid="${testId}"]:visible`); await expect(async () => { @@ -327,18 +326,3 @@ export async function clickNavTab(page: Page, testId: string) { await visibleTab.click({ timeout: 2_000 }); }).toPass({ timeout: 15_000 }); } - -// xxx: timeout??? anyway can we isolate websocket events? avoiding shared skalop -/** - * Clicks `trigger` until `expected` is visible, for clicks that open a popover or - * dialog — a revalidation re-render (e.g. another worker's websocket event) can - * swallow the press. - */ -export async function clickUntilVisible(trigger: Locator, expected: Locator) { - await expect(async () => { - if (!(await expected.isVisible())) { - await trigger.click({ timeout: 2_000 }); - } - await expect(expected).toBeVisible({ timeout: 2_000 }); - }).toPass({ timeout: 15_000 }); -} diff --git a/e2e/pages/sendouq/sendouq-match-page.ts b/e2e/pages/sendouq/sendouq-match-page.ts index 51d8b0901..2038ba992 100644 --- a/e2e/pages/sendouq/sendouq-match-page.ts +++ b/e2e/pages/sendouq/sendouq-match-page.ts @@ -2,7 +2,6 @@ import type { Page } from "@playwright/test"; import { SENDOUQ_BEST_OF } from "~/features/sendouq/q-constants"; import { sendouQMatchPage } from "~/utils/urls"; import { - clickUntilVisible, expect, navigate, selectWeapon, @@ -132,10 +131,7 @@ export class SendouQMatchPage { } async requestCancel() { - await clickUntilVisible( - this.locators.requestCancelButton, - this.page.getByTestId("confirm-button"), - ); + await this.locators.requestCancelButton.click(); await this.confirmDialog(); } diff --git a/e2e/pages/tournament/tournament-match-page.ts b/e2e/pages/tournament/tournament-match-page.ts index 1e551bb3e..10f769545 100644 --- a/e2e/pages/tournament/tournament-match-page.ts +++ b/e2e/pages/tournament/tournament-match-page.ts @@ -137,10 +137,10 @@ export class TournamentMatchPage { /** * Selects one of the pick/ban options and submits it. * - * Selecting and submitting are retried together as one unit because the match - * page revalidates on websocket messages: a re-render landing mid-click can - * swallow the submit press (see waitForPOSTResponse) and, if the panel - * remounted, the selection with it — so re-submitting alone would not be enough. + * Selecting and submitting are retried together as one unit: a re-render + * landing mid-click can swallow the submit press (see waitForPOSTResponse) + * and, if the panel remounted, the selection with it — so re-submitting + * alone would not be enough. */ pickBan(option: "first" | "last" = "first") { return waitForPOSTResponse(this.page, async () => { diff --git a/e2e/pages/user/user-card.ts b/e2e/pages/user/user-card.ts index 017347e4d..3e7c8e998 100644 --- a/e2e/pages/user/user-card.ts +++ b/e2e/pages/user/user-card.ts @@ -1,10 +1,6 @@ import type { Locator, Page } from "@playwright/test"; import { reportUserSchema } from "~/features/user-report/user-report-schemas"; -import { - clickUntilVisible, - submit, - waitForPOSTResponse, -} from "../../helpers/playwright"; +import { expect, submit, waitForPOSTResponse } from "../../helpers/playwright"; import { createFormHelpers } from "../../helpers/playwright-form"; import { UserCardEditPage } from "./user-card-edit-page"; @@ -26,10 +22,11 @@ export class UserCard { }; } - /** Opens the card popover through its trigger, retrying a swallowed press. */ + /** Opens the card popover through its trigger. */ static async open(page: Page, trigger: Locator) { const card = new UserCard(page); - await clickUntilVisible(trigger, card.locators.banner); + await trigger.click(); + await expect(card.locators.banner).toBeVisible(); return card; } diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index 458ff97b5..b438cbc6f 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -18,8 +18,6 @@ import { TournamentSeedsPage } from "./pages/tournament/tournament-seeds-page"; import { TournamentTeamsPage } from "./pages/tournament/tournament-teams-page"; import { UserResultsPage } from "./pages/user/user-results-page"; -// xxx: tournament-bracket-elim, tournament-bracket-swiss etc. and pick ban as separate and general another? - const ROSTER_SIZE = 4; type BracketProgression = TournamentSettings["bracketProgression"]; @@ -148,7 +146,6 @@ test.describe("Tournament bracket", () => { {}, { rosterSize: 5 }, ]); - // xxx: should be an option const [match] = await factories.TournamentFactory.startBracket( tournament.id, ); @@ -342,7 +339,6 @@ test.describe("Tournament bracket", () => { ); }); - // xxx: does this and every other test need so big tournaments? test("completes and finalizes a small tournament (RR->SE w/ underground bracket)", async ({ page, factories, @@ -1073,8 +1069,9 @@ test.describe("Tournament bracket", () => { winner: 1, setEnds: false, }); - await match.pickBan("last"); await expect(match.locators.counterpickText).toBeVisible(); + await match.pickBan("last"); + await expect(match.locators.selectWinnerText).toBeVisible(); await expect(match.score([1, 1])).toBeVisible(); } });