# End-to-End Testing Made Simple: How Playwright Transforms Testing for Angular Applications

## Comprehensive E2E Testing for Modern Full-Stack Applications

Testing complex applications is hard. Testing applications with OAuth authentication, role-based access control, and API integrations is even harder. That's where **Playwright** comes in — a modern, powerful testing framework that makes comprehensive E2E testing not just possible, but actually enjoyable.

![Playwright Testing Architecture](https://via.placeholder.com/800x400?text=Playwright+Testing+Architecture)

---

## 🎯 What is Playwright?

**Playwright** is Microsoft's open-source testing framework designed specifically for modern web applications.

Unlike older tools like Selenium, Playwright was built from the ground up for single-page applications (SPAs) with features like:

* **Cross-browser testing** — Test on Chromium, Firefox, and WebKit with a single codebase
* **Auto-wait functionality** — No more flaky tests due to timing issues
* **Network interception** — Mock APIs, test offline scenarios, validate requests
* **Powerful selectors** — Find elements by text, role, accessibility attributes
* **Built-in test runner** — No need for additional tools like Jest or Mocha
* **Parallel execution** — Run tests concurrently for faster feedback
* **Visual debugging** — UI mode and trace viewer for troubleshooting

**Why Playwright stands out:**

* ✅ **Fast** — Executes tests in parallel across multiple browsers
* ✅ **Reliable** — Auto-waits for elements to be ready
* ✅ **Developer-friendly** — TypeScript support with excellent IntelliSense
* ✅ **Modern** — Built for SPAs, React, Angular, Vue, and Svelte
* ✅ **Comprehensive** — E2E, API testing, and visual regression in one tool

---

## 📦 The Testing Challenge

Our tutorial project follows the **CAT Pattern** — Client (Angular), API Resource (.NET), and Token Service (IdentityServer). This architecture presents unique testing challenges:

**Authentication complexity:**
* OAuth 2.0 / OpenID Connect flows
* Token acquisition and validation
* Session management across redirects

**Multi-tier architecture:**
* Angular SPA on port 4200
* .NET API on port 44378
* IdentityServer on custom domain

**Role-based access control:**
* Employee — Read-only access
* Manager — Create/edit permissions
* HRAdmin — Full administrative access

**Cross-component workflows:**
* User logs in via IdentityServer
* Receives access token
* Angular client makes API calls with token
* API validates token against IdentityServer

Traditional testing approaches struggle with this complexity. Playwright excels at it.

---

## 🏗️ Playwright Project Structure

Our Playwright testing suite lives as a **separate Git submodule** — allowing independent development, versioning, and CI/CD pipelines.

**Repository:** `Tests/AngularNetTutorial-Playwright/`

### Core Directories

**tests/** — All test specifications organized by category

* **auth/** — Authentication and authorization flows
* **employee-management/** — Employee CRUD operations
* **department-management/** — Department operations
* **api/** — Direct API endpoint testing
* **workflows/** — End-to-end user scenarios
* **accessibility/** — ARIA compliance and keyboard navigation
* **performance/** — Load time and rendering benchmarks
* **visual/** — Visual regression testing

**fixtures/** — Reusable test helpers and utilities

* **auth.fixtures.ts** — Login/logout helpers, token management
* **data.fixtures.ts** — Test data factories and generators
* **api.fixtures.ts** — API request helpers

**page-objects/** — Page Object Models for UI abstraction

* **auth/login.page.ts** — Login page interactions
* **employee-list.page.ts** — Employee list operations
* **employee-form.page.ts** — Employee form actions

**config/** — Test configuration and credentials

* **test-users.json** — Test account credentials
* **environments.json** — Environment-specific URLs

---

## 🧪 Test Categories Explained

### 1. Authentication Tests (auth/)

Tests the complete OIDC authentication flow with IdentityServer.

**What we test:**

* Redirect to IdentityServer login page
* Successful login with valid credentials
* Token storage in browser
* Authentication state persistence
* Invalid credentials handling
* Different user roles (Employee, Manager, HRAdmin)

**Example: Login flow test**

```typescript
test('should successfully login with valid credentials', async ({ page }) => {
  // Navigate to Angular app
  await page.goto('/');

  // Click login button
  const userIcon = page.locator('button[aria-label="User menu"]');
  await userIcon.click();

  const loginOption = page.locator('button:has-text("Login")');
  await loginOption.click();

  // Wait for redirect to IdentityServer
  await page.waitForURL(/sts\.skoruba\.local.*/);

  // Fill credentials
  await page.fill('input[name="Username"]', 'ashtyn1');
  await page.fill('input[name="Password"]', 'Pa$$word123');
  await page.click('button:has-text("Login")');

  // Verify redirect back to Angular
  await expect(page).toHaveURL(/localhost:4200/);

  // Verify dashboard is visible
  await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible();
});
```

**Key technique:** We use helper functions to streamline login across tests.

```typescript
// Reusable login helper from auth.fixtures.ts
await loginAsRole(page, 'manager');  // Logs in as Manager
await loginAsRole(page, 'hradmin');  // Logs in as HRAdmin
```

---

### 2. API Tests (api/)

Direct testing of API endpoints with JWT token validation.

**What we test:**

* Token acquisition via browser authentication
* Token validation on API requests
* Token structure and claims
* Invalid/expired token rejection
* Role-specific token scopes
* API authentication headers

**Example: API authentication test**

```typescript
test('should validate token on API request', async ({ page, request }) => {
  // Login via browser to get token
  await loginAsRole(page, 'manager');
  const token = await getTokenFromProfile(page);

  expect(token).toBeTruthy();

  // Use token to make API request
  const response = await request.get('https://localhost:44378/api/v1/employees', {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Accept': 'application/json',
    },
    ignoreHTTPSErrors: true,
  });

  expect(response.status()).toBe(200);
});
```

**Token validation:** We decode JWT tokens to verify claims.

```typescript
test('should include proper claims in token', async ({ page }) => {
  await loginAsRole(page, 'manager');
  const token = await getTokenFromProfile(page);

  // Decode JWT payload (second part of token)
  const parts = token!.split('.');
  const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());

  // Verify essential claims
  expect(payload.sub).toBeDefined();  // Subject/User ID
  expect(payload.exp).toBeDefined();  // Expiration time
  expect(payload.iat).toBeDefined();  // Issued at

  // Verify token is not expired
  const now = Math.floor(Date.now() / 1000);
  expect(payload.exp).toBeGreaterThan(now);
});
```

---

### 3. Employee Management Tests (employee-management/)

CRUD operations for employee records with role-based permissions.

**What we test:**

* Creating new employees (Manager, HRAdmin only)
* Viewing employee lists (all roles)
* Editing employee details (Manager, HRAdmin only)
* Deleting employees (HRAdmin only)
* Form validation
* Permission enforcement

**Example: Role-based access control**

```typescript
test('HRAdmin can delete employees', async ({ page }) => {
  await loginAsRole(page, 'hradmin');
  await page.goto('/employees');

  // Find and click delete button
  const deleteButton = page.locator('button:has-text("Delete")').first();
  await deleteButton.click();

  // Confirm deletion
  const confirmButton = page.locator('button:has-text("Confirm")');
  await confirmButton.click();

  // Verify success message
  await expect(page.locator('text=/deleted successfully/i')).toBeVisible();
});

