Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: Unit tests
Signed-off-by: Emre Bogazliyanlioglu <emre@wormholelabs.xyz>
  • Loading branch information
emreboga committed Oct 3, 2025
commit 8c3127ee2835c6aecddf59ccffe582290a565826
93 changes: 93 additions & 0 deletions src/views/v3/Bridge/AssetPicker/ChainList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material';

import ChainList from './ChainList';
import { dark } from 'theme';

const theme = createTheme({
palette: dark as any,
});

const mockChainConfigs = [
{
key: 'Ethereum',
displayName: 'Ethereum',
sdkName: 'Ethereum' as const,
icon: 'Ethereum' as const,
explorerUrl: 'https://etherscan.io',
explorerName: 'Etherscan',
},
{
key: 'Solana',
displayName: 'Solana',
sdkName: 'Solana' as const,
icon: 'Solana' as const,
explorerUrl: 'https://solscan.io',
explorerName: 'Solscan',
},
{
key: 'Arbitrum',
displayName: 'Arbitrum',
sdkName: 'Arbitrum' as const,
icon: 'Arbitrum' as const,
explorerUrl: 'https://arbiscan.io',
explorerName: 'Arbiscan',
},
];

const mockWallet = {
type: 'Evm' as const,
address: '0x123',
currentAddress: '0x123',
error: '',
name: 'Test Wallet',
sending: { address: '0x123' },
receiving: { address: '0x456' },
};

const defaultProps = {
chainList: mockChainConfigs,
selectedChainConfig: undefined,
showSearch: false,
setShowSearch: vi.fn(),
wallet: mockWallet,
onChainSelect: vi.fn(),
};

const AppWrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);

describe('ChainList', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('renders chain list with chain buttons', () => {
render(<ChainList {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.getByText('Ethereum')).toBeInTheDocument();
expect(screen.getByText('Solana')).toBeInTheDocument();
expect(screen.getByText('Arbitrum')).toBeInTheDocument();
});

it('calls onChainSelect when a chain button is clicked', () => {
render(<ChainList {...defaultProps} />, { wrapper: AppWrapper });

const ethereumButton = screen
.getByText('Ethereum')
.closest('div[role="button"]');
fireEvent.click(ethereumButton!);

expect(defaultProps.onChainSelect).toHaveBeenCalledWith('Ethereum');
});

it('shows search interface when showSearch is true', () => {
const props = { ...defaultProps, showSearch: true };
render(<ChainList {...props} />, { wrapper: AppWrapper });

expect(screen.getByLabelText('Search')).toBeInTheDocument();
});
});
76 changes: 76 additions & 0 deletions src/views/v3/Bridge/AssetPicker/PickerHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material';

import PickerHeader from './PickerHeader';
import { dark } from 'theme';

const theme = createTheme({
palette: dark as any,
});

const defaultProps = {
onClose: vi.fn(),
showSearch: false,
onBack: vi.fn(),
};

const AppWrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);

describe('PickerHeader', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('renders with title and close button', () => {
render(<PickerHeader {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.getByText('Select token')).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
expect(screen.getByLabelText('Close routes')).toBeInTheDocument();
});

it('calls onClose when close button is clicked', () => {
render(<PickerHeader {...defaultProps} />, { wrapper: AppWrapper });

const closeButton = screen.getByLabelText('Close routes');
fireEvent.click(closeButton);

expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});

it('shows back button when showSearch is true', () => {
const props = { ...defaultProps, showSearch: true };
render(<PickerHeader {...props} />, { wrapper: AppWrapper });

expect(screen.getByLabelText('Go back')).toBeInTheDocument();
expect(screen.getByTestId('back-button')).toBeInTheDocument();
});

it('does not show back button when showSearch is false', () => {
render(<PickerHeader {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.queryByLabelText('Go back')).not.toBeInTheDocument();
expect(screen.queryByTestId('back-button')).not.toBeInTheDocument();
});

it('calls onBack when back button is clicked', () => {
const props = { ...defaultProps, showSearch: true };
render(<PickerHeader {...props} />, { wrapper: AppWrapper });

const backButton = screen.getByLabelText('Go back');
fireEvent.click(backButton);

expect(defaultProps.onBack).toHaveBeenCalledTimes(1);
});

it('does not show back button when onBack is not provided', () => {
const props = { ...defaultProps, showSearch: true, onBack: undefined };
render(<PickerHeader {...props} />, { wrapper: AppWrapper });

expect(screen.queryByLabelText('Go back')).not.toBeInTheDocument();
});
});
122 changes: 122 additions & 0 deletions src/views/v3/Bridge/AssetPicker/TokenItem.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material';

import TokenItem from './TokenItem';
import { dark } from 'theme';

const theme = createTheme({
palette: dark as any,
});

vi.mock('utils', () => ({
chainDisplayName: vi.fn((chain) => chain),
getTokenExplorerUrl: vi.fn(() => 'https://explorer.example.com'),
getTokenDisplaySymbolByTokenAddress: vi.fn(() => 'USDC'),
}));

vi.mock('components/TokenBalance', () => ({
default: ({ balance }: { balance: any }) => (
<div data-testid="token-balance">{balance ? '1,000' : '0'}</div>
),
}));

const mockToken = {
key: 'USDC',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
icon: 'usdc.svg',
addressString: '0xa0b86a33e6180d4c6d1cbe6c9e1f4a3d4b8a6c6e',
chain: 'Ethereum' as const,
address: '0xa0b86a33e6180d4c6d1cbe6c9e1f4a3d4b8a6c6e',
tokenId: {
chain: 'Ethereum' as const,
address: '0xa0b86a33e6180d4c6d1cbe6c9e1f4a3d4b8a6c6e',
},
display: 'USD Coin',
shortAddress: '0xa0b...a6c6e',
tuple: ['Ethereum', '0xa0b86a33e6180d4c6d1cbe6c9e1f4a3d4b8a6c6e'] as [
'Ethereum',
string,
],
isNativeGasToken: false,
isTokenBridgeWrappedToken: false,
nativeChain: 'Ethereum' as const,
equals: vi.fn(),
toJson: vi.fn(),
} as any;

const mockBalance = {
amount: '1000000000', // 1000 USDC
decimals: 6,
} as any;

const defaultProps = {
token: mockToken,
chain: 'Ethereum' as const,
balance: mockBalance,
price: '1.00',
onClick: vi.fn(),
isSelected: false,
isFetchingBalance: false,
isSource: true,
isDimmed: false,
};

const AppWrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);

