diff --git a/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx b/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx index 2a25804f3541..76b4a5440e52 100644 --- a/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx +++ b/app/components/UI/Predict/components/PredictBalance/PredictBalance.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; import PredictBalance from './PredictBalance'; +import { strings } from '../../../../../../locales/i18n'; // Mock React Navigation jest.mock('@react-navigation/native', () => ({ @@ -26,10 +27,10 @@ const mockUsePredictDeposit = jest.fn(); jest.mock('../../hooks/usePredictDeposit', () => ({ usePredictDeposit: () => mockUsePredictDeposit(), PredictDepositStatus: { - IDLE: 'IDLE', - PENDING: 'PENDING', - CONFIRMED: 'CONFIRMED', - FAILED: 'FAILED', + IDLE: 'idle', + PENDING: 'pending', + CONFIRMED: 'confirmed', + FAILED: 'failed', }, })); @@ -43,6 +44,12 @@ jest.mock('../../hooks/usePredictActionGuard', () => ({ }), })); +// Mock usePredictWithdraw hook +const mockUsePredictWithdraw = jest.fn(); +jest.mock('../../hooks/usePredictWithdraw', () => ({ + usePredictWithdraw: () => mockUsePredictWithdraw(), +})); + // Mock Clipboard jest.mock('@react-native-clipboard/clipboard', () => ({ setString: jest.fn(), @@ -70,7 +77,11 @@ describe('PredictBalance', () => { mockUsePredictDeposit.mockReturnValue({ deposit: jest.fn(), - status: 'IDLE', + status: 'idle', + }); + + mockUsePredictWithdraw.mockReturnValue({ + withdraw: jest.fn(), }); // Reset executeGuardedAction mock to default behavior @@ -300,7 +311,7 @@ describe('PredictBalance', () => { }); mockUsePredictDeposit.mockReturnValue({ deposit: mockDeposit, - status: 'IDLE', + status: 'idle', }); // Act @@ -313,6 +324,150 @@ describe('PredictBalance', () => { // Assert expect(mockDeposit).toHaveBeenCalledTimes(1); }); + + it('calls executeGuardedAction when Add Funds button is pressed', () => { + // Arrange + const mockDeposit = jest.fn(); + mockUsePredictBalance.mockReturnValue({ + balance: 100, + isLoading: false, + isRefreshing: false, + error: null, + loadBalance: jest.fn(), + hasNoBalance: false, + }); + mockUsePredictDeposit.mockReturnValue({ + deposit: mockDeposit, + status: 'idle', + }); + + // Act + const { getByText } = renderWithProvider(, { + state: initialState, + }); + const addFundsButton = getByText(/Add funds/i); + fireEvent.press(addFundsButton); + + // Assert - executeGuardedAction is called (it executes the deposit function) + expect(mockDeposit).toHaveBeenCalled(); + }); + + it('calls executeGuardedAction with checkBalance option when Withdraw button is pressed', () => { + // Arrange + const mockWithdraw = jest.fn(); + mockUsePredictBalance.mockReturnValue({ + balance: 100, + isLoading: false, + isRefreshing: false, + error: null, + loadBalance: jest.fn(), + hasNoBalance: false, + }); + mockUsePredictWithdraw.mockReturnValue({ + withdraw: mockWithdraw, + }); + + // Act + const { getByText } = renderWithProvider(, { + state: initialState, + }); + const withdrawButton = getByText(/Withdraw/i); + fireEvent.press(withdrawButton); + + // Assert - executeGuardedAction is called with checkBalance option (it executes the withdraw function) + expect(mockWithdraw).toHaveBeenCalled(); + }); + }); + + describe('balance refresh', () => { + it('component renders with adding funds state when deposit is pending', () => { + // Arrange - set up CONFIRMED status to test the adding funds UI + mockUsePredictDeposit.mockReturnValue({ + deposit: jest.fn(), + status: 'pending', + }); + + // Act + const { getByText } = renderWithProvider(, { + state: initialState, + }); + + // Assert - should show adding funds message + expect( + getByText(strings('predict.deposit.adding_your_funds')), + ).toBeOnTheScreen(); + }); + + it('component renders normally when deposit status is idle', () => { + // Arrange - set up IDLE status + mockUsePredictDeposit.mockReturnValue({ + deposit: jest.fn(), + status: 'idle', + }); + + // Act + const { getByTestId, queryByText } = renderWithProvider( + , + { + state: initialState, + }, + ); + + // Assert - should render balance card normally, no adding funds message + expect(getByTestId('predict-balance-card')).toBeOnTheScreen(); + expect( + queryByText(strings('predict.deposit.adding_your_funds')), + ).not.toBeOnTheScreen(); + }); + }); + + describe('onLayout callback', () => { + it('calls onLayout callback when provided', () => { + // Arrange + const mockOnLayout = jest.fn(); + + // Act + const { getByTestId } = renderWithProvider( + , + { + state: initialState, + }, + ); + + const balanceCard = getByTestId('predict-balance-card'); + + // Simulate onLayout event + fireEvent(balanceCard, 'layout', { + nativeEvent: { + layout: { + height: 200, + }, + }, + }); + + // Assert + expect(mockOnLayout).toHaveBeenCalledWith(200); + }); + + it('handles onLayout gracefully when no callback is provided', () => { + // Act + const { getByTestId } = renderWithProvider(, { + state: initialState, + }); + + const balanceCard = getByTestId('predict-balance-card'); + + // Assert - should not throw error when onLayout is called without a callback + expect(() => { + fireEvent(balanceCard, 'layout', { + nativeEvent: { + layout: { + height: 200, + }, + }, + }); + }).not.toThrow(); + }); }); describe('edge cases', () => { @@ -336,6 +491,26 @@ describe('PredictBalance', () => { expect(getByText(/\$0\.01/)).toBeOnTheScreen(); }); + it('handles very large balance', () => { + // Arrange + mockUsePredictBalance.mockReturnValue({ + balance: 123456789.123456, + isLoading: false, + isRefreshing: false, + error: null, + loadBalance: jest.fn(), + hasNoBalance: false, + }); + + // Act + const { getByText } = renderWithProvider(, { + state: initialState, + }); + + // Assert + expect(getByText(/\$123,456,789\.12/)).toBeOnTheScreen(); + }); + it('handles adding funds state', () => { // Arrange mockUsePredictBalance.mockReturnValue({ @@ -348,7 +523,7 @@ describe('PredictBalance', () => { }); mockUsePredictDeposit.mockReturnValue({ deposit: jest.fn(), - status: 'PENDING', + status: 'pending', }); // Act @@ -363,5 +538,71 @@ describe('PredictBalance', () => { expect(getByTestId('predict-balance-card')).toBeOnTheScreen(); expect(getByText(/Add funds/i)).toBeOnTheScreen(); }); + + it('shows primary button variant when balance is zero', () => { + // Arrange + mockUsePredictBalance.mockReturnValue({ + balance: 0, + isLoading: false, + isRefreshing: false, + error: null, + loadBalance: jest.fn(), + hasNoBalance: true, + }); + + // Act + const { getByText } = renderWithProvider(, { + state: initialState, + }); + + // Assert + const addFundsButton = getByText(/Add funds/i); + expect(addFundsButton).toBeOnTheScreen(); + // The button should exist, but we can't easily test the variant without more complex testing + }); + + it('shows secondary button variant when balance is greater than zero', () => { + // Arrange + mockUsePredictBalance.mockReturnValue({ + balance: 10, + isLoading: false, + isRefreshing: false, + error: null, + loadBalance: jest.fn(), + hasNoBalance: false, + }); + + // Act + const { getByText } = renderWithProvider(, { + state: initialState, + }); + + // Assert + const addFundsButton = getByText(/Add funds/i); + expect(addFundsButton).toBeOnTheScreen(); + + const withdrawButton = getByText(/Withdraw/i); + expect(withdrawButton).toBeOnTheScreen(); + }); + + it('handles undefined balance gracefully', () => { + // Arrange + mockUsePredictBalance.mockReturnValue({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + balance: undefined as any, + isLoading: false, + isRefreshing: false, + error: null, + loadBalance: jest.fn(), + hasNoBalance: true, + }); + + // Act & Assert - should not crash and render $0.00 + const { getByText } = renderWithProvider(, { + state: initialState, + }); + + expect(getByText(/\$0\.00/)).toBeOnTheScreen(); + }); }); }); diff --git a/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx b/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx index 6093d0e8324b..ccaca00a2afc 100644 --- a/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx +++ b/app/components/UI/Predict/components/PredictBalance/PredictBalance.tsx @@ -75,8 +75,13 @@ const PredictBalance: React.FC = ({ onLayout }) => { }, [deposit, executeGuardedAction]); const handleWithdraw = useCallback(() => { - withdraw(); - }, [withdraw]); + executeGuardedAction( + () => { + withdraw(); + }, + { checkBalance: true }, + ); + }, [withdraw, executeGuardedAction]); if (isLoading) { return ( diff --git a/app/components/UI/Predict/controllers/PredictController.ts b/app/components/UI/Predict/controllers/PredictController.ts index 23634c19e2ba..81d24d748d4c 100644 --- a/app/components/UI/Predict/controllers/PredictController.ts +++ b/app/components/UI/Predict/controllers/PredictController.ts @@ -1014,15 +1014,7 @@ export class PredictController extends BaseController< networkClientId, disableHook: true, disableSequential: true, - transactions: [ - { - params: { - to: signer.address as Hex, - value: '0x1', - }, - }, - ...transactions, - ], + transactions, }); const predictClaim: PredictClaim = { @@ -1288,16 +1280,7 @@ export class PredictController extends BaseController< disableHook: true, disableSequential: true, requireApproval: true, - transactions: [ - // TODO: remove this dummy transaction when confirmation handling is implemented - { - params: { - to: signer.address as Hex, - value: '0x1', - }, - }, - transaction, - ], + transactions: [transaction], }); this.update((state) => {