Skip to main content
Trezor Suite uses comprehensive testing strategies to ensure code quality and reliability. This guide covers unit testing, integration testing, and end-to-end testing practices.

Running Tests

# Run all unit tests
yarn test:unit

# Test specific package
yarn workspace @trezor/suite-common test:unit

# Run single test file
yarn workspace @trezor/suite-common test:unit --coverage=0 file.test.ts

Test Structure and Organization

File Naming and Location

1

Test files

Place tests in __tests__ folders with .test.ts extension:
my-module/
└── src/
    ├── __tests__/
    │   └── utils.test.ts
    └── utils.ts
2

Type tests

Use .type-test.ts suffix to prevent Jest execution:
packages/utils/tests/typedObjectFromEntries.type-test.ts
3

Mocks and fixtures

Place in mocks folder at package root with mock prefix:
my-module/
├── mocks/
│   ├── mockDevice.ts
│   └── index.ts
└── src/
    └── device.ts
Test folder must be in the same directory as the implementation, not in a separate root tests/ folder.

Writing Unit Tests

Using @suite/test-utils

Trezor Suite provides custom test utilities for React component testing:

Basic Component Testing

import { renderWithBasicProvider, screen, userEvent } from '@suite/test-utils';

describe('Counter', () => {
    it('should start with 0 value', () => {
        renderWithBasicProvider(<Counter />);

        expect(screen.getByLabelText('Counter value')).toHaveTextContent('0');
    });

    it('should increment value on button press', async () => {
        renderWithBasicProvider(<Counter />);
        const user = userEvent.setup();

        await user.click(screen.getByText('+'));

        expect(screen.getByLabelText('Counter value')).toHaveTextContent('1');
    });
});

Testing Hooks

import { renderHook, act } from '@suite/test-utils';

describe('useCounter', () => {
    it('should increment count', () => {
        const { result } = renderHook(() => useCounter());

        act(() => {
            result.current.increment();
        });

        expect(result.current.count).toBe(1);
    });
});

Best Practices

Avoid Testing Implementation Details

Test behavior, not implementation.
it('should show success message after form submission', async () => {
    renderWithBasicProvider(<LoginForm />);
    const user = userEvent.setup();

    await user.type(screen.getByLabelText(/email/i), '[email protected]');
    await user.type(screen.getByLabelText(/password/i), 'password123');
    await user.click(screen.getByRole('button', { name: /submit/i }));

    expect(screen.getByText('Login successful')).toBeInTheDocument();
});

Use userEvent Over fireEvent

userEvent simulates real user interactions more accurately than fireEvent.
import { renderWithBasicProvider, userEvent, screen } from '@suite/test-utils';

const user = userEvent.setup();
await user.click(screen.getByRole('button'));
await user.type(screen.getByLabelText('Email'), '[email protected]');

Prefer getByRole for Queries

Use getByRole to encourage accessible components.
Priority order
// Best - Semantic and accessible
screen.getByRole('button', { name: /submit/i });

// Good - Accessible
screen.getByLabelText('Email address');

// OK - For text content
screen.getByText('Welcome back');

// Last resort - Avoid if possible
screen.getByTestId('submit-button');

Handle Translations in Tests

Text can change with Crowdin syncs. Use translation IDs instead of literal strings.
expect(screen.getByText(getTranslation('path.to.translation'))).toBeTruthy();

// When text must not change:
expect(screen.getByText(getTranslation('path.to.translation'))).toBe(
    'I want a developer to check this important text if it is changed in Crowdin.',
);

Wait for Async Operations

Always use waitFor when testing asynchronous behavior.
import { renderWithBasicProvider, waitFor, screen, userEvent } from '@suite/test-utils';

it('should load data on mount', async () => {
    renderWithBasicProvider(<DataComponent />);
    const user = userEvent.setup();

    await user.click(screen.getByRole('button', { name: /load/i }));

    await waitFor(() => {
        expect(screen.getByText('Data loaded')).toBeInTheDocument();
    });
});

Mocks and Fixtures

Typing Mocks

All mocks must be fully typed. Using as to cast incomplete objects is a last resort.
import { Device } from '@trezor/connect';

export const mockDevice = (override?: Partial<Device>): Device => ({
    id: 'device-1',
    path: '/device/path',
    label: 'My Trezor',
    features: {
        vendor: 'trezor.io',
        major_version: 2,
        minor_version: 0,
        patch_version: 0,
        // ... all required fields
    },
    ...override,
});

