vibetunnel/web/docs/playwright-testing.md
2025-06-30 02:51:21 +01:00

248 lines
No EOL
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Playwright Testing Best Practices for VibeTunnel
## Overview
This guide documents best practices for writing reliable, non-flaky Playwright tests for VibeTunnel, based on official Playwright documentation and community best practices.
## Core Principles
### 1. Use Auto-Waiting Instead of Arbitrary Delays
**❌ Bad: Arbitrary timeouts**
```typescript
await page.waitForTimeout(1000); // Don't do this!
```
**✅ Good: Wait for specific conditions**
```typescript
// Wait for element to be visible
await page.waitForSelector('vibe-terminal', { state: 'visible' });
// Wait for loading indicator to disappear
await page.locator('.loading-spinner').waitFor({ state: 'hidden' });
// Wait for specific text to appear
await page.getByText('Session created').waitFor();
```
### 2. Use Web-First Assertions
Web-first assertions automatically wait and retry until the condition is met:
```typescript
// These assertions auto-wait
await expect(page.locator('session-card')).toBeVisible();
await expect(page).toHaveURL(/\?session=/);
await expect(sessionCard).toContainText('RUNNING');
```
### 3. Prefer User-Facing Locators
**Locator Priority (best to worst):**
1. `getByRole()` - semantic HTML roles
2. `getByText()` - visible text content
3. `getByTestId()` - explicit test IDs
4. `locator()` with CSS - last resort
```typescript
// Good examples
await page.getByRole('button', { name: 'Create Session' }).click();
await page.getByText('Session Name').fill('My Session');
await page.getByTestId('terminal-output').waitFor();
```
## VibeTunnel-Specific Patterns
### Waiting for Terminal Ready
Instead of arbitrary delays, wait for terminal indicators:
```typescript
// Wait for terminal component to be visible
await page.waitForSelector('vibe-terminal', { state: 'visible' });
// Wait for terminal to have content or structure
await page.waitForFunction(() => {
const terminal = document.querySelector('vibe-terminal');
return terminal && (
terminal.textContent?.trim().length > 0 ||
!!terminal.shadowRoot ||
!!terminal.querySelector('.xterm')
);
});
```
### Handling Session Creation
```typescript
// Wait for navigation after session creation
await expect(page).toHaveURL(/\?session=/, { timeout: 2000 });
// Wait for terminal to be ready
await page.locator('vibe-terminal').waitFor({ state: 'visible' });
```
### Managing Modal Animations
Instead of waiting for animations, wait for the modal state:
```typescript
// Wait for modal to be fully visible
await page.locator('[role="dialog"]').waitFor({ state: 'visible' });
// Wait for modal to be completely gone
await page.locator('[role="dialog"]').waitFor({ state: 'hidden' });
```
### Session List Updates
```typescript
// Wait for session cards to update
await page.locator('session-card').first().waitFor();
// Wait for specific session by name
await page.locator(`session-card:has-text("${sessionName}")`).waitFor();
```
## Common Anti-Patterns to Avoid
### 1. Storing Element References
```typescript
// ❌ Bad: Element reference can become stale
const button = await page.$('button');
await doSomething();
await button.click(); // May fail!
// ✅ Good: Re-query element when needed
await doSomething();
await page.locator('button').click();
```
### 2. Assuming Immediate Availability
```typescript
// ❌ Bad: No waiting
await page.goto('/');
await page.click('session-card'); // May not exist yet!
// ✅ Good: Wait for element
await page.goto('/');
await page.locator('session-card').waitFor();
await page.locator('session-card').click();
```
### 3. Fixed Sleep for Dynamic Content
```typescript
// ❌ Bad: Arbitrary wait for data load
await page.click('#load-data');
await page.waitForTimeout(3000);
// ✅ Good: Wait for loading state
await page.click('#load-data');
await page.locator('.loading').waitFor({ state: 'hidden' });
// Or wait for results
await page.locator('[data-testid="results"]').waitFor();
```
## Test Configuration
### Timeouts
Configure appropriate timeouts in `playwright.config.ts`:
```typescript
use: {
// Global timeout for assertions
expect: { timeout: 5000 },
// Action timeout (click, fill, etc.)
actionTimeout: 10000,
// Navigation timeout
navigationTimeout: 10000,
}
```
### Test Isolation
Each test should be independent:
```typescript
test.beforeEach(async ({ page }) => {
// Fresh start for each test
await page.goto('/');
await page.waitForSelector('vibetunnel-app', { state: 'attached' });
});
```
## Debugging Flaky Tests
### 1. Enable Trace Recording
```typescript
// In playwright.config.ts
use: {
trace: 'on-first-retry',
}
```
### 2. Use Debug Mode
```bash
# Run with headed browser and inspector
pnpm exec playwright test --debug
```
### 3. Add Strategic Logging
```typescript
console.log('Waiting for terminal to be ready...');
await page.locator('vibe-terminal').waitFor();
console.log('Terminal is ready');
```
## Terminal-Specific Patterns
### Waiting for Terminal Output
```typescript
// Wait for specific text in terminal
await page.waitForFunction(
(searchText) => {
const terminal = document.querySelector('vibe-terminal');
return terminal?.textContent?.includes(searchText);
},
'Expected output'
);
```
### Waiting for Shell Prompt
```typescript
// Wait for prompt patterns
await page.waitForFunction(() => {
const terminal = document.querySelector('vibe-terminal');
const content = terminal?.textContent || '';
return /[$>#%]\s*$/.test(content);
});
```
### Handling Server-Side Terminals
When `spawnWindow` is false, terminals run server-side:
```typescript
// Create session with server-side terminal
await sessionListPage.createNewSession(sessionName, false);
// Wait for WebSocket/SSE connection
await page.locator('vibe-terminal').waitFor({ state: 'visible' });
// Terminal content comes through WebSocket - no need for complex waits
```
## Summary
1. **Never use `waitForTimeout()`** - always wait for specific conditions
2. **Use web-first assertions** that auto-wait
3. **Prefer semantic locators** over CSS selectors
4. **Wait for observable conditions** not arbitrary time
5. **Configure appropriate timeouts** for your application
6. **Keep tests isolated** and independent
7. **Use Playwright's built-in debugging tools** for flaky tests
By following these practices, tests will be more reliable, faster, and easier to maintain.