Implementd Currency Selector

This commit is contained in:
vutukuri15 2025-02-28 11:19:22 -08:00
parent ec5521c542
commit 297b0fc026
13 changed files with 394 additions and 86 deletions

View File

@ -1,13 +1,25 @@
import React, { useState, useEffect } from "react";
import { ScrollView, Text, Alert, Button } from "react-native";
import {
ScrollView,
Text,
Alert,
Button,
View,
StyleSheet,
} from "react-native";
import PlayerSelector from "@/components/PlayerSelector";
import BuyInSelector from "@/components/BuyInSelector";
import ChipsSelector from "@/components/ChipsSelector";
import ChipDistributionSummary from "@/components/ChipDistributionSummary";
import ChipDetection from "@/components/ChipDetection";
import CurrencySelector from "@/components/CurrencySelector";
import { saveState, loadState } from "@/components/CalculatorState";
import { savePersistentState, loadPersistentState } from "@/components/PersistentState";
import {
savePersistentState,
loadPersistentState,
} from "@/components/PersistentState";
// Your existing states
export enum COLORS {
"white",
"red",
@ -21,6 +33,8 @@ const IndexScreen: React.FC = () => {
const [buyInAmount, setBuyInAmount] = useState<number>(20);
const [numberOfChips, setNumberOfChips] = useState<number>(5);
const [totalChipsCount, setTotalChipsCount] = useState<number[]>([]);
const [selectedCurrency, setSelectedCurrency] = useState<string>("$");
const [isSettingsVisible, setIsSettingsVisible] = useState(false);
// Load persistent data on startup
useEffect(() => {
@ -30,12 +44,13 @@ const IndexScreen: React.FC = () => {
const savedState = await loadPersistentState();
if (savedState) {
console.log("Persistent state restored:", savedState);
setPlayerCount(savedState.playerCount);
setBuyInAmount(savedState.buyInAmount);
setNumberOfChips(savedState.numberOfChips);
setTotalChipsCount(savedState.totalChipsCount);
setPlayerCount(savedState.playerCount || 2); // Use defaults if missing
setBuyInAmount(savedState.buyInAmount || 20); // Use defaults if missing
setNumberOfChips(savedState.numberOfChips || 5); // Use defaults if missing
setTotalChipsCount(savedState.totalChipsCount || []); // Use defaults if missing
setSelectedCurrency(savedState.selectedCurrency || "$"); // Restore the selected currency, defaulting to "$" if not available
} else {
console.log("No persistent state found.");
console.log("No persistent state found, using defaults.");
}
} catch (error) {
console.error("Error loading persistent state:", error);
@ -44,14 +59,18 @@ const IndexScreen: React.FC = () => {
loadPersistentData();
}, []);
// Save game state to selected slot
const handleSave = async (slot: "SLOT1" | "SLOT2") => {
if (buyInAmount === null) {
Alert.alert("Error", "Please select a valid buy-in amount");
return;
}
const state = { playerCount, buyInAmount, numberOfChips, totalChipsCount };
const state = {
playerCount,
buyInAmount,
numberOfChips,
totalChipsCount,
selectedCurrency,
};
try {
await saveState(slot, state);
await savePersistentState(state);
@ -63,7 +82,6 @@ const IndexScreen: React.FC = () => {
}
};
// Load game state from selected slot
const handleLoad = async (slot: "SLOT1" | "SLOT2") => {
try {
const loadedState = await loadState(slot);
@ -72,6 +90,7 @@ const IndexScreen: React.FC = () => {
setBuyInAmount(loadedState.buyInAmount);
setNumberOfChips(loadedState.numberOfChips);
setTotalChipsCount(loadedState.totalChipsCount);
setSelectedCurrency(loadedState.selectedCurrency || "$");
await savePersistentState(loadedState);
console.log(`Game state loaded from ${slot}:`, loadedState);
Alert.alert("Success", `State loaded from ${slot}`);
@ -86,29 +105,92 @@ const IndexScreen: React.FC = () => {
return (
<ScrollView contentContainerStyle={{ padding: 20, flexGrow: 1 }}>
<PlayerSelector playerCount={playerCount} setPlayerCount={setPlayerCount} />
<BuyInSelector setBuyInAmount={setBuyInAmount} />
<ChipDetection updateChipCount={(chipData) => {
const chipCountArray = Object.values(chipData);
setTotalChipsCount(chipCountArray);
}} />
<View style={styles.header}>
<Text style={styles.headerTitle}>Poker Chips Helper</Text>
<Button
title="Settings"
onPress={() => setIsSettingsVisible(!isSettingsVisible)}
/>
</View>
{isSettingsVisible && (
<View style={styles.settingsContainer}>
<Text style={styles.settingTitle}>Currency Selector</Text>
<CurrencySelector
selectedCurrency={selectedCurrency}
setSelectedCurrency={setSelectedCurrency}
/>
</View>
)}
<PlayerSelector
playerCount={playerCount}
setPlayerCount={setPlayerCount}
/>
<BuyInSelector
selectedCurrency={selectedCurrency}
setBuyInAmount={setBuyInAmount}
/>
<ChipDetection
updateChipCount={(chipData) => {
const chipCountArray = Object.values(chipData);
setTotalChipsCount(chipCountArray);
}}
/>
<ChipsSelector
totalChipsCount={totalChipsCount}
setTotalChipsCount={setTotalChipsCount}
numberOfChips={numberOfChips}
setNumberOfChips={setNumberOfChips}
/>
<ChipDistributionSummary
playerCount={playerCount}
buyInAmount={buyInAmount}
totalChipsCount={totalChipsCount}
selectedCurrency={selectedCurrency}
/>
<Button
title="Save to Slot 1"
onPress={() => handleSave("SLOT1")}
disabled={buyInAmount === null}
/>
<Button
title="Save to Slot 2"
onPress={() => handleSave("SLOT2")}
disabled={buyInAmount === null}
/>
<Button title="Save to Slot 1" onPress={() => handleSave("SLOT1")} disabled={buyInAmount === null} />
<Button title="Save to Slot 2" onPress={() => handleSave("SLOT2")} disabled={buyInAmount === null} />
<Button title="Load from Slot 1" onPress={() => handleLoad("SLOT1")} />
<Button title="Load from Slot 2" onPress={() => handleLoad("SLOT2")} />
</ScrollView>
);
};
export default IndexScreen;
const styles = StyleSheet.create({
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
},
headerTitle: {
fontSize: 24,
fontWeight: "bold",
},
settingsContainer: {
marginBottom: 20,
padding: 10,
backgroundColor: "#f5f5f5",
borderRadius: 5,
},
settingTitle: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 10,
},
});
export default IndexScreen;

View File

@ -10,11 +10,15 @@ import { MaterialIcons } from "@expo/vector-icons";
interface BuyInSelectorProps {
setBuyInAmount: React.Dispatch<React.SetStateAction<number>>;
selectedCurrency: string; // Accept selectedCurrency as a prop
}
const defaultBuyInOptions = [10, 25, 50];
const BuyInSelector: React.FC<BuyInSelectorProps> = ({ setBuyInAmount }) => {
const BuyInSelector: React.FC<BuyInSelectorProps> = ({
setBuyInAmount,
selectedCurrency,
}) => {
const [customAmount, setCustomAmount] = useState("");
const [buyInAmount, setBuyInAmountState] = useState<number | null>(null);
@ -54,7 +58,10 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({ setBuyInAmount }) => {
]}
onPress={() => handleBuyInSelection(amount)}
>
<Text style={styles.buttonText}>{amount}</Text>
<Text style={styles.buttonText}>
{selectedCurrency} {amount}{" "}
{/* Display the selected currency before the amount */}
</Text>
</TouchableOpacity>
))}
</View>
@ -69,7 +76,9 @@ const BuyInSelector: React.FC<BuyInSelectorProps> = ({ setBuyInAmount }) => {
/>
<Text style={styles.selectionText}>
Selected Buy-in: {buyInAmount !== null ? buyInAmount : "None"}
Selected Buy-in:{" "}
{buyInAmount !== null ? `${selectedCurrency} ${buyInAmount}` : "None"}{" "}
{/* Display the currency here */}
</Text>
</View>
);

View File

@ -10,9 +10,13 @@ export interface PokerState {
buyInAmount: number | null;
numberOfChips: number;
totalChipsCount: number[];
selectedCurrency: string;
}
export const saveState = async (slot: keyof typeof STORAGE_KEYS, state: PokerState) => {
export const saveState = async (
slot: keyof typeof STORAGE_KEYS,
state: PokerState
) => {
try {
await AsyncStorage.setItem(STORAGE_KEYS[slot], JSON.stringify(state));
return { success: true, message: `State saved to ${slot}` };
@ -21,7 +25,9 @@ export const saveState = async (slot: keyof typeof STORAGE_KEYS, state: PokerSta
}
};
export const loadState = async (slot: keyof typeof STORAGE_KEYS): Promise<PokerState | null> => {
export const loadState = async (
slot: keyof typeof STORAGE_KEYS
): Promise<PokerState | null> => {
try {
const storedState = await AsyncStorage.getItem(STORAGE_KEYS[slot]);
return storedState ? JSON.parse(storedState) : null;

View File

@ -7,6 +7,7 @@ interface ChipDistributionSummaryProps {
buyInAmount: number;
totalChipsCount: number[];
colors?: ColorValue[];
selectedCurrency: string; // Add the selectedCurrency as a prop here
}
const ChipDistributionSummary = ({
@ -14,6 +15,7 @@ const ChipDistributionSummary = ({
buyInAmount,
totalChipsCount,
colors = ["white", "red", "green", "blue", "black"],
selectedCurrency,
}: ChipDistributionSummaryProps) => {
const validDenominations: validDenomination[] = [
0.05, 0.1, 0.25, 0.5, 1, 2, 2.5, 5, 10, 20, 50, 100,
@ -35,7 +37,6 @@ const ChipDistributionSummary = ({
| 50
| 100;
// Return the closest (but lower) valid denomination to the target
const findFloorDenomination = (target: number): validDenomination => {
let current: validDenomination = validDenominations[0];
validDenominations.forEach((value, index) => {
@ -44,8 +45,6 @@ const ChipDistributionSummary = ({
return current;
};
// Bound for the value of the highest chip
// This is somewhat arbitray, but 1/3 to 1/4 is reasonable depending on the number of colors.
const maxDenomination = useMemo(() => {
if (totalChipsCount.length > 3) {
return findFloorDenomination(buyInAmount / 3);
@ -54,13 +53,11 @@ const ChipDistributionSummary = ({
}
}, [totalChipsCount]);
// Total value of the pot
const potValue = useMemo(
() => buyInAmount * playerCount,
[buyInAmount, playerCount]
);
// The total value of all chips distributed to a single player. Ideally should be equal to buyInAmount
const totalValue = useMemo(() => {
let value = 0;
for (let i = 0; i < totalChipsCount.length; i++) {
@ -69,14 +66,11 @@ const ChipDistributionSummary = ({
return value;
}, [distributions, denominations]);
// Maximum quantity of each chip color which may be distributed to a single player before running out
const maxPossibleDistribution = useMemo(
() => totalChipsCount.map((v) => Math.floor(v / playerCount)),
[totalChipsCount, playerCount]
);
// Redenominate the chips in case of failure to properly distribute.
// Move the shuffle index to the next lowest denomination, and keep all else same
const redenominate = useCallback(
(
invalidDenomination: validDenomination[],
@ -106,17 +100,14 @@ const ChipDistributionSummary = ({
[]
);
// Dynamically set denominations and distributions from changing inputs
useEffect(() => {
let testDenomination: validDenomination[] = [];
const numColors = totalChipsCount.length;
const testDistribution: number[] = [];
for (let i = 0; i < numColors; ++i) {
testDistribution.push(0);
}
// Start with max denominations, then push on the next adjacent lower denomination
testDenomination.push(maxDenomination);
let currentDenominationIndex: number =
validDenominations.indexOf(maxDenomination);
@ -127,9 +118,6 @@ const ChipDistributionSummary = ({
}
testDenomination.reverse();
// Distribute the chips using the test denomination
// If distribution fails to equal the buy-in, redenominate and re-try
// Algorithm could be improved with more complexity and optimization
let remainingValue = buyInAmount;
let fail = true;
let failCount = 0;
@ -167,7 +155,9 @@ const ChipDistributionSummary = ({
return (
<View style={styles.container}>
<Text style={styles.title}>Distribution & Denomination</Text>
<Text style={styles.subTitle}>${potValue} Pot</Text>
<Text style={styles.subTitle}>
{selectedCurrency} {potValue} Pot
</Text>
<View style={styles.chipContainer}>
{totalChipsCount.map((_, index) => (
<View style={styles.chipRow} key={index}>
@ -187,12 +177,14 @@ const ChipDistributionSummary = ({
...(colors[index] === "white" && styles.whiteShadow),
}}
>
${denominations[index]} each
{selectedCurrency} {denominations[index]} each
</Text>
</View>
))}
</View>
<Text style={styles.chipText}>Total Value:{totalValue}</Text>
<Text style={styles.chipText}>
Total Value: {selectedCurrency} {totalValue}
</Text>
</View>
);
};

View File

@ -0,0 +1,46 @@
import React from "react";
import { View, StyleSheet, Text } from "react-native";
import { Picker } from "@react-native-picker/picker";
interface CurrencySelectorProps {
selectedCurrency: string;
setSelectedCurrency: React.Dispatch<React.SetStateAction<string>>;
}
const CurrencySelector: React.FC<CurrencySelectorProps> = ({
selectedCurrency,
setSelectedCurrency,
}) => {
return (
<View style={styles.container}>
<Text style={styles.title}>Select Currency:</Text>
<Picker
selectedValue={selectedCurrency}
onValueChange={(itemValue) => setSelectedCurrency(itemValue)}
style={styles.picker}
testID="currency-picker" // ✅ Add testID here
>
<Picker.Item label="USD ($)" value="$" />
<Picker.Item label="Euro (€)" value="€" />
<Picker.Item label="Pound (£)" value="£" />
<Picker.Item label="INR (₹)" value="₹" />
</Picker>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 20,
},
title: {
fontSize: 18,
marginBottom: 10,
},
picker: {
height: 50,
width: 150,
},
});
export default CurrencySelector;

View File

@ -7,8 +7,18 @@ export interface PokerState {
buyInAmount: number | null;
numberOfChips: number;
totalChipsCount: number[];
selectedCurrency: string;
}
const DEFAULT_STATE: PokerState = {
playerCount: 0,
buyInAmount: null,
numberOfChips: 0,
totalChipsCount: [],
selectedCurrency: "$",
};
// 🔹 Save state with currency
export const savePersistentState = async (state: PokerState) => {
try {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(state));
@ -18,11 +28,12 @@ export const savePersistentState = async (state: PokerState) => {
}
};
export const loadPersistentState = async (): Promise<PokerState | null> => {
// 🔹 Load state with currency
export const loadPersistentState = async (): Promise<PokerState> => {
try {
const storedState = await AsyncStorage.getItem(STORAGE_KEY);
return storedState ? JSON.parse(storedState) : null;
return storedState ? JSON.parse(storedState) : DEFAULT_STATE; // Ensure default values
} catch (error) {
return null;
return DEFAULT_STATE;
}
};

View File

@ -14,28 +14,34 @@ describe("BuyInSelector Component", () => {
let getByText;
let getByPlaceholderText;
const renderComponent = () => {
const result = render(<BuyInSelector setBuyInAmount={setBuyInAmount} />);
// Render the component with the necessary props
const renderComponent = (selectedCurrency = "$") => {
const result = render(
<BuyInSelector
setBuyInAmount={setBuyInAmount}
selectedCurrency={selectedCurrency}
/>
);
getByText = result.getByText;
getByPlaceholderText = result.getByPlaceholderText;
};
beforeEach(() => {
setBuyInAmount = jest.fn();
renderComponent();
renderComponent(); // Render with default currency
});
it("renders the buy-in options and input correctly", () => {
expect(getByText("Select Buy-in Amount:")).toBeTruthy();
expect(getByText("10")).toBeTruthy();
expect(getByText("25")).toBeTruthy();
expect(getByText("50")).toBeTruthy();
expect(getByText("$ 10")).toBeTruthy();
expect(getByText("$ 25")).toBeTruthy();
expect(getByText("$ 50")).toBeTruthy();
expect(getByPlaceholderText("Enter custom buy-in")).toBeTruthy();
expect(getByText("Selected Buy-in: None")).toBeTruthy(); // Check default selection
});
it("sets a predefined buy-in amount correctly", () => {
fireEvent.press(getByText("25"));
fireEvent.press(getByText("$ 25"));
expect(setBuyInAmount).toHaveBeenCalledWith(25);
});
@ -53,7 +59,7 @@ describe("BuyInSelector Component", () => {
it("clears the custom amount when selecting a predefined option", () => {
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "100");
fireEvent.press(getByText("50"));
fireEvent.press(getByText("$ 50"));
expect(setBuyInAmount).toHaveBeenCalledWith(50);
});
@ -69,8 +75,8 @@ describe("BuyInSelector Component", () => {
});
it("triggers state update every time a buy-in option is clicked, even if it's the same", () => {
fireEvent.press(getByText("25")); // First click
fireEvent.press(getByText("25")); // Clicking the same option again
fireEvent.press(getByText("$ 25")); // First click
fireEvent.press(getByText("$ 25")); // Clicking the same option again
expect(setBuyInAmount).toHaveBeenCalledTimes(2); // Expect it to be called twice
});
@ -85,7 +91,15 @@ describe("BuyInSelector Component", () => {
it("updates state correctly when selecting predefined buy-in after entering a custom amount", () => {
fireEvent.changeText(getByPlaceholderText("Enter custom buy-in"), "200");
expect(setBuyInAmount).toHaveBeenCalledWith(200);
fireEvent.press(getByText("10"));
fireEvent.press(getByText("$ 10"));
expect(setBuyInAmount).toHaveBeenCalledWith(10);
});
it("displays selected currency correctly", () => {
renderComponent("€"); // Test with a different currency
expect(getByText("€ 10")).toBeTruthy();
expect(getByText("€ 25")).toBeTruthy();
expect(getByText("€ 50")).toBeTruthy();
expect(getByText("Selected Buy-in: None")).toBeTruthy();
});
});

View File

@ -1,6 +1,7 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { saveState, loadState, PokerState } from "../CalculatorState";
// Mock AsyncStorage
jest.mock("@react-native-async-storage/async-storage", () =>
require("@react-native-async-storage/async-storage/jest/async-storage-mock")
);
@ -11,13 +12,14 @@ describe("CalculatorState.tsx", () => {
buyInAmount: 50,
numberOfChips: 5,
totalChipsCount: [100, 200, 150, 50, 75],
selectedCurrency: "$", // Including selectedCurrency in mockState
};
beforeEach(() => {
jest.clearAllMocks();
});
it("should save state successfully", async () => {
it("should save state successfully to SLOT1", async () => {
await saveState("SLOT1", mockState);
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
"@poker_state_slot1",
@ -25,28 +27,60 @@ describe("CalculatorState.tsx", () => {
);
});
it("should save state successfully to SLOT2", async () => {
await saveState("SLOT2", mockState);
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
"@poker_state_slot2",
JSON.stringify(mockState)
);
});
it("should fail to save state if an error occurs", async () => {
jest.spyOn(AsyncStorage, "setItem").mockRejectedValueOnce(new Error("Failed to save"));
jest
.spyOn(AsyncStorage, "setItem")
.mockRejectedValueOnce(new Error("Failed to save"));
const result = await saveState("SLOT1", mockState);
expect(result.success).toBe(false);
expect(result.message).toBe("Failed to save state");
});
it("should load state successfully", async () => {
it("should load state successfully from SLOT1", async () => {
await AsyncStorage.setItem("@poker_state_slot1", JSON.stringify(mockState));
const result = await loadState("SLOT1");
expect(result).toEqual(mockState);
});
it("should return null if no state is found", async () => {
it("should load state successfully from SLOT2", async () => {
await AsyncStorage.setItem("@poker_state_slot2", JSON.stringify(mockState));
const result = await loadState("SLOT2");
expect(result).toEqual(mockState);
});
it("should return null if no state is found in SLOT1", async () => {
jest.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(null);
const result = await loadState("SLOT1");
expect(result).toBeNull();
});
it("should return null if an error occurs while loading state", async () => {
jest.spyOn(AsyncStorage, "getItem").mockRejectedValueOnce(new Error("Failed to load"));
it("should return null if no state is found in SLOT2", async () => {
jest.spyOn(AsyncStorage, "getItem").mockResolvedValueOnce(null);
const result = await loadState("SLOT2");
expect(result).toBeNull();
});
it("should return null if an error occurs while loading state from SLOT1", async () => {
jest
.spyOn(AsyncStorage, "getItem")
.mockRejectedValueOnce(new Error("Failed to load"));
const result = await loadState("SLOT1");
expect(result).toBeNull();
});
it("should return null if an error occurs while loading state from SLOT2", async () => {
jest
.spyOn(AsyncStorage, "getItem")
.mockRejectedValueOnce(new Error("Failed to load"));
const result = await loadState("SLOT2");
expect(result).toBeNull();
});
});

View File

@ -23,14 +23,18 @@ describe("ChipDistributionSummary Component", () => {
expect(getByText("Distribution & Denomination")).toBeTruthy();
expectedDistribution.forEach((count, index) => {
expect(getAllByText(new RegExp(`${count} chips:`, "i"))).toBeTruthy();
// Ensure "X chips:" appears correctly
expect(getAllByText(new RegExp(`^${count}\\s+chips:`, "i"))).toBeTruthy();
// Ensure value format matches the rendered output
expect(
getByText(new RegExp(`\\$${expectedDenominations[index]} each`, "i"))
getByText(
new RegExp(`^\\s*${expectedDenominations[index]}\\s+each$`, "i")
)
).toBeTruthy();
});
});
// Case not currently supported
test.skip("renders fallback message when no valid distribution", () => {
const { getByText } = render(
<ChipDistributionSummary
@ -42,7 +46,6 @@ describe("ChipDistributionSummary Component", () => {
expect(getByText("No valid distribution calculated yet.")).toBeTruthy();
});
// Case not currently supported
test.skip("scales down chips if exceeding MAX_CHIPS", () => {
const playerCount = 2;
let totalChipsCount = [300, 400, 500, 600, 700];
@ -56,7 +59,7 @@ describe("ChipDistributionSummary Component", () => {
);
}
const expectedDistribution = [30, 40, 50, 60, 70]; // Adjust to match actual component calculations
const expectedDistribution = [30, 40, 50, 60, 70]; // Adjust as per actual component calculations
const { getByText } = render(
<ChipDistributionSummary
@ -69,8 +72,7 @@ describe("ChipDistributionSummary Component", () => {
expect(getByText("Distribution & Denomination")).toBeTruthy();
expectedDistribution.forEach((count, index) => {
expect(getByText(new RegExp(`${count} chips:`, "i"))).toBeTruthy();
// expect(getByText(new RegExp(`$${count} each`, "i"))).toBeTruthy();
expect(getByText(new RegExp(`^${count}\\s+chips:`, "i"))).toBeTruthy();
});
});
});

View File

@ -0,0 +1,49 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react-native";
import CurrencySelector from "@/components/CurrencySelector";
describe("CurrencySelector Component", () => {
const mockSetSelectedCurrency = jest.fn();
test("renders CurrencySelector component correctly", () => {
const { getByText, getByTestId } = render(
<CurrencySelector
selectedCurrency="$"
setSelectedCurrency={mockSetSelectedCurrency}
/>
);
expect(getByText("Select Currency:")).toBeTruthy(); // Check label exists
expect(getByTestId("currency-picker")).toBeTruthy(); // Check Picker exists
});
test("calls setSelectedCurrency when a new currency is selected", () => {
const { getByTestId } = render(
<CurrencySelector
selectedCurrency="$"
setSelectedCurrency={mockSetSelectedCurrency}
/>
);
const picker = getByTestId("currency-picker"); // Get Picker
fireEvent(picker, "onValueChange", "€"); // Simulate selecting Euro (€)
expect(mockSetSelectedCurrency).toHaveBeenCalledWith("€");
});
test("updates selected currency when Picker value changes", () => {
const { getByTestId } = render(
<CurrencySelector
selectedCurrency="€"
setSelectedCurrency={mockSetSelectedCurrency}
/>
);
const picker = getByTestId("currency-picker");
fireEvent(picker, "onValueChange", "$"); // Simulate selecting USD ($)
expect(mockSetSelectedCurrency).toHaveBeenCalledWith("$");
});
});

View File

@ -1,17 +1,22 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { saveState, loadState, PokerState } from "../CalculatorState";
import {
savePersistentState,
loadPersistentState,
PokerState,
} from "../PersistentState";
jest.mock("@react-native-async-storage/async-storage", () => ({
setItem: jest.fn(),
getItem: jest.fn(),
}));
describe("CalculatorState.tsx", () => {
describe("PersistentState.tsx", () => {
const mockState: PokerState = {
playerCount: 4,
buyInAmount: 50,
numberOfChips: 5,
totalChipsCount: [100, 200, 150, 50, 75],
selectedCurrency: "$", // Including selectedCurrency in mockState
};
beforeEach(() => {
@ -19,35 +24,78 @@ describe("CalculatorState.tsx", () => {
});
it("should save state successfully", async () => {
// Mocking AsyncStorage.setItem to resolve successfully
(AsyncStorage.setItem as jest.Mock).mockResolvedValueOnce(undefined);
const result = await saveState("SLOT1", mockState);
const result = await savePersistentState(mockState);
// Check that the success flag is true and message is as expected
expect(result.success).toBe(true);
expect(result.message).toBe("State saved to SLOT1");
expect(AsyncStorage.setItem).toHaveBeenCalledWith("@poker_state_slot1", JSON.stringify(mockState));
expect(result.message).toBe("State saved successfully");
// Check that AsyncStorage.setItem was called with the correct parameters
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
"@poker_calculator_state",
JSON.stringify(mockState)
);
});
it("should fail to save state if an error occurs", async () => {
(AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(new Error("Failed to save"));
const result = await saveState("SLOT1", mockState);
// Mocking AsyncStorage.setItem to reject with an error
(AsyncStorage.setItem as jest.Mock).mockRejectedValueOnce(
new Error("Failed to save")
);
const result = await savePersistentState(mockState);
// Check that the success flag is false and message is as expected
expect(result.success).toBe(false);
expect(result.message).toBe("Failed to save state");
});
it("should load state successfully", async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockState));
const result = await loadState("SLOT1");
// Mocking AsyncStorage.getItem to resolve with the mockState
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(
JSON.stringify(mockState)
);
const result = await loadPersistentState();
// Check that the loaded state matches the mockState
expect(result).toEqual(mockState);
});
it("should return null if no state is found", async () => {
it("should load default state if no saved state is found", async () => {
// Mocking AsyncStorage.getItem to return null (no saved state)
(AsyncStorage.getItem as jest.Mock).mockResolvedValueOnce(null);
const result = await loadState("SLOT1");
expect(result).toBeNull();
const result = await loadPersistentState();
// Check that the default state is returned
expect(result).toEqual({
playerCount: 0,
buyInAmount: null,
numberOfChips: 0,
totalChipsCount: [],
selectedCurrency: "$",
});
});
it("should return null if an error occurs while loading state", async () => {
(AsyncStorage.getItem as jest.Mock).mockRejectedValueOnce(new Error("Failed to load"));
const result = await loadState("SLOT1");
expect(result).toBeNull();
it("should return default state if an error occurs while loading", async () => {
// Mocking AsyncStorage.getItem to reject with an error
(AsyncStorage.getItem as jest.Mock).mockRejectedValueOnce(
new Error("Failed to load")
);
const result = await loadPersistentState();
// Check that the default state is returned on error
expect(result).toEqual({
playerCount: 0,
buyInAmount: null,
numberOfChips: 0,
totalChipsCount: [],
selectedCurrency: "$",
});
});
});

16
package-lock.json generated
View File

@ -10,9 +10,10 @@
"dependencies": {
"@expo/vector-icons": "14.0.4",
"@react-native-async-storage/async-storage": "^2.1.1",
"@react-native-picker/picker": "^2.11.0",
"@react-navigation/bottom-tabs": "7.2.0",
"@react-navigation/native": "7.0.14",
"expo": "^52.0.37",
"expo": "52.0.37",
"expo-blur": "14.0.3",
"expo-constants": "17.0.7",
"expo-file-system": "18.0.11",
@ -3926,6 +3927,19 @@
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native-picker/picker": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.0.tgz",
"integrity": "sha512-QuZU6gbxmOID5zZgd/H90NgBnbJ3VV6qVzp6c7/dDrmWdX8S0X5YFYgDcQFjE3dRen9wB9FWnj2VVdPU64adSg==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.76.7",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.7.tgz",

View File

@ -20,6 +20,7 @@
"dependencies": {
"@expo/vector-icons": "14.0.4",
"@react-native-async-storage/async-storage": "^2.1.1",
"@react-native-picker/picker": "^2.11.0",
"@react-navigation/bottom-tabs": "7.2.0",
"@react-navigation/native": "7.0.14",
"expo": "52.0.37",