Skip to main content
AFFiNE has a comprehensive test suite covering unit tests, integration tests, and end-to-end tests.

Test Types

Unit Tests

Test individual functions and components in isolation

Integration Tests

Test how multiple components work together

E2E Tests

Test complete user workflows in a real browser

Running Tests

All Tests

Run the entire test suite:
yarn test

Unit Tests

Run unit tests with Vitest:
# Run all unit tests
yarn test

# Run specific test file
yarn test path/to/test.spec.ts

# Run tests in watch mode
yarn test --watch

# Run with UI
yarn test:ui

# Generate coverage
yarn test:coverage

Backend Tests

Run backend tests with Ava:
# All backend tests
yarn workspace @affine/server test

# Specific test file
yarn workspace @affine/server test src/__tests__/auth/service.spec.ts

# With coverage
yarn workspace @affine/server test:coverage

E2E Tests

Run end-to-end tests with Playwright:
1

Install Playwright browsers

npx playwright install
2

Start the backend server

yarn workspace @affine/server dev
3

Run E2E tests

# Local-first tests
yarn workspace @affine-test/affine-local e2e

# Cloud tests
yarn workspace @affine-test/affine-cloud e2e

# Headed mode (see browser)
yarn workspace @affine-test/affine-local e2e --headed

# Debug mode
yarn workspace @affine-test/affine-local e2e --debug

Writing Unit Tests

Frontend Unit Tests

Frontend tests use Vitest and React Testing Library:
Button.spec.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test, vi } from 'vitest';
import { Button } from './Button';

test('renders button with text', () => {
  render(<Button>Click me</Button>);
  expect(screen.getByRole('button')).toHaveTextContent('Click me');
});

test('calls onClick when clicked', async () => {
  const onClick = vi.fn();
  render(<Button onClick={onClick}>Click me</Button>);
  
  await userEvent.click(screen.getByRole('button'));
  expect(onClick).toHaveBeenCalledOnce();
});

Backend Unit Tests

Backend tests use Ava:
auth.spec.ts
import test from 'ava';
import { AuthService } from './auth.service';

test('should hash password correctly', async t => {
  const service = new AuthService();
  const password = 'secret123';
  
  const hashed = await service.hashPassword(password);
  t.not(hashed, password);
  t.true(await service.verifyPassword(password, hashed));
});

test('should generate valid JWT token', async t => {
  const service = new AuthService();
  const userId = 'user-123';
  
  const token = await service.generateToken(userId);
  t.truthy(token);
  
  const decoded = await service.verifyToken(token);
  t.is(decoded.userId, userId);
});

Writing E2E Tests

E2E tests use Playwright:
workspace.spec.ts
import { expect, test } from '@playwright/test';

test('create workspace', async ({ page }) => {
  // Navigate to app
  await page.goto('http://localhost:8080');
  
  // Click new workspace button
  await page.getByRole('button', { name: /new workspace/i }).click();
  
  // Enter workspace name
  await page.getByLabel('Workspace name').fill('My Workspace');
  
  // Click create
  await page.getByRole('button', { name: /create/i }).click();
  
  // Verify workspace created
  await expect(page.getByText('My Workspace')).toBeVisible();
});

test('create and edit document', async ({ page }) => {
  await page.goto('http://localhost:8080');
  
  // Create new page
  await page.getByRole('button', { name: /new page/i }).click();
  
  // Wait for editor
  const editor = page.locator('[data-block-is-root="true"]');
  await expect(editor).toBeVisible();
  
  // Type content
  await editor.click();
  await page.keyboard.type('Hello, world!');
  
  // Verify content
  await expect(editor).toContainText('Hello, world!');
});

Page Object Pattern

Use page objects for reusable test code:
WorkspacePage.ts
import type { Page } from '@playwright/test';

export class WorkspacePage {
  constructor(private page: Page) {}
  
  async createWorkspace(name: string) {
    await this.page.getByRole('button', { name: /new workspace/i }).click();
    await this.page.getByLabel('Workspace name').fill(name);
    await this.page.getByRole('button', { name: /create/i }).click();
  }
  
  async openWorkspace(name: string) {
    await this.page.getByText(name).click();
  }
  
  async expectWorkspaceVisible(name: string) {
    await this.page.getByText(name).waitFor({ state: 'visible' });
  }
}

// Usage in tests
test('use page object', async ({ page }) => {
  const workspace = new WorkspacePage(page);
  await workspace.createWorkspace('Test Workspace');
  await workspace.expectWorkspaceVisible('Test Workspace');
});

Testing Best Practices

