Add Playwright scaffolding and example E2E test.
This commit is contained in:
parent
25d2aee4b7
commit
9af587d918
9 changed files with 325 additions and 0 deletions
7
e2e/.env.example
Normal file
7
e2e/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Base URL of the running frontend app
|
||||||
|
#APP_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# Test user credentials
|
||||||
|
# Create with: TUDUDI_USER_EMAIL=... TUDUDI_USER_PASSWORD=... npm run backend:start
|
||||||
|
E2E_EMAIL=test@tududi.com
|
||||||
|
E2E_PASSWORD=password123
|
||||||
46
e2e/README.md
Normal file
46
e2e/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
## tududi E2E (Playwright)
|
||||||
|
|
||||||
|
End-to-end tests live in this isolated `e2e/` folder.
|
||||||
|
The suite uses Playwright and assumes the app is running locally.
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
`npm run test:ui`
|
||||||
|
|
||||||
|
1) Backend (with a test user):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TUDUDI_USER_EMAIL=test@tududi.com TUDUDI_USER_PASSWORD=password123 npm run backend:start
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Frontend dev server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run frontend:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
3) E2E tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd e2e
|
||||||
|
npm ci
|
||||||
|
npm run install-browsers
|
||||||
|
APP_URL=http://localhost:8080 E2E_EMAIL=test@tududi.com E2E_PASSWORD=password123 npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
- Only Chromium:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx playwright test --project=Chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
- UI mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- Base URL defaults to `http://localhost:8080`. Override with `APP_URL`.
|
||||||
|
- The login smoke test fills Email/Password and expects redirect to `/today`.
|
||||||
|
- Ensure a user exists. The backend start step above auto-creates/updates the user via env vars.
|
||||||
105
e2e/bin/run-e2e.sh
Executable file
105
e2e/bin/run-e2e.sh
Executable file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Config
|
||||||
|
APP_URL_DEFAULT="http://localhost:8080"
|
||||||
|
BACKEND_URL="http://localhost:3002"
|
||||||
|
BACKEND_HEALTH="${BACKEND_URL}/api/health"
|
||||||
|
FRONTEND_URL="${APP_URL:-$APP_URL_DEFAULT}"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
red() { printf "\033[31m%s\033[0m\n" "$*"; }
|
||||||
|
green() { printf "\033[32m%s\033[0m\n" "$*"; }
|
||||||
|
yellow() { printf "\033[33m%s\033[0m\n" "$*"; }
|
||||||
|
|
||||||
|
# Ensure dependencies in e2e/
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
||||||
|
E2E_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$E2E_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
cd "$E2E_DIR"
|
||||||
|
if [ ! -f package.json ]; then
|
||||||
|
red "e2e/package.json not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install e2e deps and browsers
|
||||||
|
if [ ! -d node_modules ]; then
|
||||||
|
yellow "Installing e2e dependencies..."
|
||||||
|
npm ci
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! npx playwright --version >/dev/null 2>&1; then
|
||||||
|
yellow "Installing Playwright browsers..."
|
||||||
|
npm run install-browsers
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start backend and frontend
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
yellow "Starting backend..."
|
||||||
|
TUDUDI_USER_EMAIL="${E2E_EMAIL:-test@tududi.com}" \
|
||||||
|
TUDUDI_USER_PASSWORD="${E2E_PASSWORD:-password123}" \
|
||||||
|
npm run backend:start &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
yellow "Stopping background processes..."
|
||||||
|
# Attempt graceful group termination
|
||||||
|
if [ -n "${FRONTEND_PID:-}" ]; then kill -TERM -$FRONTEND_PID >/dev/null 2>&1 || true; fi
|
||||||
|
if [ -n "${BACKEND_PID:-}" ]; then kill -TERM -$BACKEND_PID >/dev/null 2>&1 || true; fi
|
||||||
|
|
||||||
|
# Kill by known ports (best-effort)
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
FRONTEND_PIDS_KILL=$(lsof -ti tcp:8080 || true)
|
||||||
|
BACKEND_PIDS_KILL=$(lsof -ti tcp:3002 || true)
|
||||||
|
if [ -n "${FRONTEND_PIDS_KILL:-}" ]; then kill ${FRONTEND_PIDS_KILL} >/dev/null 2>&1 || true; fi
|
||||||
|
if [ -n "${BACKEND_PIDS_KILL:-}" ]; then kill ${BACKEND_PIDS_KILL} >/dev/null 2>&1 || true; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Direct child processes as fallback
|
||||||
|
if [ -n "${FRONTEND_PID:-}" ] && ps -p $FRONTEND_PID >/dev/null 2>&1; then kill $FRONTEND_PID || true; fi
|
||||||
|
if [ -n "${BACKEND_PID:-}" ] && ps -p $BACKEND_PID >/dev/null 2>&1; then kill $BACKEND_PID || true; fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Wait for backend health
|
||||||
|
yellow "Waiting for backend to be ready at ${BACKEND_HEALTH}..."
|
||||||
|
for i in {1..60}; do
|
||||||
|
if curl -sf "$BACKEND_HEALTH" >/dev/null; then
|
||||||
|
green "Backend is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
if [ "$i" -eq 60 ]; then
|
||||||
|
red "Backend did not become ready in time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
yellow "Starting frontend dev server..."
|
||||||
|
npm run frontend:dev &
|
||||||
|
FRONTEND_PID=$!
|
||||||
|
|
||||||
|
# Wait for frontend
|
||||||
|
yellow "Waiting for frontend at ${FRONTEND_URL}..."
|
||||||
|
for i in {1..60}; do
|
||||||
|
if curl -sf "$FRONTEND_URL" >/dev/null; then
|
||||||
|
green "Frontend is ready"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
if [ "$i" -eq 60 ]; then
|
||||||
|
red "Frontend did not become ready in time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cd "$E2E_DIR"
|
||||||
|
|
||||||
|
yellow "Running Playwright tests..."
|
||||||
|
APP_URL="$FRONTEND_URL" \
|
||||||
|
E2E_EMAIL="${E2E_EMAIL:-test@tududi.com}" \
|
||||||
|
E2E_PASSWORD="${E2E_PASSWORD:-password123}" \
|
||||||
|
npm test
|
||||||
92
e2e/package-lock.json
generated
Normal file
92
e2e/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
{
|
||||||
|
"name": "tududi-e2e",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "tududi-e2e",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.47.2",
|
||||||
|
"dotenv": "^16.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.54.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
|
||||||
|
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.54.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.54.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
|
||||||
|
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.54.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.54.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
|
||||||
|
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
e2e/package.json
Normal file
16
e2e/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "tududi-e2e",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "End-to-end tests for tududi using Playwright",
|
||||||
|
"scripts": {
|
||||||
|
"test": "playwright test",
|
||||||
|
"test:ui": "playwright test --ui",
|
||||||
|
"codegen": "playwright codegen ${APP_URL:-http://localhost:8080}",
|
||||||
|
"install-browsers": "playwright install --with-deps"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.47.2",
|
||||||
|
"dotenv": "^16.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
26
e2e/playwright.config.ts
Normal file
26
e2e/playwright.config.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({ path: process.cwd() + '/.env' });
|
||||||
|
|
||||||
|
const baseURL = process.env.APP_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
fullyParallel: true,
|
||||||
|
reporter: [['list']],
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
video: 'retain-on-failure',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
viewport: { width: 1280, height: 800 },
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{ name: 'Chromium', use: { ...devices['Desktop Chrome'] } },
|
||||||
|
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
|
||||||
|
{ name: 'WebKit', use: { ...devices['Desktop Safari'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
4
e2e/test-results/.last-run.json
Normal file
4
e2e/test-results/.last-run.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
28
e2e/tests/login.spec.ts
Normal file
28
e2e/tests/login.spec.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
// Simple smoke test: user can log in and gets redirected to Today page
|
||||||
|
// Requires backend and frontend dev servers running locally. By default:
|
||||||
|
// - Frontend: http://localhost:8080 (webpack dev server)
|
||||||
|
// - Backend: http://localhost:3002 (proxied by webpack dev server)
|
||||||
|
// Set APP_URL to override base URL if needed.
|
||||||
|
|
||||||
|
test('user can login and reach Today page', async ({ page, baseURL }) => {
|
||||||
|
const appUrl = baseURL ?? process.env.APP_URL ?? 'http://localhost:8080';
|
||||||
|
|
||||||
|
// Go directly to login page
|
||||||
|
await page.goto(appUrl + '/login');
|
||||||
|
|
||||||
|
// Fill credentials. Ensure a user exists, e.g. TUDUDI_USER_EMAIL/TUDUDI_USER_PASSWORD or seed.
|
||||||
|
const email = process.env.E2E_EMAIL || 'test@tududi.com';
|
||||||
|
const password = process.env.E2E_PASSWORD || 'password123';
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(email);
|
||||||
|
await page.getByLabel('Password').fill(password);
|
||||||
|
await page.getByRole('button', { name: /login/i }).click();
|
||||||
|
|
||||||
|
// Expect redirect to Today view
|
||||||
|
await expect(page).toHaveURL(/\/today$/);
|
||||||
|
|
||||||
|
// Basic sanity check: page shows some Today UI elements
|
||||||
|
await expect(page.getByText(/Today/i)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"dev": "npm run frontend:dev",
|
"dev": "npm run frontend:dev",
|
||||||
"build": "npm run frontend:build",
|
"build": "npm run frontend:build",
|
||||||
"test": "npm run backend:test",
|
"test": "npm run backend:test",
|
||||||
|
"test:ui": "bash e2e/bin/run-e2e.sh",
|
||||||
"test:watch": "npm run frontend:test:watch",
|
"test:watch": "npm run frontend:test:watch",
|
||||||
"test:coverage": "npm run frontend:test:coverage && npm run backend:test:coverage",
|
"test:coverage": "npm run frontend:test:coverage && npm run backend:test:coverage",
|
||||||
"frontend:dev": "webpack serve --config webpack.config.js --hot",
|
"frontend:dev": "webpack serve --config webpack.config.js --hot",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue