Overview
Skiff uses a comprehensive testing stack built on Jest and React Testing Library to ensure code quality and reliability across all applications.
The project uses Jest 28.1.1 with SWC for fast compilation, replacing the traditional Babel setup.
Testing Stack
Jest Version : 28.1.1Test runner and assertion library with built-in mocking and coverage reporting.
SWC Version : 0.2.15 (@swc/jest)Fast TypeScript/JavaScript compiler that replaces Babel for faster test execution.
React Testing Library Version : 12.1.2Testing utilities that encourage best practices by testing components as users interact with them.
jest-dom Version : 5.16.2Custom matchers for asserting on DOM nodes (e.g., toBeInTheDocument()).
Additional Libraries
@testing-library/react-hooks (7.0.2): Test React hooks in isolation
@testing-library/user-event (14.1.1): Simulate user interactions
@testing-library/dom (8.13.0): DOM testing utilities
jest-environment-jsdom (28.1.1): Browser-like environment for tests
jest-canvas-mock (2.4.0): Mock canvas API for skemail-web
fake-indexeddb (4.0.0): Mock IndexedDB for calendar-web tests
msw (0.48.0): Mock Service Worker for API mocking (calendar-web)
Running Tests
Application-Level Tests
Each application has its own test suite:
# Run all tests
yarn workspace skemail-web test
# Watch mode
yarn workspace skemail-web test-watch
# With coverage
yarn workspace skemail-web test --coverage
Test configuration:
Max workers: 2
Heap monitoring: Enabled
Memory limit: 4096 MB
# Run all tests
yarn workspace calendar-web test
# Watch mode
yarn workspace calendar-web test-watch
# With coverage
yarn workspace calendar-web test --coverage
Test configuration:
Max workers: 2
Heap monitoring: Enabled
Type Checking
Run TypeScript compiler without emitting files:
# Check types in skemail-web
yarn workspace skemail-web ts
# Check types in calendar-web
yarn workspace calendar-web ts
The test script runs type checking before executing tests to catch type errors early.
Test Organization
Test File Locations
Tests are organized in two ways:
Dedicated test directory : skemail-web/tests/
skemail-web/
├── tests/
│ ├── ComposePanel.test.tsx
│ ├── ThreadBlock.test.tsx
│ ├── userUtils.test.ts
│ └── ...
Co-located with source : src/**/*.test.ts
skemail-web/
├── src/
│ ├── utils/
│ │ ├── emailUtils.ts
│ │ └── emailUtils.test.ts
│ └── components/
│ └── Settings/
│ └── Filters/
│ ├── Filters.utils.ts
│ └── Filters.utils.test.ts
Naming Conventions
Component tests: ComponentName.test.tsx
Utility tests: utilityName.test.ts
Hook tests: useHookName.test.tsx
Writing Tests
Basic Test Structure
Here’s an example from the codebase:
import { formatEmailAddress } from 'skiff-front-utils' ;
import { getMailDomain , isSkiffAddress } from 'skiff-utils' ;
describe ( 'userUtils' , () => {
describe ( 'formatEmailAddress' , () => {
it ( 'returns unaltered email address if already valid' , () => {
const testEmail = '[email protected] ' ;
const actual = formatEmailAddress ( testEmail );
expect ( actual ). toBe ( testEmail );
});
it ( 'appends email domain to wallet address' , () => {
const testEthAddress = '0x1111000000000000000000000000000000001111' ;
const abbreviated = formatEmailAddress ( testEthAddress , true );
expect ( abbreviated ). toBe ( '0x111...1111' + '@' + getMailDomain ());
const fullLength = formatEmailAddress ( testEthAddress , false );
expect ( fullLength ). toBe ( testEthAddress + '@' + getMailDomain ());
});
});
describe ( 'isSkiffAddress' , () => {
const customDomains = [ 'skiff.money' , 'skiff.earth' ];
it ( 'check getMailDomain (town/city/com) works' , () => {
const address = '[email protected] ' ;
expect ( isSkiffAddress ( address , customDomains )). toBe ( true );
});
it ( 'check skiff custom domains' , () => {
const address = '[email protected] ' ;
expect ( isSkiffAddress ( address , customDomains )). toBe ( true );
});
it ( 'check non skiff domain' , () => {
const address = '[email protected] ' ;
expect ( isSkiffAddress ( address , customDomains )). toBe ( false );
});
});
});
Testing React Components
Component Rendering
User Interactions
Testing Hooks
import { render , screen } from '@testing-library/react' ;
import { Button } from './Button' ;
describe ( 'Button' , () => {
it ( 'renders with correct text' , () => {
render ( < Button > Click me </ Button > );
expect ( screen . getByText ( 'Click me' )). toBeInTheDocument ();
});
it ( 'calls onClick when clicked' , () => {
const handleClick = jest . fn ();
render ( < Button onClick = { handleClick } > Click me </ Button > );
screen . getByText ( 'Click me' ). click ();
expect ( handleClick ). toHaveBeenCalledTimes ( 1 );
});
});
import { render , screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import { ComposePanel } from './ComposePanel' ;
describe ( 'ComposePanel' , () => {
it ( 'updates recipient when user types' , async () => {
const user = userEvent . setup ();
render ( < ComposePanel /> );
const input = screen . getByLabelText ( 'To:' );
await user . type ( input , '[email protected] ' );
expect ( input ). toHaveValue ( '[email protected] ' );
});
});
import { renderHook } from '@testing-library/react-hooks' ;
import { useDefaultEmailAlias } from './useDefaultEmailAlias' ;
describe ( 'useDefaultEmailAlias' , () => {
it ( 'returns default alias' , () => {
const { result } = renderHook (() => useDefaultEmailAlias ());
expect ( result . current ). toBeDefined ();
});
it ( 'updates when user changes default' , () => {
const { result , rerender } = renderHook (
( props ) => useDefaultEmailAlias ( props . userId ),
{ initialProps: { userId: '1' } }
);
// Trigger update
rerender ({ userId: '2' });
expect ( result . current ). toBeDefined ();
});
});
Testing Utilities
Pure Functions
Async Functions
import { formatDate } from './dateUtils' ;
describe ( 'formatDate' , () => {
it ( 'formats date correctly' , () => {
const date = new Date ( '2024-03-11' );
expect ( formatDate ( date )). toBe ( 'March 11, 2024' );
});
it ( 'handles invalid dates' , () => {
expect ( formatDate ( null )). toBe ( '' );
});
});
import { fetchUserData } from './api' ;
describe ( 'fetchUserData' , () => {
it ( 'fetches user data successfully' , async () => {
const data = await fetchUserData ( 'user-123' );
expect ( data ). toHaveProperty ( 'id' , 'user-123' );
});
it ( 'handles errors gracefully' , async () => {
await expect (
fetchUserData ( 'invalid-id' )
). rejects . toThrow ( 'User not found' );
});
});
Mocking
Mocking Modules
jest . mock ( 'skiff-front-utils' , () => ({
formatEmailAddress: jest . fn (( email ) => email ),
useCurrentUser: jest . fn (() => ({ id: 'test-user' }))
}));
Mocking API Calls (MSW)
calendar-web uses MSW for API mocking:
import { rest } from 'msw' ;
import { setupServer } from 'msw/node' ;
const server = setupServer (
rest . get ( '/api/events' , ( req , res , ctx ) => {
return res ( ctx . json ({ events: [] }));
})
);
beforeAll (() => server . listen ());
afterEach (() => server . resetHandlers ());
afterAll (() => server . close ());
Mocking Browser APIs
// Mock localStorage
const localStorageMock = {
getItem: jest . fn (),
setItem: jest . fn (),
clear: jest . fn ()
};
global . localStorage = localStorageMock as any ;
// Mock IndexedDB (calendar-web)
import 'fake-indexeddb/auto' ;
// Mock canvas (skemail-web)
import 'jest-canvas-mock' ;
Test Patterns
Arrange-Act-Assert
it ( 'updates email subject' , () => {
// Arrange
const initialSubject = 'Hello' ;
const newSubject = 'Hello World' ;
// Act
const result = updateSubject ( initialSubject , newSubject );
// Assert
expect ( result ). toBe ( newSubject );
});
Testing Redux State
import { configureStore } from '@reduxjs/toolkit' ;
import { mailReducer } from './mailSlice' ;
describe ( 'mailSlice' , () => {
let store ;
beforeEach (() => {
store = configureStore ({ reducer: { mail: mailReducer } });
});
it ( 'updates selected thread' , () => {
store . dispatch ( selectThread ( 'thread-123' ));
expect ( store . getState (). mail . selectedThread ). toBe ( 'thread-123' );
});
});
Testing Styled Components
import { render } from '@testing-library/react' ;
import { ThemeProvider } from 'styled-components' ;
import { theme } from './theme' ;
const renderWithTheme = ( component ) => {
return render (
< ThemeProvider theme = { theme } >
{ component }
</ ThemeProvider >
);
};
it ( 'applies correct theme colors' , () => {
const { container } = renderWithTheme (< Button > Test </ Button > );
expect ( container . firstChild ). toHaveStyle ( 'color: #333' );
});
Code Coverage
Running Coverage Reports
# Generate coverage report
yarn workspace skemail-web test --coverage
# View coverage in browser
open coverage/lcov-report/index.html
Coverage Thresholds
Configure in jest.config.js:
module . exports = {
coverageThreshold: {
global: {
statements: 70 ,
branches: 60 ,
functions: 70 ,
lines: 70
}
}
};
Continuous Integration
Tests run automatically on:
Pull Requests : All tests must pass before merging
Commits to main : Ensures main branch stability
CI Commands
The CI pipeline runs:
# Type checking
yarn typescript
# Linting
yarn lint
# Tests
yarn test
Best Practices
Test Behavior, Not Implementation
Focus on testing what the component does, not how it does it: // Good: Tests behavior
it ( 'displays error message when email is invalid' , () => {
render (< EmailInput value = "invalid" />);
expect ( screen . getByText ( 'Invalid email' )). toBeInTheDocument ();
});
// Avoid: Tests implementation
it ( 'calls validateEmail function' , () => {
const spy = jest . spyOn ( utils , 'validateEmail' );
render (< EmailInput value = "test" />);
expect ( spy ). toHaveBeenCalled ();
});
Use Descriptive Test Names
Test names should clearly describe what’s being tested: // Good
it ( 'displays loading spinner while fetching emails' , () => { });
// Avoid
it ( 'works correctly' , () => { });
Each test should be independent: // Use beforeEach for setup
beforeEach (() => {
localStorage . clear ();
jest . clearAllMocks ();
});
// Clean up after tests
afterEach (() => {
cleanup ();
});
Don’t just test the happy path: describe ( 'validateEmail' , () => {
it ( 'accepts valid email' , () => { });
it ( 'rejects email without @' , () => { });
it ( 'rejects email without domain' , () => { });
it ( 'handles empty string' , () => { });
it ( 'handles null value' , () => { });
});
Use Testing Library Queries
Follow Testing Library best practices: // Priority order:
screen . getByRole ( 'button' , { name: /submit/ i })
screen . getByLabelText ( 'Email address' )
screen . getByPlaceholderText ( 'Enter email' )
screen . getByText ( 'Submit' )
screen . getByTestId ( 'submit-button' ) // Last resort
Debugging Tests
Running Single Test
# Run specific test file
yarn test userUtils.test.ts
# Run tests matching pattern
yarn test --testNamePattern= "formatEmailAddress"
Debug Output
import { screen , debug } from '@testing-library/react' ;
it ( 'debugging test' , () => {
render (< Component />);
// Print entire DOM
screen . debug ();
// Print specific element
screen . debug ( screen . getByRole ( 'button' ));
});
VS Code Debugging
Add to .vscode/launch.json:
{
"type" : "node" ,
"request" : "launch" ,
"name" : "Jest Current File" ,
"program" : "${workspaceFolder}/node_modules/.bin/jest" ,
"args" : [ "${file}" , "--runInBand" ],
"console" : "integratedTerminal"
}
Resources
Jest Documentation Official Jest documentation
Testing Library React Testing Library guide
Testing Best Practices Common testing mistakes to avoid
MSW Documentation Mock Service Worker docs
Next Steps
Development Setup Set up your development environment
Contributing Learn how to contribute