diff --git a/.github/workflows/ui-lint-and-test.yaml b/.github/workflows/ui-lint-and-test.yaml index 55bcb585e1..29985bd108 100644 --- a/.github/workflows/ui-lint-and-test.yaml +++ b/.github/workflows/ui-lint-and-test.yaml @@ -126,6 +126,8 @@ jobs: - name: Build UI working-directory: "application/ui" + env: + PUBLIC_API_BASE_URL: "" run: npm run build - name: Compress build @@ -353,84 +355,64 @@ jobs: path: application/ui/playwright-report/ retention-days: 30 - # e2e-tests: - # name: E2E Tests - # needs: - # - check_paths - # - build - # - generate-openapi-spec - # if: needs.check_paths.outputs.run_workflow == 'true' - # permissions: - # contents: read - # timeout-minutes: 30 - # runs-on: ubuntu-latest - # container: - # image: mcr.microsoft.com/playwright:v1.54.0-noble@sha256:18d6adb6aaccf1b0f30eba890069972e089138e4a59ddb5303d7e7290e4e38b6 - # steps: - # - name: Checkout code - # uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - # with: - # persist-credentials: false - - # - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 - # with: - # node-version-file: application/ui/.nvmrc - - # - name: Set up Python - # uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - # with: - # python-version: "3.13" - - # - name: Install uv - # uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 - # with: - # version: "0.8.8" - # enable-cache: false - - # - name: Install OpenCV dependencies - # run: | - # apt-get update - # apt-get install -y libgl1 libglib2.0-0 - - # - name: Setup Backend - # working-directory: application/backend - # run: | - # uv sync --frozen --all-extras - - # - name: Download UI build - # uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - # with: - # name: ui-dist - # path: application/ui - - # - name: Download OpenAPI spec - # uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - # with: - # name: openapi-spec - # path: application/ui/src/api - - # - name: Unpack build - # working-directory: "application/ui" - # run: tar -xzf dist.tar.gz - - # - name: Install dependencies - # working-directory: "application/ui" - # run: npm ci - - # - name: Build OpenAPI type definitions - # working-directory: "application/ui" - # run: npm run build:api - - # - name: Run E2E tests - # working-directory: application/ui - # run: npm run test:e2e - - # - name: Upload E2E results - # uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - # if: always() - # with: - # name: e2e-test-results - # path: application/ui/playwright-report/ + e2e-tests: + name: E2E Tests + needs: + - check_paths + - build + - generate-openapi-spec + if: needs.check_paths.outputs.run_workflow == 'true' + permissions: + contents: read + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Download UI build + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + name: ui-dist + path: application/ui + + - name: Unpack build + working-directory: application/ui + run: tar -xzf dist.tar.gz + + - name: Install dependencies + working-directory: application/ui + run: npm ci + + - name: Run E2E tests with Docker Compose + working-directory: application/docker + env: + E2E_ASSETS_S3_URL: ${{ vars.E2E_ASSETS_S3_URL }} + run: | + docker compose --profile e2e up \ + --abort-on-container-exit \ + --exit-code-from playwright-e2e + + - name: Copy test results from container + if: always() + working-directory: application/docker + run: | + # Results are already mounted, just ensure they're visible + ls -la ../ui/playwright-report/ || true + + - name: Upload E2E results + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + with: + name: e2e-test-results + path: application/ui/playwright-report/ + + - name: Cleanup + if: always() + working-directory: application/docker + run: docker compose --profile e2e down -v required_check: name: Required Check ui-lint-and-test diff --git a/application/.dockerignore b/application/.dockerignore index 454e4a010f..342759e111 100644 --- a/application/.dockerignore +++ b/application/.dockerignore @@ -2,6 +2,7 @@ # Backend files !backend/app +!backend/run.sh !backend/pyproject.toml !backend/uv.lock diff --git a/application/backend/readme.md b/application/backend/readme.md index 9e0eef694c..34b033f049 100644 --- a/application/backend/readme.md +++ b/application/backend/readme.md @@ -1,3 +1,103 @@ # Geti Tune Geti Tune is a full-stack application for efficiently fine-tuning state-of-the-art computer vision models for tasks like classification, detection, and segmentation. + +## Quick Start + +### Basic Usage + +```bash +# Start the server (development mode) +./run.sh +``` + +### E2E Testing Setup + +```bash +# Full E2E setup with database seeding and test file downloads +DATABASE_FILE=geti_tune_e2e.db SEED_DB=true DOWNLOAD_FILES=true ./run.sh + +## Configuration + +### What `run.sh` Does + +1. **Loads configuration** from environment variables +2. **Seeds database** (if `SEED_DB=true`): + - Creates a test project with labels + - Sets up pipeline with video source and model +3. **Downloads test files** (if `DOWNLOAD_FILES=true`): + - Test video: `data/media/video.mp4` + - Model files: `data/projects/.../model.xml` and `model.bin` +4. **Starts the FastAPI server** on `http://localhost:7860` + +### Test Assets + +By default, test assets are downloaded from a public URL. In CI/GitHub Actions, a repository variable `E2E_ASSETS_S3_URL` can be set to use a private asset location. + +## Docker + +The backend can also run in Docker: + +```bash +cd ../docker + +# Build and run E2E backend +docker compose --profile e2e up backend-e2e + +# Or run full E2E stack +docker compose --profile e2e up --abort-on-container-exit +``` + +The Docker setup uses the same `run.sh` script for consistency. + +## API Documentation + +Once the server is running, visit: +- http://localhost:7860/docs + +## Development + +### Requirements + +- Python 3.13+ +- `uv` CLI tool for dependency management +- SQLite (included with Python) + +### Project Structure + +``` +backend/ +├── app/ +│ ├── main.py # FastAPI application entry point +│ ├── cli.py # CLI commands (init-db, seed) +│ ├── api/ # API endpoints +│ ├── core/ # Core functionality (scheduler, lifecycle) +│ ├── db/ # Database models and migrations +│ ├── entities/ # Business logic entities +│ ├── repositories/ # Data access layer +│ ├── schemas/ # Pydantic schemas +│ ├── services/ # Business logic services +│ ├── webrtc/ # WebRTC streaming +│ └── workers/ # Background workers +├── data/ # Runtime data (database, media, models) +├── run.sh # Main startup script +``` + +## Troubleshooting + +### Port already in use + +```bash +lsof -ti:7860 | xargs kill -9 +``` + +### Database locked + +```bash +rm data/geti_tune.db +./run.sh +``` + +### Test file download fails + +Check that `E2E_ASSETS_S3_URL` is accessible or verify the public URL is working. The script will exit with an error if downloads fail. diff --git a/application/backend/run.sh b/application/backend/run.sh index bfff32e34d..d1a14c364c 100755 --- a/application/backend/run.sh +++ b/application/backend/run.sh @@ -5,46 +5,60 @@ set -euo pipefail # run.sh - Script to run the Geti Tune FastAPI server # # Features: -# - Optionally seed the database before starting the server by setting: -# SEED_DB=true -# - Optionally download test video and model files before starting the server by setting: -# DOWNLOAD_FILES=true +# - Optionally seed the database (SEED_DB=true) +# - Optionally download test files (DOWNLOAD_FILES=true) +# - Configure database file name (DATABASE_FILE) # # Usage: -# SEED_DB=true DOWNLOAD_FILES=true ./run.sh # Seed database, download data and launch the server -# ./run.sh # Run server without seeding or downloading files +# SEED_DB=true DOWNLOAD_FILES=true ./run.sh # Seed and download +# DATABASE_FILE=geti_tune_e2e.db SEED_DB=true ./run.sh # Use E2E database +# ./run.sh # Run with defaults # # Environment variables: -# SEED_DB If set to "true", runs `uv run app/cli seed` before starting the server. -# DOWNLOAD_FILES If set to "true", downloads test video and model files if not already present. -# APP_MODULE Python module to run (default: app/main.py) -# UV_CMD Command to launch Uvicorn (default: "uv run") +# SEED_DB If set to "true", runs database initialization and seeding +# DOWNLOAD_FILES If set to "true", downloads test video and model files +# DATABASE_FILE Name of the database file (default: geti_tune.db) +# E2E_ASSETS_S3_URL Base URL for E2E assets # # Requirements: -# - 'uv' CLI tool (Uvicorn) installed and available in PATH -# - Python modules and dependencies installed correctly +# - 'uv' CLI tool installed and available in PATH # ----------------------------------------------------------------------------- SEED_DB=${SEED_DB:-false} DOWNLOAD_FILES=${DOWNLOAD_FILES:-false} +DATABASE_FILE=${DATABASE_FILE:-geti_tune.db} APP_MODULE=${APP_MODULE:-app/main.py} UV_CMD=${UV_CMD:-uv run} +E2E_ASSETS_BASE_URL=${E2E_ASSETS_S3_URL:-https://storage.geti.intel.com/test-data/geti-tune} export PYTHONUNBUFFERED=1 export PYTHONPATH=. +export DATABASE_FILE + +DB_PATH="data/${DATABASE_FILE}" + +echo "=====================================" +echo "Starting Geti Tune Backend" +echo "Database: $DB_PATH" +echo "Assets URL: $E2E_ASSETS_BASE_URL" +echo "=====================================" if [[ "$SEED_DB" == "true" ]]; then echo "Seeding the database..." - rm data/geti_tune.db || true + # Remove existing database if it exists + if [ -f "$DB_PATH" ]; then + echo "Removing existing database: $DB_PATH" + rm "$DB_PATH" + fi $UV_CMD app/cli.py init-db $UV_CMD app/cli.py seed --with-model=True fi # URLs and target paths -VIDEO_URL="https://storage.geti.intel.com/test-data/geti-tune/media/card-video.mp4" +VIDEO_URL="${E2E_ASSETS_BASE_URL}/media/card-video.mp4" VIDEO_TARGET="data/media/video.mp4" -MODEL_XML_URL="https://storage.geti.intel.com/test-data/geti-tune/models/ssd-card-detection.xml" -MODEL_BIN_URL="https://storage.geti.intel.com/test-data/geti-tune/models/ssd-card-detection.bin" +MODEL_XML_URL="${E2E_ASSETS_BASE_URL}/models/ssd-card-detection.xml" +MODEL_BIN_URL="${E2E_ASSETS_BASE_URL}/models/ssd-card-detection.bin" MODEL_TARGET_DIR="data/projects/9d6af8e8-6017-4ebe-9126-33aae739c5fa/models/977eeb18-eaac-449d-bc80-e340fbe052ad" MODEL_XML_TARGET="$MODEL_TARGET_DIR/model.xml" MODEL_BIN_TARGET="$MODEL_TARGET_DIR/model.bin" @@ -54,26 +68,36 @@ if [[ "$DOWNLOAD_FILES" == "true" ]]; then # Download video if [ ! -f "$VIDEO_TARGET" ]; then mkdir -p "$(dirname "$VIDEO_TARGET")" - echo "Downloading test video..." - curl -fL "$VIDEO_URL" -o "$VIDEO_TARGET" + echo "Downloading test video from $VIDEO_URL..." + if curl -fL "$VIDEO_URL" -o "$VIDEO_TARGET"; then + echo "✓ Video downloaded successfully" + else + echo "✗ Failed to download video from $VIDEO_URL" + exit 1 + fi else - echo "Test video already exists at $VIDEO_TARGET" + echo "✓ Test video already exists at $VIDEO_TARGET" fi - # Download model XML - if [ ! -f "$MODEL_XML_TARGET" ]; then - mkdir -p "$MODEL_TARGET_DIR" - echo "Downloading model XML..." - curl -fL "$MODEL_XML_URL" -o "$MODEL_XML_TARGET" - else - echo "Model XML already exists at $MODEL_XML_TARGET" + + # Verify video file is valid (has content) + if [ ! -s "$VIDEO_TARGET" ]; then + echo "✗ Error: Video file is empty at $VIDEO_TARGET" + exit 1 fi - # Download model BIN - if [ ! -f "$MODEL_BIN_TARGET" ]; then + + # Download model files + if [ ! -f "$MODEL_XML_TARGET" ]; then mkdir -p "$MODEL_TARGET_DIR" - echo "Downloading model BIN..." - curl -fL "$MODEL_BIN_URL" -o "$MODEL_BIN_TARGET" + echo "Downloading model files..." + if curl -fL "$MODEL_XML_URL" -o "$MODEL_XML_TARGET" && \ + curl -fL "$MODEL_BIN_URL" -o "$MODEL_BIN_TARGET"; then + echo "✓ Model files downloaded successfully" + else + echo "✗ Failed to download model files" + exit 1 + fi else - echo "Model BIN already exists at $MODEL_BIN_TARGET" + echo "✓ Model files already exist" fi fi diff --git a/application/docker/Dockerfile b/application/docker/Dockerfile index 9f410e4a48..19627798e2 100644 --- a/application/docker/Dockerfile +++ b/application/docker/Dockerfile @@ -11,10 +11,11 @@ WORKDIR /app ENV PYTHONPATH=/app -# Install system dependencies required for OpenCV +# Install system dependencies required for OpenCV and curl for downloading test files RUN apt-get update && apt-get install -y --no-install-recommends \ libgl1=1.7.0-1+b2 \ libglx-mesa0=25.0.7-2 \ + curl \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean @@ -25,6 +26,8 @@ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-editable COPY backend/app ./app +COPY backend/run.sh ./run.sh +RUN chmod +x ./run.sh ############# # build_rest_api_specs diff --git a/application/docker/docker-compose.yaml b/application/docker/docker-compose.yaml index 439d8747dd..578659c587 100644 --- a/application/docker/docker-compose.yaml +++ b/application/docker/docker-compose.yaml @@ -9,6 +9,7 @@ x-proxies: &proxies services: geti-tune: + profiles: [prod] image: geti-tune:${TAG:-latest} restart: unless-stopped build: @@ -49,3 +50,89 @@ services: - mosquitto ports: - "8081:80" + + backend-e2e: + profiles: + - e2e + build: + context: .. + dockerfile: docker/Dockerfile + target: backend-base + <<: *proxies + working_dir: /app + environment: + - DATABASE_FILE=geti_tune_e2e.db + - SEED_DB=true + - DOWNLOAD_FILES=true + - PYTHONUNBUFFERED=1 + - E2E_ASSETS_S3_URL=${E2E_ASSETS_S3_URL} + volumes: + - ../backend/data/:/app/data/ + ports: + - "7860:7860" + healthcheck: + test: + [ + "CMD", + "python3", + "-c", + "import urllib.request; urllib.request.urlopen('http://localhost:7860/health').read()", + ] + interval: 5s + timeout: 10s + retries: 10 + start_period: 30s + command: ["sh", "-c", "./run.sh"] + develop: + watch: + - path: ../backend/app + action: sync+restart + target: /app/app + - path: ../backend/pyproject.toml + action: rebuild + + frontend-e2e: + profiles: + - e2e + image: nginx:1.26-alpine + volumes: + - ../ui/dist:/usr/share/nginx/html:ro + - ./nginx-e2e.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "3000:80" + depends_on: + backend-e2e: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--quiet", + "--tries=1", + "--spider", + "http://127.0.0.1:80", + ] + interval: 5s + timeout: 5s + retries: 5 + start_period: 5s + + playwright-e2e: + profiles: + - e2e + image: mcr.microsoft.com/playwright:v1.54.0-noble@sha256:18d6adb6aaccf1b0f30eba890069972e089138e4a59ddb5303d7e7290e4e38b6 + working_dir: /app + volumes: + - ../ui/tests:/app/tests:ro + - ../ui/playwright.config.ts:/app/playwright.config.ts:ro + - ../ui/package.json:/app/package.json:ro + - ../ui/playwright-report:/app/playwright-report + - ../ui/node_modules:/app/node_modules:ro + environment: + - CI=true + - BASE_URL=http://frontend-e2e + depends_on: + frontend-e2e: + condition: service_healthy + command: npx playwright test --project=e2e diff --git a/application/docker/nginx-e2e.conf b/application/docker/nginx-e2e.conf new file mode 100644 index 0000000000..9de22a2f83 --- /dev/null +++ b/application/docker/nginx-e2e.conf @@ -0,0 +1,46 @@ +server { + listen 80; + server_name frontend-e2e; + + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression for faster loading + gzip on; + gzip_types text/plain text/css application/json application/javascript; + + location / { + # Redirect to index.html if file not found + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, max-age=31536000"; + try_files $uri =404; + } + + # Don't cache HTML files + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + } + + location /health { + proxy_pass http://backend-e2e:7860/health; + } + location /stream { + proxy_pass http://backend-e2e:7860/stream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + location ~ ^/api/ { + proxy_pass http://backend-e2e:7860; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/application/ui/package.json b/application/ui/package.json index 2570641196..d24a8066b3 100644 --- a/application/ui/package.json +++ b/application/ui/package.json @@ -9,6 +9,7 @@ }, "scripts": { "build": "rsbuild build", + "build:e2e": "PUBLIC_API_BASE_URL='' rsbuild build", "build:api": "npx openapi-typescript ./src/api/openapi-spec.json -o ./src/api/openapi-spec.d.ts --root-types && prettier --write src/api/openapi-spec.d.ts", "build:api:download": "curl -o ./src/api/openapi-spec.json http://localhost:7860/api/openapi.json && prettier --write src/api/openapi-spec.json", "update-spec": "npm run build:api:download && npm run build:api", @@ -27,7 +28,9 @@ "test:unit:watch": "vitest --config vitest.config.ts --reporter=verbose --watch", "test:unit:coverage": "vitest --coverage", "test:component": "npx playwright test --project=component", - "test:e2e": "ENABLE_BACKEND=True npx playwright test --project=e2e", + "test:e2e": "npx playwright test --project=e2e", + "test:e2e:docker": "npm run build:e2e && cd ../docker && docker compose --profile e2e up --build --abort-on-container-exit", + "test:e2e:docker:watch": "cd ../docker && docker compose --profile e2e watch", "type-check": "tsc --noEmit", "tauri": "tauri" }, diff --git a/application/ui/playwright.config.ts b/application/ui/playwright.config.ts index 006d1f29e0..b51fded176 100644 --- a/application/ui/playwright.config.ts +++ b/application/ui/playwright.config.ts @@ -4,6 +4,11 @@ import { defineConfig, devices } from '@playwright/test'; const CI = !!process.env.CI; +// Docker mode: BASE_URL env var is set by Docker Compose (e.g., BASE_URL=http://frontend-e2e) +const USE_DOCKER = !!process.env.BASE_URL; +const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; +// Component tests only need frontend, E2E tests need backend + frontend +const IS_COMPONENT_TEST = process.argv.includes('--project=component'); const ACTION_TIMEOUT = 30000; @@ -15,8 +20,7 @@ export default defineConfig({ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + retries: 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Test timeout */ @@ -28,14 +32,18 @@ export default defineConfig({ /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [[CI ? 'github' : 'list'], ['html', { open: 'never' }]], use: { - baseURL: 'http://localhost:3000', + baseURL: BASE_URL, trace: CI ? 'on-first-retry' : 'on', video: CI ? 'on-first-retry' : 'on', launchOptions: { slowMo: 100, headless: true, - devtools: true, + devtools: !CI, + // Additional browser args for WebRTC in headless mode + args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream', '--disable-web-security'], }, + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 720 }, timezoneId: 'UTC', actionTimeout: ACTION_TIMEOUT, navigationTimeout: ACTION_TIMEOUT, @@ -47,30 +55,47 @@ export default defineConfig({ name: 'component', testDir: './tests', testIgnore: '**/e2e/**', - use: { - ...devices['Desktop Chrome'], - headless: true, - viewport: { width: 1280, height: 720 }, - }, }, { name: 'e2e', testDir: './tests/e2e', - use: { - ...devices['Desktop Chrome'], - headless: CI, - viewport: { width: 1280, height: 720 }, - }, }, ], /* Run your local dev server before starting the tests */ - webServer: !process.env.ENABLE_BACKEND - ? { - command: CI ? 'npx serve -s dist -p 3000' : 'npm run start', - url: 'http://localhost:3000', - reuseExistingServer: true, - timeout: ACTION_TIMEOUT, - } - : undefined, + webServer: USE_DOCKER + ? undefined // Docker Compose handles servers + : IS_COMPONENT_TEST + ? [ + // Component tests: only frontend server needed + { + command: 'npm run start', + url: 'http://localhost:3000', + reuseExistingServer: !CI, + timeout: 120_000, + }, + ] + : [ + // E2E tests: start both backend and frontend + { + command: './run.sh', + url: 'http://localhost:7860/health', + reuseExistingServer: !CI, + timeout: 120_000, + cwd: '../backend', + env: { + DATABASE_FILE: 'geti_tune_e2e.db', + SEED_DB: 'true', + DOWNLOAD_FILES: 'true', + PYTHONPATH: '.', + PYTHONUNBUFFERED: '1', + }, + }, + { + command: 'npm run start', + url: 'http://localhost:3000', + reuseExistingServer: !CI, + timeout: 120_000, + }, + ], }); diff --git a/application/ui/tests/e2e/README.md b/application/ui/tests/e2e/README.md new file mode 100644 index 0000000000..62e2ed1be9 --- /dev/null +++ b/application/ui/tests/e2e/README.md @@ -0,0 +1,178 @@ +# E2E Testing Guide + +## 🚀 Quick Start + +### **Local Development (Fastest - Recommended)** + +```bash +npm run test:e2e +``` + +- ✅ Start backend on port 7860 with seeded database +- ✅ Start frontend on port 3000 +- ✅ Run E2E tests +- ✅ Stop servers when done + +### **Docker (CI/Production)** + +For isolated containerized testing: + +```bash +npm run test:e2e:docker +``` + +### **Docker with Watch Mode (Development)** + +Auto-rebuild on file changes: + +```bash +npm run test:e2e:docker:watch +``` + +--- + +## 📋 Prerequisites + +### Local Development + +- Python 3.10+ with `uv` installed +- Node.js 24+ +- All dependencies installed (`npm install` in `ui/`, backend dependencies via `uv`) + +### Docker + +- Docker Desktop or Docker Engine +- Docker Compose v2.22+ (for `watch` support) + +--- + +## 🛠️ Advanced Usage + +### Run Specific Tests + +```bash +# Local +npx playwright test --project=e2e tests/e2e/main.spec.ts + +# Docker (requires manual setup) +cd ../docker +docker compose --profile e2e up -d backend-e2e frontend-e2e +docker compose --profile e2e run playwright-e2e npx playwright test tests/e2e/main.spec.ts +``` + +### Debug Tests Locally + +```bash +# UI Mode (interactive debugging) +npx playwright test --project=e2e --ui + +# Headed mode (see browser) +npx playwright test --project=e2e --headed + +# Debug specific test +npx playwright test --project=e2e --debug tests/e2e/main.spec.ts +``` + +### View Test Reports + +```bash +npx playwright show-report playwright-report +# Or open: ./playwright-report/index.html +``` + +--- + +## 🔧 How It Works + +### Local Mode (No Docker) + +1. Playwright config detects no `BASE_URL` env var +2. Starts `webServer[0]`: Backend via `./run.sh` with E2E configuration: + - `DATABASE_FILE=geti_tune_e2e.db` + - `SEED_DB=true` (initializes and seeds database) + - `DOWNLOAD_FILES=true` (downloads test video and model files) +3. Starts `webServer[1]`: Frontend via `npm run start` +4. Waits for both to be healthy +5. Runs tests against `http://localhost:3000` +6. Stops servers after tests complete + +### Docker Mode (CI) + +1. `BASE_URL=http://frontend-e2e` env var is set +2. Playwright skips `webServer` (Docker handles it) +3. Docker Compose starts: + - `backend-e2e`: Python backend using `run.sh` with E2E env vars, includes healthcheck + - `frontend-e2e`: Nginx serving built frontend with proxy to backend + - `playwright-e2e`: Test runner container +4. Backend automatically seeds database and downloads test files on startup +5. Tests run against frontend service DNS name +6. `--abort-on-container-exit` stops all when tests finish + +### Watch Mode (Docker Development) + +1. Same as Docker mode but with `watch` instead of `up` +2. Backend: `sync+restart` on `/app/app` changes +3. Frontend: `sync` on `/usr/share/nginx/html` changes +4. Tests auto-rerun on changes + +--- + +## 🐛 Troubleshooting + +### "Port already in use" + +```bash +# Kill processes on ports 3000 or 7860 +lsof -ti:3000 | xargs kill -9 +lsof -ti:7860 | xargs kill -9 +``` + +### "Backend not starting" + +Check backend logs and verify configuration: + +```bash +cd ../backend +# Check if run.sh works +DATABASE_FILE=geti_tune_e2e.db SEED_DB=true DOWNLOAD_FILES=true ./run.sh +# Should see: Database seeding, file downloads, then "Uvicorn running on http://0.0.0.0:7860" +``` + +Common issues: + +- Missing test assets: Ensure `E2E_ASSETS_S3_URL` is accessible or using public default +- Database locked: Delete `data/geti_tune_e2e.db` and restart +- Port conflict: Kill process on port 7860 (`lsof -ti:7860 | xargs kill -9`) + +### "Tests timing out" + +- Increase timeout in `playwright.config.ts`: `actionTimeout: 30000` +- Or add explicit waits in tests: `await page.waitForLoadState('networkidle')` + +### Docker build fails + +```bash +# Clean rebuild +cd ../docker +docker compose --profile e2e down -v +docker compose --profile e2e build --no-cache +``` + +--- + +### Best Practices + +- ✅ Use `test.step()` for clear test structure +- ✅ Use semantic locators: `getByText()`, `getByRole()`, `getByLabel()` +- ✅ Add explicit waits when needed: `waitForLoadState()`, `waitForURL()` +- ✅ Keep tests isolated (don't depend on other tests) +- ❌ Avoid `page.locator('.css-class')` (brittle) +- ❌ Avoid hardcoded sleeps (`await page.waitForTimeout(5000)`) + +--- + +## 🔗 Resources + +- [Playwright Docs](https://playwright.dev) +- [Docker Compose Watch](https://docs.docker.com/compose/file-watch/) +- [Our Backend API Docs](http://localhost:7860/docs) diff --git a/application/ui/tests/e2e/existing-project.spec.ts b/application/ui/tests/e2e/existing-project.spec.ts index b5237b3d24..885280bf06 100644 --- a/application/ui/tests/e2e/existing-project.spec.ts +++ b/application/ui/tests/e2e/existing-project.spec.ts @@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test'; -test('[E2E] Existing project', async ({ page }) => { +test.skip('[E2E] Existing project', async ({ page }) => { await test.step('Navigate to root page', async () => { await page.goto('/projects'); }); diff --git a/application/ui/tests/e2e/new-project.spec.ts b/application/ui/tests/e2e/new-project.spec.ts index 707c3001cf..feb2a1def9 100644 --- a/application/ui/tests/e2e/new-project.spec.ts +++ b/application/ui/tests/e2e/new-project.spec.ts @@ -20,7 +20,7 @@ const fillProjectForm = async ({ await page.getByRole('button', { name: /Confirm/ }).click(); // Edit task - await page.getByLabel(task).click(); + await page.getByLabel(task, { exact: true }).click(); // Edit first label await page.getByLabel('Label input for Object').fill(labelNames[0]); @@ -33,7 +33,7 @@ const fillProjectForm = async ({ } }; -test.skip('Project creation', async ({ page }) => { +test('Project creation', async ({ page }) => { await test.step('Navigate to projects page', async () => { await page.goto('/projects'); }); @@ -56,11 +56,14 @@ test.skip('Project creation', async ({ page }) => { await test.step('Verify project appears in project list', async () => { await page.getByText('Geti Tune').click(); // Go back to /projects - await expect(page.getByText('New Project', { exact: true })).toBeVisible(); + await expect(page.getByText('New Project')).toBeVisible(); }); await test.step('Delete created project', async () => { - await page.getByRole('button', { name: /open project options/i }).click(); + await page + .getByRole('link', { name: /New Project/ }) + .getByRole('button', { name: /open project options/i }) + .click(); await page.getByText(/Delete/).click(); await expect(page.getByText('Project deleted successfully')).toBeVisible(); await expect(page.getByText('New Project')).toBeHidden();