mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
* feat: replace request send icon with Send/Cancel buttons --------- Co-authored-by: naman-bruno <naman@usebruno.com>
471 lines
12 KiB
Markdown
471 lines
12 KiB
Markdown
# 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.getByTestId('send-arrow-icon').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.
|