Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 290 additions & 23 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,246 @@ on:
- "e2e/**"
- "playwright.config.ts"
- ".github/workflows/e2e-tests.yml"
workflow_dispatch:
inputs:
test_scope:
description: "Test scope to run"
required: true
default: "smoke"
type: choice
options:
- smoke
- critical
- full

# Cancel in-progress runs for the same PR/branch
concurrency:
group: e2e-${{ github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
e2e-tests:
# Determine which tests to run based on changed files
detect-changes:
runs-on: ubuntu-latest
outputs:
run_full_suite: ${{ steps.changes.outputs.critical }}
run_smoke_only: ${{ steps.changes.outputs.smoke_only }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Detect changed files
id: changes
run: |
# For manual triggers, use input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "${{ inputs.test_scope }}" = "full" ]; then
echo "critical=true" >> $GITHUB_OUTPUT
echo "smoke_only=false" >> $GITHUB_OUTPUT
elif [ "${{ inputs.test_scope }}" = "critical" ]; then
echo "critical=true" >> $GITHUB_OUTPUT
echo "smoke_only=false" >> $GITHUB_OUTPUT
else
echo "critical=false" >> $GITHUB_OUTPUT
echo "smoke_only=true" >> $GITHUB_OUTPUT
fi
exit 0
fi

# For push to main, always run full suite
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "critical=true" >> $GITHUB_OUTPUT
echo "smoke_only=false" >> $GITHUB_OUTPUT
exit 0
fi

# Get changed files for PRs
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
else
BASE_SHA=${{ github.event.before }}
HEAD_SHA=${{ github.sha }}
fi

CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA 2>/dev/null || echo "")

# Critical paths that require full E2E suite
CRITICAL_PATTERNS=(
"nsc-events-nestjs/src/auth/"
"nsc-events-nestjs/src/activity/"
"nsc-events-nestjs/src/event-registration/"
"nsc-events-nestjs/src/user/"
"nsc-events-nextjs/app/auth/"
"nsc-events-nextjs/app/create-event/"
"nsc-events-nextjs/app/event-detail/"
"nsc-events-nextjs/components/AttendDialog"
"nsc-events-nextjs/components/EditDialog"
"nsc-events-nextjs/utility/queries"
"e2e/"
"playwright.config.ts"
)

RUN_FULL=false
for pattern in "${CRITICAL_PATTERNS[@]}"; do
if echo "$CHANGED_FILES" | grep -q "$pattern"; then
RUN_FULL=true
echo "Critical path changed: $pattern"
break
fi
done

if [ "$RUN_FULL" = "true" ]; then
echo "critical=true" >> $GITHUB_OUTPUT
echo "smoke_only=false" >> $GITHUB_OUTPUT
else
echo "critical=false" >> $GITHUB_OUTPUT
echo "smoke_only=true" >> $GITHUB_OUTPUT
fi

# Smoke tests - fast, run on every PR
smoke-tests:
needs: detect-changes
if: needs.detect-changes.outputs.run_smoke_only == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15

services:
postgres:
image: postgres:14
env:
POSTGRES_DB: nsc_events
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@v4

- name: Use Node.js 22
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "npm"
cache-dependency-path: "package-lock.json"

- name: Clean install dependencies
run: HUSKY=0 npm ci --include=optional

- name: Install sharp for Linux platform
run: npm install --os=linux --cpu=x64 sharp
working-directory: ./nsc-events-nestjs

- name: Get Playwright version
id: playwright-version
run: echo "version=$(npm ls @playwright/test --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT

- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}

- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium --with-deps

- name: Install Playwright system dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium

- name: Build backend
run: npm run build:backend

- name: Build frontend
env:
NEXT_PUBLIC_API_URL: http://localhost:3000/api
run: npm run build:frontend

- name: Wait for database
run: |
for i in {1..30}; do
if pg_isready -h localhost -p 5432 -U postgres; then
echo "Database is ready"
exit 0
fi
echo "Attempt $i: Database not ready yet..."
sleep 2
done
echo "Database did not become ready in time"
exit 1

- name: Run Smoke Tests (Chromium only, critical paths)
env:
PLAYWRIGHT_BASE_URL: http://localhost:8080
PLAYWRIGHT_API_URL: http://localhost:3000/api
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DATABASE: nsc_events
NODE_ENV: test
JWT_SECRET: e2e-test-jwt-secret-key
run: npx playwright test --project=chromium --grep @smoke

- name: Archive Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-smoke
path: playwright-report/
retention-days: 7

- name: Comment PR with smoke test results
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const resultsPath = 'test-results/results.json';
if (!fs.existsSync(resultsPath)) {
return;
}
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
const totalTests = results.stats?.expected || 0;
const passedTests = results.stats?.expected - results.stats?.unexpected || 0;
const failedTests = results.stats?.unexpected || 0;

let comment = `## πŸš€ E2E Smoke Test Results\n\n`;
comment += `> Running smoke tests only (non-critical files changed)\n\n`;
comment += `| Status | Count |\n|--------|-------|\n`;
comment += `| βœ… Passed | ${passedTests} |\n`;
comment += `| ❌ Failed | ${failedTests} |\n`;
comment += `| πŸ“Š Total | ${totalTests} |\n\n`;
comment += `[View detailed report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n`;
comment += `πŸ’‘ *To run full E2E suite, add \`[e2e-full]\` to your commit message or trigger manually.*`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
});

