handleBettingSquareClick(bettingSquareName)}
pendingBets={pendingBets}
diff --git a/src/components/roulette/SpinResult.js b/src/components/roulette/SpinResult.js
index 671074a..86ae5cd 100644
--- a/src/components/roulette/SpinResult.js
+++ b/src/components/roulette/SpinResult.js
@@ -10,18 +10,28 @@ export function SpinResult(props) {
const [bgColor, setBgColor] = useState("inherit");
useEffect(() => {
- if (props.spinResult) {
+ if (props.spinResult !== null && props.spinResult !== undefined) {
setBgColor(getWheelNumberColor(props.spinResult));
setMostRecentSpinResultText(props.spinResult === 37 ? "00" : props.spinResult);
- } else {
- rouletteContractEvents.on('ExecutedWager', (playerAddress, wheelNumber) => {
- if (playerAddress === props.playerAddress) {
- setBgColor(getWheelNumberColor(parseInt(wheelNumber, 10)));
- setMostRecentSpinResultText(parseInt(wheelNumber, 10) === 37 ? "00" : parseInt(wheelNumber, 10));
- }
- });
}
- }, [mostRecentSpinResultText, props.spinResult, props.playerAddress]);
+ }, [props.spinResult]);
+
+ useEffect(() => {
+ const handleExecutedWager = (playerAddress, wheelNumber) => {
+ if (playerAddress === props.playerAddress) {
+ const wheelNum = parseInt(wheelNumber, 10);
+ setBgColor(getWheelNumberColor(wheelNum));
+ setMostRecentSpinResultText(wheelNum === 37 ? "00" : wheelNum);
+ }
+ };
+
+ rouletteContractEvents.on('ExecutedWager', handleExecutedWager);
+
+ // Cleanup function to remove event listener
+ return () => {
+ rouletteContractEvents.off('ExecutedWager', handleExecutedWager);
+ };
+ }, [props.playerAddress]);
return (
{
+ describe('getCompleteResultsOfRound', () => {
+ test('calculates winning straight bet correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [new PendingBet('STRAIGHT_UP_7', 10)];
+ const winningWheelNumber = 7;
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.startingBalance).toBe(100);
+ expect(results.winningWheelNumber).toBe(7);
+ expect(results.resultsOfBets.STRAIGHT_UP_7.didBetWin).toBe(true);
+ expect(results.resultsOfBets.STRAIGHT_UP_7.winningsOnBet).toBe(350); // 10 * 35
+ expect(results.resultsOfBets.STRAIGHT_UP_7.betReturned).toBe(10);
+ expect(results.finalBalance).toBe(450); // 100 - 10 + 350 + 10
+ });
+
+ test('calculates losing bet correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [new PendingBet('STRAIGHT_UP_7', 10)];
+ const winningWheelNumber = 23;
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.resultsOfBets.STRAIGHT_UP_7.didBetWin).toBe(false);
+ expect(results.resultsOfBets.STRAIGHT_UP_7.winningsOnBet).toBe(0);
+ expect(results.resultsOfBets.STRAIGHT_UP_7.betReturned).toBe(0);
+ expect(results.finalBalance).toBe(90); // 100 - 10
+ });
+
+ test('calculates winning color bet correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [new PendingBet('RED', 20)];
+ const winningWheelNumber = 7; // 7 is red
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.resultsOfBets.RED.didBetWin).toBe(true);
+ expect(results.resultsOfBets.RED.winningsOnBet).toBe(20); // 20 * 1
+ expect(results.resultsOfBets.RED.betReturned).toBe(20);
+ expect(results.finalBalance).toBe(120); // 100 - 20 + 20 + 20
+ });
+
+ test('calculates winning dozen bet correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [new PendingBet('FIRST_DOZEN', 30)];
+ const winningWheelNumber = 11; // In first dozen
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.resultsOfBets.FIRST_DOZEN.didBetWin).toBe(true);
+ expect(results.resultsOfBets.FIRST_DOZEN.winningsOnBet).toBe(60); // 30 * 2
+ expect(results.resultsOfBets.FIRST_DOZEN.betReturned).toBe(30);
+ expect(results.finalBalance).toBe(160); // 100 - 30 + 60 + 30
+ });
+
+ test('handles multiple bets on same number correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [
+ new PendingBet('STRAIGHT_UP_7', 5),
+ new PendingBet('STRAIGHT_UP_7', 10)
+ ];
+ const winningWheelNumber = 7;
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.resultsOfBets.STRAIGHT_UP_7.betAmount).toBe(15);
+ expect(results.resultsOfBets.STRAIGHT_UP_7.winningsOnBet).toBe(525); // 15 * 35
+ expect(results.resultsOfBets.STRAIGHT_UP_7.betReturned).toBe(15);
+ expect(results.finalBalance).toBe(625); // 100 - 15 + 525 + 15
+ });
+
+ test('handles 0 correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [new PendingBet('STRAIGHT_UP_0', 10)];
+ const winningWheelNumber = 0;
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.resultsOfBets.STRAIGHT_UP_0.didBetWin).toBe(true);
+ expect(results.resultsOfBets.STRAIGHT_UP_0.winningsOnBet).toBe(350); // 10 * 35
+ });
+
+ test('handles 00 (37) correctly', () => {
+ const startingBalance = 100;
+ const pendingBets = [new PendingBet('STRAIGHT_UP_00', 10)];
+ const winningWheelNumber = 37; // 00
+
+ const results = getCompleteResultsOfRound(
+ startingBalance,
+ pendingBets,
+ winningWheelNumber
+ );
+
+ expect(results.resultsOfBets.STRAIGHT_UP_00.didBetWin).toBe(true);
+ expect(results.resultsOfBets.STRAIGHT_UP_00.winningsOnBet).toBe(350); // 10 * 35
+ });
+ });
+
+ describe('BetResultsInfo Component', () => {
+ test('displays winning bet results correctly', () => {
+ const mockResults = {
+ startingBalance: 100,
+ winningWheelNumber: 7,
+ finalBalance: 450,
+ resultsOfBets: {
+ STRAIGHT_UP_7: {
+ betAmount: 10,
+ winningsOnBet: 350,
+ betReturned: 10,
+ didBetWin: true
+ }
+ }
+ };
+
+ render();
+
+ // Check that winning information is displayed
+ expect(screen.getByText(/Starting Balance:/)).toBeInTheDocument();
+ expect(screen.getByText(/100/)).toBeInTheDocument();
+ expect(screen.getByText(/Winning Wheel Number:/)).toBeInTheDocument();
+ expect(screen.getByText(/7/)).toBeInTheDocument();
+ expect(screen.getByText(/Final Balance:/)).toBeInTheDocument();
+ expect(screen.getByText(/450/)).toBeInTheDocument();
+ });
+
+ test('displays multiple bet results correctly', () => {
+ const mockResults = {
+ startingBalance: 100,
+ winningWheelNumber: 7,
+ finalBalance: 110,
+ resultsOfBets: {
+ STRAIGHT_UP_7: {
+ betAmount: 5,
+ winningsOnBet: 175,
+ betReturned: 5,
+ didBetWin: true
+ },
+ RED: {
+ betAmount: 10,
+ winningsOnBet: 10,
+ betReturned: 10,
+ didBetWin: true
+ },
+ SECOND_DOZEN: {
+ betAmount: 20,
+ winningsOnBet: 0,
+ betReturned: 0,
+ didBetWin: false
+ }
+ }
+ };
+
+ render();
+
+ // Check all bets are displayed
+ expect(screen.getByText(/STRAIGHT_UP_7/)).toBeInTheDocument();
+ expect(screen.getByText(/RED/)).toBeInTheDocument();
+ expect(screen.getByText(/SECOND_DOZEN/)).toBeInTheDocument();
+ });
+
+ test('displays nothing when no results', () => {
+ const { container } = render();
+
+ // Component should render but be empty
+ expect(container.firstChild).toBeInTheDocument();
+ expect(screen.queryByText(/Starting Balance:/)).not.toBeInTheDocument();
+ });
+
+ test('displays 00 correctly for wheel number 37', () => {
+ const mockResults = {
+ startingBalance: 100,
+ winningWheelNumber: 37,
+ finalBalance: 450,
+ resultsOfBets: {
+ STRAIGHT_UP_00: {
+ betAmount: 10,
+ winningsOnBet: 350,
+ betReturned: 10,
+ didBetWin: true
+ }
+ }
+ };
+
+ render();
+
+ // Should display 00 instead of 37
+ expect(screen.getByText(/00/)).toBeInTheDocument();
+ expect(screen.queryByText(/37/)).not.toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/test/components/roulette/EventListenerComponents.test.js b/src/test/components/roulette/EventListenerComponents.test.js
new file mode 100644
index 0000000..569ddaf
--- /dev/null
+++ b/src/test/components/roulette/EventListenerComponents.test.js
@@ -0,0 +1,266 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { MostRecentSpinResults } from '../../../components/roulette/MostRecentSpinResults';
+import { SpinResult } from '../../../components/roulette/SpinResult';
+import { NumbersHitTracker } from '../../../components/roulette/NumbersHitTracker';
+import * as blockchainWrapper from '../../../common/blockchainWrapper';
+import { ethers } from 'ethers';
+
+jest.mock('../../../common/blockchainWrapper');
+
+describe('Event Listener Components', () => {
+ const mockPlayerAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
+ let mockEventEmitter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockEventEmitter = {
+ on: jest.fn(),
+ off: jest.fn(),
+ emit: jest.fn(),
+ };
+
+ blockchainWrapper.rouletteContractEvents = mockEventEmitter;
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent.mockResolvedValue([]);
+ });
+
+ describe('MostRecentSpinResults', () => {
+ test('registers and cleans up event listener', () => {
+ const { unmount } = render(
+
+ );
+
+ // Check event listener was registered
+ expect(mockEventEmitter.on).toHaveBeenCalledWith(
+ 'ExecutedWager',
+ expect.any(Function)
+ );
+
+ // Unmount and check cleanup
+ unmount();
+ expect(mockEventEmitter.off).toHaveBeenCalledWith(
+ 'ExecutedWager',
+ expect.any(Function)
+ );
+ });
+
+ test('updates spin results when event is fired', async () => {
+ render();
+
+ // Get the registered event handler
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+
+ // Simulate multiple spins
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(23));
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(37)); // 00
+
+ // Check results are displayed
+ await waitFor(() => {
+ expect(screen.getByText('7')).toBeInTheDocument();
+ expect(screen.getByText('23')).toBeInTheDocument();
+ expect(screen.getByText('00')).toBeInTheDocument(); // 37 displays as 00
+ });
+ });
+
+ test('only keeps last 20 results', async () => {
+ render();
+
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+
+ // Simulate 25 spins
+ for (let i = 1; i <= 25; i++) {
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(i));
+ }
+
+ // Wait for updates
+ await waitFor(() => {
+ // First 5 should not be visible (1-5)
+ expect(screen.queryByText('1')).not.toBeInTheDocument();
+ expect(screen.queryByText('5')).not.toBeInTheDocument();
+
+ // Last ones should be visible (21-25)
+ expect(screen.getByText('21')).toBeInTheDocument();
+ expect(screen.getByText('25')).toBeInTheDocument();
+ });
+ });
+
+ test('ignores events for other players', async () => {
+ render();
+
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+
+ // Event for different player
+ eventHandler('0xDifferentAddress', ethers.BigNumber.from(7));
+
+ // Should not display
+ await waitFor(() => {
+ expect(screen.queryByText('7')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('SpinResult', () => {
+ test('displays prop value when provided', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('15')).toBeInTheDocument();
+ });
+
+ test('displays 00 for wheel number 37', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('00')).toBeInTheDocument();
+ });
+
+ test('updates from blockchain events', async () => {
+ render();
+
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(23));
+
+ await waitFor(() => {
+ expect(screen.getByText('23')).toBeInTheDocument();
+ });
+ });
+
+ test('cleans up event listener on unmount', () => {
+ const { unmount } = render(
+
+ );
+
+ unmount();
+ expect(mockEventEmitter.off).toHaveBeenCalled();
+ });
+ });
+
+ describe('NumbersHitTracker', () => {
+ test('fetches initial data on mount', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent)
+ .toHaveBeenCalledWith(mockPlayerAddress);
+ });
+ });
+
+ test('highlights hit numbers', async () => {
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent
+ .mockResolvedValue([7, 23, 37]);
+
+ render();
+
+ await waitFor(() => {
+ // Check highlighted numbers have yellow background
+ const seven = screen.getByText('7');
+ expect(seven).toHaveStyle({ backgroundColor: 'yellow', color: 'black' });
+
+ const twentyThree = screen.getByText('23');
+ expect(twentyThree).toHaveStyle({ backgroundColor: 'yellow', color: 'black' });
+
+ const doubleZero = screen.getByText('00'); // 37 displays as 00
+ expect(doubleZero).toHaveStyle({ backgroundColor: 'yellow', color: 'black' });
+
+ // Check non-hit number
+ const one = screen.getByText('1');
+ expect(one).toHaveStyle({ backgroundColor: 'inherit', color: 'gray' });
+ });
+ });
+
+ test('refreshes data when ExecutedWager event fires', async () => {
+ let callCount = 0;
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent
+ .mockImplementation(() => {
+ callCount++;
+ if (callCount === 1) return Promise.resolve([7]);
+ return Promise.resolve([7, 23]);
+ });
+
+ render();
+
+ // Wait for initial load
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent)
+ .toHaveBeenCalledTimes(1);
+ });
+
+ // Fire event
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(23));
+
+ // Wait for debounced update
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent)
+ .toHaveBeenCalledTimes(2);
+ }, { timeout: 1000 });
+ });
+
+ test('debounces rapid event updates', async () => {
+ render();
+
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+
+ // Fire multiple events rapidly
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(1));
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(2));
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(3));
+
+ // Initial call + only one debounced call
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent)
+ .toHaveBeenCalledTimes(2);
+ }, { timeout: 1000 });
+ });
+
+ test('cleans up timer and event listener on unmount', async () => {
+ const { unmount } = render(
+
+ );
+
+ // Fire event to start timer
+ const eventHandler = mockEventEmitter.on.mock.calls[0][1];
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+
+ // Unmount immediately
+ unmount();
+
+ // Check cleanup
+ expect(mockEventEmitter.off).toHaveBeenCalled();
+
+ // Wait to ensure timer doesn't fire after unmount
+ await new Promise(resolve => setTimeout(resolve, 600));
+
+ // Should only have been called once (initial mount)
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent)
+ .toHaveBeenCalledTimes(1);
+ });
+
+ test('handles errors gracefully', async () => {
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent
+ .mockRejectedValue(new Error('Network error'));
+
+ render();
+
+ await waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ "Error fetching current number set:",
+ expect.any(Error)
+ );
+ });
+
+ consoleErrorSpy.mockRestore();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/test/components/roulette/Roulette.bugfixes.test.js b/src/test/components/roulette/Roulette.bugfixes.test.js
new file mode 100644
index 0000000..5a3e6b0
--- /dev/null
+++ b/src/test/components/roulette/Roulette.bugfixes.test.js
@@ -0,0 +1,291 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { ethers } from 'ethers';
+import { Roulette } from '../../../components/roulette/Roulette';
+import * as blockchainWrapper from '../../../common/blockchainWrapper';
+
+// Mock the blockchain wrapper
+jest.mock('../../../common/blockchainWrapper');
+
+describe('Roulette Bug Fix Tests', () => {
+ const mockPlayerAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
+ let mockEventEmitter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockEventEmitter = {
+ on: jest.fn(),
+ off: jest.fn(),
+ emit: jest.fn(),
+ };
+
+ blockchainWrapper.rouletteContractEvents = mockEventEmitter;
+ blockchainWrapper.getBlock.mockResolvedValue({ number: 100 });
+ blockchainWrapper.getTokenBalance.mockResolvedValue('100.0');
+ blockchainWrapper.getPlayerAllowance.mockResolvedValue('1000.0');
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent.mockResolvedValue([]);
+ blockchainWrapper.getPlayerNumberCompletionSetsCounter.mockResolvedValue(0);
+ });
+
+ test('event listeners are properly cleaned up (memory leak fix)', async () => {
+ const { unmount } = render();
+
+ // Wait for initial mount
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // Count initial event listeners
+ const initialListeners = mockEventEmitter.on.mock.calls.filter(
+ call => call[0] === 'ExecutedWager'
+ );
+
+ // Place bet and spin to create more listeners
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ const mockTx = { wait: jest.fn().mockResolvedValue({ blockNumber: 101 }) };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ fireEvent.click(screen.getByText('SPIN'));
+
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalled();
+ });
+
+ // Count all registered handlers
+ const allHandlers = mockEventEmitter.on.mock.calls
+ .filter(call => call[0] === 'ExecutedWager')
+ .map(call => call[1]);
+
+ // Unmount
+ unmount();
+
+ // Verify all handlers were cleaned up
+ allHandlers.forEach(handler => {
+ expect(mockEventEmitter.off).toHaveBeenCalledWith('ExecutedWager', handler);
+ });
+ });
+
+ test('no infinite re-render loop in NumbersHitTracker', async () => {
+ const { container } = render();
+
+ // Initial call
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent).toHaveBeenCalledTimes(1);
+ });
+
+ // Wait a bit to ensure no additional calls
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // Should still be only 1 call (no infinite loop)
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent).toHaveBeenCalledTimes(1);
+ });
+
+ test('race condition fix - UI uses blockchain result not frontend prediction', async () => {
+ const { container } = render();
+
+ // Place bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Setup transaction
+ const mockTx = { wait: jest.fn().mockResolvedValue({ blockNumber: 101 }) };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ // Spin
+ fireEvent.click(screen.getByText('SPIN'));
+
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalled();
+ });
+
+ // Simulate blockchain returning a different number than expected
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )?.[1];
+
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(23)); // Different from bet
+
+ // UI should show blockchain result (23), not the bet (7)
+ await waitFor(() => {
+ const spinResult = container.querySelector('.SpinResult-component .spin-result-label');
+ expect(spinResult).toHaveTextContent('23');
+ });
+ });
+
+ test('transaction error handling', async () => {
+ const { container } = render();
+
+ // Place bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Mock transaction failure
+ blockchainWrapper.executeWager.mockRejectedValue(new Error('Transaction failed'));
+
+ // Try to spin
+ fireEvent.click(screen.getByText('SPIN'));
+
+ // Check error is displayed
+ await waitFor(() => {
+ const errorElement = container.querySelector('.error-message');
+ expect(errorElement).toBeTruthy();
+ expect(errorElement.textContent).toContain('Failed to execute wager');
+ });
+
+ // Check that wheel is no longer spinning
+ await waitFor(() => {
+ const spinButton = screen.getByText('SPIN');
+ expect(spinButton).toBeInTheDocument();
+ });
+ });
+
+ test('precision handling with BigNumbers', async () => {
+ // Test with precise balance
+ blockchainWrapper.getTokenBalance.mockResolvedValue('1.123456789012345678');
+
+ const { container } = render();
+
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // Should be able to place $1 bet
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // No alert should be shown
+ expect(alertSpy).not.toHaveBeenCalled();
+
+ // Now try to place another $1 bet (should fail)
+ const bettingSquares2 = screen.getAllByText('23');
+ const bettingSquare2 = bettingSquares2.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare2);
+
+ // This should trigger alert since total would be $2 > balance
+ expect(alertSpy).toHaveBeenCalledWith("You don't have enough money to place that bet!");
+
+ alertSpy.mockRestore();
+ });
+
+ test('multiple recent spin results without duplicates', async () => {
+ const { container } = render();
+
+ // Get event handler
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )?.[1];
+
+ // Simulate multiple spins
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(23));
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(37)); // 00
+
+ await waitFor(() => {
+ const recentResults = container.querySelector('.MostRecentSpinResults-component');
+ expect(recentResults).toHaveTextContent('7');
+ expect(recentResults).toHaveTextContent('23');
+ expect(recentResults).toHaveTextContent('00');
+ });
+
+ // Check no duplicates when re-rendering
+ const results = container.querySelectorAll('.recent-spin-result');
+ expect(results.length).toBe(3);
+ });
+
+ test('numbers hit tracker updates properly on new spins', async () => {
+ // Start with some numbers already hit
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent.mockResolvedValue([7, 23]);
+
+ const { container } = render();
+
+ await waitFor(() => {
+ const seven = Array.from(container.querySelectorAll('.hit-number'))
+ .find(el => el.textContent === '7');
+ expect(seven).toHaveStyle({ backgroundColor: 'yellow' });
+ });
+
+ // Simulate a new spin
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )?.[1];
+
+ // Update mock to include new number
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent.mockResolvedValue([7, 23, 15]);
+
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(15));
+
+ // Should fetch updated set after debounce
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent).toHaveBeenCalledTimes(2);
+ }, { timeout: 1000 });
+
+ // Check new number is highlighted
+ await waitFor(() => {
+ const fifteen = Array.from(container.querySelectorAll('.hit-number'))
+ .find(el => el.textContent === '15');
+ expect(fifteen).toHaveStyle({ backgroundColor: 'yellow' });
+ });
+ });
+
+ test('balance updates correctly after transaction confirmation', async () => {
+ const { container } = render();
+
+ // Initial balance
+ await waitFor(() => {
+ const playerInfo = container.querySelector('.PlayerInfo-component');
+ expect(playerInfo).toHaveTextContent('100');
+ });
+
+ // Place bet and spin
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ const mockTx = { wait: jest.fn().mockResolvedValue({ blockNumber: 101 }) };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ fireEvent.click(screen.getByText('SPIN'));
+
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalled();
+ });
+
+ // Update balance mock for win
+ blockchainWrapper.getTokenBalance.mockResolvedValue('135.0');
+
+ // Trigger event
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )?.[1];
+
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+
+ // Balance should update
+ await waitFor(() => {
+ const playerInfo = container.querySelector('.PlayerInfo-component');
+ expect(playerInfo).toHaveTextContent('135');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/test/components/roulette/Roulette.integration.test.js b/src/test/components/roulette/Roulette.integration.test.js
new file mode 100644
index 0000000..b301c96
--- /dev/null
+++ b/src/test/components/roulette/Roulette.integration.test.js
@@ -0,0 +1,273 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
+import { ethers } from 'ethers';
+import { Roulette } from '../../../components/roulette/Roulette';
+import * as blockchainWrapper from '../../../common/blockchainWrapper';
+
+// Mock the blockchain wrapper
+jest.mock('../../../common/blockchainWrapper');
+
+describe('Roulette Integration Tests', () => {
+ const mockPlayerAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
+ let mockEventEmitter;
+
+ beforeEach(() => {
+ // Reset all mocks
+ jest.clearAllMocks();
+
+ // Create a mock event emitter
+ mockEventEmitter = {
+ on: jest.fn(),
+ off: jest.fn(),
+ emit: jest.fn(),
+ };
+
+ // Setup default mock implementations
+ blockchainWrapper.rouletteContractEvents = mockEventEmitter;
+ blockchainWrapper.getBlock.mockResolvedValue({ number: 100 });
+ blockchainWrapper.getTokenBalance.mockResolvedValue('100.0');
+ blockchainWrapper.getPlayerAllowance.mockResolvedValue('1000.0');
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent.mockResolvedValue([]);
+ blockchainWrapper.getPlayerNumberCompletionSetsCounter.mockResolvedValue(0);
+ });
+
+ test('renders all components correctly', async () => {
+ await act(async () => {
+ render();
+ });
+
+ // Check main components are rendered
+ expect(screen.getByText('SPIN')).toBeInTheDocument();
+
+ // Check chip selection is rendered
+ expect(screen.getByText('$1')).toBeInTheDocument();
+ });
+
+ test('handles betting workflow correctly', async () => {
+ render();
+
+ // Wait for initial data load
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalledWith(mockPlayerAddress);
+ });
+
+ // Click on a betting square (e.g., number 7)
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Check that pending bet is displayed
+ await waitFor(() => {
+ expect(screen.getByText(/STRAIGHT_UP_7/)).toBeInTheDocument();
+ });
+ });
+
+ test('prevents betting more than available balance', async () => {
+ // Set a low balance
+ blockchainWrapper.getTokenBalance.mockResolvedValue('0.5');
+
+ render();
+
+ // Wait for balance to load
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // Select a high chip amount (5)
+ const chip5 = screen.getByText('$5');
+ fireEvent.click(chip5);
+
+ // Mock window.alert
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ // Try to place a bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ expect(alertSpy).toHaveBeenCalledWith("You don't have enough money to place that bet!");
+ alertSpy.mockRestore();
+ });
+
+ test('handles spin workflow with blockchain interaction', async () => {
+ const mockTx = {
+ wait: jest.fn().mockResolvedValue({ blockNumber: 101 })
+ };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ render();
+
+ // Place a bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Click spin button
+ const spinButton = screen.getByText('SPIN');
+ fireEvent.click(spinButton);
+
+ // Verify executeWager was called
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalledWith(mockPlayerAddress);
+ });
+
+ // Verify transaction was waited for
+ expect(mockTx.wait).toHaveBeenCalled();
+
+ // Simulate blockchain event
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )[1];
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+
+ // Check that the wheel number is displayed
+ await waitFor(() => {
+ expect(screen.getAllByText('7').length).toBeGreaterThan(1);
+ });
+ });
+
+ test('handles multiple bets restriction', async () => {
+ render();
+
+ // Place first bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Place second bet
+ fireEvent.click(screen.getByText('Red'));
+
+ // Mock alert
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ // Try to spin
+ fireEvent.click(screen.getByText('SPIN'));
+
+ expect(alertSpy).toHaveBeenCalledWith(
+ "Only one bet per spin is supported right now. Please remove extra bets or wait for multi-bet support."
+ );
+ alertSpy.mockRestore();
+ });
+
+ test('handles blockchain errors gracefully', async () => {
+ const mockError = new Error('Transaction failed');
+ blockchainWrapper.executeWager.mockRejectedValue(mockError);
+
+ render();
+
+ // Place a bet and spin
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+ fireEvent.click(screen.getByText('SPIN'));
+
+ // Check error is displayed
+ await waitFor(() => {
+ expect(screen.getByText(/Failed to execute wager/)).toBeInTheDocument();
+ });
+ });
+
+ test('cleans up event listeners on unmount', async () => {
+ const { unmount } = render();
+
+ // Place bet and start spin to register event listener
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ const mockTx = { wait: jest.fn().mockResolvedValue({ blockNumber: 101 }) };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ fireEvent.click(screen.getByText('SPIN'));
+
+ await waitFor(() => {
+ expect(mockEventEmitter.on).toHaveBeenCalled();
+ });
+
+ // Unmount component
+ unmount();
+
+ // Verify cleanup - off should be called for any registered handlers
+ const onCalls = mockEventEmitter.on.mock.calls;
+ const registeredHandlers = onCalls.filter(call => call[0] === 'ExecutedWager');
+
+ if (registeredHandlers.length > 0) {
+ expect(mockEventEmitter.off).toHaveBeenCalled();
+ }
+ });
+
+ test('updates balance after successful spin', async () => {
+ const mockTx = { wait: jest.fn().mockResolvedValue({ blockNumber: 101 }) };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ // Initial balance
+ blockchainWrapper.getTokenBalance.mockResolvedValue('100.0');
+
+ render();
+
+ // Place bet and spin
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+ fireEvent.click(screen.getByText('SPIN'));
+
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalled();
+ });
+
+ // Update balance after spin
+ blockchainWrapper.getTokenBalance.mockResolvedValue('135.0'); // Won the bet
+
+ // Trigger the event
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )[1];
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+
+ // Verify balance was refreshed
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ test('handles precision correctly with ethers BigNumber', async () => {
+ // Set balance with many decimal places
+ blockchainWrapper.getTokenBalance.mockResolvedValue('0.123456789012345678');
+
+ render();
+
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // Select a small chip amount - default is already 1, but let's make sure
+ const chip1 = screen.getByText('$1');
+ fireEvent.click(chip1);
+
+ // Should be able to place bet with 0.123... balance
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // No alert should be shown
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+ expect(alertSpy).not.toHaveBeenCalled();
+ alertSpy.mockRestore();
+ });
+});
\ No newline at end of file
diff --git a/src/test/components/roulette/Roulette.workflow.test.js b/src/test/components/roulette/Roulette.workflow.test.js
new file mode 100644
index 0000000..9f82266
--- /dev/null
+++ b/src/test/components/roulette/Roulette.workflow.test.js
@@ -0,0 +1,294 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom'; // Add jest-dom matchers
+import { ethers } from 'ethers';
+import { Roulette } from '../../../components/roulette/Roulette';
+import * as blockchainWrapper from '../../../common/blockchainWrapper';
+
+// Mock the blockchain wrapper
+jest.mock('../../../common/blockchainWrapper');
+
+describe('Roulette Workflow Tests', () => {
+ const mockPlayerAddress = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
+ let mockEventEmitter;
+
+ beforeEach(() => {
+ // Reset all mocks
+ jest.clearAllMocks();
+
+ // Create a mock event emitter
+ mockEventEmitter = {
+ on: jest.fn(),
+ off: jest.fn(),
+ emit: jest.fn(),
+ };
+
+ // Setup default mock implementations
+ blockchainWrapper.rouletteContractEvents = mockEventEmitter;
+ blockchainWrapper.getBlock.mockResolvedValue({ number: 100 });
+ blockchainWrapper.getTokenBalance.mockResolvedValue('100.0');
+ blockchainWrapper.getPlayerAllowance.mockResolvedValue('1000.0');
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent.mockResolvedValue([]);
+ blockchainWrapper.getPlayerNumberCompletionSetsCounter.mockResolvedValue(0);
+ });
+
+ test('complete betting and spinning workflow', async () => {
+ const { container } = render();
+
+ // Wait for initial data to load
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // 1. Select a chip amount ($5)
+ const chip5 = screen.getByText('$5');
+ fireEvent.click(chip5);
+
+ // 2. Click on a betting square (number 7)
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // 3. Verify the bet is placed - check if chip shows $5 on the square
+ await waitFor(() => {
+ const chips = container.querySelectorAll('.betting-square-chip');
+ const placedChip = Array.from(chips).find(chip =>
+ chip.textContent === '$5' && chip.style.display !== 'none'
+ );
+ expect(placedChip).toBeTruthy();
+ });
+
+ // 4. Setup mock for successful transaction
+ const mockTx = {
+ wait: jest.fn().mockResolvedValue({ blockNumber: 101 })
+ };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ // 5. Click spin button
+ const spinButton = screen.getByText('SPIN');
+ fireEvent.click(spinButton);
+
+ // 6. Verify executeWager was called
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalledWith(mockPlayerAddress);
+ });
+
+ // 7. Verify transaction was confirmed
+ expect(mockTx.wait).toHaveBeenCalled();
+
+ // 8. Simulate blockchain event with winning number
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )?.[1];
+
+ expect(eventHandler).toBeDefined();
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+
+ // 9. Verify UI updates with result
+ await waitFor(() => {
+ // Check that the spin result shows 7
+ const spinResult = container.querySelector('.SpinResult-component .spin-result-label');
+ expect(spinResult).toHaveTextContent('7');
+ });
+
+ // 10. Verify balance refresh was triggered
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ test('prevents betting more than balance', async () => {
+ // Set low balance
+ blockchainWrapper.getTokenBalance.mockResolvedValue('2.0');
+
+ render();
+
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // Select high chip amount ($5)
+ const chip5 = screen.getByText('$5');
+ fireEvent.click(chip5);
+
+ // Mock alert
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ // Try to place bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ expect(alertSpy).toHaveBeenCalledWith("You don't have enough money to place that bet!");
+ alertSpy.mockRestore();
+ });
+
+ test('handles transaction errors gracefully', async () => {
+ const { container } = render();
+
+ // Place a bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Mock transaction failure
+ blockchainWrapper.executeWager.mockRejectedValue(new Error('Transaction failed'));
+
+ // Try to spin
+ fireEvent.click(screen.getByText('SPIN'));
+
+ // Check error is displayed
+ await waitFor(() => {
+ const errorElement = container.querySelector('.error-message');
+ expect(errorElement).toHaveTextContent('Failed to execute wager');
+ });
+ });
+
+ test('properly tracks numbers hit', async () => {
+ // Set some numbers already hit
+ blockchainWrapper.getPlayerNumberCompletionSetCurrent
+ .mockResolvedValue([7, 23, 37]); // 37 is "00"
+
+ const { container } = render();
+
+ // Wait for numbers to load
+ await waitFor(() => {
+ expect(blockchainWrapper.getPlayerNumberCompletionSetCurrent)
+ .toHaveBeenCalledWith(mockPlayerAddress);
+ });
+
+ // Check that hit numbers are highlighted
+ await waitFor(() => {
+ const hitNumbers = container.querySelectorAll('.hit-number');
+ const seven = Array.from(hitNumbers).find(el => el.textContent === '7');
+ const twentyThree = Array.from(hitNumbers).find(el => el.textContent === '23');
+ const doubleZero = Array.from(hitNumbers).find(el => el.textContent === '00');
+
+ expect(seven).toHaveStyle({ backgroundColor: 'yellow' });
+ expect(twentyThree).toHaveStyle({ backgroundColor: 'yellow' });
+ expect(doubleZero).toHaveStyle({ backgroundColor: 'yellow' });
+ });
+ });
+
+ test('enforces one bet per spin restriction', async () => {
+ render();
+
+ // Place first bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare7 = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare7);
+
+ // Place second bet
+ const redSquare = screen.getByText('Red');
+ fireEvent.click(redSquare);
+
+ // Mock alert
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ // Try to spin
+ fireEvent.click(screen.getByText('SPIN'));
+
+ expect(alertSpy).toHaveBeenCalledWith(
+ "Only one bet per spin is supported right now. Please remove extra bets or wait for multi-bet support."
+ );
+ alertSpy.mockRestore();
+ });
+
+ test('updates UI correctly after winning bet', async () => {
+ const { container } = render();
+
+ // Place bet on 7
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+ fireEvent.click(bettingSquare);
+
+ // Setup transaction
+ const mockTx = { wait: jest.fn().mockResolvedValue({ blockNumber: 101 }) };
+ blockchainWrapper.executeWager.mockResolvedValue(mockTx);
+
+ // Spin
+ fireEvent.click(screen.getByText('SPIN'));
+
+ await waitFor(() => {
+ expect(blockchainWrapper.executeWager).toHaveBeenCalled();
+ });
+
+ // Simulate winning on 7
+ const eventHandler = mockEventEmitter.on.mock.calls.find(
+ call => call[0] === 'ExecutedWager'
+ )?.[1];
+
+ // Update balance to reflect winning
+ blockchainWrapper.getTokenBalance.mockResolvedValue('135.0'); // Won 35:1 on $1 bet
+
+ eventHandler(mockPlayerAddress, ethers.BigNumber.from(7));
+
+ // Check that the result is displayed
+ await waitFor(() => {
+ const spinResult = container.querySelector('.SpinResult-component .spin-result-label');
+ expect(spinResult).toHaveTextContent('7');
+ expect(spinResult).toHaveStyle({ backgroundColor: 'rgb(217, 72, 72)' }); // Red color
+ });
+
+ // Verify balance was updated
+ await waitFor(() => {
+ const balanceElement = container.querySelector('.PlayerInfo-component');
+ expect(balanceElement).toHaveTextContent(/135/);
+ });
+ });
+
+ test('cleans up event listeners on unmount', async () => {
+ const { unmount } = render();
+
+ // Wait for component to mount and register listeners
+ await waitFor(() => {
+ expect(mockEventEmitter.on).toHaveBeenCalled();
+ });
+
+ // Get all registered event handlers
+ const registeredHandlers = mockEventEmitter.on.mock.calls
+ .filter(call => call[0] === 'ExecutedWager')
+ .map(call => call[1]);
+
+ // Unmount
+ unmount();
+
+ // Verify all handlers were cleaned up
+ registeredHandlers.forEach(handler => {
+ expect(mockEventEmitter.off).toHaveBeenCalledWith('ExecutedWager', handler);
+ });
+ });
+
+ test('handles precision with small balances', async () => {
+ // Set precise balance
+ blockchainWrapper.getTokenBalance.mockResolvedValue('1.123456789012345678');
+
+ render();
+
+ await waitFor(() => {
+ expect(blockchainWrapper.getTokenBalance).toHaveBeenCalled();
+ });
+
+ // Should be able to place $1 bet
+ const bettingSquares = screen.getAllByText('7');
+ const bettingSquare = bettingSquares.find(el =>
+ el.closest('.ClickableBet-component') !== null
+ );
+
+ // No alert should appear
+ const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
+ fireEvent.click(bettingSquare);
+ expect(alertSpy).not.toHaveBeenCalled();
+ alertSpy.mockRestore();
+ });
+});
\ No newline at end of file