test('Employee cannot delete employees', async ({ page }) => {
  await loginAsRole(page, 'employee');
  await page.goto('/employees');

  // Delete button should NOT be visible for Employee role
  const deleteButton = page.locator('button:has-text("Delete")');
  await expect(deleteButton).not.toBeVisible();
});
```

---

### 4. Workflow Tests (workflows/)

End-to-end scenarios that span multiple components and user actions.

**What we test:**

* Complete hiring workflow (create employee, assign department, set salary)
* Department reorganization (move employees between departments)
* Salary adjustment workflows (raise, promotion)
* Cross-component integration

**Example: Complete hiring workflow**

```typescript
test('complete hiring workflow', async ({ page }) => {
  // Login as HRAdmin (full permissions)
  await loginAsRole(page, 'hradmin');

  // Step 1: Create new employee
  await page.goto('/employees/new');
  await page.fill('input[name="firstName"]', 'Jane');
  await page.fill('input[name="lastName"]', 'Smith');
  await page.fill('input[name="email"]', 'jane.smith@company.com');

  // Step 2: Assign department
  await page.selectOption('select[name="departmentId"]', 'Engineering');

  // Step 3: Set position and salary
  await page.selectOption('select[name="positionId"]', 'Software Engineer');
  await page.fill('input[name="salary"]', '85000');

  // Step 4: Submit form
  await page.click('button:has-text("Create Employee")');

  // Step 5: Verify success
  await expect(page.locator('text=/employee created successfully/i')).toBeVisible();

  // Step 6: Verify employee appears in list
  await page.goto('/employees');
  await expect(page.locator('text=Jane Smith')).toBeVisible();
});
```

---

### 5. Accessibility Tests (accessibility/)

ARIA compliance and keyboard navigation testing.

**What we test:**

* Proper ARIA labels on interactive elements
* Keyboard navigation (Tab, Enter, Escape)
* Focus management
* Screen reader compatibility
* Form accessibility

**Example: Keyboard navigation test**

```typescript
test('should navigate employee list with keyboard', async ({ page }) => {
  await loginAsRole(page, 'manager');
  await page.goto('/employees');

  // Focus on first employee row
  await page.keyboard.press('Tab');

  // Navigate with arrow keys
  await page.keyboard.press('ArrowDown');
  await page.keyboard.press('ArrowDown');

  // Open details with Enter
  await page.keyboard.press('Enter');

  // Verify employee details modal opened
  await expect(page.locator('[role="dialog"]')).toBeVisible();

  // Close with Escape
  await page.keyboard.press('Escape');

  // Verify modal closed
  await expect(page.locator('[role="dialog"]')).not.toBeVisible();
});
```

---

### 6. Visual Regression Tests (visual/)

Screenshot comparison to catch unintended UI changes.

**What we test:**

* Dashboard layout consistency
* Form rendering across browsers
* Responsive design breakpoints
* Theme consistency

**Example: Visual regression test**

```typescript
test('dashboard should match visual baseline', async ({ page }) => {
  await loginAsRole(page, 'manager');
  await page.goto('/dashboard');

  // Wait for all content to load
  await page.waitForLoadState('networkidle');

  // Take screenshot and compare to baseline
  await expect(page).toHaveScreenshot('dashboard.png', {
    fullPage: true,
    maxDiffPixels: 100,  // Allow minor rendering differences
  });
});
```

---

## 🚀 Running the Tests

### Prerequisites: Start All Services

**IMPORTANT:** Before running Playwright tests, you must have all three services running. Start them in this order:

**Terminal 1: Start IdentityServer (must start first)**

```bash
cd TokenService/Duende-IdentityServer/src/Duende.STS.Identity
dotnet run
```

**Wait for:** `Now listening on: https://localhost:44310`

