mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +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
|
run: uv run ruff check
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: uv run python manage.py test
|
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
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
.env.e2e
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -19,9 +19,12 @@ import environ
|
|||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
env = environ.Env()
|
env = environ.Env()
|
||||||
# find .env in root
|
# Allow overriding the .env file path (useful for E2E testing/CI)
|
||||||
environ.Env.read_env(os.path.join(BASE_DIR.parent, ".env"))
|
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
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|||||||
@@ -25,3 +25,5 @@ dist-ssr
|
|||||||
|
|
||||||
# Test
|
# Test
|
||||||
coverage/
|
coverage/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|||||||
+14
-3
@@ -31,11 +31,12 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.1.4",
|
"@vitest/coverage-v8": "^4.1.4",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"fake-indexeddb": "^6.2.5",
|
"fake-indexeddb": "^6.2.5",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
"msw": "^2.13.2",
|
"msw": "^2.13.2",
|
||||||
@@ -240,7 +241,7 @@
|
|||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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": ["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=="],
|
"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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
"happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/jost": "^5.2.8",
|
"@fontsource-variable/jost": "^5.2.8",
|
||||||
@@ -42,11 +44,12 @@
|
|||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.12.2",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.1.4",
|
"@vitest/coverage-v8": "^4.1.4",
|
||||||
|
"dotenv": "^17.4.2",
|
||||||
"fake-indexeddb": "^6.2.5",
|
"fake-indexeddb": "^6.2.5",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
"msw": "^2.13.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