From 9af587d91842aaca5394588ba25e12d3fae71c1d Mon Sep 17 00:00:00 2001 From: antanst <> Date: Fri, 8 Aug 2025 18:53:45 +0300 Subject: [PATCH] Add Playwright scaffolding and example E2E test. --- e2e/.env.example | 7 +++ e2e/README.md | 46 ++++++++++++++ e2e/bin/run-e2e.sh | 105 ++++++++++++++++++++++++++++++++ e2e/package-lock.json | 92 ++++++++++++++++++++++++++++ e2e/package.json | 16 +++++ e2e/playwright.config.ts | 26 ++++++++ e2e/test-results/.last-run.json | 4 ++ e2e/tests/login.spec.ts | 28 +++++++++ package.json | 1 + 9 files changed, 325 insertions(+) create mode 100644 e2e/.env.example create mode 100644 e2e/README.md create mode 100755 e2e/bin/run-e2e.sh create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/test-results/.last-run.json create mode 100644 e2e/tests/login.spec.ts diff --git a/e2e/.env.example b/e2e/.env.example new file mode 100644 index 0000000..1ee24d5 --- /dev/null +++ b/e2e/.env.example @@ -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 diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000..449e22b --- /dev/null +++ b/e2e/README.md @@ -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. diff --git a/e2e/bin/run-e2e.sh b/e2e/bin/run-e2e.sh new file mode 100755 index 0000000..c3ecbc9 --- /dev/null +++ b/e2e/bin/run-e2e.sh @@ -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 diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000..ecadb8c --- /dev/null +++ b/e2e/package-lock.json @@ -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" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..66b96d9 --- /dev/null +++ b/e2e/package.json @@ -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" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..4d57e64 --- /dev/null +++ b/e2e/playwright.config.ts @@ -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'] } }, + ], +}); diff --git a/e2e/test-results/.last-run.json b/e2e/test-results/.last-run.json new file mode 100644 index 0000000..cbcc1fb --- /dev/null +++ b/e2e/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/e2e/tests/login.spec.ts b/e2e/tests/login.spec.ts new file mode 100644 index 0000000..d463f98 --- /dev/null +++ b/e2e/tests/login.spec.ts @@ -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(); +}); diff --git a/package.json b/package.json index e9db742..46facfb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev": "npm run frontend:dev", "build": "npm run frontend:build", "test": "npm run backend:test", + "test:ui": "bash e2e/bin/run-e2e.sh", "test:watch": "npm run frontend:test:watch", "test:coverage": "npm run frontend:test:coverage && npm run backend:test:coverage", "frontend:dev": "webpack serve --config webpack.config.js --hot",