diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 544eb0564..8e7304398 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -22,6 +22,22 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Start MinIO + run: docker compose up -d minio + + - name: Wait for MinIO to be ready + run: | + for i in {1..30}; do + if curl -sf http://127.0.0.1:9000/minio/health/live; then + echo "MinIO is ready" + exit 0 + fi + echo "Waiting for MinIO... ($i/30)" + sleep 2 + done + echo "MinIO failed to start" + exit 1 + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' @@ -36,6 +52,10 @@ jobs: - name: Run E2E tests run: npm run test:e2e + - name: Stop MinIO + if: always() + run: docker compose down + - uses: actions/upload-artifact@v4 if: failure() with: diff --git a/.gitignore b/.gitignore index 03de3c332..4be7dd4ff 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,6 @@ dump /test-results/ /playwright-report/ /playwright/.cache/ +.e2e-minio-started notepad.txt diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 04571cc7d..4f6984daf 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -176,6 +176,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [ nzapUser, users, fixAdminId, + makeArtists, adminUserWeaponPool, userProfiles, userMapModePreferences, @@ -264,6 +265,8 @@ function wipeDB() { "GroupMatchMap", "GroupMatch", "Group", + "TaggedArt", + "ArtTag", "ArtUserMetadata", "Art", "UnvalidatedUserSubmittedImage", @@ -348,6 +351,14 @@ function makeAdminTournamentOrganizer() { .run(); } +function makeArtists() { + sql + .prepare( + `update "User" set "isArtist" = 1 where id in (${ADMIN_ID}, ${NZAP_TEST_ID})`, + ) + .run(); +} + function adminUserWeaponPool() { for (const [i, weaponSplId] of [200, 1100, 2000, 4000].entries()) { sql diff --git a/e2e/art.spec.ts b/e2e/art.spec.ts new file mode 100644 index 000000000..d0759afa2 --- /dev/null +++ b/e2e/art.spec.ts @@ -0,0 +1,46 @@ +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"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +test.describe("Art", () => { + test("uploads art as NZAP, admin approves, art displays on user page", async ({ + page, + }) => { + await seed(page); + await impersonate(page, NZAP_TEST_ID); + + await navigate({ page, url: "/art/new" }); + + const testImagePath = path.join(__dirname, "fixtures/test-image.png"); + await page.locator('input[type="file"]').setInputFiles(testImagePath); + + await expect(page.locator("form img")).toBeVisible(); + + await page.getByRole("button", { name: "Save" }).click(); + + await expect(page).toHaveURL(/\/u\/.*\/art/); + await expect(page.getByText(/pending moderator approval/i)).toBeVisible(); + + await impersonate(page); + await navigate({ page, url: "/upload/admin" }); + + await expect(page.locator("img").first()).toBeVisible(); + + await page.getByRole("button", { name: /All .* above ok/ }).click(); + + await expect(page.getByText("All validated!")).toBeVisible(); + + await navigate({ page, url: "/u/nzap/art" }); + + const artImage = page.locator("img").first(); + await expect(artImage).toBeVisible(); + + const box = await artImage.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.width).toBeGreaterThan(0); + expect(box!.height).toBeGreaterThan(0); + }); +}); diff --git a/e2e/fixtures/test-image.png b/e2e/fixtures/test-image.png new file mode 100644 index 000000000..040b661e9 Binary files /dev/null and b/e2e/fixtures/test-image.png differ diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 1bfe05960..7c612a107 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -6,11 +6,57 @@ import { E2E_BASE_PORT } from "~/utils/playwright"; const WORKER_COUNT = Number(process.env.E2E_WORKERS) || 4; const DEBUG = process.env.E2E_DEBUG === "true"; const SERVER_PROCESSES: ChildProcess[] = []; +const MINIO_MARKER_FILE = ".e2e-minio-started"; declare global { var __E2E_SERVERS__: ChildProcess[]; } +async function waitForMinio(timeout = 60000): Promise { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + const response = await fetch("http://127.0.0.1:9000/minio/health/live"); + if (response.ok) { + return true; + } + } catch { + // MinIO not ready yet + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + return false; +} + +async function ensureMinioRunning(): Promise { + // Check if MinIO is already running + try { + const response = await fetch("http://127.0.0.1:9000/minio/health/live"); + if (response.ok) { + // biome-ignore lint/suspicious/noConsole: CLI script output + console.log("MinIO is already running"); + return false; + } + } catch { + // MinIO not running, we need to start it + } + + // biome-ignore lint/suspicious/noConsole: CLI script output + console.log("Starting MinIO..."); + execSync("docker compose up -d minio", { stdio: "inherit" }); + + const isReady = await waitForMinio(); + if (!isReady) { + throw new Error("MinIO failed to start within timeout"); + } + + // biome-ignore lint/suspicious/noConsole: CLI script output + console.log("MinIO is ready"); + + fs.writeFileSync(MINIO_MARKER_FILE, ""); + return true; +} + function killProcessOnPort(port: number): void { try { // Try to find and kill any process on this port (macOS/Linux) @@ -43,6 +89,9 @@ async function globalSetup(_config: FullConfig) { // biome-ignore lint/suspicious/noConsole: CLI script output console.log(`\nStarting e2e test setup with ${WORKER_COUNT} workers...`); + // Start MinIO if not already running + await ensureMinioRunning(); + // 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 @@ -92,6 +141,11 @@ async function globalSetup(_config: FullConfig) { SESSION_SECRET: "secret", VITE_SITE_DOMAIN: `http://localhost:${port}`, VITE_E2E_TEST_RUN: "true", + STORAGE_END_POINT: "http://127.0.0.1:9000", + STORAGE_ACCESS_KEY: "minio-user", + STORAGE_SECRET: "minio-password", + STORAGE_REGION: "us-east-1", + STORAGE_BUCKET: "sendou", }, detached: false, }); diff --git a/e2e/global-teardown.ts b/e2e/global-teardown.ts index a2350f37e..10bcb3cf7 100644 --- a/e2e/global-teardown.ts +++ b/e2e/global-teardown.ts @@ -1,5 +1,9 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; import type { FullConfig } from "@playwright/test"; +const MINIO_MARKER_FILE = ".e2e-minio-started"; + declare global { var __E2E_SERVERS__: import("node:child_process").ChildProcess[]; } @@ -19,6 +23,18 @@ async function globalTeardown(_config: FullConfig) { // Give processes a moment to clean up await new Promise((resolve) => setTimeout(resolve, 1000)); + // Stop MinIO if we started it (check for marker file) + if (fs.existsSync(MINIO_MARKER_FILE)) { + // biome-ignore lint/suspicious/noConsole: CLI script output + console.log("Stopping MinIO..."); + try { + execSync("docker compose stop minio", { stdio: "inherit" }); + } catch { + // Ignore errors - MinIO might already be stopped + } + fs.unlinkSync(MINIO_MARKER_FILE); + } + // biome-ignore lint/suspicious/noConsole: CLI script output console.log("All servers stopped.\n"); } diff --git a/e2e/seeds/db-seed-DEFAULT.sqlite3 b/e2e/seeds/db-seed-DEFAULT.sqlite3 index 25a2acba7..3999441d7 100644 Binary files a/e2e/seeds/db-seed-DEFAULT.sqlite3 and b/e2e/seeds/db-seed-DEFAULT.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 index 31b15d581..aeba94df0 100644 Binary files a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 and b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 index 72a9bb566..a71f98ccc 100644 Binary files a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 and b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 index 28210be3e..fc19e7fe9 100644 Binary files a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 and b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 index 6252d2928..146f884ca 100644 Binary files a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 and b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 differ diff --git a/e2e/seeds/db-seed-REG_OPEN.sqlite3 b/e2e/seeds/db-seed-REG_OPEN.sqlite3 index 760a39904..a5e0160ef 100644 Binary files a/e2e/seeds/db-seed-REG_OPEN.sqlite3 and b/e2e/seeds/db-seed-REG_OPEN.sqlite3 differ diff --git a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 index b8c2fce4d..b62127837 100644 Binary files a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 and b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 differ