# E2E Testing Guide

Best practices and patterns for writing reliable, non-flaky Playwright e2e tests in this codebase.

## Core Principles

1. **No arbitrary timeouts** - Never use `page.waitForTimeout()`. Always wait for specific conditions.
2. **Use data-testid attributes** - Prefer `[data-testid="..."]` selectors over CSS classes or text content.
3. **Clean up after tests** - Use unique temp directories and clean them up in `afterAll`.
4. **Test isolation** - Each test should be independent and not rely on state from other tests.

## Setting Up Test State

### Use Setup Utilities (Recommended)

Use the provided utility functions to set up localStorage state. These utilities hide the internal store structure and version details, making tests more maintainable.

```typescript
import { setupWelcomeView, setupRealProject } from './utils';

// Show welcome view with workspace directory configured
await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR });

// Show welcome view with recent projects
await setupWelcomeView(page, {
  workspaceDir: TEST_TEMP_DIR,
  recentProjects: [
    {
      id: 'project-123',
      name: 'My Project',
      path: '/path/to/project',
      lastOpened: new Date().toISOString(),
    },
  ],
});

// Set up a real project on the filesystem
await setupRealProject(page, projectPath, projectName, {
  setAsCurrent: true, // Opens board view (default)
});
```

### Why Use Utilities Instead of Raw localStorage

1. **Version management** - Store versions are centralized in one place
2. **Less brittle** - If store structure changes, update one file instead of every test
3. **Cleaner tests** - Focus on test logic, not setup boilerplate
4. **Type safety** - Utilities provide typed interfaces for test data

### Manual LocalStorage Setup (Advanced)

If you need custom setup not covered by utilities, use `page.addInitScript()`.
Store versions are defined in `tests/utils/project/setup.ts`:

- `APP_STORE`: version 2 (matches `app-store.ts`)
- `SETUP_STORE`: version 0 (matches `setup-store.ts` default)

### Temp Directory Management

Create unique temp directories for test isolation:

```typescript
import { createTempDirPath, cleanupTempDir } from './utils';

const TEST_TEMP_DIR = createTempDirPath('my-test-name');

test.describe('My Tests', () => {
  test.beforeAll(async () => {
    if (!fs.existsSync(TEST_TEMP_DIR)) {
      fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
    }
  });

  test.afterAll(async () => {
    cleanupTempDir(TEST_TEMP_DIR);
  });
});
```

## Waiting for Elements

### Prefer `toBeVisible()` over `waitForSelector()`

```typescript
// Good - uses Playwright's auto-waiting with expect
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });

// Avoid - manual waiting
await page.waitForSelector('[data-testid="welcome-view"]');
```

### Wait for page load after navigation

**Important:** Use `load` state, NOT `networkidle`. This app has persistent connections (websockets, polling) that prevent the network from ever becoming "idle", causing `networkidle` to timeout.

```typescript
await page.goto('/');
await page.waitForLoadState('load');

// Then wait for specific elements to verify the page is ready
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });
```

**Why not `networkidle`?**

- `networkidle` requires no network activity for 500ms
- Modern SPAs with real-time features (websockets, polling, SSE) never reach this state
- Using `networkidle` causes 30+ second timeouts and flaky tests
- The `load` state fires when the page finishes loading, which is sufficient
- Always follow up with element visibility checks for reliability

### Use appropriate timeouts

- Quick UI updates: 5000ms (default)
- Page loads/navigation: 10000ms
- Async operations (API calls, file system): 15000ms

```typescript
// Fast UI element
await expect(button).toBeVisible();

// Page load
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });

// Async operation completion
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
```

## Element Selection

### Use data-testid attributes

```typescript
// Good - stable selector
const button = page.locator('[data-testid="create-new-project"]');

// Avoid - brittle selectors
const button = page.locator('.btn-primary');
const button = page.getByText('Create');
```

### Scope selectors when needed

When text appears in multiple places, scope to a parent:

```typescript
// Bad - might match multiple elements
await expect(page.getByText(projectName)).toBeVisible();

// Good - scoped to specific container
await expect(page.locator('[data-testid="project-selector"]').getByText(projectName)).toBeVisible();
```

### Handle strict mode violations

If a selector matches multiple elements:

```typescript
// Use .first() if you need the first match
await page.locator('[data-testid="item"]').first().click();

// Or scope to a unique parent
await page.locator('[data-testid="sidebar"]').locator('[data-testid="item"]').click();
```

