-
Notifications
You must be signed in to change notification settings - Fork 2
[draft] ui testing #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[draft] ui testing #154
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ name: E2E Tests | |
|
|
||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This runs the suite after every service completes its AWS deploy. I think this is a bit OTT, but I can't see an easy way of saying "when all the deploys are done", without having one big mega-job that everything is tied into. Open to suggestions here.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this works, how long do the tests take to run? I assume not very long. |
||
| permissions: | ||
| contents: read | ||
| id-token: write # Required for AWS OIDC authentication | ||
|
|
||
| on: | ||
| # Manual trigger for development iteration | ||
|
|
@@ -19,6 +20,10 @@ on: | |
| branches: | ||
| - main | ||
|
|
||
| env: | ||
| AWS_REGION: eu-west-1 | ||
| ENV: dev | ||
|
|
||
| jobs: | ||
| e2e-tests: | ||
| runs-on: ubuntu-latest | ||
|
|
@@ -28,5 +33,60 @@ jobs: | |
| github.event.workflow_run.conclusion == 'success' | ||
|
|
||
| steps: | ||
| - name: E2E Tests Placeholder | ||
| run: echo "TODO" | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Configure AWS credentials | ||
| uses: aws-actions/configure-aws-credentials@v4 | ||
| with: | ||
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | ||
| role-session-name: GitHub_E2E_Tests | ||
| aws-region: ${{ env.AWS_REGION }} | ||
|
|
||
| - name: Get deployment URL from CDK stack | ||
| id: get-url | ||
| run: | | ||
| BASE_URL=$(aws cloudformation describe-stacks \ | ||
| --stack-name "StickerlandiaSharedResources-${{ env.ENV }}" \ | ||
| --query "Stacks[0].Outputs[?OutputKey=='BaseUrl'].OutputValue" \ | ||
| --output text) | ||
| echo "BASE_URL=${BASE_URL}" >> $GITHUB_OUTPUT | ||
| echo "Deployment URL: ${BASE_URL}" | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '22' | ||
| cache: 'npm' | ||
| cache-dependency-path: e2e/package-lock.json | ||
|
|
||
| - name: Install Playwright dependencies | ||
| working-directory: e2e | ||
| run: npm ci | ||
|
|
||
| - name: Install Playwright browsers | ||
| working-directory: e2e | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: Run E2E tests against AWS | ||
| working-directory: e2e | ||
| run: npx playwright test | ||
| env: | ||
| CI: true | ||
| BASE_URL: ${{ steps.get-url.outputs.BASE_URL }} | ||
|
|
||
| - name: Upload Playwright report | ||
| uses: actions/upload-artifact@v4 | ||
| if: always() | ||
| with: | ||
| name: playwright-report | ||
| path: e2e/playwright-report/ | ||
| retention-days: 7 | ||
|
|
||
| - name: Upload test results | ||
| uses: actions/upload-artifact@v4 | ||
| if: failure() | ||
| with: | ||
| name: test-results | ||
| path: e2e/test-results/ | ||
| retention-days: 7 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # E2E Test Configuration | ||
| # Copy this file to .env to override defaults | ||
|
|
||
| # Target deployment URL (default: http://localhost:8080) | ||
| BASE_URL=http://localhost:8080 | ||
|
|
||
| # Test user credentials (defaults from README - can be overridden) | ||
| # TEST_USER_EMAIL=user@stickerlandia.com | ||
| # TEST_USER_PASSWORD=Stickerlandia2025! | ||
|
|
||
| # CI mode (set automatically in CI environments) | ||
| CI=false |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # E2E Tests | ||
|
|
||
| Playwright-based end-to-end tests for Stickerlandia. Can be pointed at any deployment. | ||
|
|
||
| ## Test Suites | ||
|
|
||
| | Suite | Description | | ||
| |-------|-------------| | ||
| | `public` | Landing page, public dashboard | | ||
| | `auth` | Login and registration flows | | ||
| | `authenticated` | Protected routes (dashboard, collection, sticker detail) | | ||
| | `api` | Health checks, sticker catalogue endpoints | | ||
|
|
||
| ## Running Tests | ||
|
|
||
| ```bash | ||
| # Against local Docker Compose | ||
| mise run compose:ui-test | ||
|
|
||
| # Against AWS (uses DEPLOYMENT_HOST_URL from .env) | ||
| mise run aws:ui-test | ||
|
|
||
| # Interactive mode | ||
| mise run compose:ui-test:ui | ||
|
|
||
| # Against custom URL | ||
| BASE_URL=https://example.com npx playwright test | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| Copy `.env.example` to `.env`: | ||
|
|
||
| ```bash | ||
| BASE_URL=http://localhost:8080 | ||
| TEST_USER_EMAIL=user@stickerlandia.com | ||
| TEST_USER_PASSWORD=Stickerlandia2025! | ||
| ``` | ||
|
|
||
| ## Structure | ||
|
|
||
| ``` | ||
| e2e/ | ||
| ├── tests/ | ||
| │ ├── auth/ # Login, registration, logout | ||
| │ ├── authenticated/ # Protected route tests | ||
| │ ├── public/ # Public page tests | ||
| │ └── api/ # API endpoint tests | ||
| ├── fixtures/ # Shared test helpers | ||
| ├── page-objects/ # Page object models | ||
| └── playwright.config.ts | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import { test as base, expect, type Page } from '@playwright/test'; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A fixture to get us into a logged in state |
||
| import { LOGIN_SELECTORS, waitForTokenProcessing } from './selectors'; | ||
|
|
||
| // Extend the base test with custom fixtures | ||
| export const test = base.extend<{ | ||
| authenticatedPage: Page; | ||
| }>({ | ||
| authenticatedPage: async ({ browser }, use) => { | ||
| // Create a new context with stored auth state | ||
| const context = await browser.newContext({ | ||
| storageState: '.auth/user.json', | ||
| }); | ||
| const page = await context.newPage(); | ||
| await use(page); | ||
| await context.close(); | ||
| }, | ||
| }); | ||
|
|
||
| export { expect }; | ||
|
|
||
| /** | ||
| * Helper function to perform login via the OIDC flow. | ||
| * Used by auth setup and login tests. | ||
| * | ||
| * Note: The assertions in this function are sanity check assertions - they verify | ||
| * that the login flow elements are present and functional. If these fail, it | ||
| * indicates an infrastructure or IdP issue rather than a test-specific failure. | ||
| */ | ||
| export async function performLogin(page: Page): Promise<void> { | ||
| const baseUrl = process.env.BASE_URL || 'http://localhost:8080'; | ||
| const testEmail = process.env.TEST_USER_EMAIL || 'user@stickerlandia.com'; | ||
| const testPassword = process.env.TEST_USER_PASSWORD || 'Stickerlandia2025!'; | ||
|
|
||
| console.log(`[performLogin] Starting login flow to ${baseUrl}`); | ||
|
|
||
| // Navigate to home page | ||
| await page.goto(baseUrl); | ||
| await page.waitForLoadState('networkidle'); | ||
| console.log(`[performLogin] Home page loaded`); | ||
|
|
||
| // Click the "Sign In" button in the header to initiate login | ||
| const signInButton = page.getByRole('button', { name: /sign in/i }); | ||
| await expect(signInButton).toBeVisible({ timeout: 10000 }); | ||
| await expect(signInButton).toBeEnabled({ timeout: 5000 }); | ||
| await signInButton.click(); | ||
| console.log(`[performLogin] Clicked Sign In button`); | ||
|
|
||
| // Wait for navigation away from home page | ||
| await page.waitForURL((url) => url.href !== baseUrl && url.href !== `${baseUrl}/`, { | ||
| timeout: 30000, | ||
| }); | ||
| console.log(`[performLogin] Navigated to: ${page.url()}`); | ||
|
|
||
| // Wait for the page to fully load (the login form is server-rendered) | ||
| await page.waitForLoadState('networkidle'); | ||
| await page.waitForLoadState('domcontentloaded'); | ||
| console.log(`[performLogin] Page loaded, looking for email input`); | ||
|
|
||
| // Define the email input locator | ||
| const emailInput = page.locator(LOGIN_SELECTORS.emailInput).first(); | ||
|
|
||
| // Wait for login form with longer timeout - the server-rendered page can be slow | ||
| try { | ||
| await expect(emailInput).toBeVisible({ timeout: 20000 }); | ||
| console.log(`[performLogin] Email input visible`); | ||
| } catch { | ||
| // Check if we were redirected back with a token (already authenticated) | ||
| const currentUrl = page.url(); | ||
| console.log(`[performLogin] Email input not found. Current URL: ${currentUrl}`); | ||
| if (currentUrl.includes('access_token=') || currentUrl.includes('/dashboard')) { | ||
| console.log(`[performLogin] Already authenticated, skipping login`); | ||
| // Already authenticated, skip login | ||
| if (currentUrl.includes('access_token=')) { | ||
| await page.waitForFunction( | ||
| () => !window.location.search.includes('access_token'), | ||
| { timeout: 15000 } | ||
| ); | ||
| } | ||
| if (!page.url().includes('/dashboard')) { | ||
| await page.goto(`${baseUrl}/dashboard`); | ||
| } | ||
| await page.waitForLoadState('networkidle'); | ||
| await page.waitForSelector('main#main', { timeout: 10000 }); | ||
| return; | ||
| } | ||
| // Re-throw with more context | ||
| throw new Error(`Login form did not appear within timeout. Current URL: ${page.url()}`); | ||
| } | ||
|
|
||
| // Fill in credentials | ||
| console.log(`[performLogin] Filling credentials for ${testEmail}`); | ||
| await emailInput.fill(testEmail); | ||
|
|
||
| const passwordInput = page.locator(LOGIN_SELECTORS.passwordInput).first(); | ||
| await expect(passwordInput).toBeVisible({ timeout: 5000 }); | ||
| await passwordInput.fill(testPassword); | ||
|
|
||
| // Click submit | ||
| const submitButton = page.locator(LOGIN_SELECTORS.submitButton).first(); | ||
| await expect(submitButton).toBeVisible({ timeout: 5000 }); | ||
| await submitButton.click(); | ||
| console.log(`[performLogin] Clicked submit button`); | ||
|
|
||
| // Wait for redirect back to the app - either with token or to dashboard | ||
| await page.waitForURL( | ||
| (url) => { | ||
| const href = url.href; | ||
| return ( | ||
| href.includes('access_token=') || | ||
| href.includes('/dashboard') || | ||
| (href.includes('localhost:8080') && !href.includes('/api/users')) | ||
| ); | ||
| }, | ||
| { timeout: 30000 } | ||
| ); | ||
| console.log(`[performLogin] Redirected to: ${page.url()}`); | ||
|
|
||
| // Handle token URL processing | ||
| if (page.url().includes('access_token=')) { | ||
| console.log(`[performLogin] Waiting for token to be processed`); | ||
| await waitForTokenProcessing(page); | ||
| console.log(`[performLogin] Token processed, URL now: ${page.url()}`); | ||
| } | ||
|
|
||
| // Give the app time to process auth state | ||
| await page.waitForTimeout(1000); | ||
|
|
||
| // Navigate to dashboard if not already there | ||
| if (!page.url().includes('/dashboard')) { | ||
| console.log(`[performLogin] Navigating to dashboard`); | ||
| await page.goto(`${baseUrl}/dashboard`); | ||
| } | ||
|
|
||
| await page.waitForLoadState('networkidle'); | ||
| console.log(`[performLogin] On dashboard, waiting for content`); | ||
|
|
||
| // Wait for actual dashboard content (not just the hidden main element) | ||
| // The dashboard shows "Welcome Back" when user data is loaded | ||
| await page.getByText(/Welcome Back/i).waitFor({ state: 'visible', timeout: 15000 }); | ||
| console.log(`[performLogin] Login complete - dashboard content visible`); | ||
| } | ||
|
|
||
| // Helper to check if user is logged in | ||
| export async function isLoggedIn(page: Page): Promise<boolean> { | ||
| try { | ||
| await page.goto('/dashboard'); | ||
| await page.waitForURL(/\/dashboard/, { timeout: 5000 }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.