mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 14:22:02 +00:00
- Changed instances of `waitForLoadState('networkidle')` to `waitForLoadState('load')` across multiple test files and utility functions to improve test reliability in applications with persistent connections.
- Added documentation to the e2e testing guide explaining the rationale behind using 'load' state instead of 'networkidle' to prevent timeouts and flaky tests.
344 lines
9.7 KiB
Markdown
344 lines
9.7 KiB
Markdown
# 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.
|