Mock Organization

1

Location

Place mocks in the same package where the type declaration resides:
device-types/
├── mocks/
│   ├── mockDevice.ts
│   └── index.ts
└── src/
    └── device.ts
2

Naming

Use mock prefix: DevicemockDevice
3

Factory pattern

Prefer factories over static objects:
mockDevice(data: Partial<Device>): Device => ({ ... })
4

Export

Export from package via separate entry:
import { mockDevice } from '@common/device-types/mocks';

Mock Reusability

Keep shared mocks generic and non-opinionated.
Simple test: Changes in shared mocks should NOT break existing tests (or make fixes trivial).

Common Testing Patterns

Testing Forms

import { renderWithBasicProvider, screen, userEvent, waitFor } from '@suite/test-utils';

describe('LoginForm', () => {
    it('should submit form with valid data', async () => {
        const onSubmit = jest.fn();
        const user = userEvent.setup();

        renderWithBasicProvider(<LoginForm onSubmit={onSubmit} />);

        await user.type(screen.getByLabelText(/email/i), '[email protected]');
        await user.type(screen.getByLabelText(/password/i), 'password123');
        await user.click(screen.getByRole('button', { name: /submit/i }));

        await waitFor(() => {
            expect(onSubmit).toHaveBeenCalledWith({
                email: '[email protected]',
                password: 'password123',
            });
        });
    });

    it('should show validation errors', async () => {
        const user = userEvent.setup();

        renderWithBasicProvider(<LoginForm />);

        await user.click(screen.getByRole('button', { name: /submit/i }));

        expect(screen.getByText('Email is required')).toBeInTheDocument();
        expect(screen.getByText('Password is required')).toBeInTheDocument();
    });
});

Testing Async Actions

import { renderWithStoreProvider, initStoreForTests, waitFor } from '@suite/test-utils';

describe('fetchAccountData', () => {
    it('should fetch and store account data', async () => {
        const { store } = initStoreForTests();

        store.dispatch(fetchAccountData('account-1'));

        await waitFor(() => {
            const state = store.getState();
            expect(state.wallet.accounts).toHaveLength(1);
            expect(state.wallet.accounts[0].id).toBe('account-1');
        });
    });
});

Testing Error States

import { renderWithBasicProvider, screen } from '@suite/test-utils';

describe('DataComponent', () => {
    it('should display error message on fetch failure', async () => {
        const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'));

        renderWithBasicProvider(<DataComponent fetchData={mockFetch} />);

        await waitFor(() => {
            expect(screen.getByRole('alert')).toHaveTextContent('Failed to load data');
        });
    });
});

End-to-End Testing

Trezor Suite uses Playwright for E2E testing. See the full E2E guide for details.

E2E Best Practices

1

Use page objects

Organize selectors and actions in page object classes
2

Tag tests appropriately

Use @group-name tags for test organization
3

Handle retries

Configure retries for flaky tests
4

Use proper locators

Prefer data-testid over CSS selectors

Test Coverage

Aim for meaningful coverage, not 100% coverage.
# Generate coverage report
yarn test:unit --coverage

# View coverage report
open coverage/lcov-report/index.html
Focus on:
  • Critical user paths
  • Complex business logic
  • Error handling
  • Edge cases
Don’t focus on:
  • Simple getters/setters
  • Third-party library wrappers
  • Configuration files

Debugging Tests

# Add .only to run single test
it.only('should do something', () => {
    // Test code
});

# Or use --testNamePattern
yarn test:unit --testNamePattern="should do something"
Add to .vscode/launch.json:
{
    "type": "node",
    "request": "launch",
    "name": "Jest Debug",
    "program": "${workspaceFolder}/node_modules/.bin/jest",
    "args": ["--runInBand", "--coverage=0"],
    "console": "integratedTerminal"
}
import { screen, debug } from '@suite/test-utils';

// Print entire DOM
screen.debug();

// Print specific element
screen.debug(screen.getByRole('button'));

Resources

Testing Library

Official Testing Library docs

Jest Documentation

Jest testing framework

Playwright E2E

End-to-end testing with Playwright

Kent C. Dodds Blog

Testing best practices and tips

Build docs developers (and LLMs) love