File size: 10,777 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# 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);
  });
});
```

### Git isolation: never use the main project path

E2E tests must **never** use the workspace/repo root (the project you're developing in) as the project path. The app and server can run git commands (checkout, worktree add, merge, etc.) on the current project; if that path is the main repo, tests can leave it in a different branch or with merge conflicts.

- **Allowed:** Paths under `tests/` (e.g. `createTempDirPath('...')` or `tests/fixtures/projectA`) or under `os.tmpdir()`.
- **Not allowed:** Workspace root or any path outside `tests/` or temp.

`setupRealProject` and `setupProjectWithFixture` enforce this: they throw if the project path is the workspace root or outside the allowed bases. Use `createTempDirPath()` for test-specific project dirs and the fixture path for fixture-based tests.

## 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 3108 is already in use", kill the process:

```bash
lsof -ti:3108 | 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.