mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 09:54:36 -05:00
Optimize E2E test run time (#2686)
This commit is contained in:
parent
dd9e74d1b6
commit
a4b9b66efc
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,6 +11,7 @@ notes.md
|
|||
|
||||
db*.sqlite3*
|
||||
!db-test.sqlite3
|
||||
!e2e/seeds/*.sqlite3
|
||||
dump
|
||||
|
||||
.DS_Store
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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
131
e2e/global-setup.ts
Normal 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
26
e2e/global-teardown.ts
Normal 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;
|
||||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
BIN
e2e/seeds/db-seed-DEFAULT.sqlite3
Normal file
BIN
e2e/seeds/db-seed-DEFAULT.sqlite3
Normal file
Binary file not shown.
BIN
e2e/seeds/db-seed-NO_SCRIMS.sqlite3
Normal file
BIN
e2e/seeds/db-seed-NO_SCRIMS.sqlite3
Normal file
Binary file not shown.
BIN
e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3
Normal file
BIN
e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3
Normal file
Binary file not shown.
BIN
e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3
Normal file
BIN
e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3
Normal file
Binary file not shown.
BIN
e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3
Normal file
BIN
e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3
Normal file
Binary file not shown.
BIN
e2e/seeds/db-seed-REG_OPEN.sqlite3
Normal file
BIN
e2e/seeds/db-seed-REG_OPEN.sqlite3
Normal file
Binary file not shown.
BIN
e2e/seeds/db-seed-SMALL_SOS.sqlite3
Normal file
BIN
e2e/seeds/db-seed-SMALL_SOS.sqlite3
Normal file
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
},
|
||||
|
|
|
|||
55
scripts/generate-e2e-seed-dbs.ts
Normal file
55
scripts/generate-e2e-seed-dbs.ts
Normal 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);
|
||||
15
scripts/seed-single-variation.ts
Normal file
15
scripts/seed-single-variation.ts
Normal 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);
|
||||
Loading…
Reference in New Issue
Block a user