Skip to main content

Overview

Documenso uses Playwright for end-to-end (E2E) testing. Tests are organized by type and run in parallel to ensure fast feedback during development.

Test Stack

  • Framework: Playwright 1.56.1
  • Test Location: packages/app-tests/e2e/
  • Configuration: packages/app-tests/playwright.config.ts
  • Test Types: API tests, UI tests, License tests

Test Organization

Tests are organized into three categories:

API Tests

  • Location: packages/app-tests/e2e/api/**/*.spec.ts
  • Purpose: Test API endpoints and tRPC routes
  • Workers: 10 parallel workers
  • Run faster: No browser rendering required

UI Tests

  • Location: packages/app-tests/e2e/**/*.spec.ts (excluding api/ and license/)
  • Purpose: Test user interface and workflows
  • Workers: Dynamic (2-6 workers based on CPU cores)
  • Browser: Desktop Chrome (1920x1200 viewport)

License Tests

  • Location: packages/app-tests/e2e/license/**/*.spec.ts
  • Purpose: Test enterprise license features
  • Workers: 1 (serial execution due to shared license file)
  • Browser: Desktop Chrome

Running Tests

Quick Start

1

Ensure services are running

Start the development environment:
# Start Docker services and database
npm run dx

# Start the application
npm run dev
The application must be running on http://localhost:3000 before running tests.
2

Run tests in development

npm run test:dev -w @documenso/app-tests
This runs all tests against your local development server.

Test Commands

# Run all tests
npm run test:dev -w @documenso/app-tests

# Run with UI mode (interactive)
npm run test-ui:dev -w @documenso/app-tests

# Run specific test file
npm run test:dev -w @documenso/app-tests -- e2e/api/auth.spec.ts

# Run tests matching a pattern
npm run test:dev -w @documenso/app-tests -- --grep "document signing"

Test Projects

Playwright is configured with three test projects:

API Tests Project

# Run only API tests
playwright test --project=api
  • Fastest tests (no browser)
  • 10 parallel workers
  • Tests HTTP endpoints and tRPC routes

UI Tests Project

# Run only UI tests
playwright test --project=ui
  • Browser-based tests
  • Dynamic workers (2-6 based on CPU)
  • Tests user interactions and workflows

License Tests Project

# Run only license tests
playwright test --project=license
  • Serial execution (1 worker)
  • Tests enterprise features
  • Requires license file setup

Playwright UI Mode

The Playwright UI provides an interactive testing experience:
1

Start UI mode

npm run test-ui:dev -w @documenso/app-tests
2

Use the interface

The UI mode allows you to:
  • Run individual tests or test suites
  • Watch tests execute in real-time
  • Inspect each step with screenshots
  • Debug failed tests
  • Time-travel through test execution

Writing Tests

Test File Structure

Tests should follow this structure:
import { expect, test } from '@playwright/test';

test.describe('Feature Name', () => {
  test.beforeEach(async ({ page }) => {
    // Setup before each test
  });

  test('should perform action', async ({ page }) => {
    // Test implementation
    await page.goto('/path');
    await expect(page.locator('h1')).toContainText('Expected Text');
  });

  test('should handle error case', async ({ page }) => {
    // Error handling test
  });
});

API Test Example

import { expect, test } from '@playwright/test';

test.describe('API: Authentication', () => {
  test('POST /api/auth/signup should create user', async ({ request }) => {
    const response = await request.post('/api/auth/signup', {
      data: {
        email: 'test@example.com',
        password: 'SecurePassword123',
        name: 'Test User',
      },
    });

    expect(response.ok()).toBeTruthy();
    const body = await response.json();
    expect(body).toHaveProperty('id');
    expect(body.email).toBe('test@example.com');
  });
});

UI Test Example

import { expect, test } from '@playwright/test';

test.describe('Document Signing', () => {
  test('should complete signing workflow', async ({ page }) => {
    // Navigate to sign page
    await page.goto('/sign/abc123');

    // Fill in signature field
    await page.locator('[data-testid="signature-canvas"]').click();
    await page.locator('[data-testid="confirm-signature"]').click();

    // Submit
    await page.locator('button:has-text("Complete")').click();

    // Verify success
    await expect(page.locator('text=Document signed successfully')).toBeVisible();
  });
});

Test Configuration

The Playwright configuration is located at packages/app-tests/playwright.config.ts.

Key Configuration Options

