# Browser Testing Reference

Comprehensive guide to E2E browser testing with Playwright.

## Playwright

Cross-browser E2E testing automation framework.

**Container**: `mcr.microsoft.com/playwright:latest`
**Port**: 3000

### Basic Test Structure

```javascript
// playwright-tests/example.spec.js
const { test, expect } = require('@playwright/test');

test.describe('User Authentication', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('http://target:8080');
  });

  test('should login successfully', async ({ page }) => {
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');

    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator('.welcome-message')).toContainText('Welcome');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.fill('[data-testid="email"]', 'invalid@example.com');
    await page.fill('[data-testid="password"]', 'wrongpassword');
    await page.click('[data-testid="login-button"]');

    await expect(page.locator('.error-message')).toBeVisible();
    await expect(page.locator('.error-message')).toContainText('Invalid credentials');
  });
});
```

### Configuration

```javascript
// playwright.config.js
const { defineConfig, devices } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['list'],
    ['html', { outputFolder: 'playwright-report' }],
    ['allure-playwright'],
    ['junit', { outputFile: 'results.xml' }]
  ],
  use: {
    baseURL: 'http://target:8080',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  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'] },
    },
  ],
});
```

### Page Object Model

```javascript
// page-objects/LoginPage.js
class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.locator('[data-testid="email"]');
    this.passwordInput = page.locator('[data-testid="password"]');
    this.loginButton = page.locator('[data-testid="login-button"]');
    this.errorMessage = page.locator('.error-message');
  }

  async navigate() {
    await this.page.goto('/login');
  }

  async login(email, password) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async getError() {
    return await this.errorMessage.textContent();
  }
}

// page-objects/DashboardPage.js
class DashboardPage {
  constructor(page) {
    this.page = page;
    this.welcomeMessage = page.locator('.welcome-message');
    this.logoutButton = page.locator('[data-testid="logout"]');
    this.userMenu = page.locator('.user-menu');
  }

  async isLoggedIn() {
    return await this.welcomeMessage.isVisible();
  }

  async logout() {
    await this.userMenu.click();
    await this.logoutButton.click();
  }
}

module.exports = { LoginPage, DashboardPage };
```

```javascript
// tests/login.spec.js (using Page Objects)
const { test, expect } = require('@playwright/test');
const { LoginPage, DashboardPage } = require('../page-objects');

test.describe('Login Flow', () => {
  let loginPage;
  let dashboardPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    dashboardPage = new DashboardPage(page);
  });

  test('successful login', async ({ page }) => {
    await loginPage.navigate();
    await loginPage.login('test@example.com', 'password123');
    await expect(dashboardPage.welcomeMessage).toBeVisible();
  });
});
```

### Common Actions

```javascript
// Navigation
await page.goto('https://example.com');
await page.goBack();
await page.goForward();
await page.reload();

// Interactions
await page.click('button');
await page.dblclick('button');
await page.fill('input', 'text');
await page.type('input', 'text', { delay: 100 });
await page.press('input', 'Enter');
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');
await page.selectOption('select', 'value');
await page.hover('.element');

// Waiting
await page.waitForSelector('.element');
await page.waitForURL(/.*dashboard/);
await page.waitForLoadState('networkidle');
await page.waitForResponse(response => response.url().includes('/api'));
await page.waitForTimeout(1000); // Use sparingly

// Screenshots
await page.screenshot({ path: 'screenshot.png' });
await page.screenshot({ path: 'fullpage.png', fullPage: true });
await element.screenshot({ path: 'element.png' });

// PDF
await page.pdf({ path: 'page.pdf', format: 'A4' });
```

### Assertions

```javascript
// Page assertions
await expect(page).toHaveTitle('Page Title');
await expect(page).toHaveURL(/.*dashboard/);

// Element assertions
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toContainText('text');
await expect(locator).toHaveText('exact text');
await expect(locator).toHaveValue('value');
await expect(locator).toHaveAttribute('href', '/link');
await expect(locator).toHaveClass(/active/);
await expect(locator).toHaveCount(5);

// Soft assertions (continue on failure)
await expect.soft(locator).toBeVisible();
```

### Locator Strategies

```javascript
// By test ID (recommended)
page.locator('[data-testid="submit"]');
page.getByTestId('submit');

// By role
page.getByRole('button', { name: 'Submit' });
page.getByRole('link', { name: 'Home' });
page.getByRole('heading', { level: 1 });

// By text
page.getByText('Submit');
page.getByText(/submit/i);  // Case insensitive

// By label
page.getByLabel('Email');

// By placeholder
page.getByPlaceholder('Enter email');

// CSS selector
page.locator('.class-name');
page.locator('#id');
page.locator('button.primary');

// XPath
page.locator('//button[@type="submit"]');

// Chaining
page.locator('.form').locator('button');
page.locator('.list').locator('li').nth(0);
```

### API Testing in Playwright

```javascript
const { test, expect, request } = require('@playwright/test');

test.describe('API Tests', () => {
  test('should return user data', async ({ request }) => {
    const response = await request.get('/api/users/1');
    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(200);

    const json = await response.json();
    expect(json).toHaveProperty('id');
    expect(json).toHaveProperty('name');
  });

  test('should create user', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: {
        name: 'Test User',
        email: 'test@example.com'
      }
    });
    expect(response.status()).toBe(201);
  });
});
```

