Optimize E2E test run time (#2686)

This commit is contained in:
Kalle 2026-01-03 19:25:38 +02:00 committed by GitHub
parent dd9e74d1b6
commit a4b9b66efc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 494 additions and 83 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ notes.md
db*.sqlite3*
!db-test.sqlite3
!e2e/seeds/*.sqlite3
dump
.DS_Store

View File

@ -4,5 +4,6 @@ export const NZAP_TEST_DISCORD_ID = "455039198672453645";
export const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339"; // https://cdn.discordapp.com/avatars/455039198672453645/f809176af93132c3db5f0a5019e96339.webp?size=160
export const NZAP_TEST_ID = 2;
export const REGULAR_USER_TEST_ID = 2;
export const ORG_ADMIN_TEST_ID = 3;
export const AMOUNT_OF_CALENDAR_EVENTS = 200;

View File

@ -86,6 +86,7 @@ import {
NZAP_TEST_AVATAR,
NZAP_TEST_DISCORD_ID,
NZAP_TEST_ID,
ORG_ADMIN_TEST_ID,
} from "./constants";
import placements from "./placements.json";
@ -739,7 +740,10 @@ function patrons() {
.all() as any[]
)
.map((u) => u.id)
.filter((id) => id !== NZAP_TEST_ID && id !== ADMIN_ID) as number[];
.filter(
(id) =>
id !== NZAP_TEST_ID && id !== ADMIN_ID && id !== ORG_ADMIN_TEST_ID,
) as number[];
const givePatronStm = sql.prepare(
`update user set "patronTier" = $patronTier, "patronSince" = $patronSince where id = $id`,
@ -2688,7 +2692,7 @@ async function organization() {
roleDisplayName: null,
},
{
userId: 3,
userId: ORG_ADMIN_TEST_ID,
role: "ADMIN",
roleDisplayName: null,
},

View File

@ -1,12 +1,19 @@
import fs from "node:fs";
import path from "node:path";
import type { ActionFunction } from "react-router";
import { z } from "zod";
import { seed } from "~/db/seed";
import { sql } from "~/db/sql";
import { DANGEROUS_CAN_ACCESS_DEV_CONTROLS } from "~/features/admin/core/dev-controls";
import { SEED_VARIATIONS } from "~/features/api-private/constants";
import { refreshBannedCache } from "~/features/ban/core/banned.server";
import { refreshSendouQInstance } from "~/features/sendouq/core/SendouQ.server";
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
import { cache } from "~/utils/cache.server";
import { logger } from "~/utils/logger";
import { parseRequestPayload } from "~/utils/remix.server";
const E2E_SEEDS_DIR = "e2e/seeds";
const seedSchema = z.object({
variation: z.enum(SEED_VARIATIONS).nullish(),
});
@ -25,10 +32,99 @@ export const action: ActionFunction = async ({ request }) => {
schema: seedSchema,
});
await seed(variation);
const variationName = variation ?? "DEFAULT";
const preSeededDbPath = path.join(
E2E_SEEDS_DIR,
`db-seed-${variationName}.sqlite3`,
);
if (!fs.existsSync(preSeededDbPath)) {
// Fall back to slow seed if pre-seeded db doesn't exist
logger.warn(
`Pre-seeded database not found for variation "${variationName}", falling back to seeding via code.`,
);
const { seed } = await import("~/db/seed");
await seed(variation);
} else {
restoreFromPreSeeded(preSeededDbPath);
adjustSeedDatesToCurrent(variationName);
}
clearAllTournamentDataCache();
cache.clear();
await refreshBannedCache();
await refreshSendouQInstance();
return Response.json(null);
};
const REG_OPEN_TOURNAMENT_IDS = [1, 3];
function adjustSeedDatesToCurrent(variation: SeedVariation) {
const halfAnHourFromNow = Math.floor((Date.now() + 1000 * 60 * 30) / 1000);
const oneHourAgo = Math.floor((Date.now() - 1000 * 60 * 60) / 1000);
const now = Math.floor(Date.now() / 1000);
const tournamentEventIds = sql
.prepare(
`SELECT id, tournamentId FROM "CalendarEvent" WHERE tournamentId IS NOT NULL`,
)
.all() as Array<{ id: number; tournamentId: number }>;
for (const { id, tournamentId } of tournamentEventIds) {
const isRegOpen =
variation === "REG_OPEN" &&
REG_OPEN_TOURNAMENT_IDS.includes(tournamentId);
sql
.prepare(`UPDATE "CalendarEventDate" SET startTime = ? WHERE eventId = ?`)
.run(isRegOpen ? halfAnHourFromNow : oneHourAgo, id);
}
sql
.prepare(
`UPDATE "Group" SET latestActionAt = ?, createdAt = ? WHERE status != 'INACTIVE'`,
)
.run(now, now);
sql.prepare(`UPDATE "GroupLike" SET createdAt = ?`).run(now);
}
function restoreFromPreSeeded(sourcePath: string) {
sql.exec(`ATTACH DATABASE '${sourcePath}' AS source`);
try {
const tables = sql
.prepare(
"SELECT name FROM source.sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
)
.all() as Array<{ name: string }>;
sql.exec("PRAGMA foreign_keys = OFF");
for (const { name } of tables) {
sql.exec(`DELETE FROM main."${name}"`);
// Get non-generated columns for this table (table_xinfo includes hidden column info)
const columns = sql
.prepare(`PRAGMA main.table_xinfo("${name}")`)
.all() as Array<{ name: string; hidden: number }>;
// hidden = 2 or 3 means virtual/stored generated column
const nonGeneratedCols = columns
.filter((c) => c.hidden === 0)
.map((c) => c.name);
if (nonGeneratedCols.length > 0) {
const colList = nonGeneratedCols.map((c) => `"${c}"`).join(", ");
sql.exec(
`INSERT INTO main."${name}" (${colList}) SELECT ${colList} FROM source."${name}"`,
);
}
}
sql.exec("PRAGMA foreign_keys = ON");
} finally {
sql.exec("DETACH DATABASE source");
}
}

View File

@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { db } from "~/db/sql";
import { refreshUserSkills } from "~/features/mmr/tiered.server";
import * as PrivateUserNoteRepository from "~/features/sendouq/PrivateUserNoteRepository.server";
import { dbInsertUsers, dbReset } from "~/utils/Test";
import * as SQGroupRepository from "../SQGroupRepository.server";
@ -359,6 +360,10 @@ describe("SendouQ", () => {
});
describe("tier sorting", () => {
beforeEach(() => {
refreshUserSkills(1);
});
test("sorts full groups by tier when viewer has a tier", async () => {
await insertSkill(1, 1000);
await insertSkill(2, 500);

View File

@ -1,8 +1,42 @@
import { expect, type Locator, type Page } from "@playwright/test";
import {
test as base,
expect,
type Locator,
type Page,
} from "@playwright/test";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import type { SeedVariation } from "~/features/api-private/routes/seed";
import { tournamentBracketsPage } from "./urls";
const BASE_PORT = 6173;
type WorkerFixtures = {
workerPort: number;
workerBaseURL: string;
};
export const test = base.extend<object, WorkerFixtures>({
workerPort: [
// biome-ignore lint/correctness/noEmptyPattern: Playwright requires object destructuring
async ({}, use, workerInfo) => {
const port = BASE_PORT + workerInfo.parallelIndex;
await use(port);
},
{ scope: "worker" },
],
workerBaseURL: [
async ({ workerPort }, use) => {
await use(`http://localhost:${workerPort}`);
},
{ scope: "worker" },
],
baseURL: async ({ workerBaseURL }, use) => {
await use(workerBaseURL);
},
});
export { expect };
export async function selectWeapon({
page,
name,
@ -65,7 +99,15 @@ export async function selectUser({
/** page.goto that waits for the page to be hydrated before proceeding */
export async function navigate({ page, url }: { page: Page; url: string }) {
await page.goto(url);
// Rewrite absolute URLs with localhost to use the worker's baseURL
// This handles invite links and other URLs embedded with VITE_SITE_DOMAIN
let targetUrl = url;
if (url.startsWith("http://localhost:")) {
const urlObj = new URL(url);
// Extract just the path and search params, let Playwright use the correct baseURL
targetUrl = urlObj.pathname + urlObj.search;
}
await page.goto(targetUrl);
await expectIsHydrated(page);
}

View File

@ -300,6 +300,8 @@ export const tournamentEditPage = (eventId: number) =>
export const calendarReportWinnersPage = (eventId: number) =>
`/calendar/${eventId}/report-winners`;
export const tournamentPage = (tournamentId: number) => `/to/${tournamentId}`;
export const tournamentTeamsPage = (tournamentId: number) =>
`/to/${tournamentId}/teams`;
export const tournamentTeamPage = ({
tournamentId,
tournamentTeamId,

Binary file not shown.

View File

@ -1,10 +1,11 @@
import { expect, test } from "@playwright/test";
import {
expect,
impersonate,
isNotVisible,
navigate,
seed,
selectWeapon,
test,
} from "~/utils/playwright";
import { ANALYZER_URL } from "~/utils/urls";

View File

@ -1,12 +1,13 @@
import test, { expect } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
isNotVisible,
navigate,
seed,
submit,
test,
} from "~/utils/playwright";
import { associationsPage, scrimsPage } from "~/utils/urls";

View File

@ -1,5 +1,11 @@
import { expect, test } from "@playwright/test";
import { impersonate, navigate, seed, selectUser } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
selectUser,
test,
} from "~/utils/playwright";
import { badgePage } from "~/utils/urls";
import { NZAP_TEST_ID } from "../app/db/seed/constants";

View File

@ -1,10 +1,12 @@
import test, { expect, type Page } from "@playwright/test";
import type { Page } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
navigate,
seed,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { ADMIN_PAGE, SUSPENDED_PAGE } from "~/utils/urls";

View File

@ -1,13 +1,15 @@
import { expect, type Page, test } from "@playwright/test";
import type { Page } from "@playwright/test";
import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants";
import type { GearType } from "~/db/tables";
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
navigate,
seed,
selectWeapon,
submit,
test,
} from "~/utils/playwright";
import { BUILDS_PAGE, userBuildsPage, userNewBuildPage } from "~/utils/urls";

