Documentation Index Fetch the complete documentation index at: https://mintlify.com/toeverything/AFFiNE/llms.txt
Use this file to discover all available pages before exploring further.
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:
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:
Install Playwright browsers
Start the backend server
yarn workspace @affine/server dev
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:
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:
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:
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:
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
Test behavior, not implementation
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 ();
});
Use meaningful test descriptions
// 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
Use data attributes for selectors
// 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 ();
Wait for elements properly
// 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' );
Test user workflows, not API calls
// 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:
Lint code
Type check
Run unit tests
Run integration tests
Run E2E tests
Generate coverage
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
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'
});
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