{
  testDir: './e2e',
  fullyParallel: true,
  workers: 10, // For API tests
  maxFailures: process.env.CI ? 1 : undefined,
  retries: process.env.CI ? 4 : 1,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
    actionTimeout: 15_000,
    navigationTimeout: 30_000,
  },
  timeout: 60_000,
}

Timeouts

  • Test timeout: 60 seconds (entire test)
  • Action timeout: 15 seconds (individual actions)
  • Navigation timeout: 30 seconds (page navigation)

Retries

  • CI: 4 retries on failure
  • Local: 1 retry on failure

Trace & Video

Traces and videos are captured on test failures for debugging:
# View test report with traces
npx playwright show-report

Environment Variables for Tests

Test-specific environment variables are defined in .env:
# E2E Test Configuration
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"

# Disable rate limiting for tests (optional)
DANGEROUS_BYPASS_RATE_LIMITS=true

Test Helpers and Fixtures

Common test utilities and fixtures should be created for reusable functionality:

Authentication Fixture Example

import { test as base } from '@playwright/test';

type Fixtures = {
  authenticatedPage: Page;
};

export const test = base.extend<Fixtures>({
  authenticatedPage: async ({ page }, use) => {
    // Login
    await page.goto('/signin');
    await page.fill('[name="email"]', process.env.E2E_TEST_AUTHENTICATE_USER_EMAIL);
    await page.fill('[name="password"]', process.env.E2E_TEST_AUTHENTICATE_USER_PASSWORD);
    await page.click('button[type="submit"]');
    
    await page.waitForURL('/documents');
    
    await use(page);
  },
});
Usage:
import { test, expect } from './fixtures/auth';

test('should access authenticated route', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/settings');
  await expect(authenticatedPage.locator('h1')).toContainText('Settings');
});

Debugging Tests

Debugging Failed Tests

1

Run in debug mode

npx playwright test --debug
2

Use Playwright Inspector

The Playwright Inspector allows you to:
  • Step through test execution
  • Inspect page state
  • View console logs
  • Modify selectors in real-time
3

View traces

After a test failure:
npx playwright show-report
Click on the failed test to view:
  • Screenshots at each step
  • Network activity
  • Console logs
  • Test timeline

Common Debugging Commands

# Run with headed browser (see what's happening)
playwright test --headed

# Run a single test in debug mode
playwright test e2e/api/auth.spec.ts --debug

# Generate test code using Codegen
playwright codegen http://localhost:3000

# Show test report
playwright show-report

Continuous Integration

Tests run automatically in CI/CD pipelines:

CI Command

npm run ci
This command:
  1. Builds the Remix application
  2. Runs all E2E tests

CI Configuration

In CI environments:
  • Tests fail fast (stop on first failure)
  • 4 retries per test
  • Traces and videos captured on failure
  • Test reports uploaded as artifacts

Performance Optimization

Worker Calculation

The UI test workers are calculated based on CPU cores:
function calculateWorkers() {
  const total = os.cpus().length;
  // Reserve 2 cores for the system
  const usable = Math.max(total - 2, 1);
  // 1 worker per 2 cores, minimum 1
  const workers = Math.max(Math.floor(usable / 2), 1);
  // Max 6 workers
  return Math.min(workers, 6);
}

Test Isolation

Each test runs in isolation:
  • Separate browser context
  • Clean database state (via test fixtures)
  • No shared state between tests

Best Practices

Test Writing

  1. Use data-testid attributes for stable selectors:
    await page.locator('[data-testid="submit-button"]').click();
    
  2. Wait for explicit conditions:
    await expect(page.locator('text=Success')).toBeVisible();
    
  3. Avoid hard-coded waits:
    // Bad
    await page.waitForTimeout(5000);
    
    // Good
    await page.waitForURL('/dashboard');
    
  4. Clean up test data:
    test.afterEach(async ({ page }) => {
      // Delete test documents
    });
    

Test Organization

  1. Group related tests in describe blocks
  2. Use descriptive test names
  3. Keep tests focused and atomic
  4. Share setup logic via fixtures

Troubleshooting

Tests Timing Out

Increase timeouts for slow operations:
test('slow operation', async ({ page }) => {
  test.setTimeout(120_000); // 2 minutes
  
  await page.goto('/slow-page');
});

Flaky Tests

  1. Add explicit waits:
    await expect(element).toBeVisible();
    
  2. Disable animations: The config already disables animations via cookie.
  3. Use retry logic for network requests:
    await expect(async () => {
      const response = await request.get('/api/status');
      expect(response.ok()).toBeTruthy();
    }).toPass();
    

Browser Not Found

Install Playwright browsers:
npx playwright install

Next Steps