import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAnalytics } from '../../core/analytics/hooks/useAnalytics';
import { SwapEvent } from '../../core/analytics/types';
import type { Token } from '../../token';
import { DAI_TOKEN, ETH_TOKEN, USDC_TOKEN } from '../mocks';
import type { SwapContextType } from '../types';
import { SwapAmountInput } from './SwapAmountInput';
import { useSwapContext } from './SwapProvider';
vi.mock('../../token', () => ({
TokenChip: vi.fn(() =>
TokenChip
),
TokenSelectDropdown: vi.fn(({ setToken, options }) => (
setToken(options[1])}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setToken(options[1]);
}
}}
role="button"
tabIndex={0}
>
TokenSelectDropdown {mockSwappableTokens[1].symbol}
)),
}));
vi.mock('./SwapProvider', () => ({
useSwapContext: vi.fn(),
}));
const useSwapContextMock = useSwapContext as unknown as ReturnType<
typeof vi.fn
>;
const mockContextValue = {
address: '0x123',
from: {
amount: '10',
amountUSD: '1000',
balance: '0.0002851826238227',
setAmount: vi.fn(),
setLoading: vi.fn(),
setToken: vi.fn(),
loading: false,
token: undefined,
},
to: {
amount: '20',
amountUSD: '2000',
symbol: 'USDC',
setAmount: vi.fn(),
setLoading: vi.fn(),
setToken: vi.fn(),
loading: false,
token: undefined,
},
loading: false,
handleToggle: vi.fn(),
handleSubmit: vi.fn(),
handleAmountChange: vi.fn(),
} as unknown as SwapContextType;
vi.mock('../../internal/utils/getRoundedAmount', () => ({
getRoundedAmount: vi.fn((value) => value.slice(0, 10)),
}));
const mockSwappableTokens: Token[] = [ETH_TOKEN, USDC_TOKEN, DAI_TOKEN];
vi.mock('../../core/analytics/hooks/useAnalytics', () => ({
useAnalytics: vi.fn(() => ({
sendAnalytics: vi.fn(),
})),
}));
describe('SwapAmountInput', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render the component with the correct label and token', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
expect(screen.getByText('From')).toBeDefined();
});
it('should render from token input with max button and balance', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
expect(screen.getByText('Balance: 0.00028518')).toBeDefined();
expect(screen.getByTestId('ockSwapAmountInput_MaxButton')).toBeDefined();
});
it('should not render max button for to token input', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
expect(screen.queryByTestId('ockSwapAmountInput_MaxButton')).toBeNull();
});
it('should not render max button if wallet not connected', () => {
useSwapContextMock.mockReturnValue({ ...mockContextValue, address: '' });
render();
expect(screen.queryByTestId('ockSwapAmountInput_MaxButton')).toBeNull();
});
it('should update input value with balance amount on max button click', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
const maxButton = screen.getByTestId('ockSwapAmountInput_MaxButton');
fireEvent.click(maxButton);
expect(mockContextValue.from.setAmount).toHaveBeenCalledWith(
'0.0002851826238227',
);
});
it('should not update input value with balance amount on max button click when balance is undefined', () => {
const mockContextValueWithNoBalance = {
...mockContextValue,
from: {
...mockContextValue.from,
balance: undefined,
},
};
useSwapContextMock.mockReturnValue(mockContextValueWithNoBalance);
render();
const maxButton = screen.getByTestId('ockSwapAmountInput_MaxButton');
fireEvent.click(maxButton);
expect(mockContextValue.from.setAmount).not.toHaveBeenCalled();
});
it('should display the correct amount when this type is "from"', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
const input = screen.getByTestId('ockTextInput_Input');
expect(input).toHaveValue('10');
});
it('should display the correct amount when this type is "to"', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
const input = screen.getByTestId('ockTextInput_Input');
expect(input).toHaveValue('20');
});
it('should call setFromAmount when type is "from" and valid input is entered', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
const input = screen.getByTestId('ockTextInput_Input');
fireEvent.change(input, { target: { value: '15' } });
expect(mockContextValue.from.setAmount).toHaveBeenCalledWith('15');
});
it('should call setToAmount when type is "to" and valid input is entered', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
const input = screen.getByTestId('ockTextInput_Input');
fireEvent.change(input, { target: { value: '15' } });
expect(mockContextValue.to.setAmount).toHaveBeenCalledWith('15');
});
it('should not call setAmount when invalid input is entered', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
const input = screen.getByTestId('ockTextInput_Input');
fireEvent.change(input, { target: { value: 'invalid' } });
expect(mockContextValue.from.setAmount).not.toHaveBeenCalled();
});
it('should call setFromToken when type is "from" and token prop is provided', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
expect(mockContextValue.from.setToken).toHaveBeenCalledWith(ETH_TOKEN);
});
it('should call setToToken when type is "to" and token prop is provided', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render();
expect(mockContextValue.to.setToken).toHaveBeenCalledWith(ETH_TOKEN);
});
it('should call handleAmountChange when type is "from" and delayMs is 0', async () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(
,
);
const input = screen.getByTestId('ockTextInput_Input');
fireEvent.change(input, { target: { value: '15' } });
expect(mockContextValue.from.setAmount).toHaveBeenCalledWith('15');
await waitFor(() => {
expect(mockContextValue.handleAmountChange).toHaveBeenCalled();
});
});
it('should correctly computes sourceTokenOptions excluding destination token', () => {
const mockContextValueWithTokens = {
...mockContextValue,
to: {
...mockContextValue.to,
token: ETH_TOKEN,
},
};
useSwapContextMock.mockReturnValue(mockContextValueWithTokens);
render(
,
);
const dropdown = screen.getByText(/TokenSelectDropdown/i);
expect(dropdown).toBeDefined();
});
it('should correctly select a token from the dropdown using mouse and keyboard', () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(
,
);
const tokenSelectDropdown = screen.getByTestId(
'mock-token-select-dropdown',
);
expect(tokenSelectDropdown).toBeDefined();
expect(tokenSelectDropdown.textContent).toContain('USDC');
fireEvent.click(tokenSelectDropdown);
expect(mockContextValue.from.setToken).toHaveBeenCalledWith(USDC_TOKEN);
expect(mockContextValue.handleAmountChange).toHaveBeenCalledWith(
'from',
'10',
USDC_TOKEN,
);
vi.clearAllMocks();
fireEvent.keyDown(tokenSelectDropdown, { key: 'Enter' });
expect(mockContextValue.from.setToken).toHaveBeenCalledWith(USDC_TOKEN);
expect(mockContextValue.handleAmountChange).toHaveBeenCalledWith(
'from',
'10',
USDC_TOKEN,
);
});
it('should hasInsufficientBalance be true when balance is less than amount for type "from"', () => {
const mockContextValueWithLowBalance = {
...mockContextValue,
from: {
...mockContextValue.from,
balance: '5',
amount: '10',
},
};
useSwapContextMock.mockReturnValue(mockContextValueWithLowBalance);
render();
const input = screen.getByTestId('ockTextInput_Input');
expect(input.className).toContain('text-ock-error');
});
it('should render a TokenChip component if swappableTokens are not passed as prop', () => {
useSwapContextMock.mockReturnValue({
...mockContextValue,
to: {
...mockContextValue.to,
token: USDC_TOKEN,
},
});
render();
const chips = screen.getAllByText('TokenChip');
expect(chips.length).toBeGreaterThan(0);
expect(chips[0]).toBeDefined();
});
it('should apply the given className to the button', async () => {
useSwapContextMock.mockReturnValue(mockContextValue);
render(
,
);
expect(
screen.getByTestId('ockSwapAmountInput_Container').className,
).toContain('custom-class');
});
it('should not display anything when amountUSD is null', () => {
const mockContextValueWithNullUSD = {
...mockContextValue,
from: {
...mockContextValue.from,
amountUSD: null,
},
};
useSwapContextMock.mockReturnValue(mockContextValueWithNullUSD);
expect(screen.queryByText(/\$/)).toBeNull();
});
it('should return null when amount is falsy', () => {
useSwapContextMock.mockReturnValue({
...mockContextValue,
from: {
...mockContextValue.from,
amountUSD: '',
},
});
render();
expect(screen.queryByText(/\$/)).toBeNull();
});
describe('analytics', () => {
it('should send analytics when token is selected', () => {
const mockSendAnalytics = vi.fn();
(useAnalytics as unknown as ReturnType).mockReturnValue({
sendAnalytics: mockSendAnalytics,
});
useSwapContextMock.mockReturnValue(mockContextValue);
render(
,
);
const tokenSelectDropdown = screen.getByTestId(
'mock-token-select-dropdown',
);
fireEvent.click(tokenSelectDropdown);
expect(mockSendAnalytics).toHaveBeenCalledWith(SwapEvent.TokenSelected, {
token: 'USDC',
});
});
it('should send analytics when token is selected via keyboard', () => {
const mockSendAnalytics = vi.fn();
(useAnalytics as unknown as ReturnType).mockReturnValue({
sendAnalytics: mockSendAnalytics,
});
useSwapContextMock.mockReturnValue(mockContextValue);
render(
,
);
const tokenSelectDropdown = screen.getByTestId(
'mock-token-select-dropdown',
);
fireEvent.keyDown(tokenSelectDropdown, { key: 'Enter' });
expect(mockSendAnalytics).toHaveBeenCalledWith(SwapEvent.TokenSelected, {
token: 'USDC',
});
});
it('should not send analytics when token selection is not available', () => {
const mockSendAnalytics = vi.fn();
(useAnalytics as unknown as ReturnType).mockReturnValue({
sendAnalytics: mockSendAnalytics,
});
useSwapContextMock.mockReturnValue({
...mockContextValue,
to: {
...mockContextValue.to,
token: USDC_TOKEN,
},
});
render();
expect(mockSendAnalytics).not.toHaveBeenCalled();
});
});
describe('custom render prop', () => {
it('should render the custom render prop', async () => {
const mockSendAnalytics = vi.fn();
(useAnalytics as unknown as ReturnType).mockReturnValue({
sendAnalytics: mockSendAnalytics,
});
useSwapContextMock.mockReturnValue(mockContextValue);
render(
{
return (
Custom Render
);
}}
/>,
);
await act(async () => {
fireEvent.click(screen.getByText('Set Token'));
});
expect(mockSendAnalytics).toHaveBeenCalledWith(SwapEvent.TokenSelected, {
token: 'USDC',
});
expect(screen.getByText('Custom Render')).toBeDefined();
});
});
});