214 lines
5.8 KiB
Markdown
214 lines
5.8 KiB
Markdown
# Bifrost E2E Tests
|
|
|
|
End-to-end tests for the Bifrost UI using Playwright.
|
|
|
|
## Setup
|
|
|
|
```bash
|
|
# Install dependencies
|
|
npm install
|
|
|
|
# Install Playwright browsers
|
|
npx playwright install
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# Run all E2E tests
|
|
make run-e2e
|
|
|
|
# Run specific feature tests
|
|
make run-e2e FLOW=providers
|
|
make run-e2e FLOW=virtual-keys
|
|
make run-e2e FLOW=dashboard
|
|
make run-e2e FLOW=logs
|
|
make run-e2e FLOW=mcp-logs
|
|
make run-e2e FLOW=mcp-registry
|
|
make run-e2e FLOW=routing-rules
|
|
make run-e2e FLOW=observability
|
|
make run-e2e FLOW=config
|
|
make run-e2e FLOW=plugins
|
|
|
|
# Run tests in headed mode (visible browser)
|
|
make run-e2e-headed
|
|
|
|
# Run tests with Playwright UI
|
|
make run-e2e-ui
|
|
|
|
# Run specific feature tests via npm
|
|
npm run test:providers
|
|
npm run test:virtual-keys
|
|
npm run test:dashboard
|
|
npm run test:logs
|
|
npm run test:mcp-logs
|
|
npm run test:mcp-registry
|
|
npm run test:routing-rules
|
|
npm run test:observability
|
|
npm run test:config
|
|
npm run test:plugins
|
|
|
|
# View test report
|
|
npm run report
|
|
```
|
|
|
|
### Parallel flows on CI
|
|
|
|
The GitHub Actions workflow **E2E Tests** (`.github/workflows/e2e-tests.yml`) runs each flow in a **separate job in parallel**, since flows are independent. It triggers on push/PR when `ui/`, `tests/e2e/`, or the workflow file change. You can also run it manually (Actions → E2E Tests → Run workflow) and optionally pass a comma-separated list of flows (e.g. `providers,config,plugins`) to run only those.
|
|
|
|
## Folder Structure
|
|
|
|
```text
|
|
tests/e2e/
|
|
├── playwright.config.ts # Playwright configuration
|
|
├── core/ # Shared utilities & fixtures
|
|
│ ├── fixtures/ # Custom test fixtures
|
|
│ ├── pages/ # Base page objects
|
|
│ ├── actions/ # Reusable actions
|
|
│ └── utils/ # Utilities and helpers
|
|
└── features/ # Feature-specific tests
|
|
├── providers/ # Provider tests
|
|
├── virtual-keys/ # Virtual key tests
|
|
├── dashboard/ # Dashboard tests
|
|
├── logs/ # LLM logs tests
|
|
├── mcp-logs/ # MCP logs tests
|
|
├── mcp-registry/ # MCP registry tests
|
|
├── routing-rules/ # Routing rules tests
|
|
├── plugins/ # Plugins tests
|
|
├── observability/ # Observability connectors tests
|
|
└── config/ # Config settings tests
|
|
```
|
|
|
|
## Writing Tests
|
|
|
|
### Using Page Objects
|
|
|
|
```typescript
|
|
import { test, expect } from '../../core/fixtures/base.fixture'
|
|
|
|
test('should create provider', async ({ providersPage }) => {
|
|
await providersPage.goto()
|
|
await providersPage.selectProvider('openai')
|
|
// ...
|
|
})
|
|
```
|
|
|
|
### Test Data
|
|
|
|
Use factory functions from the `*.data.ts` files for generating test data:
|
|
|
|
```typescript
|
|
import { createProviderKeyData } from './providers.data'
|
|
|
|
const keyData = createProviderKeyData({ name: 'My Key' })
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Environment variables:
|
|
- `BASE_URL` - Base URL of the application (default: http://localhost:3000)
|
|
- `CI` - Set to true in CI environments
|
|
|
|
## Debugging
|
|
|
|
```bash
|
|
# Run with Playwright Inspector
|
|
npm run test:debug
|
|
|
|
# Generate code with Codegen
|
|
npm run codegen
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Wait Strategies
|
|
|
|
Use semantic waits instead of hardcoded timeouts:
|
|
|
|
```typescript
|
|
// ✅ Good: Semantic waits
|
|
await page.waitForLoadState('networkidle')
|
|
await element.waitFor({ state: 'visible' })
|
|
await expect(element).toBeVisible({ timeout: 5000 })
|
|
|
|
// ❌ Bad: Hardcoded timeouts (flaky and slow)
|
|
await page.waitForTimeout(2000)
|
|
```
|
|
|
|
### Selectors
|
|
|
|
Use `data-testid` attributes for robust selectors:
|
|
|
|
```typescript
|
|
// ✅ Good: Test IDs are resilient to UI changes
|
|
page.locator('[data-testid="chart-log-volume"]')
|
|
page.getByTestId('create-btn')
|
|
|
|
// ❌ Bad: Brittle chained parent selectors
|
|
page.locator('text=Volume').locator('..').locator('..')
|
|
```
|
|
|
|
### Resource Cleanup
|
|
|
|
Always clean up resources created during tests:
|
|
|
|
```typescript
|
|
// ✅ Good: Clean up after assertions
|
|
test('should create item', async ({ page }) => {
|
|
await page.createItem(data)
|
|
expect(await page.itemExists(data.name)).toBe(true)
|
|
// Cleanup
|
|
await page.deleteItem(data.name)
|
|
})
|
|
```
|
|
|
|
### Deterministic Assertions
|
|
|
|
Avoid conditional logic that always passes:
|
|
|
|
```typescript
|
|
// ❌ Bad: Always passes (count >= 0 is always true)
|
|
const count = await page.getCount()
|
|
expect(count >= 0).toBe(true)
|
|
|
|
// ✅ Good: Deterministic assertion
|
|
const count = await page.getCount()
|
|
if (count === 0) {
|
|
expect(emptyState).toBeVisible()
|
|
} else {
|
|
expect(count).toBeGreaterThan(0)
|
|
expect(emptyState).not.toBeVisible()
|
|
}
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
1. **`waitForTimeout()`** - Always use semantic waits instead
|
|
2. **`{ force: true }`** - Fix underlying visibility issues instead
|
|
3. **Chained parent locators** (`.locator('..')`) - Use `data-testid` attributes
|
|
4. **Conditional assertions that always pass** - Write deterministic tests
|
|
5. **Static test data names** - Use timestamps for uniqueness
|
|
6. **Missing cleanup** - Delete created resources to prevent pollution
|
|
|
|
## Troubleshooting
|
|
|
|
### Tests Failing Intermittently
|
|
|
|
1. Replace `waitForTimeout()` with proper semantic waits
|
|
2. Ensure toasts are dismissed: `await page.dismissToasts()`
|
|
3. Add `waitForPageLoad()` after navigation
|
|
4. Wait for sheets/modals to complete animation: `await page.waitForSheetAnimation()`
|
|
|
|
### Tests Pass Individually but Fail Together
|
|
|
|
1. Add cleanup for created resources
|
|
2. Use unique names with `Date.now()` timestamps
|
|
3. Check for leftover state from previous tests
|
|
|
|
### Element Not Clickable
|
|
|
|
1. Ensure element is visible: `await element.waitFor({ state: 'visible' })`
|
|
2. Scroll element into view: `await element.scrollIntoViewIfNeeded()`
|
|
3. Dismiss overlaying toasts: `await page.dismissToasts()`
|
|
4. Don't use `{ force: true }` - fix the root cause
|