# Full E2E tests - comprehensive, run on critical changes
full-e2e-tests:
needs: detect-changes
if: needs.detect-changes.outputs.run_full_suite == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
# Run browsers in parallel shards
shard: [1, 2]

services:
postgres:
Expand All @@ -40,10 +275,10 @@ jobs:
- 5432:5432

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Use Node.js 22
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "npm"
Expand Down Expand Up @@ -96,7 +331,7 @@ jobs:
echo "Database did not become ready in time"
exit 1

- name: Run E2E tests
- name: Run E2E tests (Shard ${{ matrix.shard }}/2)
env:
PLAYWRIGHT_BASE_URL: http://localhost:8080
PLAYWRIGHT_API_URL: http://localhost:3000/api
Expand All @@ -107,47 +342,79 @@ jobs:
POSTGRES_DATABASE: nsc_events
NODE_ENV: test
JWT_SECRET: e2e-test-jwt-secret-key
run: npm run test:e2e
run: npx playwright test --shard=${{ matrix.shard }}/2

- name: Archive Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
name: playwright-report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 30

- name: Archive test results
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-test-results
name: e2e-test-results-shard-${{ matrix.shard }}
path: test-results/
retention-days: 30

- name: Comment PR with test results
if: always() && github.event_name == 'pull_request'
# Merge reports and comment on PR
report-results:
needs: [detect-changes, full-e2e-tests]
if: always() && needs.detect-changes.outputs.run_full_suite == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Download all test results
uses: actions/download-artifact@v4
with:
pattern: e2e-test-results-shard-*
path: all-results
merge-multiple: true

- name: Comment PR with full test results
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const resultsPath = 'test-results/results.json';
if (!fs.existsSync(resultsPath)) {
return;
const path = require('path');

let totalPassed = 0;
let totalFailed = 0;
let totalTests = 0;

// Aggregate results from all shards
const resultsDir = 'all-results';
if (fs.existsSync(resultsDir)) {
const files = fs.readdirSync(resultsDir);
for (const file of files) {
if (file.endsWith('results.json')) {
try {
const results = JSON.parse(fs.readFileSync(path.join(resultsDir, file), 'utf8'));
totalTests += results.stats?.expected || 0;
totalFailed += results.stats?.unexpected || 0;
} catch (e) {
console.log(`Error reading ${file}:`, e);
}
}
}
}
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
const totalTests = results.stats?.expected || 0;
const passedTests = results.stats?.expected - results.stats?.unexpected || 0;
const failedTests = results.stats?.unexpected || 0;

totalPassed = totalTests - totalFailed;
const status = totalFailed > 0 ? '❌' : 'βœ…';

let comment = `## E2E Test Results\n\n`;
comment += `- Total: ${totalTests}\n`;
comment += `- Passed: ${passedTests}\n`;
comment += `- Failed: ${failedTests}\n\n`;
let comment = `## ${status} Full E2E Test Results\n\n`;
comment += `> Running full test suite (critical files changed)\n\n`;
comment += `| Status | Count |\n|--------|-------|\n`;
comment += `| βœ… Passed | ${totalPassed} |\n`;
comment += `| ❌ Failed | ${totalFailed} |\n`;
comment += `| πŸ“Š Total | ${totalTests} |\n\n`;
comment += `[View detailed report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})`;


github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
Expand All @@ -157,8 +424,8 @@ jobs:

- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
files: ./test-results/junit.xml
files: ./all-results/**/junit.xml
fail_ci_if_error: false
verbose: true
2 changes: 1 addition & 1 deletion e2e/tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ test.describe('Accessibility', () => {
// Run accessibility tests serially to avoid cold-start race conditions
test.describe.configure({ mode: 'serial' });

test('should have accessible navigation structure', async ({ page }) => {
test('should have accessible navigation structure @smoke', async ({ page }) => {
// Increase timeout for cold start
test.setTimeout(60000);

Expand Down
Loading