**Terminal 2: Start the API (requires IdentityServer running)**

```bash
cd ApiResources/TalentManagement-API/TalentManagementAPI.WebApi
dotnet run
```

**Wait for:** `Now listening on: https://localhost:44378`

**Terminal 3: Start Angular Client**

```bash
cd Clients/TalentManagement-Angular-Material/talent-management
npm start
```

**Wait for:** `Angular Live Development Server is listening on localhost:4200`

**Verify all services are running:**

* **IdentityServer:** https://localhost:44310
* **API:** https://localhost:44378
* **Angular:** http://localhost:4200

---

### Quick Start

**Install Playwright dependencies:**

```bash
cd Tests/AngularNetTutorial-Playwright
npm install
npx playwright install
```

**Run all tests:**

```bash
npx playwright test
```

**Run tests with UI (interactive debugging):**

```bash
npx playwright test --ui
```

**Run specific test category:**

```bash
npx playwright test tests/auth/           # Authentication tests only
npx playwright test tests/employee-management/  # Employee CRUD tests
```

**Run in headed mode (see browser):**

```bash
npx playwright test --headed
```

**Run on specific browser:**

```bash
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit
```

**View test report:**

```bash
npx playwright show-report
```

---

## 🎓 Key Testing Patterns

### 1. Page Object Model (POM)

