Back to Bruno

Playwright Testing Guide for Bruno

docs/playwright-testing-guide.md

3.3.012.0 KB
Original Source

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

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


For questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository.