feat: implement end-to-end testing infrastructure with Playwright and automated containerized database setup

This commit is contained in:
ramvignesh-b
2026-04-15 01:00:51 +05:30
parent 0fb31b9f0b
commit c259d98acb
11 changed files with 389 additions and 7 deletions
+25
View File
@@ -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
+43
View File
@@ -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
+1
View File
@@ -1,5 +1,6 @@
# Environment variables
.env
.env.e2e
# Frontend
node_modules/
+5 -2
View File
@@ -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/
+2
View File
@@ -25,3 +25,5 @@ dist-ssr
# Test
coverage/
test-results/
playwright-report/
+14 -3
View File
@@ -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=="],
+94
View File
@@ -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! ✅ ---<<");
});
});
+46
View File
@@ -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<string> => {
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}`);
},
};
+5 -2
View File
@@ -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",
+56
View File
@@ -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,
},
});
+98
View File
@@ -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."