mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: add Playwright testing guide for Bruno application
This commit is contained in:
470
docs/playwright-testing-guide.md
Normal file
470
docs/playwright-testing-guide.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# Playwright Testing Guide for Bruno
|
||||
|
||||
This guide explains how to create and run Playwright test cases for the Bruno application using the UI.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Creating Tests Using Codegen](#creating-tests-using-codegen)
|
||||
- [Manual Test Creation](#manual-test-creation)
|
||||
- [Test Structure and Organization](#test-structure-and-organization)
|
||||
- [Available Test Fixtures](#available-test-fixtures)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Examples](#examples)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Overview
|
||||
|
||||
Bruno uses Playwright for end-to-end testing of its Electron application. The testing setup includes custom fixtures for Electron app testing and utilities for managing test data.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js installed
|
||||
- All dependencies installed (`npm install`)
|
||||
- Electron app can be built and run
|
||||
|
||||
## Creating Tests Using Codegen
|
||||
|
||||
The easiest way to create tests is using Playwright's codegen feature, which records your UI interactions and generates test code.
|
||||
|
||||
### Using the Built-in Codegen Script
|
||||
|
||||
```bash
|
||||
# Generate a test with a specific name
|
||||
npm run test:codegen my-new-test
|
||||
|
||||
# Generate a test without specifying a name (will prompt for input)
|
||||
npm run test:codegen
|
||||
```
|
||||
|
||||
### What Happens During Codegen
|
||||
|
||||
1. The Electron app launches automatically
|
||||
2. Playwright Inspector opens in a separate window
|
||||
3. You interact with the Bruno UI
|
||||
4. Actions are recorded and converted to test code
|
||||
5. The generated test file is saved in `e2e-tests/`
|
||||
|
||||
### Codegen Workflow
|
||||
|
||||
1. **Start Recording**: Run the codegen command
|
||||
2. **Interact with UI**: Perform the actions you want to test
|
||||
3. **Add Assertions**: Use the inspector to add assertions
|
||||
4. **Save Test**: The test file is automatically generated
|
||||
5. **Review and Refine**: Edit the generated test as needed
|
||||
|
||||
## Manual Test Creation
|
||||
|
||||
You can also create tests manually by following the established patterns.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Test description', async ({ page }) => {
|
||||
// Test steps here
|
||||
await page.getByLabel('Some Label').click();
|
||||
|
||||
// Assertions
|
||||
await expect(page.getByText('Expected Text')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Test with Temporary Data
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Test with temporary data', async ({ page, createTmpDir }) => {
|
||||
// Create temporary directory for test data
|
||||
const testDir = await createTmpDir('test-collection');
|
||||
|
||||
// Test steps
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
|
||||
// Assertions
|
||||
await expect(page.getByText('test-collection')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Test Structure and Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
e2e-tests/
|
||||
├── 001-sanity-tests/ # Basic functionality tests
|
||||
│ ├── 001-home-screen.spec.ts
|
||||
│ └── 002-create-new-collection-and-new-request.spec.ts
|
||||
├── 002-feature-tests/ # Specific feature tests
|
||||
├── 003-integration-tests/ # Complex workflow tests
|
||||
└── bruno-testbench/ # Test utilities and helpers
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Files**: Use descriptive names with `.spec.ts` extension
|
||||
- **Tests**: Use clear, descriptive test names
|
||||
- **Folders**: Use numbered prefixes for ordering
|
||||
|
||||
### Test File Template
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
test('should perform specific action', async ({ page }) => {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
});
|
||||
|
||||
test('should handle error case', async ({ page }) => {
|
||||
// Test error scenarios
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Available Test Fixtures
|
||||
|
||||
The Bruno Playwright setup provides several custom fixtures:
|
||||
|
||||
### Core Fixtures
|
||||
|
||||
- `page`: Main page for testing
|
||||
- `context`: Browser context
|
||||
- `electronApp`: Electron application instance
|
||||
|
||||
### Utility Fixtures
|
||||
|
||||
- `createTmpDir`: Creates temporary directories for test data
|
||||
- `newPage`: Creates a new page instance
|
||||
- `pageWithUserData`: Page with custom user data
|
||||
- `launchElectronApp`: Launches a new Electron app instance
|
||||
- `reuseOrLaunchElectronApp`: Reuses existing app or launches new one
|
||||
|
||||
### Using Fixtures
|
||||
|
||||
```typescript
|
||||
test('Test with multiple fixtures', async ({ page, createTmpDir, electronApp }) => {
|
||||
const testDir = await createTmpDir('test-data');
|
||||
|
||||
// Your test logic here
|
||||
});
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test:e2e
|
||||
|
||||
# Run specific test file
|
||||
npx playwright test e2e-tests/001-sanity-tests/001-home-screen.spec.ts
|
||||
|
||||
# Run tests in a specific folder
|
||||
npx playwright test e2e-tests/001-sanity-tests/
|
||||
```
|
||||
|
||||
### Advanced Options
|
||||
|
||||
```bash
|
||||
# Run with UI mode (for debugging)
|
||||
npx playwright test --ui
|
||||
|
||||
# Run in headed mode (see browser)
|
||||
npx playwright test --headed
|
||||
|
||||
# Run with specific browser
|
||||
npx playwright test --project="Bruno Electron App"
|
||||
|
||||
# Run with debugging
|
||||
npx playwright test --debug
|
||||
|
||||
# Run with trace recording
|
||||
npx playwright test --trace on
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```bash
|
||||
# Install browsers for CI
|
||||
npx playwright install
|
||||
|
||||
# Run tests in CI mode
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Semantic Selectors
|
||||
|
||||
**Preferred:**
|
||||
|
||||
```typescript
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByLabel('Collection Name').fill('test');
|
||||
await page.getByText('Success message').toBeVisible();
|
||||
```
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```typescript
|
||||
await page.locator('.btn-primary').click();
|
||||
await page.locator('#collection-name').fill('test');
|
||||
```
|
||||
|
||||
### 2. Create Isolated Tests
|
||||
|
||||
Each test should be independent and not rely on other tests:
|
||||
|
||||
```typescript
|
||||
test('should create collection', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('collection-test');
|
||||
|
||||
// Test creates its own data
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
|
||||
// Clean up happens automatically via createTmpDir
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Add Meaningful Assertions
|
||||
|
||||
Always verify the expected outcomes:
|
||||
|
||||
```typescript
|
||||
test('should save request successfully', async ({ page }) => {
|
||||
// Arrange
|
||||
await page.getByLabel('Create Collection').click();
|
||||
|
||||
// Act
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Assert
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'GET request' })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Handle Async Operations
|
||||
|
||||
```typescript
|
||||
test('should wait for network requests', async ({ page }) => {
|
||||
// Wait for specific network request
|
||||
await page.waitForResponse((response) => response.url().includes('/api/endpoint'));
|
||||
|
||||
// Or wait for element to be stable
|
||||
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Use Test Data Management
|
||||
|
||||
```typescript
|
||||
test('should work with test data', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('test-data');
|
||||
|
||||
// Create test files
|
||||
await fs.writeFile(path.join(testDir, 'test.bru'), testContent);
|
||||
|
||||
// Use in test
|
||||
await page.getByLabel('Open Collection').click();
|
||||
await page.getByText(testDir).click();
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Basic Collection Creation
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should create a new collection', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('new-collection');
|
||||
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('My Test Collection');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await expect(page.getByText('My Test Collection')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Example 2: Request Creation and Execution
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should create and execute HTTP request', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('request-test');
|
||||
|
||||
// Create collection
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('Request Test');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Create request
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('Test Request');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('http://localhost:8081/ping');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Execute request
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
// Verify response
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
```
|
||||
|
||||
### Example 3: Environment Management
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should create and use environment variables', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('env-test');
|
||||
|
||||
// Setup collection
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('Environment Test');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Create environment
|
||||
await page.getByRole('button', { name: 'Environments' }).click();
|
||||
await page.getByRole('button', { name: 'Add Environment' }).click();
|
||||
await page.getByLabel('Environment Name').fill('Development');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Add variable
|
||||
await page.getByRole('button', { name: 'Add Variable' }).click();
|
||||
await page.getByLabel('Variable Name').fill('API_URL');
|
||||
await page.getByLabel('Variable Value').fill('http://localhost:3000');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.getByText('API_URL')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Electron App Not Starting**
|
||||
|
||||
```bash
|
||||
# Ensure dependencies are installed
|
||||
npm install
|
||||
|
||||
# Try running the app manually first
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
2. **Tests Timing Out**
|
||||
|
||||
```typescript
|
||||
// Increase timeout for specific test
|
||||
test('slow test', async ({ page }) => {
|
||||
test.setTimeout(60000); // 60 seconds
|
||||
// Test steps
|
||||
});
|
||||
```
|
||||
|
||||
3. **Element Not Found**
|
||||
|
||||
```typescript
|
||||
// Wait for element to be present
|
||||
await page.waitForSelector('[data-testid="element"]');
|
||||
|
||||
// Or use more specific selectors
|
||||
await page.getByRole('button', { name: 'Exact Button Text' }).click();
|
||||
```
|
||||
|
||||
4. **Flaky Tests**
|
||||
|
||||
```typescript
|
||||
// Use stable selectors
|
||||
await page.getByTestId('stable-id').click();
|
||||
|
||||
// Wait for state changes
|
||||
await page.waitForLoadState('networkidle');
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Run with debug mode
|
||||
npx playwright test --debug
|
||||
|
||||
# Run specific test in debug mode
|
||||
npx playwright test --debug e2e-tests/001-sanity-tests/001-home-screen.spec.ts
|
||||
```
|
||||
|
||||
### Trace Analysis
|
||||
|
||||
```bash
|
||||
# Run with trace recording
|
||||
npx playwright test --trace on
|
||||
|
||||
# View trace in browser
|
||||
npx playwright show-trace test-results/trace-*.zip
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The Playwright configuration is in `playwright.config.ts`:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
testDir: './e2e-tests',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? undefined : 1,
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'Bruno Electron App'
|
||||
}
|
||||
],
|
||||
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run dev:web',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
{
|
||||
command: 'npm start --workspace=packages/bruno-tests',
|
||||
url: 'http://localhost:8081/ping',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Playwright Test API](https://playwright.dev/docs/api/class-test)
|
||||
- [Electron Testing with Playwright](https://playwright.dev/docs/api/class-electronapplication)
|
||||
- [Bruno Project Structure](../readme.md)
|
||||
|
||||
---
|
||||
|
||||
For questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository.
|
||||
Reference in New Issue
Block a user