Files
bifrost/tests/e2e/README.md
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

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