describe('TokenItem', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('renders token information correctly', () => {
render(<TokenItem {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.getAllByText('USDC')).toHaveLength(2); // Symbol appears twice in UI
expect(screen.getByRole('button')).toBeInTheDocument();
});

it('calls onClick when token item is clicked', () => {
render(<TokenItem {...defaultProps} />, { wrapper: AppWrapper });

const tokenButton = screen.getByRole('button');
fireEvent.mouseDown(tokenButton);

expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
});

it('displays balance when provided', () => {
render(<TokenItem {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.getByTestId('token-balance')).toBeInTheDocument();
expect(screen.getByText('1,000')).toBeInTheDocument();
});

it('shows loading state when fetching balance', () => {
const props = { ...defaultProps, isFetchingBalance: true };
render(<TokenItem {...props} />, { wrapper: AppWrapper });

expect(screen.getByTestId('token-balance')).toBeInTheDocument();
});

it('displays "0" when balance is null', () => {
const props = { ...defaultProps, balance: null };
render(<TokenItem {...props} />, { wrapper: AppWrapper });

expect(screen.getByTestId('token-balance')).toBeInTheDocument();
expect(screen.getByText('0')).toBeInTheDocument();
});

it('shows external link for explorer URL', () => {
render(<TokenItem {...defaultProps} />, { wrapper: AppWrapper });

const link = screen.getByRole('link');
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://explorer.example.com');
});
});
104 changes: 104 additions & 0 deletions src/views/v3/Bridge/AssetPicker/TokenList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material';

import TokenList from './TokenList';
import { dark } from 'theme';

const theme = createTheme({
palette: dark as any,
});

// Mock SearchableList to simplify testing
vi.mock('views/v3/Bridge/AssetPicker/SearchableList', () => ({
default: ({ searchQuery, onQueryChange }: any) => (
<div>
<input
placeholder="Search tokens"
value={searchQuery}
onChange={(e) => onQueryChange?.(e.target.value)}
/>
<div>Mocked SearchableList</div>
</div>
),
}));

vi.mock('hooks/useTokenListWithSearch', () => ({
useTokenListWithSearch: vi.fn(() => ({
sortedTokens: [],
tokenPrices: {},
})),
}));

vi.mock('hooks/useTokenListGrouping', () => ({
useTokenListGrouping: vi.fn(() => ({
groupedTokens: [],
})),
}));

const mockChainConfig = {
key: 'Ethereum',
displayName: 'Ethereum',
sdkName: 'Ethereum' as const,
icon: 'Ethereum' as const,
explorerUrl: 'https://etherscan.io',
explorerName: 'Etherscan',
};

const mockWallet = {
type: 'Evm' as const,
address: '0x123',
currentAddress: '0x123',
error: '',
name: 'Test Wallet',
};

const defaultProps = {
tokenList: [],
balances: {},
isFetchingBalances: false,
isFetching: false,
isConnectingWallet: false,
selectedChainConfig: mockChainConfig,
selectedToken: undefined,
sourceToken: undefined,
isSameChainSwap: false,
isSource: true,
wallet: mockWallet,
searchQuery: '',
onSearchQueryChange: vi.fn(),
onSelectToken: vi.fn(),
fetchTokensProgress: null,
};

const AppWrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider theme={theme}>{children}</ThemeProvider>
);

describe('TokenList', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('renders token list with search input', () => {
render(<TokenList {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.getByPlaceholderText('Search tokens')).toBeInTheDocument();
});

it('renders mocked searchable list', () => {
render(<TokenList {...defaultProps} />, { wrapper: AppWrapper });

expect(screen.getByText('Mocked SearchableList')).toBeInTheDocument();
});

it('calls onSearchQueryChange when search input changes', () => {
render(<TokenList {...defaultProps} />, { wrapper: AppWrapper });

const searchInput = screen.getByPlaceholderText('Search tokens');
fireEvent.change(searchInput, { target: { value: 'USDC' } });

expect(defaultProps.onSearchQueryChange).toHaveBeenCalledWith('USDC');
});
});
File renamed without changes.
Loading