mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
6.2 KiB
6.2 KiB
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
await page.waitForTimeout(1000); // Don't do this!
✅ Good: Wait for specific conditions
// 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:
// 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):
getByRole()- semantic HTML rolesgetByText()- visible text contentgetByTestId()- explicit test IDslocator()with CSS - last resort
// 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:
// 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
// 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:
// 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
// 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
// ❌ 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
// ❌ 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
// ❌ 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:
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:
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
// In playwright.config.ts
use: {
trace: 'on-first-retry',
}
2. Use Debug Mode
# Run with headed browser and inspector
pnpm exec playwright test --debug
3. Add Strategic Logging
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
// 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
// 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:
// 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
- Never use
waitForTimeout()- always wait for specific conditions - Use web-first assertions that auto-wait
- Prefer semantic locators over CSS selectors
- Wait for observable conditions not arbitrary time
- Configure appropriate timeouts for your application
- Keep tests isolated and independent
- Use Playwright's built-in debugging tools for flaky tests
By following these practices, tests will be more reliable, faster, and easier to maintain.