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
206 changes: 206 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
name: Nightly Tests

on:
schedule:
- cron: '0 2 * * *' # Run at 2:00 AM UTC daily
workflow_dispatch: # Allow manual trigger

permissions:
contents: read
checks: write
actions: read

jobs:
e2e:
name: E2E Tests (Including Nightly)
runs-on: ubuntu-latest
timeout-minutes: 120

services:
postgres:
image: postgres:18-alpine
env:
POSTGRES_USER: hmcts
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U hmcts -d postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 5
--health-start-period 10s

redis:
image: redis:8-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 5
--health-start-period 10s

env:
# Application configuration
BASE_URL: https://localhost:8080
PORT: 8080
SESSION_SECRET: ${{ secrets.SESSION_SECRET }}

# Database Configuration (using service containers)
DATABASE_URL: postgresql://hmcts@localhost:5432/postgres
REDIS_URL: redis://localhost:6379

# SSO Configuration (using existing GitHub Secrets)
SSO_CLIENT_ID: ${{ secrets.SSO_CLIENT_ID }}
SSO_CLIENT_SECRET: ${{ secrets.SSO_CLIENT_SECRET }}
SSO_IDENTITY_METADATA: ${{ secrets.SSO_CONFIG_ENDPOINT }}
SSO_SYSTEM_ADMIN_GROUP_ID: ${{ secrets.SSO_SG_SYSTEM_ADMIN }}
SSO_INTERNAL_ADMIN_CTSC_GROUP_ID: ${{ secrets.SSO_SG_ADMIN_CTSC }}
SSO_INTERNAL_ADMIN_LOCAL_GROUP_ID: ${{ secrets.SSO_SG_ADMIN_LOCAL }}
SSO_ALLOW_HTTP_REDIRECT: false

# Test user credentials (using existing GitHub Secrets)
SSO_TEST_SYSTEM_ADMIN_EMAIL: ${{ secrets.SSO_TEST_SYSTEM_ADMIN_ACCOUNT_USER }}
SSO_TEST_SYSTEM_ADMIN_PASSWORD: ${{ secrets.SSO_TEST_SYSTEM_ADMIN_ACCOUNT_PWD }}
SSO_TEST_LOCAL_ADMIN_EMAIL: ${{ secrets.SSO_TEST_ADMIN_LOCAL_ACCOUNT_USER }}
SSO_TEST_LOCAL_ADMIN_PASSWORD: ${{ secrets.SSO_TEST_ADMIN_LOCAL_ACCOUNT_PWD }}
SSO_TEST_CTSC_ADMIN_EMAIL: ${{ secrets.SSO_TEST_ADMIN_ACCOUNT_CTSC_USER }}
SSO_TEST_CTSC_ADMIN_PASSWORD: ${{ secrets.SSO_TEST_ADMIN_ACCOUNT_CTSC_PWD }}
SSO_TEST_NO_ROLES_EMAIL: ${{ secrets.SSO_TEST_NO_ROLES_ACCOUNT_USER }}
SSO_TEST_NO_ROLES_PASSWORD: ${{ secrets.SSO_TEST_NO_ROLES_ACCOUNT_PWD }}

# CFT IDAM Configuration
ENABLE_CFT_IDAM: true
CFT_IDAM_URL: https://idam-web-public.aat.platform.hmcts.net
CFT_IDAM_CLIENT_SECRET: ${{ secrets.CFT_IDAM_CLIENT_SECRET }}

# CFT IDAM Test Credentials
CFT_VALID_TEST_ACCOUNT: ${{ secrets.CFT_VALID_TEST_ACCOUNT }}
CFT_VALID_TEST_ACCOUNT_PASSWORD: ${{ secrets.CFT_VALID_TEST_ACCOUNT_PASSWORD }}
CFT_INVALID_TEST_ACCOUNT: ${{ secrets.CFT_INVALID_TEST_ACCOUNT }}
CFT_INVALID_TEST_ACCOUNT_PASSWORD: ${{ secrets.CFT_INVALID_TEST_ACCOUNT_PASSWORD }}

# API Authentication Configuration (for blob ingestion endpoint)
AZURE_TENANT_ID: ${{ secrets.APP_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.APP_PIP_DATA_MANAGEMENT_ID }}
APP_PIP_DATA_MANAGEMENT_SCOPE: ${{ secrets.APP_PIP_DATA_MANAGEMENT_SCOPE }}

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Enable Corepack
run: corepack enable

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.11.1'

- name: Cache Yarn dependencies
uses: actions/cache@v4
with:
path: |
.yarn/cache
.yarn/install-state.gz
node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Install dependencies
run: yarn install --immutable

- name: Generate Prisma client
run: yarn db:generate

- name: Run database migrations
run: yarn db:migrate

- name: Seed test database
run: |
echo "Running seed script..."
yarn workspace @hmcts/postgres run seed
echo "Seed script completed"

- name: Verify seed data exists
run: |
echo "Verifying seed data for locationId=9..."
ARTEFACT_COUNT=$(psql $DATABASE_URL -t -c "SELECT COUNT(*) FROM artefact WHERE location_id = '9';")
ARTEFACT_COUNT=$(echo $ARTEFACT_COUNT | xargs)
echo "Found $ARTEFACT_COUNT artefacts for locationId=9"
if [ "$ARTEFACT_COUNT" -lt 3 ]; then
echo "ERROR: Expected at least 3 seeded artefacts, found $ARTEFACT_COUNT"
exit 1
fi
echo "✓ Seed data verification successful"

