# E2E Test Infrastructure

This document describes the E2E testing infrastructure for the MetaDrivenPlatform4AI (NexusCRM) project.

## Directory Structure

```
tests/e2e/
├── playwright.config.ts          # Main Playwright configuration
├── global-setup.ts               # Global setup (DB reset, auth state creation)
├── .auth/                        # Storage state for authenticated tests
│   ├── admin.json               # Admin user auth state
│   ├── testuser.json            # Test user auth state
│   └── readonly.json            # Read-only user auth state
├── fixtures/
│   ├── index.ts                  # Merged fixture exports
│   ├── auth.fixture.ts           # Authentication fixtures
│   └── data.fixture.ts           # Test data fixtures
├── utils/
│   ├── app-helpers.ts            # App-level helpers
│   └── wait-helpers.ts           # Wait utilities
├── seed/
│   ├── index.ts                  # Seed orchestrator and exports
│   ├── accounts.seed.ts          # Account seed data and factories
│   ├── contacts.seed.ts          # Contact seed data and factories
│   └── opportunities.seed.ts     # Opportunity seed data and factories
├── specs/                        # Test specifications (to be filled by other agents)
│   └── (test files go here)
└── scripts/
    ├── reset-db.sh               # Database reset script
    └── seed-test-data.go         # Data seeding script
```

## Quick Start

### Running Tests

```bash
# Run all tests headlessly
npm run e2e

# Run tests with browser UI visible
npm run e2e:headed

# Run tests in UI mode (interactive)
npm run e2e:ui

# Run tests in debug mode
npm run e2e:debug

# View test report
npm run e2e:report

# Generate selectors using codegen
npm run e2e:codegen
```

### Prerequisites

Before running tests, ensure:
1. Frontend is running on `http://localhost:5173`
2. Backend API is running on `http://localhost:3001`
3. Test database is accessible

The `global-setup.ts` will automatically:
- Reset the test database
- Seed test data
- Create authentication states

## Using Fixtures

Fixtures are the primary way to set up test preconditions. Import them from the fixtures module:

```typescript
import { test, expect } from '../fixtures';

test('example test', async ({ authenticatedPage, testData, dbCleanup }) => {
  // authenticatedPage is already logged in as admin
  await authenticatedPage.goto('/accounts');

  // Access predefined test data
  const account = testData.accounts[0];

  // Created records are automatically cleaned up
  dbCleanup.register('account', createdId);
});
```

### Available Fixtures

#### Authentication Fixtures

| Fixture | Description | Use Case |
|---------|-------------|----------|
| `authenticatedPage` | Page logged in as admin | Admin-level operations |
| `testUserPage` | Page logged in as testuser | Standard user operations |
| `readonlyPage` | Page logged in as readonly | View-only operations |
| `guestPage` | Page without authentication | Login flows, public pages |
| `createAuthPage` | Factory for custom auth | Dynamic auth scenarios |

#### Data Fixtures

| Fixture | Description | Use Case |
|---------|-------------|----------|
| `testData` | Predefined seed data | Access consistent test data |
| `createTestData` | Factory functions | Create custom test data |
| `dbCleanup` | Auto-cleanup registry | Automatic test data cleanup |
| `apiRequest` | API request helper | Direct API interactions |

### Example: Using Authentication Fixtures

```typescript
import { test, expect } from '../fixtures';

// Test with admin authentication
test('admin can create account', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/accounts/new');
  await authenticatedPage.getByTestId('name-input').fill('Test Corp');
  await authenticatedPage.getByTestId('save-button').click();
  await expect(authenticatedPage.getByText('Account created')).toBeVisible();
});

// Test with standard user authentication
test('user can view accounts', async ({ testUserPage }) => {
  await testUserPage.goto('/accounts');
  await expect(testUserPage.getByTestId('accounts-table')).toBeVisible();
});

// Test without authentication (login flow)
test('guest can login', async ({ guestPage }) => {
  await guestPage.goto('/login');
  await guestPage.getByTestId('email-input').fill('admin@nexuscrm.test');
  await guestPage.getByTestId('password-input').fill(process.env.TEST_ADMIN_PASSWORD!);
  await guestPage.getByTestId('login-button').click();
  await expect(guestPage).toHaveURL('/dashboard');
});
```

## Selector Conventions

Follow this priority order for maximum test stability:

### 1. First Choice: `data-testid` Attributes (RECOMMENDED)

```typescript
// Using Playwright's built-in getByTestId
await page.getByTestId('login-button').click();
await page.getByTestId('email-input').fill('test@test.com');
await expect(page.getByTestId('success-message')).toBeVisible();
```