Abstracts UI interactions into reusable classes.

**Benefits:**

* ✅ Reduces code duplication
* ✅ Makes tests more maintainable
* ✅ Centralizes selector management
* ✅ Improves test readability

**Example: Employee List Page Object**

```typescript
// page-objects/employee-list.page.ts
export class EmployeeListPage {
  constructor(private page: Page) {}

  async navigateTo() {
    await this.page.goto('/employees');
  }

  async searchEmployee(name: string) {
    await this.page.fill('input[placeholder="Search"]', name);
    await this.page.keyboard.press('Enter');
  }

  async clickCreateButton() {
    await this.page.click('button:has-text("Create Employee")');
  }

  async getEmployeeCount() {
    return await this.page.locator('table tbody tr').count();
  }
}

// Usage in test
const employeeList = new EmployeeListPage(page);
await employeeList.navigateTo();
await employeeList.searchEmployee('John');
expect(await employeeList.getEmployeeCount()).toBeGreaterThan(0);
```

---

### 2. Test Fixtures

Reusable setup code that runs before tests.

**Example: Authentication fixture**

```typescript
// fixtures/auth.fixtures.ts
export async function loginAsRole(page: Page, role: 'employee' | 'manager' | 'hradmin') {
  const credentials = {
    employee: { username: 'employee1', password: 'Pa$$word123' },
    manager: { username: 'ashtyn1', password: 'Pa$$word123' },
    hradmin: { username: 'admin1', password: 'Pa$$word123' },
  };

  const { username, password } = credentials[role];

  await page.goto('/');
  await page.locator('button[aria-label="User menu"]').click();
  await page.locator('button:has-text("Login")').click();
  await page.waitForURL(/sts\.skoruba\.local.*/);
  await page.fill('input[name="Username"]', username);
  await page.fill('input[name="Password"]', password);
  await page.click('button:has-text("Login")');
  await page.waitForURL(/localhost:4200/);
}
```

---

### 3. API Request Interception

Validate API calls without relying on backend state.

**Example: Verify API request headers**

```typescript
test('should send Bearer token in API requests', async ({ page }) => {
  await loginAsRole(page, 'manager');

  // Intercept API requests
  await page.route('**/api/v1/employees', (route) => {
    const headers = route.request().headers();

    // Verify Authorization header
    expect(headers['authorization']).toMatch(/^Bearer .+/);

    // Continue with request
    route.continue();
  });

  // Trigger API call
  await page.goto('/employees');
});
```

---

## 💡 Best Practices

### Write Maintainable Tests

**DO:**

* ✅ Use descriptive test names: `should login successfully with valid credentials`
* ✅ One assertion per concept: Test one thing at a time
* ✅ Use Page Object Model: Abstract UI interactions
* ✅ Leverage fixtures: Reuse setup code
* ✅ Auto-wait for elements: Let Playwright handle timing

**DON'T:**

* ❌ Use hard-coded waits: `await page.waitForTimeout(5000)`
* ❌ Test implementation details: Focus on user behavior
* ❌ Couple tests together: Each test should be independent
* ❌ Use brittle selectors: Prefer semantic selectors (text, role, label)

---

### Selector Best Practices

