Comprehensive guide for running and developing end-to-end tests for better-chatbot using Playwright.
# Install dependencies (if not already done)
pnpm install
# Install Playwright browsers
pnpm playwright:install
# Run all e2e tests
pnpm test:e2e
# Run tests with UI (interactive mode)
pnpm test:e2e:ui
# Run specific test file
pnpm test:e2e -- tests/agents/agent-creation.spec.ts
# Run tests in debug mode
pnpm test:e2e:debugOur e2e tests are designed for reliability, speed, and maintainability:
tests/
├── lifecycle/ # Setup and teardown for tests
│ ├── auth.setup.ts # User registration & authentication
│ └── teardown.global.ts # Test data cleanup
├── core/ # Core tests for landing page, auth flows, etc.
│ └── unauthenticated.spec.ts # Landing page & auth flows
├── agents/ # Agent tests
│ ├── agent-creation.spec.ts # Agent CRUD operations
│ ├── agent-visibility.spec.ts # Multi-user sharing & permissions
│ └── agents.spec.ts # Basic agent functionality
└── models/ # Model selection & persistence
└── model-selection.spec.ts # Model selection & persistence
- ✅ Automated user registration with unique test accounts
- ✅ Multi-user testing for sharing & permissions
- ✅ Automatic cleanup of test data
- ✅ Parallel execution for speed
- ✅ Robust selectors using data-testid attributes
Tests require these environment variables:
# Database (required)
POSTGRES_URL=postgres://user:password@localhost:5432/database
# Authentication (required)
BETTER_AUTH_SECRET=your-secret-here
# At least one LLM provider (required)
OPENAI_API_KEY=your-openai-key
# OR
ANTHROPIC_API_KEY=your-anthropic-key
# OR
GOOGLE_GENERATIVE_AI_API_KEY=your-google-key
# Optional: Set default model for tests - will need to be corelated with API keys
E2E_DEFAULT_MODEL=openai/gpt-4o-miniWe recommend using the Playwright extension for VSCode. It provides a lot of helpful features for writing and debugging tests.
pnpm docker:pgTests authenticate 4 users 1 admin, 1 editor, 1 editor2, and 1 regular by default on setup. - This is to test multi-user functionality like agent or workspace sharing. These users are defined in tests/constants/test-users.ts.
To test as an authenticated user (nearly all tests), you can use the test.use({ storageState: TEST_USERS.editor.authFile }); or test.use({ storageState: TEST_USERS.editor2.authFile }); or test.use({ storageState: TEST_USERS.regular.authFile }); or test.use({ storageState: TEST_USERS.admin.authFile }); in the test file. Without this, the test will run as an unauthenticated user. This can go in the describe block or the test block.
Playwright is designed to run tests in parallel. This means that each test will run in its own browser instance. For tests that need to test multi-user functionality, you can set the tests to run sequentially by using the test.describe.configure({ mode: 'serial' }); decorator. See tests/agents/agent-visibility.spec.ts for an example.
Example:
// Most tests use single user authentication
import { TEST_USERS } from '../constants/test-users';
test.describe('Agent Creation', () => {
test.use({ storageState: TEST_USERS.editor.authFile });
test('should create agent', async ({ page }) => {
// Test logic here
});
});import { TEST_USERS } from '../constants/test-users';
test.describe('Agent Creation', () => {
test.use({ storageState: TEST_USERS.editor2.authFile });
test('should create agent', async ({ page }) => {
// Test logic here
});
});This is the most common use case for multi-user testing.
import { TEST_USERS } from '../constants/test-users';
test.describe('Agent Sharing', () => {
test('user sharing workflow', async ({ browser }) => {
// User1 creates agent
const user1Context = await browser.newContext({
storageState: TEST_USERS.editor.authFile,
});
const user1Page = await user1Context.newPage();
// User2 interacts with shared agent
const user2Context = await browser.newContext({
storageState: TEST_USERS.editor2.authFile,
});
const user2Page = await user2Context.newPage();
});
});- No duplicate test runs - Regular tests run once with user1
- Efficient multi-user testing - Only when needed for sharing features
- Clean isolation - Each test gets fresh authentication state
Always use data-testid attributes for stable selectors:
// ✅ Good - stable and semantic
await page.getByTestId('agent-name-input').fill('My Agent');
await page.getByTestId('agent-save-button').click();
// ❌ Avoid - fragile and language-dependent
await page.locator('input[placeholder="Enter agent name"]').fill('My Agent');
await page.getByText('Save').click();Use appropriate waiting strategies for reliability:
// Wait for network activity to settle
await page.waitForLoadState('networkidle');
// Wait for specific API responses
const responsePromise = page.waitForResponse(
(response) => response.url().includes('/api/agent/') && response.request().method() === 'PUT'
);
await page.getByTestId('save-button').click();
await responsePromise;
// Wait for navigation
await page.waitForURL('**/agents', { timeout: 10000 });Generate unique data to avoid conflicts:
const testSuffix = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
const agentName = `Test Agent ${testSuffix}`;# Run specific test with browser visible
pnpm test:e2e -- tests/agents/agent-creation.spec.ts --headed
# Debug mode with breakpoints
pnpm test:e2e:debug
# Run single test
npx playwright test -g "should create agent"
# Generate test report
npx playwright show-reportAdd debug information to tests:
// Take screenshots for debugging
await page.screenshot({ path: 'debug-agent-creation.png', fullPage: true });
// Log page content
console.log('Current URL:', page.url());
const agents = await page.locator('[data-testid="agent-card-name"]').all();
console.log(`Found ${agents.length} agents`);Tests timing out:
- Ensure
E2E_DEFAULT_MODELis set to a fast model - Check database connection and API keys
- Increase timeout for slow operations
Authentication failures:
- Verify
BETTER_AUTH_SECRETis set - Check PostgreSQL connection
- Ensure auth setup completes successfully
Element not found:
- Verify data-testid exists in component
- Check for loading states
- Use proper waiting strategies
Tests run automatically on GitHub Actions with:
- PostgreSQL 17 test database
- Parallel execution across multiple workers
- Automatic artifact upload for debugging
- Clean test environment isolated from production
import { test, expect } from '@playwright/test';
import { TEST_USERS } from '../constants/test-users';
test.describe('Your Feature', () => {
test.use({ storageState: TEST_USERS.editor.authFile });
test('should perform action', async ({ page }) => {
// Navigate to page
await page.goto('/your-feature');
// Perform actions
await page.getByTestId('input-field').fill('test value');
await page.getByTestId('submit-button').click();
// Wait for response
await page.waitForURL('**/success', { timeout: 10000 });
// Verify results
await expect(page.getByTestId('success-message')).toBeVisible();
});
});import { TEST_USERS } from '../constants/test-users';
test('multi-user workflow', async ({ browser }) => {
const testId = Date.now().toString(36);
// User1 setup
const user1Context = await browser.newContext({
storageState: TEST_USERS.editor.authFile,
});
const user1Page = await user1Context.newPage();
try {
// User1 actions
await user1Page.goto('/create');
// ... user1 workflow
} finally {
await user1Context.close();
}
// User2 verification
const user2Context = await browser.newContext({
storageState: 'tests/.auth/user2.json',
});
const user2Page = await user2Context.newPage();
try {
// User2 actions
await user2Page.goto('/shared');
// ... user2 workflow
} finally {
await user2Context.close();
}
});Tests automatically clean up after themselves:
- User identification by email patterns (
playwright.*@example.com) - Cascade deletion respecting foreign key constraints
- Complete cleanup of test users and related data
No manual cleanup required - the system handles it automatically!
For more examples, see the existing test files in the tests/ directory. Each test demonstrates different patterns and best practices for reliable e2e testing.