- name: Build packages
run: yarn build

- name: Generate self-signed certificates for CI
run: |
chmod +x ./scripts/generate-ci-certs.sh
./scripts/generate-ci-certs.sh

- name: Restore Playwright browsers cache
uses: actions/cache/restore@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('e2e-tests/package.json') }}
restore-keys: |
${{ runner.os }}-playwright-

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

- name: Install Playwright system dependencies (if cached)
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: yarn workspace e2e-tests run playwright install-deps chromium

- name: Save Playwright browsers cache
if: steps.playwright-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('e2e-tests/package.json') }}

- name: Run E2E tests (including nightly)
id: e2e-tests
run: |
# Capture all output including server logs
set -o pipefail
yarn workspace e2e-tests run test:e2e:all 2>&1 | tee e2e-server-logs.txt

- name: Test Report
id: test-report
uses: dorny/test-reporter@v2
if: always()
with:
name: Nightly E2E Test Results
path: e2e-tests/junit-results.xml
reporter: java-junit
fail-on-error: false

- name: Upload Playwright test results
uses: actions/upload-artifact@v5
if: always()
with:
name: playwright-test-results-${{ github.run_id }}
path: |
e2e-tests/test-results/
e2e-tests/playwright-report/
retention-days: 7

- name: Upload application logs
uses: actions/upload-artifact@v5
if: always()
with:
name: application-logs-${{ github.run_id }}
path: e2e-server-logs.txt
retention-days: 7
119 changes: 119 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,125 @@ describe('UserService', () => {
});
```

### E2E Testing with Playwright

#### CRITICAL: Minimize Test Count

**Create the minimum number of tests.** Each test should represent a complete end-to-end user journey. Do NOT create separate tests for individual validations, accessibility checks, or Welsh translations. Instead, include all validations, accessibility checks, and Welsh translations within the journey test itself.

**Key Principle:** One test per user journey, not one test per validation or feature.

✅ **Good - Minimum tests, each covering complete journey:**
```typescript
// One test for the subscription journey - includes validations, Welsh, accessibility
test('user can subscribe to updates @nightly', async ({ page }) => {
await page.goto('/subscribe');

// Test validation along the journey
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('Enter your email')).toBeVisible();

// Test Welsh translation along the journey
await page.getByRole('link', { name: 'Cymraeg' }).click();
await expect(page.getByText('Rhowch eich e-bost')).toBeVisible();

// Test accessibility along the journey
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);

// Complete the journey
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('Subscription confirmed')).toBeVisible();
});

// Separate test only for a DIFFERENT user journey (e.g., unsubscribe)
test('user can unsubscribe @nightly', async ({ page }) => {
await page.goto('/unsubscribe');
// ... complete unsubscribe journey with validations, Welsh, accessibility
});
```

❌ **Bad - Too many separate tests:**
```typescript
test('shows validation error for email', async ({ page }) => { /* ... */ });
test('shows validation error for name', async ({ page }) => { /* ... */ });
test('Welsh translation works on subscribe page', async ({ page }) => { /* ... */ });
test('accessibility passes on subscribe page', async ({ page }) => { /* ... */ });
test('user completes subscription', async ({ page }) => { /* ... */ });
```

#### Test Organization

- Location: `e2e-tests/`
- Naming: `*.spec.ts`
- Tag nightly-only tests with `@nightly` in test title

#### What to Include in Each Journey Test
1. Complete user journey from start to finish
2. All relevant validation checks encountered in the journey
3. Welsh translation checks at key points
4. Accessibility checks at key points
5. Keyboard navigation where relevant
6. Successful completion of the journey

#### Example Pattern

```typescript
test('user can complete journey @nightly', async ({ page }) => {
// 1. Test main journey
await page.goto('/start');
await page.getByRole('button', { name: 'Start now' }).click();

// 2. Test Welsh
await page.getByRole('link', { name: 'Cymraeg' }).click();
expect(await page.getByRole('heading', { level: 1 })).toContainText('Dechrau nawr');

// 3. Test accessibility inline
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);

// 4. Test keyboard navigation
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');

// 5. Continue journey...
});
```

#### Correct Selectors (Priority Order)

1. `getByRole()` - Preferred for accessibility
2. `getByLabel()` - For form inputs
3. `getByText()` - For specific text
4. `getByTestId()` - Last resort only

#### DO NOT Test

- Font sizes
- Background colors
- Margins/padding
- Any visual styling
- UI design aspects

#### Test Data Management

- Use global-setup.ts for reference data seeding
- Use test-specific data creation in tests
- Clean up test data in global-teardown.ts

#### Coverage Expectations

- E2E tests: Cover critical user journeys
- Accessibility: Test inline with journeys (not separately)

#### Running Tests

```bash
yarn test:e2e # Run E2E tests (excludes @nightly)
yarn test:e2e:all # Run all E2E tests (including @nightly)
```

## Code Quality Standards

- **TypeScript**: Strict mode enabled, no `any` without justification
Expand Down
Loading
Loading