diff --git a/.github/workflows/ui-end2end.yml b/.github/workflows/ui-end2end.yml new file mode 100644 index 0000000000..56454d94cb --- /dev/null +++ b/.github/workflows/ui-end2end.yml @@ -0,0 +1,79 @@ +name: UI - E2E Tests + +on: + pull_request: + branches: + - master + - "v5.*" + paths: + - 'ui/**' + +env: + SERVICES_TO_START: "api-dev postgres valkey worker-beat worker-dev" + DOCKER_COMPOSE_FILE: "docker-compose-dev.yml" + +jobs: + e2e: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: './ui/package-lock.json' + + # - name: Cache Playwright Browsers + # uses: actions/cache@v4 + # with: + # path: ~/.cache/ms-playwright + # key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + # restore-keys: | + # playwright-${{ runner.os }}- + + - name: Install dependencies + run: npm ci + working-directory: ./ui + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + working-directory: ./ui + + - name: Set up Docker Compose + uses: docker/setup-compose-action@364cc21a5de5b1ee4a7f5f9d3fa374ce0ccde746 #v1.2.0 + - name: Start Docker Compose + run: docker compose -f ${DOCKER_COMPOSE_FILE} up -d ${SERVICES_TO_START} + + - name: Wait for API to be ready + run: | + for i in {1..30}; do + if curl -s http://localhost:8000/api/v1; then + echo "API is up!" + break + fi + echo "Waiting for API..." + sleep 5 + done + - name: Run Playwright tests + run: npx playwright test + working-directory: ./ui + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: ./ui/playwright-report + + - name: Upload Playwright videos + uses: actions/upload-artifact@v4 + with: + name: test-videos + path: ./ui/test-results/**/*.webm + + - name: Docker Compose Down + if: always() + run: docker compose -f ${DOCKER_COMPOSE_FILE} down \ No newline at end of file diff --git a/ui/.gitignore b/ui/.gitignore index 45c1abce86..b0d30bcce4 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -34,3 +34,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Playwright test artifacts +playwright-report/ +test-results/ diff --git a/ui/package.json b/ui/package.json index b514ada9c3..24235cfbfa 100644 --- a/ui/package.json +++ b/ui/package.json @@ -98,7 +98,7 @@ "format:check": "./node_modules/.bin/prettier --check ./app", "format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app", "prepare": "husky", - "test:e2e": "playwright test" + "test:e2e": "npx playwright test && npx playwright show-report" }, "overrides": { "@react-types/shared": "3.26.0" diff --git a/ui/playwright.config.ts b/ui/playwright.config.ts index fa77a30016..22e5ed59c4 100644 --- a/ui/playwright.config.ts +++ b/ui/playwright.config.ts @@ -1,38 +1,35 @@ import { defineConfig, devices } from "@playwright/test"; -/** - * See https://playwright.dev/docs/test-configuration. - */ +const isLocal = process.env.LOCAL === "true"; + export default defineConfig({ timeout: 60 * 1000, testDir: "./tests/e2e", - /* Run tests in files in parallel */ fullyParallel: true, + forbidOnly: !isLocal, + retries: isLocal ? 0 : 2, + workers: isLocal ? undefined : 1, reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + globalSetup: require.resolve("./tests/e2e/global-setup"), use: { - /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://localhost:3000", - /* 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", - // Record video for failed tests video: "retain-on-failure", }, /* Configure projects for major browsers */ projects: [ - // { - // name: 'chromium', - // use: { ...devices['Desktop Chrome'] }, - // }, - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // { // name: 'webkit', // use: { ...devices['Desktop Safari'] }, @@ -63,7 +60,7 @@ export default defineConfig({ webServer: { command: "npm run dev", url: "http://localhost:3000", - reuseExistingServer: true, + reuseExistingServer: !isLocal, timeout: 300 * 1000, // 5 minute }, }); diff --git a/ui/tests/e2e/README.md b/ui/tests/e2e/README.md new file mode 100644 index 0000000000..150364044f --- /dev/null +++ b/ui/tests/e2e/README.md @@ -0,0 +1,24 @@ +# Playwright E2E Testing + +## 📦 Installation + +Playwright is already set up. To install dependencies: + +```bash +cd ui +npm install + +# Run all tests (headless) +npm run test:e2e + +# Run specific file (headless) +npx playwright test tests/e2e/root.spec.ts + +# Run all tests with UI (headed mode) +npx playwright test --headed + +# Run specific file with UI (headed mode) +npx playwright test tests/e2e/root.spec.ts --headed + +# Open the HTML report from last test run +npx playwright show-report \ No newline at end of file diff --git a/ui/tests/e2e/auth/authentication.spec.ts b/ui/tests/e2e/auth/authentication.spec.ts deleted file mode 100644 index 0f0fdbe869..0000000000 --- a/ui/tests/e2e/auth/authentication.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// Test credentials from environment variables -const TEST_USER_EMAIL = process.env.TEST_USER_EMAIL || 'dev@prowler.com'; -const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || 'thisisapassword123'; - -test('should render login page with expected elements', async ({ page }) => { - await page.goto('/login'); - await expect(page.locator('input[name="email"]')).toBeVisible(); - await expect(page.locator('input[name="password"]')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Log In' })).toBeVisible(); -}); - -test('should show error for invalid login', async ({ page }) => { - await page.goto('/login'); - await page.fill('input[name="email"]', 'test@example.com'); - await page.fill('input[name="password"]', 'password'); - await page.click('button[type="submit"]'); - - await expect(page.getByText(/invalid email or password/i)).toBeVisible(); -}); - -test('should login and show "No results" message on Findings page search', async ({ page }) => { - await page.goto('/login'); - const title = await page.title(); - - await page.fill('input[name="email"]', TEST_USER_EMAIL); - await page.fill('input[name="password"]', TEST_USER_PASSWORD); - await page.click('button[type="submit"]'); - await page.waitForURL('/'); - await page.goto('/findings'); - await page.waitForSelector('input[type="text"]'); - await page.fill('input[type="text"]', 'findingsTest'); - await page.waitForSelector('table'); - await expect(page.getByText('No results.')).toBeVisible(); -}); \ No newline at end of file diff --git a/ui/tests/e2e/global-setup.ts b/ui/tests/e2e/global-setup.ts new file mode 100644 index 0000000000..eb3cf7b4d7 --- /dev/null +++ b/ui/tests/e2e/global-setup.ts @@ -0,0 +1,30 @@ +import { FullConfig } from '@playwright/test'; +import { execSync } from 'child_process'; + +async function globalSetup(config: FullConfig) { + const isLocal = process.env.LOCAL === 'true'; + + if (isLocal) { + console.log('LOCAL=true detected — skipping Docker startup.'); + return; + } + + // Start Docker containers (for CI or non-local runs) + console.log('Starting Docker containers...'); + execSync( + 'docker-compose -f ../docker-compose-dev.yml up -d --build api-dev postgres valkey worker-beat worker-dev', + { stdio: 'inherit' } + ); + + // Wait for services to be ready + console.log('Waiting for services to be ready...'); + await new Promise((resolve) => setTimeout(resolve, 30000)); // Wait 30 seconds + + // Register cleanup function + process.on('exit', () => { + console.log('Cleaning up containers...'); + execSync('docker-compose -f ../docker-compose-dev.yml down', { stdio: 'inherit' }); + }); +} + +export default globalSetup; diff --git a/ui/tests/e2e/root.spec.ts b/ui/tests/e2e/root.spec.ts new file mode 100644 index 0000000000..d1ffed2685 --- /dev/null +++ b/ui/tests/e2e/root.spec.ts @@ -0,0 +1,10 @@ +import { test, expect } from '@playwright/test'; + +test('Unauthenticated users are redirected to sign-in and can navigate to sign-up', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveURL(/\/sign-in/); + await expect(page.getByText('Sign In')).toBeVisible(); + + await page.getByRole('link', { name: /sign up/i }).click(); + await expect(page).toHaveURL(/\/sign-up/); +}); \ No newline at end of file