View File

@ -1,11 +1,12 @@
import { expect, test } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import {
expect,
expectIsHydrated,
impersonate,
isNotVisible,
navigate,
seed,
test,
} from "~/utils/playwright";
import { calendarPage } from "~/utils/urls";

131
e2e/global-setup.ts Normal file
View File

@ -0,0 +1,131 @@
import { type ChildProcess, execSync, spawn } from "node:child_process";
import fs from "node:fs";
import type { FullConfig } from "@playwright/test";
const BASE_PORT = 6173;
const WORKER_COUNT = Number(process.env.E2E_WORKERS) || 4;
const DEBUG = process.env.E2E_DEBUG === "true";
const SERVER_PROCESSES: ChildProcess[] = [];
declare global {
var __E2E_SERVERS__: ChildProcess[];
}
function killProcessOnPort(port: number): void {
try {
// Try to find and kill any process on this port (macOS/Linux)
execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, {
stdio: "pipe",
});
} catch {
// Ignore errors - port might already be free
}
}
async function waitForServer(port: number, timeout = 120000): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const response = await fetch(`http://localhost:${port}/`);
if (response.ok || response.status === 404) {
// 404 is fine - server is up, just no route at /
return;
}
} catch {
// Server not ready yet
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(`Server on port ${port} did not start within ${timeout}ms`);
}
async function globalSetup(_config: FullConfig) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`\nStarting e2e test setup with ${WORKER_COUNT} workers...`);
// Build the app once with E2E test flag so VITE_E2E_TEST_RUN is embedded
// Use port 6173 as the base - tests will rewrite URLs as needed
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("Building the application...");
execSync("npm run build", {
stdio: "inherit",
env: {
...process.env,
VITE_E2E_TEST_RUN: "true",
VITE_SITE_DOMAIN: `http://localhost:${BASE_PORT}`,
},
});
// Prepare databases and start servers for each worker
const serverPromises: Promise<void>[] = [];
// Kill any existing processes on our ports before starting
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("Cleaning up any existing processes on e2e ports...");
for (let i = 0; i < WORKER_COUNT; i++) {
killProcessOnPort(BASE_PORT + i);
}
// Wait briefly for ports to be released
await new Promise((resolve) => setTimeout(resolve, 500));
for (let i = 0; i < WORKER_COUNT; i++) {
const port = BASE_PORT + i;
const dbPath = `db-test-e2e-${i}.sqlite3`;
// Ensure database exists with migrations
if (!fs.existsSync(dbPath)) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Setting up database for worker ${i}: ${dbPath}`);
execSync(`DB_PATH=${dbPath} npm run migrate up`, { stdio: "inherit" });
}
// Start server
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Starting server for worker ${i} on port ${port}...`);
const serverProcess = spawn("npm", ["start"], {
env: {
...process.env,
DB_PATH: dbPath,
PORT: String(port),
DISCORD_CLIENT_ID: "123",
DISCORD_CLIENT_SECRET: "secret",
SESSION_SECRET: "secret",
VITE_SITE_DOMAIN: `http://localhost:${port}`,
VITE_E2E_TEST_RUN: "true",
},
detached: false,
});
SERVER_PROCESSES.push(serverProcess);
if (DEBUG) {
serverProcess.stdout?.on("data", (data) => {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`[Worker ${i}] ${data.toString()}`);
});
serverProcess.stderr?.on("data", (data) => {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error(`[Worker ${i} ERROR] ${data.toString()}`);
});
}
serverPromises.push(
waitForServer(port).then(() => {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Server for worker ${i} is ready on port ${port}`);
}),
);
}
// Wait for all servers to be ready
await Promise.all(serverPromises);
// Store server processes globally for teardown
global.__E2E_SERVERS__ = SERVER_PROCESSES;
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("\nAll servers started successfully!\n");
}
export default globalSetup;

26
e2e/global-teardown.ts Normal file
View File

@ -0,0 +1,26 @@
import type { FullConfig } from "@playwright/test";
declare global {
var __E2E_SERVERS__: import("node:child_process").ChildProcess[];
}
async function globalTeardown(_config: FullConfig) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("\nStopping e2e test servers...");
const servers = global.__E2E_SERVERS__ || [];
for (const server of servers) {
if (server && !server.killed) {
server.kill("SIGTERM");
}
}
// Give processes a moment to clean up
await new Promise((resolve) => setTimeout(resolve, 1000));
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("All servers stopped.\n");
}
export default globalTeardown;

View File

@ -1,5 +1,11 @@
import test, { expect } from "@playwright/test";
import { impersonate, navigate, seed, submit } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
submit,
test,
} from "~/utils/playwright";
import { LFG_PAGE } from "~/utils/urls";
test.describe("LFG", () => {

View File

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

View File

@ -1,13 +1,14 @@
import test, { expect } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { NZAP_TEST_ID, ORG_ADMIN_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
isNotVisible,
navigate,
seed,
selectUser,
submit,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import {
@ -154,11 +155,9 @@ test.describe("Tournament Organization", () => {
test("allows member of established org to create tournament", async ({
page,
}) => {
const ORG_ADMIN_ID = 3; // 3 = org admin, but not site admin
await seed(page);
await impersonate(page, ORG_ADMIN_ID);
await impersonate(page, ORG_ADMIN_TEST_ID);
await navigate({
page,
url: TOURNAMENT_NEW_PAGE,
@ -178,7 +177,7 @@ test.describe("Tournament Organization", () => {
page.getByTestId("is-established-switch").click(),
);
await impersonate(page, ORG_ADMIN_ID);
await impersonate(page, ORG_ADMIN_TEST_ID);
await navigate({
page,
url: TOURNAMENT_NEW_PAGE,

View File

@ -1,13 +1,14 @@
import test, { expect } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
navigate,
seed,
selectUser,
setDateTime,
submit,
test,
} from "~/utils/playwright";
import { newScrimPostPage, scrimsPage } from "~/utils/urls";
@ -56,8 +57,6 @@ test.describe("Scrims", () => {
test("requests an existing scrim post & cancels the request", async ({
page,
}) => {
const INITIAL_AVAILABLE_TO_REQUEST_COUNT = 15;
await seed(page);
await impersonate(page, ADMIN_ID);
await navigate({
@ -68,13 +67,16 @@ test.describe("Scrims", () => {
const requestScrimButtonLocator = page.getByTestId("request-scrim-button");
await page.getByTestId("available-scrims-tab").click();
await expect(requestScrimButtonLocator.first()).toBeVisible();
const initialCount = await requestScrimButtonLocator.count();
await requestScrimButtonLocator.first().click();
await submit(page);
await expect(requestScrimButtonLocator).toHaveCount(
INITIAL_AVAILABLE_TO_REQUEST_COUNT - 1,
);
await expect(requestScrimButtonLocator).toHaveCount(initialCount - 1);
const togglePendingRequestsButton = page.getByTestId(
"toggle-pending-requests-button",
@ -89,9 +91,7 @@ test.describe("Scrims", () => {
});
await cancelButton.click();
await expect(requestScrimButtonLocator).toHaveCount(
INITIAL_AVAILABLE_TO_REQUEST_COUNT,
);
await expect(requestScrimButtonLocator).toHaveCount(initialCount);
});
test("accepts a request", async ({ page }) => {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,13 @@
import test, { expect } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { impersonate, navigate, seed, submit } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
submit,
test,
} from "~/utils/playwright";
import {
SENDOUQ_LOOKING_PAGE,
SENDOUQ_PAGE,

View File

@ -1,5 +1,11 @@
import test, { expect } from "@playwright/test";
import { impersonate, navigate, seed } from "~/utils/playwright";
import {
expect,
impersonate,
navigate,
seed,
test,
waitForPOSTResponse,
} from "~/utils/playwright";
import { SETTINGS_PAGE } from "~/utils/urls";
test.describe("Settings", () => {
@ -22,9 +28,9 @@ test.describe("Settings", () => {
url: SETTINGS_PAGE,
});
await page
.getByTestId("UPDATE_DISABLE_BUILD_ABILITY_SORTING-switch")
.click();
await waitForPOSTResponse(page, () =>
page.getByTestId("UPDATE_DISABLE_BUILD_ABILITY_SORTING-switch").click(),
);
await navigate({
page,

View File

@ -1,13 +1,14 @@
import { expect, test } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID, ADMIN_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
isNotVisible,
modalClickConfirmButton,
navigate,
seed,
submit,
test,
} from "~/utils/playwright";
import {
editTeamPage,

View File

@ -1,5 +1,5 @@
import { expect, type Locator, type Page, test } from "@playwright/test";
import { navigate } from "~/utils/playwright";
import type { Locator, Page } from "@playwright/test";
import { expect, navigate, test } from "~/utils/playwright";
import { TIER_LIST_MAKER_URL } from "~/utils/urls";
test.describe("Tier List Maker", () => {

View File

@ -1,5 +1,4 @@
import { expect, test } from "@playwright/test";
import { navigate, seed } from "~/utils/playwright";
import { expect, navigate, seed, test } from "~/utils/playwright";
import { topSearchPage, userPage } from "~/utils/urls";
test.describe("Top search", () => {

View File

@ -1,7 +1,8 @@
import { expect, type Page, test } from "@playwright/test";
import type { Page } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_DISCORD_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
isNotVisible,
navigate,
@ -9,6 +10,7 @@ import {
selectUser,
startBracket,
submit,
test,
} from "~/utils/playwright";
import {
NOTIFICATIONS_URL,
@ -17,6 +19,7 @@ import {
tournamentBracketsPage,
tournamentMatchPage,
tournamentPage,
tournamentTeamsPage,
userResultsPage,
} from "~/utils/urls";
@ -530,7 +533,10 @@ test.describe("Tournament bracket", () => {
});
await submit(page);
await page.getByTestId("teams-tab").click();
await navigate({
page,
url: tournamentTeamsPage(tournamentId),
});
await expect(
page.getByTestId("team-member-name").getByText("Sendou"),
@ -966,6 +972,13 @@ test.describe("Tournament bracket", () => {
await submit(page, "confirm-finalize-bracket-button");
await navigate({
page,
url: tournamentBracketsPage({ tournamentId }),
});
await page.getByRole("button", { name: "Great White" }).click();
await expect(page.getByTestId("prepared-maps-check-icon")).toBeVisible();
// we did not prepare maps for group stage

View File

@ -1,7 +1,7 @@
import test, { expect } from "@playwright/test";
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import {
expect,
impersonate,
isNotVisible,
modalClickConfirmButton,
@ -10,6 +10,7 @@ import {
selectUser,
startBracket,
submit,
test,
} from "~/utils/playwright";
import {
tournamentAdminPage,

View File

@ -1,19 +1,20 @@
import { expect, test } from "@playwright/test";
// import { NZAP_TEST_ID } from "~/db/seed/constants";
// import { ADMIN_ID } from "~/features/admin/admin-constants";
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 invariant from "~/utils/invariant";
import {
expect,
impersonate,
isNotVisible,
navigate,
seed,
// selectUser,
submit,
test,
} from "~/utils/playwright";
import { tournamentBracketsPage, tournamentPage } from "~/utils/urls";
import {
tournamentBracketsPage,
tournamentPage,
tournamentTeamsPage,
} from "~/utils/urls";
// TODO: restore operates admin controls after single fetch tested in prod
@ -263,7 +264,10 @@ test.describe("Tournament", () => {
await submit(page);
await page.getByTestId("teams-tab").click();
await navigate({
page,
url: tournamentTeamsPage(1),
});
await expect(page.getByTestId("team-name").first()).not.toHaveText(
"Chimera",
);

View File

@ -1,13 +1,15 @@
import { expect, type Page, test } from "@playwright/test";
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 {
expect,
impersonate,
isNotVisible,
navigate,
seed,
selectWeapon,
submit,
test,
} from "~/utils/playwright";
import { userEditProfilePage, userPage } from "~/utils/urls";

View File

@ -1,5 +1,6 @@
import test, { expect, type Page } from "@playwright/test";
import type { Page } from "@playwright/test";
import {
expect,
impersonate,
isNotVisible,
navigate,
@ -8,6 +9,7 @@ import {
selectUser,
selectWeapon,
submit,
test,
} from "~/utils/playwright";
import { newVodPage, VODS_PAGE, vodVideoPage } from "~/utils/urls";

View File

@ -24,6 +24,7 @@
"test:unit": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only run",
"test:e2e": "npx playwright test",
"test:e2e:flaky-detect": "npx playwright test --repeat-each=10 --max-failures=1",
"test:e2e:generate-seeds": "cross-env DB_PATH=db-test.sqlite3 npx vite-node scripts/generate-e2e-seed-dbs.ts",
"checks": "npm run biome:fix && npm run test:unit && npm run check-translation-jsons && npm run typecheck && npm run knip",
"setup": "cross-env DB_PATH=db.sqlite3 vite-node ./scripts/setup.ts",
"i18n:sync": "i18next-locales-sync -e true -p en -s da de es-ES es-US fr-CA fr-EU he it ja ko nl pl pt-BR ru zh -l locales && npm run biome:fix",

View File

@ -1,11 +1,7 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
const WORKER_COUNT = Number(process.env.E2E_WORKERS) || 4;
/**
* See https://playwright.dev/docs/test-configuration.
@ -25,16 +21,19 @@ const config: PlaywrightTestConfig = {
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
retries: 2,
/* Opt out of parallel tests. */
workers: 1,
retries: 0,
/* Number of parallel workers */
workers: WORKER_COUNT,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "list",
/* Global setup and teardown for managing multiple server instances */
globalSetup: "./e2e/global-setup.ts",
globalTeardown: "./e2e/global-teardown.ts",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
/* Base URL will be set per-worker by the fixture */
baseURL: "http://localhost:6173",
trace: "retain-on-failure",
@ -97,24 +96,6 @@ const config: PlaywrightTestConfig = {
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
// uncomment to see server logs output
// stdout: "pipe",
env: {
DB_PATH: "db-test-e2e.sqlite3",
DISCORD_CLIENT_ID: "123",
DISCORD_CLIENT_SECRET: "secret",
SESSION_SECRET: "secret",
PORT: "6173",
VITE_SITE_DOMAIN: "http://localhost:6173",
VITE_E2E_TEST_RUN: "true",
},
command: "npm run build && npm start",
port: 6173,
reuseExistingServer: false,
timeout: 60_000 * 2, // 2 minutes
},
build: {
external: ["**/*.json"],
},

View File

@ -0,0 +1,55 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
import { SEED_VARIATIONS } from "../app/features/api-private/constants";
const E2E_SEEDS_DIR = "e2e/seeds";
const BASE_TEST_DB = "db-test.sqlite3";
async function generatePreSeededDatabases() {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("Generating pre-seeded databases for e2e tests...\n");
if (!fs.existsSync(E2E_SEEDS_DIR)) {
fs.mkdirSync(E2E_SEEDS_DIR, { recursive: true });
}
const baseDbPath = path.resolve(BASE_TEST_DB);
if (!fs.existsSync(baseDbPath)) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error(
`Base test database not found: ${baseDbPath}. Run migrations first.`,
);
process.exit(1);
}
for (const variation of SEED_VARIATIONS) {
const outputPath = path.join(E2E_SEEDS_DIR, `db-seed-${variation}.sqlite3`);
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Generating ${variation}...`);
fs.copyFileSync(baseDbPath, outputPath);
execSync(
`npx vite-node scripts/seed-single-variation.ts -- ${variation} ${outputPath}`,
{ stdio: "inherit" },
);
const db = new Database(outputPath);
db.pragma("wal_checkpoint(TRUNCATE)");
db.close();
const stats = fs.statSync(outputPath);
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(
`${variation}: ${(stats.size / 1024 / 1024).toFixed(2)} MB\n`,
);
}
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Done! Pre-seeded databases saved to ${E2E_SEEDS_DIR}/`);
}
// biome-ignore lint/suspicious/noConsole: CLI script output
generatePreSeededDatabases().catch(console.error);

View File

@ -0,0 +1,15 @@
import type { SeedVariation } from "~/features/api-private/routes/seed";
const variation = process.argv[2] as SeedVariation;
const dbPath = process.argv[3];
if (!variation || !dbPath) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.error("Usage: seed-single-variation.ts <variation> <dbPath>");
process.exit(1);
}
process.env.DB_PATH = dbPath;
const { seed } = await import("../app/db/seed/index");
await seed(variation === "DEFAULT" ? null : variation);