Skip to main content

Testing

This guide covers testing React components that use the Privacy Boost SDK.

Mocking the SDK

Create Mock SDK

// test/mocks/sdk.ts
import { PrivacyBoost } from '@testinprod-io/privacy-boost';

export function createMockSDK(overrides = {}): Partial<PrivacyBoost> {
  return {
    auth: {
      connect: jest.fn().mockResolvedValue({ address: '0x123', chainId: 1 }),
      disconnect: jest.fn().mockResolvedValue(undefined),
      login: jest.fn().mockResolvedValue({ privacyAddress: '0x04...', mpk: '0x...' }),
      logout: jest.fn().mockResolvedValue(undefined),
      isConnected: jest.fn().mockReturnValue(false),
      isAuthenticated: jest.fn().mockReturnValue(false),
      getAddress: jest.fn().mockReturnValue(null),
      getPrivacyAddress: jest.fn().mockReturnValue(null),
      getMpk: jest.fn().mockReturnValue(null),
      exportSession: jest.fn().mockReturnValue({}),
      importSession: jest.fn().mockResolvedValue(undefined),
      getSessionInfo: jest.fn().mockResolvedValue(null),
      ...overrides,
    },
    vault: {
      deposit: jest.fn().mockResolvedValue({ txHash: '0x...', commitment: '0x...' }),
      withdraw: jest.fn().mockResolvedValue({ txHash: '0x...' }),
      send: jest.fn().mockResolvedValue({ txHash: '0x...' }),
      getBalance: jest.fn().mockResolvedValue(0n),
      getAllBalances: jest.fn().mockResolvedValue([]),
      syncBalance: jest.fn().mockResolvedValue(undefined),
      syncAllBalances: jest.fn().mockResolvedValue(undefined),
      ...overrides,
    },
    contacts: {
      getContacts: jest.fn().mockReturnValue([]),
      addContact: jest.fn().mockResolvedValue({ id: '1', name: 'Test' }),
      removeContact: jest.fn().mockResolvedValue(undefined),
      updateContact: jest.fn().mockResolvedValue({ id: '1', name: 'Updated' }),
      findByAddress: jest.fn().mockReturnValue(undefined),
      search: jest.fn().mockReturnValue([]),
      ...overrides,
    },
    ...overrides,
  } as unknown as PrivacyBoost;
}

Mock Provider

// test/mocks/MockProvider.tsx
import React from 'react';
import { PrivacyBoostProvider } from '@testinprod-io/privacy-boost-react';
import { createMockSDK } from './sdk';

interface MockProviderProps {
  children: React.ReactNode;
  sdkOverrides?: Partial<ReturnType<typeof createMockSDK>>;
}

export function MockPrivacyBoostProvider({
  children,
  sdkOverrides,
}: MockProviderProps) {
  const mockSDK = createMockSDK(sdkOverrides);

  return (
    <PrivacyBoostProvider sdk={mockSDK as any}>
      {children}
    </PrivacyBoostProvider>
  );
}

Testing Components

Testing useAuth

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { WalletButton } from '../components/WalletButton';

describe('WalletButton', () => {
  it('shows connect button when not authenticated', () => {
    render(
      <MockPrivacyBoostProvider>
        <WalletButton />
      </MockPrivacyBoostProvider>
    );

    expect(screen.getByText('Connect Wallet')).toBeInTheDocument();
  });

  it('shows address when authenticated', () => {
    const mockSDK = createMockSDK({
      auth: {
        isAuthenticated: () => true,
        getPrivacyAddress: () => '0x04abc123...',
      },
    });

    render(
      <MockPrivacyBoostProvider sdkOverrides={mockSDK}>
        <WalletButton />
      </MockPrivacyBoostProvider>
    );

    expect(screen.getByText(/0x04abc123/)).toBeInTheDocument();
  });

  it('calls authenticate on click', async () => {
    const mockConnect = jest.fn().mockResolvedValue({ address: '0x123' });
    const mockLogin = jest.fn().mockResolvedValue({ privacyAddress: '0x04...' });

    render(
      <MockPrivacyBoostProvider
        sdkOverrides={{
          auth: { connect: mockConnect, login: mockLogin },
        }}
      >
        <WalletButton />
      </MockPrivacyBoostProvider>
    );

    fireEvent.click(screen.getByText('Connect Wallet'));

    await waitFor(() => {
      expect(mockConnect).toHaveBeenCalled();
    });
  });
});

Testing useVault

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { DepositForm } from '../components/DepositForm';

