E2E Testing with Playwright
Tenanto includes a comprehensive end-to-end test suite with 484 tests covering all critical user journeys.
Overview
| Category | Tests | Description |
|---|---|---|
| Authentication | 45 | Login, logout, registration, password reset |
| Admin Panel | 120 | Tenants, users, licenses CRUD operations |
| Tenant Panel | 95 | Projects, tasks, teams, settings |
| API Endpoints | 65 | REST API authentication and CRUD |
| Tenant Isolation | 41 | Cross-tenant security, permission enforcement |
| Navigation | 48 | Sidebar, breadcrumbs, responsive layout |
| Error Pages | 25 | 403, 404, 500 error handling |
| Onboarding | 20 | Wizard flow, progress tracking |
| Profile & Settings | 25 | User profile management |
Why Playwright?
- Modern framework for E2E testing
- Supports all major browsers (Chromium, Firefox, WebKit)
- Auto-wait for elements (no flaky
sleep()calls) - Excellent debugging tools (Trace Viewer, Inspector)
- Native API testing via request context
- Video recording and screenshots on failure
Installation
# Install Playwright and browsers
npm install
npx playwright install
# Or install specific browsers
npx playwright install chromium firefox webkit
Running Tests
Quick Start
# All tests (all browsers, all projects)
npx playwright test
# All chromium tests (recommended for development)
npx playwright test --project=admin-chromium --project=tenant-chromium
# With HTML report
npx playwright test --reporter=html
Available Projects
Tests are organized by category. Each project targets specific functionality:
| Project | Tests | Description |
|---|---|---|
admin-chromium |
84 | System admin panel (tenants, users, licenses) |
tenant-chromium |
171 | Tenant panel (projects, tasks, teams, billing) |
navigation-chromium |
48 | Sidebar, breadcrumbs, responsive layout |
auth-chromium |
45 | Login, logout, registration, password reset |
api-chromium |
65 | REST API endpoints |
isolation-chromium |
41 | Cross-tenant security tests |
profile-chromium |
25 | User profile management |
onboarding-chromium |
20 | Wizard flow |
errors-chromium |
25 | Error pages (403, 404, 500) |
Cross-browser projects: admin-firefox, tenant-firefox, admin-webkit, tenant-webkit
Mobile projects: tenant-mobile-chrome, tenant-mobile-safari
Running Specific Projects
# Admin panel tests only
npx playwright test --project=admin-chromium
# Tenant panel tests only
npx playwright test --project=tenant-chromium
# Multiple projects
npx playwright test --project=admin-chromium --project=tenant-chromium
# Specific file within a project
npx playwright test e2e/admin/licenses.spec.ts --project=admin-chromium
Debugging & Watching Tests
# UI mode (best for debugging - visual timeline, step through)
npx playwright test --ui
# Debug mode (pause at each step, inspector)
npx playwright test --project=tenant-chromium --debug
# Headed mode (watch browser, single worker for clarity)
npx playwright test --project=tenant-chromium --headed --workers=1
# Specific test in debug mode
npx playwright test e2e/tenant/billing.spec.ts:20 --project=tenant-chromium --debug
Test Structure
e2e/
├── admin/ # System admin panel tests
│ ├── licenses.spec.ts # License management CRUD
│ ├── tenants.spec.ts # Tenant management CRUD
│ └── users.spec.ts # User management CRUD
├── api/ # REST API tests
│ └── api.spec.ts # Auth, Projects, Tasks API
├── auth/ # Authentication tests
│ ├── login.spec.ts # Login flow
│ ├── logout.spec.ts # Logout flow
│ ├── password-reset.spec.ts # Password reset
│ ├── registration.spec.ts # User registration
│ └── session.spec.ts # Session management
├── errors/ # Error page tests
│ └── error-pages.spec.ts # 403, 404, 500 pages
├── helpers/ # Test utilities
│ ├── auth.ts # Login helpers, test users
│ └── api.ts # API request helpers
├── isolation/ # Security isolation tests
│ ├── cross-tenant-api.spec.ts # API tenant isolation
│ └── permission-isolation.spec.ts # Role-based access
├── navigation/ # UI navigation tests
│ └── navigation.spec.ts # Sidebar, breadcrumbs
├── onboarding/ # Onboarding wizard tests
│ └── onboarding.spec.ts # Wizard flow
├── profile/ # User profile tests
│ └── profile.spec.ts # Profile management
└── tenant/ # Tenant panel tests
├── projects.spec.ts # Project CRUD
├── settings.spec.ts # Tenant settings
├── tasks.spec.ts # Task CRUD
└── teams.spec.ts # Team management
Test Helpers
Authentication (e2e/helpers/auth.ts)
import { testUsers, loginAsSystemAdmin, loginAsTenantOwner } from '../helpers/auth';
// Pre-configured test users
testUsers.systemAdmin // [email protected]
testUsers.demoOwner // [email protected]
testUsers.demoAdmin // [email protected]
testUsers.demoMember // [email protected]
// Login helpers
await loginAsSystemAdmin(page);
await loginAsTenantOwner(page);
await loginAsTenantAdmin(page);
await loginAsTenantMember(page);
API Helpers (e2e/helpers/api.ts)
import { apiHeaders, authHeader, getProjects } from '../helpers/api';
// Get API token
const response = await request.post(`${baseUrl}/api/v1/auth/login`, {
headers: apiHeaders,
data: { email, password },
});
const { token } = (await response.json()).data;
// Make authenticated request
const projects = await getProjects(request, token, baseUrl);
Configuration
The test configuration is in playwright.config.ts:
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 2,
workers: process.env.CI ? 1 : 2,
timeout: 60000,
use: {
baseURL: 'http://demo.tenanto.local',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
actionTimeout: 15000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
],
});
Rate Limiting
E2E tests perform many login requests which would normally hit rate limits. Tenanto handles this by increasing rate limits in local and testing environments:
- Default limit: 5 attempts per minute (production)
- Local/testing limit: 100 attempts per minute
This is implemented in:
app/Livewire/Forms/LoginForm.php- Tenant panel login (Breeze/Livewire)app/Filament/Admin/Pages/Auth/Login.php- Admin panel login (Filament)
Both use app()->environment('local', 'testing') to detect the environment and apply higher limits.
Note: No session persistence (
storageState) is needed. Each test logs in fresh, which provides better isolation and simpler test maintenance.
Test Isolation
Tests are configured for proper isolation:
- Serial mode for tests that share state (API token caching)
- Retries to handle occasional timing issues with Livewire/Filament
- Fresh login per test for complete isolation (rate limit increased for testing)
// Run tests in order (no parallel)
test.describe.configure({ mode: 'serial' });
// Skip on non-chromium (API tests don't need all browsers)
test.beforeEach(async ({ browserName }) => {
if (browserName !== 'chromium') test.skip();
});
Example Tests
Login Test
test('user can login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', '[email protected]');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*\/app/);
});
API Test
test('can create project via API', async ({ request }) => {
const token = await getToken(request);
const response = await request.post(`${baseUrl}/api/v1/projects`, {
headers: authHeader(token),
data: { name: `Test Project ${Date.now()}` },
});
expect(response.ok()).toBeTruthy();
});
Tenant Isolation Test
test('user from tenant A cannot access tenant B projects via API', async ({ request }) => {
// Login as demo tenant user
const demoToken = await getTokenForTenant(request, 'demo');
// Login as acme tenant user
const acmeToken = await getTokenForTenant(request, 'acme');
// Create project in demo tenant
const projectRes = await request.post(`${demoUrl}/api/v1/projects`, {
headers: authHeader(demoToken),
data: { name: 'Secret Project' },
});
const projectId = (await projectRes.json()).data.id;
// Acme user should NOT be able to access demo's project
const accessRes = await request.get(`${acmeUrl}/api/v1/projects/${projectId}`, {
headers: authHeader(acmeToken),
});
expect(accessRes.status()).toBe(404); // Or 403 - either is secure
});
Debugging
Trace Viewer
When tests fail, trace files are saved automatically:
npx playwright show-trace test-results/*/trace.zip
Screenshots
Screenshots are saved on failure in test-results/.
Video Recording
Videos are recorded on first retry. View in test-results/.
Inspector Mode
Step through tests interactively:
npx playwright test --debug
CI/CD Integration
GitLab CI
e2e-tests:
stage: test
image: mcr.microsoft.com/playwright:v1.40.0-jammy
script:
- npm ci
- npx playwright install --with-deps
- npx playwright test --project=chromium
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 1 week
only:
- merge_requests
- main
GitHub Actions
- name: Run E2E tests
run: |
npm ci
npx playwright install --with-deps
npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
Best Practices
- Test Isolation - Each test should be independent
- Use Helpers - Centralize login logic and common operations
- Avoid Sleep - Playwright auto-waits, use
expect()assertions - Serial for State - Use serial mode when tests share state (tokens)
- Browser Targeting - Skip non-chromium for API-only tests
- Fresh Logins - Each test logs in fresh (rate limit is 100 in local/testing)
Related Documentation
- Testing Manual - Manual testing procedures
- E2E Test Plan - Test planning document
- API Documentation - API endpoint reference
- Security Guide - Security testing considerations