**Naming conventions for data-testid:**
- Use kebab-case: `submit-button`, `email-input`
- Be specific: `account-save-button` (not just `save-button`)
- Include component context: `account-table-row`, `contact-form-input`
- For lists: `table-row-{id}` or `item-{index}`

### 2. Second Choice: ARIA Roles + Accessible Names

```typescript
// Using semantic roles
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Dashboard' }).click();
await page.getByRole('heading', { name: 'Accounts' }).isVisible();
await page.getByRole('textbox', { name: 'Email' }).fill('test@test.com');
```

### 3. Third Choice: Text Content

```typescript
// Using text content
await page.getByText('Login').click();
await page.getByText('Welcome back!').isVisible();
await page.getByText(/error/i).isVisible(); // Case-insensitive regex
```

### 4. Last Resort: CSS Selectors

```typescript
// Avoid if possible - brittle
await page.locator('.btn-primary').click();
await page.locator('#main-form').fill();
```

## Wait Helpers

Use the wait helpers in `utils/wait-helpers.ts` for consistent waiting behavior:

```typescript
import { waitForLoad } from '../utils/wait-helpers';

// Wait for page to fully load (DOM + network idle + loading indicators)
await waitForLoad(page);

// Skip network idle wait when not needed
await waitForLoad(page, { waitForNetwork: false });
```

### Available Wait Functions

| Function | Description | Default Timeout |
|----------|-------------|-----------------|
| `waitForLoad(page, options?)` | Wait for page load + network idle + loading indicators to disappear | 15s |

## Seed Data

Access predefined test data from the `seed` module:

```typescript
import { test, expect } from '../fixtures';
import {
  testAccounts,
  testContacts,
  testOpportunities,
  createAccount,
  createContact,
  createOpportunity,
} from '../seed';

test('using seed data', async ({ authenticatedPage }) => {
  // Use predefined test accounts
  const account = testAccounts[0];
  expect(account.name).toBe('Acme Corporation');

  // Create custom test data with factory
  const customAccount = createAccount({
    name: 'My Custom Corp',
    industry: 'Technology',
  });

  // Create multiple accounts
  const accounts = createAccount.createMultipleAccounts(5, {
    industry: 'Healthcare',
  });
});
```

### Predefined Test Accounts

| Name | Industry | Annual Revenue |
|------|----------|----------------|
| Acme Corporation | Technology | $50M |
| Global Industries Inc | Manufacturing | $120M |
| TechStart Solutions | Software | $5M |
| Healthcare Partners LLC | Healthcare | $75M |
| Financial Services Group | Financial Services | $200M |
| Retail Masters Corp | Retail | $350M |
| EduTech Innovations | Education | $15M |
| Green Energy Systems | Energy | $45M |

### Predefined Test Contacts

| Name | Title | Company |
|------|-------|---------|
| John Smith | CTO | Acme Corporation |
| Sarah Johnson | VP Operations | Acme Corporation |
| Michael Chen | Director Procurement | Global Industries |
| David Wilson | CEO & Founder | TechStart Solutions |
| Jennifer Anderson | Managing Director | Financial Services Group |

### Opportunity Stages

```typescript
import { OPPORTUNITY_STAGES, STAGE_PROBABILITIES } from '../seed';

// Available stages
OPPORTUNITY_STAGES.PROSPECTING        // 10%
OPPORTUNITY_STAGES.QUALIFICATION      // 20%
OPPORTUNITY_STAGES.NEED_ANALYSIS      // 30%
OPPORTUNITY_STAGES.VALUE_PROPOSITION  // 40%
OPPORTUNITY_STAGES.DECISION_MAKERS    // 50%
OPPORTUNITY_STAGES.PERCEPTION_ANALYSIS// 60%
OPPORTUNITY_STAGES.PROPOSAL           // 70%
OPPORTUNITY_STAGES.NEGOTIATION        // 80%
OPPORTUNITY_STAGES.CLOSED_WON         // 100%
OPPORTUNITY_STAGES.CLOSED_LOST        // 0%
```

## Writing a New Test

### Basic Test Structure

```typescript
import { test, expect } from '../fixtures';
import { waitForLoad } from '../utils/wait-helpers';
import { createAccount } from '../seed';

test.describe('Account Management', () => {
  // Setup runs before each test
  test.beforeEach(async ({ authenticatedPage }) => {
    await authenticatedPage.goto('/accounts');
    await waitForLoad(authenticatedPage);
  });

  test('can create new account', async ({ authenticatedPage, dbCleanup }) => {
    // Navigate to create form
    await authenticatedPage.getByTestId('add-button').click();

    // Fill form
    const accountData = createAccount();
    await authenticatedPage.getByTestId('name-input').fill(accountData.name);
    await authenticatedPage.getByTestId('industry-input').fill(accountData.industry);

    // Submit
    await authenticatedPage.getByTestId('save-button').click();

    // Wait for success
    await expect(authenticatedPage.getByText('Account created successfully')).toBeVisible();

    // Register for cleanup
    // dbCleanup.register('account', createdId);
  });

  test('can view account list', async ({ authenticatedPage }) => {
    // Verify table is visible
    const table = authenticatedPage.getByTestId('accounts-table');
    await expect(table).toBeVisible();

    // Verify at least one row exists
    const firstRow = table.getByTestId(/^table-row-/).first();
    await expect(firstRow).toBeVisible();
  });
});
```