Unit Tests

Focus on what the component does, not how it does it:
// Good: Test behavior
test('shows error when email is invalid', () => {
  render(<LoginForm />);
  userEvent.type(screen.getByLabel('Email'), 'invalid');
  expect(screen.getByText('Invalid email')).toBeVisible();
});

// Bad: Test implementation
test('calls validateEmail function', () => {
  const { validateEmail } = render(<LoginForm />);
  expect(validateEmail).toHaveBeenCalled();
});
// Good
test('creates workspace with specified name', () => {});
test('shows error when workspace name is empty', () => {});

// Bad
test('workspace test 1', () => {});
test('it works', () => {});
Each test should be able to run in isolation:
test('test 1', () => {
  // Set up state
  const state = createInitialState();
  // Run test
  // Clean up if needed
});

test('test 2', () => {
  // Don't rely on test 1's state
  const state = createInitialState();
});

E2E Tests

// Good: Stable selector
await page.locator('[data-testid="create-workspace-btn"]').click();

// Bad: Fragile selector
await page.locator('div.sidebar > button:nth-child(2)').click();
// Good: Wait explicitly
await page.getByText('Welcome').waitFor({ state: 'visible' });
await page.getByRole('button', { name: 'Submit' }).click();

// Bad: Use arbitrary timeouts
await page.waitForTimeout(1000);
await page.click('button');
// Good: Test as a user would
test('user can share document', async ({ page }) => {
  await page.goto('/doc/123');
  await page.getByRole('button', { name: 'Share' }).click();
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByRole('button', { name: 'Send' }).click();
  await expect(page.getByText('Shared successfully')).toBeVisible();
});

// Bad: Test implementation details
test('share API is called', async ({ page }) => {
  const response = await page.request.post('/api/share', {});
  expect(response.ok()).toBe(true);
});

Mocking

Mocking Functions

import { vi } from 'vitest';

test('mocks API call', async () => {
  const fetchMock = vi.fn().mockResolvedValue({
    ok: true,
    json: async () => ({ id: '123', name: 'Test' })
  });
  
  globalThis.fetch = fetchMock;
  
  const result = await getWorkspace('123');
  expect(fetchMock).toHaveBeenCalledWith('/api/workspace/123');
  expect(result.name).toBe('Test');
});

Mocking Modules

import { vi } from 'vitest';

vi.mock('./auth.service', () => ({
  AuthService: class {
    async getCurrentUser() {
      return { id: '123', name: 'Test User' };
    }
  }
}));

test('uses mocked auth service', async () => {
  const user = await authService.getCurrentUser();
  expect(user.name).toBe('Test User');
});

Coverage

Generate test coverage reports:
# Frontend coverage
yarn test:coverage

# Backend coverage
yarn workspace @affine/server test:coverage

# View coverage report
open coverage/index.html
Coverage Targets:
  • Statements: >80%
  • Branches: >75%
  • Functions: >80%
  • Lines: >80%

Continuous Integration

Tests run automatically on:
  • Every pull request
  • Every commit to main
  • Nightly builds
CI Pipeline:
  1. Lint code
  2. Type check
  3. Run unit tests
  4. Run integration tests
  5. Run E2E tests
  6. Generate coverage
  7. Upload artifacts

Debugging Tests

Debug Unit Tests

# Run single test in debug mode
yarn test --inspect-brk path/to/test.spec.ts

# Or use VS Code debugger
# Add breakpoint and press F5

Debug E2E Tests

# Run in headed mode
yarn workspace @affine-test/affine-local e2e --headed

# Run in debug mode (opens inspector)
yarn workspace @affine-test/affine-local e2e --debug

# Run with slow motion
yarn workspace @affine-test/affine-local e2e --headed --slow-mo=1000

Playwright Inspector

# Open Playwright Inspector
PWDEBUG=1 yarn workspace @affine-test/affine-local e2e

Test Utilities

Custom Matchers

import { expect } from 'vitest';

expect.extend({
  toBeWorkspace(received) {
    const pass = received && received.id && received.name;
    return {
      pass,
      message: () => `Expected ${received} to be a workspace object`
    };
  }
});

// Usage
test('returns workspace', () => {
  const workspace = createWorkspace();
  expect(workspace).toBeWorkspace();
});

Test Fixtures

fixtures.ts
export const createMockUser = () => ({
  id: 'user-123',
  name: 'Test User',
  email: 'test@example.com'
});

export const createMockWorkspace = () => ({
  id: 'workspace-123',
  name: 'Test Workspace',
  ownerId: 'user-123'
});