### Network Interception

```javascript
// Mock API response
await page.route('**/api/users', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([{ id: 1, name: 'Mock User' }])
  });
});

// Modify request
await page.route('**/api/**', async route => {
  const headers = {
    ...route.request().headers(),
    'Authorization': 'Bearer mock-token'
  };
  await route.continue({ headers });
});

// Abort request
await page.route('**/analytics/**', route => route.abort());

// Wait for specific response
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button.load-users');
const response = await responsePromise;
```

### Authentication

```javascript
// Global setup for auth
// global-setup.js
const { chromium } = require('@playwright/test');

module.exports = async config => {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('http://target:8080/login');
  await page.fill('[data-testid="email"]', 'test@example.com');
  await page.fill('[data-testid="password"]', 'password123');
  await page.click('[data-testid="login"]');

  // Save storage state
  await page.context().storageState({ path: 'auth.json' });
  await browser.close();
};

// Use auth state in tests
// playwright.config.js
module.exports = defineConfig({
  use: {
    storageState: 'auth.json'
  }
});
```

### Visual Regression Testing

```javascript
test('visual comparison', async ({ page }) => {
  await page.goto('/dashboard');

  // Full page screenshot comparison
  await expect(page).toHaveScreenshot('dashboard.png');

  // Element screenshot comparison
  const chart = page.locator('.chart');
  await expect(chart).toHaveScreenshot('chart.png');

  // With threshold
  await expect(page).toHaveScreenshot('dashboard.png', {
    maxDiffPixels: 100
  });

  // With mask
  await expect(page).toHaveScreenshot('dashboard.png', {
    mask: [page.locator('.dynamic-content')]
  });
});
```

### Security-Focused Tests

```javascript
test.describe('Security Tests', () => {
  test('@security XSS prevention', async ({ page }) => {
    const xssPayload = '<script>alert("XSS")</script>';

    await page.goto('/search');
    await page.fill('[data-testid="search"]', xssPayload);
    await page.click('[data-testid="search-button"]');

    // Verify script not executed
    const alerts = [];
    page.on('dialog', dialog => {
      alerts.push(dialog.message());
      dialog.dismiss();
    });

    await page.waitForTimeout(1000);
    expect(alerts).toHaveLength(0);
  });

  test('@security CSRF token present', async ({ page }) => {
    await page.goto('/form');

    const csrfToken = await page.locator('input[name="csrf_token"]').inputValue();
    expect(csrfToken).toBeTruthy();
    expect(csrfToken.length).toBeGreaterThan(20);
  });

  test('@security secure cookie attributes', async ({ page }) => {
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login"]');

    const cookies = await page.context().cookies();
    const sessionCookie = cookies.find(c => c.name === 'session');

    expect(sessionCookie.httpOnly).toBe(true);
    expect(sessionCookie.secure).toBe(true);
    expect(sessionCookie.sameSite).toBe('Strict');
  });

  test('@security SQL injection protection', async ({ page }) => {
    const sqlPayload = "'; DROP TABLE users; --";

    await page.goto('/login');
    await page.fill('[data-testid="email"]', sqlPayload);
    await page.fill('[data-testid="password"]', 'test');
    await page.click('[data-testid="login"]');

    // Should not expose SQL errors
    const pageContent = await page.content();
    expect(pageContent).not.toContain('SQL');
    expect(pageContent).not.toContain('syntax error');
    expect(pageContent).not.toContain('mysql');
  });
});
```

### Running Tests

```bash
# Run all tests
docker exec playwright npx playwright test

# Run specific test file
docker exec playwright npx playwright test tests/login.spec.js

# Run tests with tag
docker exec playwright npx playwright test --grep @security

# Run in headed mode
docker exec playwright npx playwright test --headed

# Run specific browser
docker exec playwright npx playwright test --project=chromium

# Debug mode
docker exec playwright npx playwright test --debug

# Generate report
docker exec playwright npx playwright show-report

# Update snapshots
docker exec playwright npx playwright test --update-snapshots
```

### Test Organization

```
playwright-tests/
├── tests/
│   ├── auth/
│   │   ├── login.spec.js
│   │   ├── logout.spec.js
│   │   └── password-reset.spec.js
│   ├── dashboard/
│   │   ├── widgets.spec.js
│   │   └── navigation.spec.js
│   ├── security/
│   │   ├── xss.spec.js
│   │   ├── csrf.spec.js
│   │   └── injection.spec.js
│   └── visual/
│       ├── homepage.spec.js
│       └── dashboard.spec.js
├── page-objects/
│   ├── LoginPage.js
│   ├── DashboardPage.js
│   └── index.js
├── fixtures/
│   ├── users.json
│   └── products.json
├── utils/
│   ├── helpers.js
│   └── api.js
├── playwright.config.js
└── global-setup.js
```

## Best Practices

1. **Use data-testid attributes** - Resilient to UI changes
2. **Implement Page Object Model** - Maintainable tests
3. **Run tests in parallel** - Faster feedback
4. **Use auto-wait** - Avoid explicit waits
5. **Test critical paths first** - Prioritize important flows
6. **Include visual regression** - Catch UI changes
7. **Test across browsers** - Ensure compatibility
8. **Mock external services** - Isolate tests
9. **Use fixtures** - Share test data
10. **Tag tests** - Organize by type (@smoke, @security, @visual)