**Priority order (best to worst):**

1. **Text content** — `page.locator('button:has-text("Login")')`
2. **ARIA roles** — `page.locator('[role="button"]')`
3. **Labels** — `page.locator('button[aria-label="Submit"]')`
4. **Test IDs** — `page.locator('[data-testid="login-btn"]')`
5. **CSS classes** — Avoid if possible (implementation detail)

---

### Debugging Failed Tests

**Generate trace for debugging:**

```bash
npx playwright test --trace on
```

**View trace file:**

```bash
npx playwright show-trace trace.zip
```

**Run test in debug mode:**

```bash
npx playwright test --debug
```

**Use Playwright Inspector:**

The trace viewer shows:

* ✅ Step-by-step execution
* ✅ Network requests
* ✅ Console logs
* ✅ Screenshots at each step
* ✅ DOM snapshots

---

## 📊 Project Progress

**Current Testing Coverage:**

* **39 test files** across 16 categories
* **Authentication** — 8 test scenarios covering OIDC flows
* **API Testing** — 15 tests for token validation and endpoints
* **CRUD Operations** — Employee, Department, Position, Salary Range tests
* **Accessibility** — ARIA compliance and keyboard navigation
* **Visual Regression** — Dashboard and form rendering
* **Performance** — Load time benchmarks

**Test Execution:**

* **Cross-browser** — Chromium, Firefox, WebKit
* **Parallel execution** — Faster feedback
* **Retry logic** — Automatic retries on failures
* **Screenshots** — Captured on test failure
* **HTML Reports** — Detailed test results

---

## 🎯 Benefits of This Testing Approach

**For Developers:**

* ✅ **Confidence** — Catch bugs before production
* ✅ **Fast feedback** — Know immediately when something breaks
* ✅ **Documentation** — Tests serve as living documentation
* ✅ **Regression prevention** — Ensure fixes stay fixed

**For Teams:**

* ✅ **Quality assurance** — Automated testing reduces manual QA burden
* ✅ **Collaboration** — Tests define expected behavior
* ✅ **Onboarding** — New developers understand workflows through tests
* ✅ **Deployment confidence** — Deploy with confidence

**For Users:**

* ✅ **Reliability** — Fewer bugs reach production
* ✅ **Consistent experience** — UI behavior is validated
* ✅ **Accessibility** — ARIA compliance ensures usability
* ✅ **Performance** — Load times are monitored

---

## 🔗 Learn More

**Playwright Resources:**

* **Official Documentation** — [playwright.dev](https://playwright.dev)
* **Best Practices Guide** — [playwright.dev/best-practices](https://playwright.dev/docs/best-practices)
* **API Reference** — [playwright.dev/api](https://playwright.dev/docs/api/class-playwright)

**Our Tutorial Series:**

* **Main Repository** — [AngularNetTutorial](https://github.com/workcontrolgit/AngularNetTutorial)
* **Playwright Tests** — [AngularNetTutorial-Playwright](https://github.com/workcontrolgit/AngularNetTutorial-Playwright)
* **Complete Tutorial** — See TUTORIAL.md in docs folder

---

## 🎉 Conclusion

Playwright transforms E2E testing from a painful chore into a powerful development tool. By organizing tests into clear categories, using Page Object Models, and leveraging fixtures, we've built a comprehensive testing suite that:

* ✅ Tests complex OAuth authentication flows
* ✅ Validates API integration and token handling
* ✅ Enforces role-based access control
* ✅ Ensures accessibility compliance
* ✅ Catches visual regressions
* ✅ Runs fast and reliably

**The result?** Confidence in every deployment. Quality in every release. A better experience for every user.

**Ready to get started?** Clone the repository and run your first Playwright test today.

```bash
git clone --recurse-submodules https://github.com/workcontrolgit/AngularNetTutorial.git
cd Tests/AngularNetTutorial-Playwright
npm install
npx playwright test --ui
```

Happy testing!

