diff --git a/.env.e2e.example b/.env.e2e.example new file mode 100644 index 0000000..62e776f --- /dev/null +++ b/.env.e2e.example @@ -0,0 +1,25 @@ +# Pi Ku E2E Environment Configuration Template + +# Database (Postgres) +E2E_DB_NAME=piku_e2e_db +E2E_DB_PORT=5433 +E2E_DB_USER=piku_test +E2E_DB_PASS=piku_test +E2E_DB_DB=piku_e2e + +# Backend (Django) +E2E_BACKEND_PORT=8001 +SECRET_KEY=e2e-secret-key-for-piku-testing +DEBUG=True +ALLOWED_HOSTS=* +CORS_ALLOWED_ORIGINS=http://localhost:5173 +FRONTEND_URL=http://localhost:5173 +EMAIL_HOST=localhost +EMAIL_PORT=1025 +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +FROM_EMAIL=testing@piku.local +MAILPIT_API_URL=http://localhost:8025/api/v1 + +# Frontend (Vite/Playwright) +VITE_API_URL=http://localhost:8001 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0cf183..04ff0a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,3 +61,46 @@ jobs: run: uv run ruff check - name: Run Tests run: uv run python manage.py test + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: piku_e2e + POSTGRES_USER: piku_test + POSTGRES_PASSWORD: piku_test + ports: + - 5433:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + mailpit: + image: axllent/mailpit:latest + ports: + - 8025:8025 + - 1025:1025 + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "latest" + - uses: oven-sh/setup-bun@v2 + - name: Install Frontend dependencies + run: cd frontend && bun install + - name: Install Playwright Browsers + run: cd frontend && bun x playwright install --with-deps + - name: Create .env.e2e + run: cp .env.e2e.example .env.e2e + - name: Run E2E Script + run: ./scripts/run-e2e.sh + env: + CI: "true" + - name: Upload Playwright Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: frontend/playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 405c31b..f10ac95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Environment variables .env +.env.e2e # Frontend node_modules/ diff --git a/backend/config/settings.py b/backend/config/settings.py index 6889d3e..69ceedd 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -19,9 +19,12 @@ import environ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +# Load environment variables env = environ.Env() -# find .env in root -environ.Env.read_env(os.path.join(BASE_DIR.parent, ".env")) +# Allow overriding the .env file path (useful for E2E testing/CI) +env_file = os.environ.get("PIKU_ENV_FILE", os.path.join(BASE_DIR.parent, ".env")) +if os.path.exists(env_file): + environ.Env.read_env(env_file) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ diff --git a/frontend/.gitignore b/frontend/.gitignore index 4229c76..00b3014 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -25,3 +25,5 @@ dist-ssr # Test coverage/ +test-results/ +playwright-report/ diff --git a/frontend/bun.lock b/frontend/bun.lock index f97a2bd..366f167 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -31,11 +31,12 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.12.2", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.4", + "dotenv": "^17.4.2", "fake-indexeddb": "^6.2.5", "jsdom": "^29.0.2", "msw": "^2.13.2", @@ -240,7 +241,7 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -344,6 +345,8 @@ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -658,7 +661,7 @@ "undici": ["undici@7.24.7", "", {}, "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], @@ -720,12 +723,16 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@types/ws/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cssstyle/@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], "fabric/jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + "happy-dom/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], + "happy-dom/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], @@ -736,6 +743,8 @@ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "cssstyle/@asamuzakjp/css-color/@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], @@ -762,6 +771,8 @@ "fabric/jsdom/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "happy-dom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "cssstyle/@asamuzakjp/css-color/@csstools/css-color-parser/@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "fabric/jsdom/tough-cookie/tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts new file mode 100644 index 0000000..a313ab6 --- /dev/null +++ b/frontend/e2e/auth.spec.ts @@ -0,0 +1,94 @@ +import { expect, test } from "@playwright/test"; +import { MailpitHelper } from "./utils/mailpit"; + +test.describe("Authentication Flow (Real Backend)", () => { + // Use unique email for each run to avoid conflicts in shared DB + const timestamp = Date.now(); + const email = `testuser-${timestamp}@example.com`; + const fullName = `Test User ${timestamp}`; + const password = "Password123!"; + + test("should register, activate via email, and login successfully", async ({ + page, + }) => { + // 1. Registration + console.log(">>--- Navigating to Onboard Page..."); + await page.goto("/onboard"); + + // Fill the registration form + await page.getByLabel(/full name/i).fill(fullName); + await page.getByLabel("Email", { exact: true }).fill(email); + await page.getByLabel("Password", { exact: true }).fill(password); + await page.getByLabel(/confirm password/i).fill(password); + + // Submit Registration + await page.getByRole("button", { name: /^register$/i }).click(); + + // Verify redirect to check-email page + console.log(">>--- Verifying redirect to check-email..."); + await expect(page).toHaveURL(/\/verify-email/); + await expect(page.getByText(/check your email/i)).toBeVisible(); + + // 2. Activation via Mailpit + console.log(`>>--- Polling Mailpit for activation email for ${email}...`); + const activationLink = await MailpitHelper.getActivationLink(email); + console.log(`>>--- Found activation link: ${activationLink}`); + + // Navigate to the activation link (this should activate and redirect to login) + await page.goto(activationLink); + + // Verify activation success + console.log(">>--- Verifying activation success..."); + await expect(page.getByText(/account activated/i)).toBeVisible(); + + // Click "Start Writing" to go to Login + await page.getByRole("button", { name: /start writing/i }).click(); + + // Verify redirect to login + console.log(">>--- Verifying redirect to login..."); + await expect(page).toHaveURL(/\/login/); + + // 3. Login + console.log(">>--- Navigated to Login. Handling Welcome Modal..."); + const welcomeButton = page.getByRole("button", { name: /i understand/i }); + await welcomeButton.waitFor({ state: "visible", timeout: 10000 }); + await welcomeButton.click(); + await expect(welcomeButton).toBeHidden(); + + console.log(">>--- Performing Login..."); + const loginButton = page.getByRole("button", { name: /sign in/i }); + await expect(loginButton).toBeVisible(); + + await page.getByLabel("Email", { exact: true }).fill(email); + await page.getByLabel("Password", { exact: true }).fill(password); + await loginButton.click(); + + // 4. Verify Success - Redirect to Drawer + console.log(">>--- Verifying redirect to Drawer..."); + await expect(page).toHaveURL(/\/drawer/); + + // 5. Verify Zero-Knowledge Artifacts in IndexedDB + console.log(">>--- Verifying MasterKey in IndexedDB..."); + const masterKeyExists = await page.evaluate(async () => { + return new Promise((resolve) => { + const request = indexedDB.open("piku-keys"); + request.onsuccess = (event: any) => { + const db = event.target.result; + try { + const transaction = db.transaction(["master-key"], "readonly"); + const store = transaction.objectStore("master-key"); + const getReq = store.get("masterKey"); + getReq.onsuccess = () => resolve(!!getReq.result); + getReq.onerror = () => resolve(false); + } catch (_e) { + resolve(false); + } + }; + request.onerror = () => resolve(false); + }); + }); + + expect(masterKeyExists).toBe(true); + console.log(">>--- E2E Flow Completed Successfully! ✅ ---<<"); + }); +}); diff --git a/frontend/e2e/utils/mailpit.ts b/frontend/e2e/utils/mailpit.ts new file mode 100644 index 0000000..f16df52 --- /dev/null +++ b/frontend/e2e/utils/mailpit.ts @@ -0,0 +1,46 @@ +import { request } from "@playwright/test"; + +export interface MailpitMessage { + ID: string; + Subject: string; + Snippet: string; + To: { Address: string }[]; +} + +const MAILPIT_API_URL = process.env.MAILPIT_API_URL; + +export const MailpitHelper = { + getActivationLink: async ( + email: string, + timeout = 10000, + ): Promise => { + const startTime = Date.now(); + const requestContext = await request.newContext(); + + while (Date.now() - startTime < timeout) { + // Search specifically for the recipient to reduce data transfer + const response = await requestContext.get(`${MAILPIT_API_URL}/search`, { + params: { query: `to:${email}`, limit: 1 }, + }); + + if (response.ok()) { + const data = await response.json(); + if (data.messages?.length > 0) { + const msgId = data.messages[0].ID; + const detailRes = await requestContext.get( + `${MAILPIT_API_URL}/message/${msgId}`, + ); + const details = await detailRes.json(); + + const body = details.HTML || details.Text || ""; + const match = body.match(/https?:\/\/\S+activate\/\S+/); + + if (match) return match[0]; + } + } + await new Promise((r) => setTimeout(r, 1000)); + } + + throw new Error(`Timeout: Could not find activation link for ${email}`); + }, +}; diff --git a/frontend/package.json b/frontend/package.json index 9e63bdb..04b2adc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,9 @@ "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@fontsource-variable/jost": "^5.2.8", @@ -42,11 +44,12 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@types/node": "^24.12.2", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^4.1.4", + "dotenv": "^17.4.2", "fake-indexeddb": "^6.2.5", "jsdom": "^29.0.2", "msw": "^2.13.2", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..fc66cf2 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,56 @@ +import path from "node:path"; +import process from "node:process"; +import { defineConfig, devices } from "@playwright/test"; +import dotenv from "dotenv"; + +/** + * Read environment variables from file. + */ +dotenv.config({ path: path.resolve(process.cwd(), "../.env.e2e") }); +export default defineConfig({ + timeout: 60000, + expect: { + timeout: 10000, + }, + testDir: "./e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.FRONTEND_URL, + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 20000, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + /* Capture screenshot on failure */ + screenshot: "only-on-failure", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "bun run dev", + url: process.env.FRONTEND_URL, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/scripts/run-e2e.sh b/scripts/run-e2e.sh new file mode 100755 index 0000000..da75428 --- /dev/null +++ b/scripts/run-e2e.sh @@ -0,0 +1,98 @@ +#!/bin/bash +set -e + +# Get absolute path of project root +PROJECT_ROOT=$(pwd) + +# Configuration +ENV_FILE="$PROJECT_ROOT/.env.e2e" + +if [ -f "$ENV_FILE" ]; then + echo "[INFO] Loading configuration from $ENV_FILE..." + set -a + source "$ENV_FILE" + set +a +elif [ "$CI" != "true" ]; then + echo "[ERROR] $ENV_FILE not found! Please create it for local testing (use .env.e2e.example as template)." + exit 1 +else + echo "[INFO] Running in CI mode (using direct environment variables)..." +fi + +# Map E2E variables to Django expected names +# In CI, these should be set via GitHub Actions env variables +export DB_NAME=${E2E_DB_DB:-piku_e2e} +export DB_USER=${E2E_DB_USER:-piku_test} +export DB_PASSWORD=${E2E_DB_PASS:-piku_test} +export DB_HOST=${E2E_DB_HOST:-localhost} +export DB_PORT=${E2E_DB_PORT:-5433} +export E2E_BACKEND_PORT=${E2E_BACKEND_PORT:-8001} + +echo "[START] Initializing E2E Test Environment..." + +# 1. Cleanup / Start Services (Skip in CI) +if [ "$CI" != "true" ]; then + if podman ps -a --format "{{.Names}}" | grep -q "^$E2E_DB_NAME$"; then + echo "[CLEANUP] Removing existing container $E2E_DB_NAME..." + podman rm -f $E2E_DB_NAME + fi + + echo "[DB] Starting disposable Postgres on port $DB_PORT..." + podman run --name $E2E_DB_NAME \ + -e POSTGRES_DB=$DB_NAME \ + -e POSTGRES_USER=$DB_USER \ + -e POSTGRES_PASSWORD=$DB_PASSWORD \ + -p $DB_PORT:5432 \ + -d docker.io/library/postgres:16-alpine > /dev/null + + echo "[DB] Waiting for Postgres to be ready..." + until podman exec $E2E_DB_NAME pg_isready -U $DB_USER > /dev/null 2>&1; do + sleep 1 + done + echo "[DB] Postgres is ready." +fi + +# Trap to ensure cleanup +cleanup() { + echo "[CLEANUP] Stopping services..." + if [ "$CI" != "true" ]; then + podman rm -f $E2E_DB_NAME || true + fi + if [ ! -z "$BACKEND_PID" ]; then + kill "$BACKEND_PID" 2>/dev/null || true + fi +} +trap cleanup EXIT + +# 2. Prepare Backend +echo "[BACKEND] Running database migrations..." +export PIKU_ENV_FILE="$ENV_FILE" +(cd backend && uv run manage.py migrate --noinput) + +echo "[BACKEND] Starting server on port $E2E_BACKEND_PORT..." +(cd backend && uv run manage.py runserver $E2E_BACKEND_PORT) > /tmp/piku_e2e_backend.log 2>&1 & +BACKEND_PID=$! + +echo "[BACKEND] Waiting for server to respond..." +until curl -s http://localhost:$E2E_BACKEND_PORT > /dev/null; do + sleep 1 + if ! kill -0 $BACKEND_PID 2>/dev/null; then + echo "[ERROR] Backend failed to start. Logs:" + cat /tmp/piku_e2e_backend.log + exit 1 + fi +done +echo "[BACKEND] Server is ready." + +# 3. Run Playwright +export VITE_API_URL="http://localhost:$E2E_BACKEND_PORT" + +if [ "$CI" = "true" ]; then + echo "[TEST] Running Playwright Tests (CI)..." + (cd frontend && bun run test:e2e --project=chromium) +else + echo "[TEST] Running Playwright Tests in Distrobox..." + (cd frontend && distrobox-enter --name ubuntu-24.04 -- env VITE_API_URL=$VITE_API_URL bun run test:e2e --project=chromium) +fi + +echo "[SUCCESS] E2E Tests Completed."