E2E Testing

Auto Commerce uses Playwright for end-to-end testing, providing reliable cross-browser testing for the entire application workflow.

Overview

E2E tests verify complete user workflows by automating browser interactions. They test the integration between the Next.js frontend, Laravel backend API, and database layer.

Testing Stack

  • Playwright - Cross-browser E2E testing framework
  • TypeScript - Type-safe test definitions
  • Chromium/Firefox/WebKit - Multi-browser support
  • Storage State - Persistent authentication across tests

Prerequisites

Before running E2E tests:

  1. Backend running - Laravel API server with a test tenant
  2. Frontend dev server - Started automatically by Playwright
  3. Test tenant configured - Created via artisan command

Setup

1. Create Test Tenant

Using Docker:

cd backend
docker compose exec app php artisan e2e:setup

Or manually:

php artisan tenant:create e2e-test "E2E Test Tenant"
php artisan tenant:seed e2e-test

2. Configure Environment

Default test credentials in frontend/.env:

TEST_TENANT=e2e-test
TEST_EMAIL=admin@e2e-test.com
TEST_PASSWORD=password123

Override via environment variables:

export TEST_TENANT=e2e-test
export TEST_EMAIL=admin@e2e-test.com
export TEST_PASSWORD=password123

3. Install Playwright Browsers

cd frontend
npx playwright install --with-deps chromium

Running Tests

# Run all E2E tests (recommended: 2 workers)
npm run test:e2e

# Interactive UI mode
npm run test:e2e:ui

# Browser visible
npm run test:e2e:headed

# Debug mode (step through tests)
npm run test:e2e:debug

# Specific test file
npm run test:e2e -- auth.spec.ts

# Filter by test name
npm run test:e2e -- --grep "login"

# With specific worker count (recommended for stability)
npx playwright test --workers=2

Test Structure

frontend/e2e/
├── .auth/              # Stored auth state (gitignored)
├── fixtures/           # Test fixtures and utilities
│   └── test-fixtures.ts
├── auth.setup.ts       # Authentication setup (runs first)
├── auth.spec.ts        # Auth flow tests
├── dashboard.spec.ts   # Dashboard tests
├── modules.spec.ts     # Module page tests
└── README.md

Test Categories

Authentication Tests (auth.spec.ts)

  • Login form display and validation
  • Login success/failure handling
  • Signup flow and validation
  • 2FA verification
  • Logout functionality
  • Protected route redirects

Dashboard Tests (dashboard.spec.ts)

  • Dashboard display states
  • Widget loading
  • Navigation elements
  • Responsive layout

Module Tests (modules.spec.ts)

  • Orders module navigation
  • Products catalog
  • Customer management
  • Settings pages
  • Integrations display

Architecture

Authentication Handling

E2E tests handle authentication in two layers:

1. Storage State (auth.setup.ts)

Logs in once before all tests and saves browser state:

// auth.setup.ts
await page.goto('/login')
await page.locator('#tenant').fill(tenant)
await page.locator('#email').fill(email)
await page.locator('#password').fill(password)
await page.getByRole('button', { name: /sign in/i }).click()

// Save state for reuse
await page.context().storageState({ path: authFile })

2. ensureLoggedIn() Helper

Since Next.js SSR doesn't have localStorage access, initial page loads may redirect to login. The helper handles this:

export async function ensureLoggedIn(page: any): Promise<void> {
  await page.waitForLoadState('networkidle')

  if (page.url().includes('/login')) {
    // Fill login form and submit
    await page.locator('#tenant').fill(testData.tenant)
    await page.locator('#email').fill(testData.admin.email)
    await page.locator('#password').fill(testData.admin.password)
    await page.getByRole('button', { name: /sign in/i }).click()

    await page.waitForURL((url) => !url.pathname.includes('/login'))
  }
}

Loading State Management

Module pages have loading states that must complete before assertions:

async function waitForPageContent(page: any) {
  const loadingIndicators = page.locator(
    '[class*="animate-spin"], [class*="loading"], .skeleton'
  )
  try {
    await loadingIndicators.first().waitFor({
      state: 'hidden',
      timeout: 5000
    })
  } catch {
    // Loading may have already completed
  }
  await page.waitForTimeout(500)
}

Flexible Assertions

Tests handle both populated and empty states:

// Accept either content or empty state
const hasContent = await content.isVisible().catch(() => false)
const hasEmptyState = await emptyState.isVisible().catch(() => false)
expect(hasContent || hasEmptyState).toBeTruthy()

Writing New Tests

Basic Test Structure

import { test, expect } from './fixtures/test-fixtures'
import { ensureLoggedIn, gotoAuthenticated } from './fixtures/test-fixtures'

test.describe('My Feature', () => {
  test('should display feature page', async ({ page }) => {
    await gotoAuthenticated(page, '/my-feature')
    await expect(page.getByRole('heading', { name: /my feature/i }))
      .toBeVisible()
  })
})

With Authentication Check

test.beforeEach(async ({ page }) => {
  await ensureLoggedIn(page)
})

test('should access protected resource', async ({ page }) => {
  await page.goto('/protected-page')
  await page.waitForLoadState('networkidle')
  await expect(page.getByText(/protected content/i)).toBeVisible()
})

With API Context

import { test, expect, testData } from './fixtures/test-fixtures'

test('should create and verify item', async ({ page, apiContext }) => {
  // Create via API
  const result = await apiContext.post('/items', { name: 'Test Item' })

  // Verify in UI
  await page.goto('/items')
  await expect(page.getByText('Test Item')).toBeVisible()

  // Cleanup
  await apiContext.delete(`/items/${result.id}`)
})

Configuration

playwright.config.ts

Key configuration options:

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  workers: process.env.CI ? 1 : 2,  // Limit workers for stability
  retries: process.env.CI ? 2 : 0,

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
  ],

  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

Troubleshooting

Tests fail with "Tenant not found"

  • Ensure the test tenant exists: php artisan tenant:list
  • Check TEST_TENANT environment variable

Auth tests fail

  • Verify test credentials are correct
  • Check if backend is running: curl http://localhost:8000/api/v1/health
  • Clear auth state: rm -rf frontend/e2e/.auth/

Timeout errors

  • Increase timeout in playwright.config.ts
  • Run with fewer workers: npx playwright test --workers=2
  • Check API response times

Tests redirected to login unexpectedly

  • Expected due to Next.js SSR (no localStorage on server)
  • Use ensureLoggedIn() helper in tests
  • Verify backend is running and credentials are correct

Tests fail intermittently

  • Due to parallel execution and race conditions
  • Run with --workers=2 for stability
  • Or run sequentially: --workers=1

Strict mode violations

  • Error: "strict mode violation: multiple elements match"
  • Fix: Use .first() or more specific selectors

CI/CD Integration

GitHub Actions Example

name: E2E Tests

on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: password
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        run: |
          cd frontend
          npm ci
          npx playwright install --with-deps chromium

      - name: Setup backend
        run: |
          cd backend
          composer install
          php artisan migrate
          php artisan e2e:setup

      - name: Run E2E tests
        run: |
          cd frontend
          npm run test:e2e
        env:
          CI: true

      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: frontend/playwright-report/

Best Practices

  1. Use storage state - Authenticate once, reuse across tests
  2. Handle SSR redirects - Always use ensureLoggedIn() for authenticated tests
  3. Wait for loading - Use helpers to wait for loading states to complete
  4. Flexible assertions - Handle both empty and populated states
  5. Limit parallelism - Use --workers=2 for stability
  6. Clean test data - Create and cleanup test data within tests
  7. Use role selectors - Prefer getByRole() over CSS selectors
  8. Keep tests focused - One behavior per test

Related Documentation