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:
- Backend running - Laravel API server with a test tenant
- Frontend dev server - Started automatically by Playwright
- 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_TENANTenvironment 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=2for 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
- Use storage state - Authenticate once, reuse across tests
- Handle SSR redirects - Always use
ensureLoggedIn()for authenticated tests - Wait for loading - Use helpers to wait for loading states to complete
- Flexible assertions - Handle both empty and populated states
- Limit parallelism - Use
--workers=2for stability - Clean test data - Create and cleanup test data within tests
- Use role selectors - Prefer
getByRole()over CSS selectors - Keep tests focused - One behavior per test
Related Documentation
- Testing Overview - Backend testing with Pest PHP
- Module Testing - Module-specific tests
- Installation Guide - Setting up the environment