## Clicking Elements

### Always verify visibility before clicking

```typescript
const button = page.locator('[data-testid="submit"]');
await expect(button).toBeVisible();
await button.click();
```

### Handle dialogs that may close quickly

Some dialogs may appear briefly or auto-close. Don't rely on clicking them:

```typescript
// Instead of trying to close a dialog that might disappear:
// await expect(dialog).toBeVisible();
// await closeButton.click();  // May fail if dialog closes first

// Just verify the end state:
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
```

## Filesystem Verification

Verify files were created after async operations:

```typescript
// Wait for UI to confirm operation completed first
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });

// Then verify filesystem
const projectPath = path.join(TEST_TEMP_DIR, projectName);
expect(fs.existsSync(projectPath)).toBe(true);

const appSpecPath = path.join(projectPath, '.automaker', 'app_spec.txt');
expect(fs.existsSync(appSpecPath)).toBe(true);

const content = fs.readFileSync(appSpecPath, 'utf-8');
expect(content).toContain(projectName);
```

## Test Structure

### Use descriptive test names

```typescript
test('should create a new blank project from welcome view', async ({ page }) => {
  // ...
});
```

### Group related tests with describe blocks

```typescript
test.describe('Project Creation', () => {
  test('should create a new blank project from welcome view', ...);
  test('should create a project from template', ...);
});
```

### Use serial mode when tests depend on each other

```typescript
test.describe.configure({ mode: 'serial' });
```

## Common Patterns

### Waiting for either of two outcomes

When multiple outcomes are possible (e.g., dialog or direct navigation):

```typescript
// Wait for either the dialog or the board view
await Promise.race([
  initDialog.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}),
  boardView.waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}),
]);

// Then handle whichever appeared
if (await initDialog.isVisible()) {
  await closeButton.click();
}

await expect(boardView).toBeVisible();
```

### Generating unique test data

```typescript
const projectName = `test-project-${Date.now()}`;
```

## Running Tests

```bash
# Run all tests
npm run test

# Run specific test file
npm run test -- project-creation.spec.ts

# Run with headed browser (see what's happening)
npm run test:headed -- project-creation.spec.ts

# Run multiple times to check for flakiness
npm run test -- project-creation.spec.ts --repeat-each=5
```

## Debugging Failed Tests

1. Check the screenshot in `test-results/`
2. Read the error context markdown file in `test-results/`
3. Run with `--headed` to watch the test
4. Add `await page.pause()` to pause execution at a specific point

## Common Pitfalls

### Timeout on `waitForLoadState('networkidle')`

If tests timeout waiting for network idle, the app likely has persistent connections. Use `load` state instead:

```typescript
// Bad - will timeout with persistent connections
await page.waitForLoadState('networkidle');

// Good - completes when page loads
await page.waitForLoadState('load');
await expect(page.locator('[data-testid="my-element"]')).toBeVisible();
```

### Port conflicts

If you see "Port 3008 is already in use", kill the process:

```bash
lsof -ti:3008 | xargs kill -9
```

## Available Test Utilities

Import from `./utils`:

### State Setup Utilities

- `setupWelcomeView(page, options?)` - Set up empty state showing welcome view
  - `options.workspaceDir` - Pre-configure workspace directory
  - `options.recentProjects` - Add projects to recent list (not current)
- `setupRealProject(page, path, name, options?)` - Set up state with a real filesystem project
  - `options.setAsCurrent` - Open board view (default: true)
  - `options.additionalProjects` - Add more projects to list
- `setupMockProject(page)` - Set up mock project for unit-style tests
- `setupComplete(page)` - Mark setup wizard as complete

### Filesystem Utilities

- `createTempDirPath(prefix)` - Create unique temp directory path
- `cleanupTempDir(path)` - Remove temp directory
- `createTestGitRepo(tempDir)` - Create a git repo for testing

### Waiting Utilities

- `waitForNetworkIdle(page)` - Wait for page to load (uses `load` state, not `networkidle`)
- `waitForElement(page, testId)` - Wait for element by test ID
- `waitForBoardView(page)` - Navigate to board and wait for it to be visible

### Async File Verification

Use `expect().toPass()` for polling filesystem operations:

```typescript
await expect(async () => {
  expect(fs.existsSync(filePath)).toBe(true);
}).toPass({ timeout: 10000 });
```

See `tests/utils/index.ts` for the full list of available utilities.