### Test Organization

Organize tests by feature/module in the `specs/` directory:

```
tests/e2e/specs/
├── auth/
│   ├── login.spec.ts
│   ├── logout.spec.ts
│   └── password-reset.spec.ts
├── accounts/
│   ├── accounts-list.spec.ts
│   ├── accounts-create.spec.ts
│   ├── accounts-edit.spec.ts
│   └── accounts-delete.spec.ts
├── contacts/
│   ├── contacts-list.spec.ts
│   └── contacts-create.spec.ts
├── opportunities/
│   ├── opportunities-list.spec.ts
│   └── opportunities-create.spec.ts
└── settings/
    └── settings.spec.ts
```

## Best Practices

### 1. Use Fixtures for Setup

```typescript
// Good: Using fixtures
test('example', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/accounts');
});

// Bad: Manual setup
test('example', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('/login');
  // ... manual login
});
```

### 2. Use data-testid for Selectors

```typescript
// Good: Stable selector
await page.getByTestId('submit-button').click();

// Bad: Brittle selector
await page.locator('.btn.btn-primary.mt-4').click();
```

### 3. Use Wait Helpers

```typescript
// Good: Using wait helper
await waitForLoad(page);

// Bad: Hardcoded wait
await page.waitForTimeout(5000);
```

### 4. Clean Up Test Data

```typescript
// Good: Auto cleanup
test('example', async ({ apiRequest, dbCleanup }) => {
  const account = await apiRequest('/api/accounts', {
    method: 'POST',
    data: { name: 'Test' },
  });
  dbCleanup.register('account', account.id);
});
```

### 5. Keep Tests Independent

```typescript
// Good: Each test sets up its own data
test('test 1', async ({ apiRequest }) => {
  const account = await apiRequest('/api/accounts', { method: 'POST', data: { name: 'Test 1' } });
  // ... test logic
});

test('test 2', async ({ apiRequest }) => {
  const account = await apiRequest('/api/accounts', { method: 'POST', data: { name: 'Test 2' } });
  // ... test logic - independent of test 1
});

// Bad: Tests depend on each other
let sharedAccount;
test('test 1', async () => {
  sharedAccount = await createAccount();
});
test('test 2', async () => {
  // Depends on sharedAccount from test 1 - DON'T DO THIS
});
```

### 6. Use Descriptive Test Names

```typescript
// Good: Clear description
test('admin can create account with valid data', async () => {});
test('user cannot delete account without permission', async () => {});

// Bad: Vague description
test('test account', async () => {});
test('should work', async () => {});
```

## Configuration

### Playwright Configuration

See `playwright.config.ts` for:
- Base URL: `http://localhost:5173`
- Timeout: 30s per test, 60s per fixture
- Retries: 2 on CI, 0 locally
- Workers: 4 (parallel execution)
- Browsers: Chromium, Firefox

### Environment Variables

Create `tests/e2e/.env.test` with:

```bash
# Frontend
PLAYWRIGHT_BASE_URL=http://localhost:5173

# Backend API
PLAYWRIGHT_API_URL=http://localhost:3001

# Database
TIDB_HOST=localhost
TIDB_PORT=4000
TIDB_USER=root
TIDB_PASSWORD=
TIDB_DATABASE=nexuscrm_test
```

## Troubleshooting

### Tests Fail Due to Authentication

Run global setup manually:
```bash
npm run e2e:setup
```

Or delete auth states and rerun:
```bash
rm -rf tests/e2e/.auth/*.json
npm run e2e
```

### Database Connection Issues

Reset the database:
```bash
./tests/e2e/scripts/reset-db.sh
```

### Tests Timeout

Increase timeout in specific test:
```typescript
test('slow test', async ({ page }) => {
  test.setTimeout(60000); // 60 seconds
  // ... test logic
});
```

### Flaky Tests

1. Use `waitForLoad` instead of hardcoded timeouts
2. Ensure selectors are stable (use data-testid)
3. Check for race conditions with API calls
4. Use Playwright's built-in `page.waitForResponse()` for async operations

## Support

For questions or issues with the E2E infrastructure:
1. Check this documentation
2. Review existing test examples in `specs/`
3. Consult the Playwright documentation at https://playwright.dev
