mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 00:56:34 +00:00
feat: implement end-to-end testing infrastructure with Playwright and automated containerized database setup
This commit is contained in:
@@ -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
|
||||
@@ -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,5 +1,6 @@
|
||||
# Environment variables
|
||||
.env
|
||||
.env.e2e
|
||||
|
||||
# Frontend
|
||||
node_modules/
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -25,3 +25,5 @@ dist-ssr
|
||||
|
||||
# Test
|
||||
coverage/
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
+14
-3
@@ -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=="],
|
||||
|
||||
@@ -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! ✅ ---<<");
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
},
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Executable
+98
@@ -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."
|
||||
Reference in New Issue
Block a user