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('[email protected]');
  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: '[email protected]'
});

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

Development Setup

Set up your local environment

Coding Guidelines

Follow our coding standards

Vitest Docs

Learn more about Vitest

Playwright Docs

Learn more about Playwright

Build docs developers (and LLMs) love