chore(e2e): add GitHub Actions workflow for Playwright UI E2E tests

This commit is contained in:
sumit_chaturvedi
2025-06-24 16:41:08 +05:30
parent a5c7cfc752
commit b081027f5e
8 changed files with 162 additions and 54 deletions
+79
View File
@@ -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
+4
View File
@@ -34,3 +34,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# Playwright test artifacts
playwright-report/
test-results/
+1 -1
View File
@@ -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"
+14 -17
View File
@@ -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
},
});
+24
View File
@@ -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
-36
View File
@@ -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();
});
+30
View File
@@ -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;
+10
View File
@@ -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/);
});