This commit is contained in:
Kalle
2026-07-31 10:18:32 +03:00
parent 57a5bad70a
commit 60d4c3385e
7 changed files with 28 additions and 42 deletions

View File

@@ -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");

View File

@@ -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,
});

View File

@@ -285,9 +285,9 @@ export async function waitForPOSTResponse(page: Page, cb: () => Promise<void>) {
// 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 });
}

View File

@@ -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();
}

View File

@@ -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 () => {

View File

@@ -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;
}

View File

@@ -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();
}
});