import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { base, baseSepolia } from 'viem/chains';
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';
import { http, WagmiProvider, createConfig, mock, useAccount } from 'wagmi';
import { ETH_BY_CHAIN } from '../constants';
import type { BridgeableToken } from '../types';
import { AppchainBridgeInput } from './AppchainBridgeInput';
import { useAppchainBridgeContext } from './AppchainBridgeProvider';
const queryClient = new QueryClient();
const mockConfig = createConfig({
chains: [baseSepolia],
connectors: [
mock({
accounts: ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'],
}),
],
transports: {
[baseSepolia.id]: http(),
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
const mockToken = {
name: 'ETH',
address: '',
symbol: 'ETH',
decimals: 18,
image:
'https://wallet-api-production.s3.amazonaws.com/uploads/tokens/eth_288.png',
chainId: base.id,
remoteToken: ETH_BY_CHAIN[base.id].address,
} as BridgeableToken;
const mockBridgeContext = {
balance: '1.5',
bridgeParams: {
amount: '',
amountUSD: '0.00',
recipient: '0x123',
token: mockToken,
},
to: {
id: 8453,
icon: '🔵',
},
isPriceLoading: false,
handleAmountChange: vi.fn(),
setIsAddressModalOpen: vi.fn(),
bridgeableTokens: [mockToken],
};
vi.mock('wagmi', async (importOriginal) => {
return {
...(await importOriginal()),
useAccount: vi.fn(),
};
});
vi.mock('./AppchainBridgeProvider', () => ({
useAppchainBridgeContext: vi.fn(),
}));
vi.mock('@/internal/hooks/usePreferredColorScheme', () => ({
usePreferredColorScheme: vi.fn().mockReturnValue('light'),
}));
describe('AppchainBridgeInput', () => {
beforeEach(() => {
vi.resetAllMocks();
(useAccount as Mock).mockReturnValue({
address: '0x123',
status: 'connected',
});
(useAppchainBridgeContext as Mock).mockReturnValue(mockBridgeContext);
});
it('renders correctly with default props', () => {
render(, { wrapper });
expect(
screen.getByTestId('ockBridgeAmountInput_Container'),
).toBeInTheDocument();
expect(screen.getByPlaceholderText('0.00')).toBeInTheDocument();
expect(screen.getByText('~$0.00')).toBeInTheDocument();
expect(screen.getByText('Balance: 1.5')).toBeInTheDocument();
});
it('handles amount changes', async () => {
const mockHandleAmountChange = vi.fn();
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
handleAmountChange: mockHandleAmountChange,
resetDepositStatus: vi.fn(),
});
render(, { wrapper });
const input = screen.getByPlaceholderText('0.00');
await userEvent.type(input, '2');
await waitFor(() => {
expect(mockHandleAmountChange).toHaveBeenCalledWith({
amount: '2',
token: mockToken,
});
});
});
it('shows loading state when price is loading', () => {
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
isPriceLoading: true,
});
render(, { wrapper });
expect(
screen
.getByTestId('ockBridgeAmountInput_Container')
.querySelector('.animate-pulse'),
).toBeInTheDocument();
});
it('shows insufficient funds message when balance is too low', async () => {
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
balance: '0.5',
bridgeParams: {
...mockBridgeContext.bridgeParams,
amount: '0.6',
},
});
render(, { wrapper });
const input = screen.getByPlaceholderText('0.00');
await userEvent.type(input, '0.6');
await waitFor(() => {
expect(input).toHaveValue('0.6');
expect(screen.getByText('Insufficient funds')).toBeInTheDocument();
});
});
it('handles wallet disconnected state', () => {
(useAccount as Mock).mockReturnValue({
address: undefined,
status: 'disconnected',
});
render(, { wrapper });
expect(screen.queryByText('Balance:')).not.toBeInTheDocument();
expect(
screen.queryByTestId('ockBridgeAmountInput_MaxButton'),
).not.toBeInTheDocument();
});
it('handles custom bridgeable tokens', () => {
const customToken = {
...mockToken,
symbol: 'CUSTOM',
};
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
bridgeableTokens: [customToken],
});
render(, {
wrapper,
});
expect(screen.getByText('CUSTOM')).toBeInTheDocument();
});
it('opens address modal when clicking recipient address', async () => {
const mockSetIsAddressModalOpen = vi.fn();
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
setIsAddressModalOpen: mockSetIsAddressModalOpen,
});
render(, { wrapper });
const addressButton = screen.getByText('0x123...x123');
await userEvent.click(addressButton);
expect(mockSetIsAddressModalOpen).toHaveBeenCalledWith(true);
});
it('displays NaN amount correctly', () => {
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
bridgeParams: {
...mockBridgeContext.bridgeParams,
amountUSD: 'NaN',
},
});
render(, { wrapper });
expect(screen.queryByText(/\$|~\$/)).not.toBeInTheDocument();
});
it('handles max button click correctly', async () => {
const mockHandleAmountChange = vi.fn();
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
handleAmountChange: mockHandleAmountChange,
balance: '1.5',
});
render(, { wrapper });
const maxButton = screen.getByTestId('ockBridgeAmountInput_MaxButton');
await userEvent.click(maxButton);
expect(mockHandleAmountChange).toHaveBeenCalledWith({
amount: '1.5',
token: mockToken,
});
});
it('formats balance with correct decimal places', () => {
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
balance: '1.23456789',
});
render(, { wrapper });
expect(screen.getByText('Balance: 1.23457')).toBeInTheDocument();
});
it('formats whole number balance without decimal places', () => {
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
balance: '100',
});
render(, { wrapper });
expect(screen.getByText('Balance: 100')).toBeInTheDocument();
});
it('shows default address when recipient is not set', () => {
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
bridgeParams: {
...mockBridgeContext.bridgeParams,
recipient: '',
},
});
render(, { wrapper });
expect(screen.getByText('0x0000...0000')).toBeInTheDocument();
});
it('handles token selection correctly', async () => {
const mockHandleAmountChange = vi.fn();
const mockResetDepositStatus = vi.fn();
const customToken = {
...mockToken,
symbol: 'CUSTOM',
address: '0x456',
} as BridgeableToken;
(useAppchainBridgeContext as Mock).mockReturnValue({
...mockBridgeContext,
handleAmountChange: mockHandleAmountChange,
resetDepositStatus: mockResetDepositStatus,
bridgeParams: {
...mockBridgeContext.bridgeParams,
amount: '1.0',
},
bridgeableTokens: [mockToken, customToken],
});
render(, { wrapper });
await waitFor(async () => {
// Open token dropdown
const tokenButton = screen.getByText('ETH');
await userEvent.click(tokenButton);
// Select new token
const customTokenOption = screen.getByText('CUSTOM');
await userEvent.click(customTokenOption);
});
// Verify handleAmountChange was called with correct params
expect(mockHandleAmountChange).toHaveBeenCalledWith({
amount: '1.0',
token: customToken,
});
expect(mockResetDepositStatus).toHaveBeenCalled();
});
});