E2E TESTING
Ecosistema completo para organización y gestión de contenido con interfaces espaciales
E2E Testing Guide - User Management Module
Guide for implementing End-to-End tests with Playwright for the user-management module.
Table of Contents
Overview
E2E tests validate complete user workflows in a real browser environment:
- ✅ User CRUD operations
- ✅ Search and filter functionality
- ✅ Bulk operations
- ✅ Form validation
- ✅ Navigation flows
- ✅ Error handling
Testing Stack:
- Playwright: Browser automation
- TypeScript: Type-safe test code
- Page Object Model: Maintainable test structure
Setup
Install Dependencies
npm install -D @playwright/test
npx playwright install
Playwright Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: {
command: 'npx nx serve gograb-space',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
},
});
Test Scenarios
Critical User Journeys
1. User List and Search
Given: User is on /users page
When: User searches for "john"
Then: Only users matching "john" are displayed
2. Create New User
Given: User clicks "Create User" button
When: User fills form with valid data
And: User clicks "Save"
Then: New user appears in list
And: Success message is shown
3. Edit Existing User
Given: User clicks edit icon for a user
When: User updates first name to "Jane"
And: User clicks "Save"
Then: User name is updated in list
And: Success message is shown
4. Delete User
Given: User clicks delete icon for a user
When: User confirms deletion
Then: User is removed from list
And: Success message is shown
5. Bulk Operations
Given: User selects multiple users
When: User clicks "Activate Selected"
Then: All selected users status changes to "active"
And: Success message shows count
6. Filter and Pagination
Given: User is on /users page
When: User filters by status "active"
And: User navigates to page 2
Then: Only active users from page 2 are shown
Page Object Model
User List Page
// e2e/pages/user-list.page.ts
import { Page, Locator } from '@playwright/test';
export class UserListPage {
readonly page: Page;
readonly searchInput: Locator;
readonly createButton: Locator;
readonly userCards: Locator;
readonly statusFilter: Locator;
readonly roleFilter: Locator;
readonly paginationNext: Locator;
readonly paginationPrev: Locator;
readonly bulkSelectAll: Locator;
readonly bulkActionButton: Locator;
constructor(page: Page) {
this.page = page;
this.searchInput = page.getByPlaceholder('Search users...');
this.createButton = page.getByRole('button', { name: 'Create User' });
this.userCards = page.locator('lib-user-card');
this.statusFilter = page.getByLabel('Status');
this.roleFilter = page.getByLabel('Role');
this.paginationNext = page.getByRole('button', { name: 'Next' });
this.paginationPrev = page.getByRole('button', { name: 'Previous' });
this.bulkSelectAll = page.getByLabel('Select all users');
this.bulkActionButton = page.getByRole('button', { name: 'Bulk Actions' });
}
async goto() {
await this.page.goto('/users');
}
async search(query: string) {
await this.searchInput.fill(query);
await this.searchInput.press('Enter');
}
async filterByStatus(status: string) {
await this.statusFilter.selectOption(status);
}
async filterByRole(role: string) {
await this.roleFilter.selectOption(role);
}
async getUserCardByEmail(email: string) {
return this.page.locator(`lib-user-card:has-text("${email}")`);
}
async clickEditForUser(email: string) {
const card = await this.getUserCardByEmail(email);
await card.getByRole('button', { name: 'Edit' }).click();
}
async clickDeleteForUser(email: string) {
const card = await this.getUserCardByEmail(email);
await card.getByRole('button', { name: 'Delete' }).click();
}
async selectUsersForBulk(emails: string[]) {
for (const email of emails) {
const card = await this.getUserCardByEmail(email);
await card.getByRole('checkbox').check();
}
}
async performBulkAction(action: string) {
await this.bulkActionButton.click();
await this.page.getByRole('menuitem', { name: action }).click();
}
async goToNextPage() {
await this.paginationNext.click();
}
async goToPreviousPage() {
await this.paginationPrev.click();
}
async getUserCount() {
return await this.userCards.count();
}
}
User Form Page
// e2e/pages/user-form.page.ts
import { Page, Locator } from '@playwright/test';
export class UserFormPage {
readonly page: Page;
readonly emailInput: Locator;
readonly usernameInput: Locator;
readonly firstNameInput: Locator;
readonly lastNameInput: Locator;
readonly roleSelect: Locator;
readonly statusSelect: Locator;
readonly saveButton: Locator;
readonly cancelButton: Locator;
readonly errorMessages: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.usernameInput = page.getByLabel('Username');
this.firstNameInput = page.getByLabel('First Name');
this.lastNameInput = page.getByLabel('Last Name');
this.roleSelect = page.getByLabel('Primary Role');
this.statusSelect = page.getByLabel('Status');
this.saveButton = page.getByRole('button', { name: 'Save' });
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
this.errorMessages = page.locator('.error-message');
}
async goto() {
await this.page.goto('/users/create');
}
async gotoEdit(userId: string) {
await this.page.goto(`/users/${userId}/edit`);
}
async fillForm(data: {
email?: string;
username?: string;
firstName?: string;
lastName?: string;
role?: string;
status?: string;
}) {
if (data.email) await this.emailInput.fill(data.email);
if (data.username) await this.usernameInput.fill(data.username);
if (data.firstName) await this.firstNameInput.fill(data.firstName);
if (data.lastName) await this.lastNameInput.fill(data.lastName);
if (data.role) await this.roleSelect.selectOption(data.role);
if (data.status) await this.statusSelect.selectOption(data.status);
}
async save() {
await this.saveButton.click();
}
async cancel() {
await this.cancelButton.click();
}
async getValidationErrors() {
return await this.errorMessages.allTextContents();
}
}
User Detail Page
// e2e/pages/user-detail.page.ts
import { Page, Locator } from '@playwright/test';
export class UserDetailPage {
readonly page: Page;
readonly userName: Locator;
readonly userEmail: Locator;
readonly userStatus: Locator;
readonly userRoles: Locator;
readonly editButton: Locator;
readonly deleteButton: Locator;
constructor(page: Page) {
this.page = page;
this.userName = page.locator('.user-name');
this.userEmail = page.locator('.user-email');
this.userStatus = page.locator('lib-user-status-badge');
this.userRoles = page.locator('.role-badge');
this.editButton = page.getByRole('button', { name: 'Edit' });
this.deleteButton = page.getByRole('button', { name: 'Delete' });
}
async goto(userId: string) {
await this.page.goto(`/users/${userId}`);
}
async edit() {
await this.editButton.click();
}
async delete() {
await this.deleteButton.click();
}
async getRoleNames() {
return await this.userRoles.allTextContents();
}
}
Example Tests
User List Tests
// e2e/user-list.spec.ts
import { test, expect } from '@playwright/test';
import { UserListPage } from './pages/user-list.page';
test.describe('User List', () => {
let userListPage: UserListPage;
test.beforeEach(async ({ page }) => {
userListPage = new UserListPage(page);
await userListPage.goto();
});
test('should display users', async () => {
const count = await userListPage.getUserCount();
expect(count).toBeGreaterThan(0);
});
test('should search users', async ({ page }) => {
await userListPage.search('john');
// Wait for search results
await page.waitForTimeout(500);
const count = await userListPage.getUserCount();
expect(count).toBeGreaterThan(0);
// Verify all results contain 'john'
const cards = userListPage.userCards;
const texts = await cards.allTextContents();
texts.forEach(text => {
expect(text.toLowerCase()).toContain('john');
});
});
test('should filter by status', async ({ page }) => {
await userListPage.filterByStatus('active');
await page.waitForTimeout(500);
const statusBadges = page.locator('lib-user-status-badge');
const count = await statusBadges.count();
for (let i = 0; i < count; i++) {
const badge = statusBadges.nth(i);
await expect(badge).toHaveAttribute('data-status', 'active');
}
});
test('should paginate', async ({ page }) => {
const initialCount = await userListPage.getUserCount();
await userListPage.goToNextPage();
await page.waitForTimeout(500);
const secondPageCount = await userListPage.getUserCount();
expect(secondPageCount).toBeGreaterThan(0);
// Users should be different
// (more complex verification could compare actual user IDs)
});
});
User CRUD Tests
// e2e/user-crud.spec.ts
import { test, expect } from '@playwright/test';
import { UserListPage } from './pages/user-list.page';
import { UserFormPage } from './pages/user-form.page';
test.describe('User CRUD Operations', () => {
test('should create new user', async ({ page }) => {
const userListPage = new UserListPage(page);
const userFormPage = new UserFormPage(page);
await userListPage.goto();
await userListPage.createButton.click();
// Fill form
const timestamp = Date.now();
await userFormPage.fillForm({
email: `test${timestamp}@example.com`,
username: `testuser${timestamp}`,
firstName: 'Test',
lastName: 'User',
role: 'user',
status: 'active'
});
await userFormPage.save();
// Verify redirect to list
await expect(page).toHaveURL('/users');
// Verify user appears in list
const userCard = await userListPage.getUserCardByEmail(`test${timestamp}@example.com`);
await expect(userCard).toBeVisible();
});
test('should edit user', async ({ page }) => {
const userListPage = new UserListPage(page);
const userFormPage = new UserFormPage(page);
await userListPage.goto();
// Edit first user
await userListPage.clickEditForUser('[email protected]');
// Update first name
await userFormPage.firstNameInput.clear();
await userFormPage.firstNameInput.fill('Jane');
await userFormPage.save();
// Verify redirect
await expect(page).toHaveURL('/users');
// Verify update
const userCard = await userListPage.getUserCardByEmail('[email protected]');
await expect(userCard).toContainText('Jane');
});
test('should delete user', async ({ page }) => {
const userListPage = new UserListPage(page);
await userListPage.goto();
const initialCount = await userListPage.getUserCount();
// Delete user
await userListPage.clickDeleteForUser('[email protected]');
// Confirm deletion
await page.getByRole('button', { name: 'Confirm' }).click();
// Wait for deletion
await page.waitForTimeout(500);
// Verify user removed
const newCount = await userListPage.getUserCount();
expect(newCount).toBe(initialCount - 1);
// Verify specific user not found
const userCard = userListPage.page.locator('lib-user-card:has-text("[email protected]")');
await expect(userCard).not.toBeVisible();
});
});
Form Validation Tests
// e2e/user-form-validation.spec.ts
import { test, expect } from '@playwright/test';
import { UserFormPage } from './pages/user-form.page';
test.describe('User Form Validation', () => {
let userFormPage: UserFormPage;
test.beforeEach(async ({ page }) => {
userFormPage = new UserFormPage(page);
await userFormPage.goto();
});
test('should show error for invalid email', async () => {
await userFormPage.emailInput.fill('invalid-email');
await userFormPage.emailInput.blur();
const errors = await userFormPage.getValidationErrors();
expect(errors).toContain('Invalid email format');
});
test('should show error for empty required fields', async () => {
await userFormPage.save();
const errors = await userFormPage.getValidationErrors();
expect(errors.length).toBeGreaterThan(0);
});
test('should show error for duplicate email', async () => {
await userFormPage.fillForm({
email: '[email protected]',
username: 'newuser',
firstName: 'New',
lastName: 'User',
role: 'user'
});
await userFormPage.save();
const errors = await userFormPage.getValidationErrors();
expect(errors).toContain('Email already exists');
});
});
Bulk Operations Tests
// e2e/bulk-operations.spec.ts
import { test, expect } from '@playwright/test';
import { UserListPage } from './pages/user-list.page';
test.describe('Bulk Operations', () => {
let userListPage: UserListPage;
test.beforeEach(async ({ page }) => {
userListPage = new UserListPage(page);
await userListPage.goto();
});
test('should activate multiple users', async ({ page }) => {
// Select users
await userListPage.selectUsersForBulk([
'[email protected]',
'[email protected]',
'[email protected]'
]);
// Perform bulk activate
await userListPage.performBulkAction('Activate Selected');
// Wait for operation
await page.waitForTimeout(500);
// Verify success message
const successMessage = page.locator('.success-message');
await expect(successMessage).toContainText('3 users activated');
// Verify status changed
for (const email of ['[email protected]', '[email protected]', '[email protected]']) {
const card = await userListPage.getUserCardByEmail(email);
const statusBadge = card.locator('lib-user-status-badge');
await expect(statusBadge).toHaveAttribute('data-status', 'active');
}
});
test('should deactivate multiple users', async ({ page }) => {
await userListPage.selectUsersForBulk(['[email protected]', '[email protected]']);
await userListPage.performBulkAction('Deactivate Selected');
await page.waitForTimeout(500);
const successMessage = page.locator('.success-message');
await expect(successMessage).toContainText('2 users deactivated');
});
test('should delete multiple users with confirmation', async ({ page }) => {
await userListPage.selectUsersForBulk(['[email protected]', '[email protected]']);
await userListPage.performBulkAction('Delete Selected');
// Confirm deletion
await page.getByRole('button', { name: 'Confirm' }).click();
await page.waitForTimeout(500);
// Verify users removed
const card1 = userListPage.page.locator('lib-user-card:has-text("[email protected]")');
const card2 = userListPage.page.locator('lib-user-card:has-text("[email protected]")');
await expect(card1).not.toBeVisible();
await expect(card2).not.toBeVisible();
});
});
Best Practices
1. Use Page Object Model
Keep tests clean by separating page logic:
// ✅ Good
const userListPage = new UserListPage(page);
await userListPage.goto();
await userListPage.search('john');
// ❌ Bad
await page.goto('/users');
await page.getByPlaceholder('Search...').fill('john');
2. Wait for Elements
Always wait for elements to be ready:
// ✅ Good
await expect(element).toBeVisible();
await element.click();
// ❌ Bad
await element.click(); // May fail if element not ready
3. Use Descriptive Test Names
// ✅ Good
test('should display validation error when email format is invalid', async () => {});
// ❌ Bad
test('email test', async () => {});
4. Clean Up Test Data
test.afterEach(async ({ page }) => {
// Clean up test users created during test
await cleanupTestUsers();
});
5. Use Fixtures for Common Setup
// e2e/fixtures.ts
import { test as base } from '@playwright/test';
import { UserListPage } from './pages/user-list.page';
export const test = base.extend<{
userListPage: UserListPage;
}>({
userListPage: async ({ page }, use) => {
const userListPage = new UserListPage(page);
await userListPage.goto();
await use(userListPage);
},
});
6. Run Tests in Parallel
// playwright.config.ts
export default defineConfig({
fullyParallel: true,
workers: 4, // Run 4 tests concurrently
});
7. Take Screenshots on Failure
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
await page.screenshot({
path: `screenshots/${testInfo.title}-${Date.now()}.png`
});
}
});
CI/CD Integration
GitHub Actions
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build application
run: npx nx build gograb-space
- name: Run E2E tests
run: npx playwright test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
Run Tests Locally
# Run all E2E tests
npx playwright test
# Run specific test file
npx playwright test e2e/user-crud.spec.ts
# Run in headed mode (see browser)
npx playwright test --headed
# Run in debug mode
npx playwright test --debug
# Run in specific browser
npx playwright test --project=chromium
# Generate test report
npx playwright show-report
Performance Testing
Measure Page Load Time
test('user list should load within 2 seconds', async ({ page }) => {
const startTime = Date.now();
await page.goto('/users');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(2000);
});
Monitor API Response Times
test('API calls should complete within 1 second', async ({ page }) => {
let apiCallTime = 0;
page.on('response', response => {
if (response.url().includes('/api/users')) {
apiCallTime = response.timing().responseEnd;
}
});
await page.goto('/users');
await page.waitForLoadState('networkidle');
expect(apiCallTime).toBeLessThan(1000);
});
Accessibility Testing
import { injectAxe, checkA11y } from 'axe-playwright';
test('user list should be accessible', async ({ page }) => {
await page.goto('/users');
await injectAxe(page);
await checkA11y(page);
});
Troubleshooting
Common Issues
Issue: Tests timing out
// Solution: Increase timeout
test('slow test', async ({ page }) => {
test.setTimeout(60000); // 60 seconds
// ...
});
Issue: Flaky tests
// Solution: Add proper waits
await expect(element).toBeVisible();
await page.waitForLoadState('networkidle');
Issue: Element not found
// Solution: Use more specific selectors
const button = page.getByRole('button', { name: 'Save', exact: true });
Resources
- Playwright Documentation: https://playwright.dev
- Best Practices: https://playwright.dev/docs/best-practices
- Debugging: https://playwright.dev/docs/debug
- CI/CD: https://playwright.dev/docs/ci