describe('DepositForm', () => {
  it('calls deposit with correct params', async () => {
    const mockDeposit = jest.fn().mockResolvedValue({ txHash: '0x...' });

    render(
      <MockPrivacyBoostProvider
        sdkOverrides={{
          vault: { deposit: mockDeposit },
          auth: { isAuthenticated: () => true },
        }}
      >
        <DepositForm tokenAddress="0xtoken" />
      </MockPrivacyBoostProvider>
    );

    const input = screen.getByPlaceholderText('Amount');
    fireEvent.change(input, { target: { value: '1.5' } });

    fireEvent.click(screen.getByText('Deposit'));

    await waitFor(() => {
      expect(mockDeposit).toHaveBeenCalledWith({
        tokenAddress: '0xtoken',
        amount: '1.5',
        onProgress: expect.any(Function),
      });
    });
  });

  it('shows error on failure', async () => {
    const mockDeposit = jest.fn().mockRejectedValue(new Error('Insufficient balance'));

    render(
      <MockPrivacyBoostProvider
        sdkOverrides={{
          vault: { deposit: mockDeposit },
          auth: { isAuthenticated: () => true },
        }}
      >
        <DepositForm tokenAddress="0xtoken" />
      </MockPrivacyBoostProvider>
    );

    fireEvent.change(screen.getByPlaceholderText('Amount'), {
      target: { value: '100' },
    });
    fireEvent.click(screen.getByText('Deposit'));

    await waitFor(() => {
      expect(screen.getByText(/Insufficient balance/)).toBeInTheDocument();
    });
  });
});

Testing useBalances

import { render, screen } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { BalanceList } from '../components/BalanceList';

describe('BalanceList', () => {
  it('displays balances', () => {
    const mockBalances = [
      {
        tokenAddress: '0x1',
        symbol: 'USDC',
        shielded: 1000000n,
        wallet: 500000n,
        decimals: 6,
        formattedShielded: '1.0',
        formattedWallet: '0.5',
      },
    ];

    // Mock the store directly
    jest.mock('@testinprod-io/privacy-boost', () => ({
      useBalanceStore: () => ({
        balances: new Map([['0x1', mockBalances[0]]]),
      }),
    }));

    render(
      <MockPrivacyBoostProvider>
        <BalanceList />
      </MockPrivacyBoostProvider>
    );

    expect(screen.getByText('USDC')).toBeInTheDocument();
    expect(screen.getByText('1.0')).toBeInTheDocument();
  });
});

Testing Hooks

Custom Hook Testing

import { renderHook, act } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';
import { useDepositFlow } from '../hooks/useDepositFlow';

describe('useDepositFlow', () => {
  it('handles deposit flow', async () => {
    const mockDeposit = jest.fn().mockResolvedValue({ txHash: '0x...' });

    const wrapper = ({ children }) => (
      <MockPrivacyBoostProvider
        sdkOverrides={{
          vault: { deposit: mockDeposit },
          auth: { isAuthenticated: () => true },
        }}
      >
        {children}
      </MockPrivacyBoostProvider>
    );

    const { result } = renderHook(() => useDepositFlow(), { wrapper });

    expect(result.current.loading).toBe(false);

    await act(async () => {
      await result.current.deposit('0xtoken', '1.0');
    });

    expect(mockDeposit).toHaveBeenCalled();
    expect(result.current.txHash).toBe('0x...');
  });
});

Mocking window.ethereum

beforeEach(() => {
  // Mock window.ethereum
  Object.defineProperty(window, 'ethereum', {
    value: {
      isMetaMask: true,
      request: jest.fn(),
      on: jest.fn(),
      removeListener: jest.fn(),
    },
    writable: true,
  });
});

afterEach(() => {
  // @ts-ignore
  delete window.ethereum;
});

Test Utilities

// test/utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { MockPrivacyBoostProvider } from './mocks/MockProvider';

function customRender(
  ui: React.ReactElement,
  sdkOverrides?: any,
  options?: Omit<RenderOptions, 'wrapper'>
) {
  return render(ui, {
    wrapper: ({ children }) => (
      <MockPrivacyBoostProvider sdkOverrides={sdkOverrides}>
        {children}
      </MockPrivacyBoostProvider>
    ),
    ...options,
  });
}

export * from '@testing-library/react';
export { customRender as render };

Best Practices

  1. Mock at the SDK level, not individual hooks
  2. Test user interactions, not implementation details
  3. Use async utilities (waitFor, findBy*) for async operations
  4. Clean up mocks between tests
  5. Test error states as well as success states

Next Steps