Implemented Currency Selector # 34 #46

Merged
Vutukuri15 merged 4 commits from vutukuri15/34 into main 2025-03-01 12:08:46 -08:00
13 changed files with 487 additions and 89 deletions

View File

@ -1,12 +1,25 @@
import React, { useState, useEffect } from "react";
import { ScrollView, Text, Alert, Button } from "react-native";
import {
ScrollView,
Text,
Alert,
Button,
View,
StyleSheet,
TouchableOpacity,
} from "react-native";
import { FontAwesome } from "@expo/vector-icons";
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";
export enum COLORS {
"white",
@ -21,8 +34,9 @@ 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(() => {
const loadPersistentData = async () => {
try {
@ -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);
setBuyInAmount(savedState.buyInAmount || 20);
setNumberOfChips(savedState.numberOfChips || 5);
setTotalChipsCount(savedState.totalChipsCount || []);
setSelectedCurrency(savedState.selectedCurrency || "$");
} 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,95 @@ 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}>
<TouchableOpacity
onPress={() => setIsSettingsVisible(!isSettingsVisible)}
>
<Text>
<FontAwesome name="cog" size={30} color="black" />
</Text>
</TouchableOpacity>
</View>
{isSettingsVisible && (
<View style={styles.settingsContainer}>
<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="Load from Slot 1" onPress={() => handleLoad("SLOT1")} />
<Button title="Load from Slot 2" onPress={() => handleLoad("SLOT2")} />
<View style={styles.buttonContainer}>
<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")} />
</View>
</ScrollView>
);
};
export default IndexScreen;
const styles = StyleSheet.create({
header: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
marginBottom: 20,
},
settingsContainer: {
marginBottom: 20,
padding: 10,
backgroundColor: "#f5f5f5",
borderRadius: 5,
},
settingTitle: {
fontSize: 18,
fontWeight: "bold",
marginBottom: 10,
},
buttonContainer: {
marginTop: 20,
},
});
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
djwesty commented 2025-02-28 17:10:16 -08:00 (Migrated from github.com)
Review

For a global setting like this, contexts might be a better choice.

If our app was to grow and gain complexity, we would want to avoid the inevitable 'prop drilling' as described in that article.

However, you do not need to change this. Just be aware of the context feature for future reference and personal learning. Passing as props is ok for our circumstances given the scope of our app.

For a global setting like this, [contexts](https://react.dev/learn/passing-data-deeply-with-context) might be a better choice. If our app was to grow and gain complexity, we would want to avoid the inevitable 'prop drilling' as described in that article. However, you do not need to change this. Just be aware of the context feature for future reference and personal learning. Passing as props is ok for our circumstances given the scope of our app.
Vutukuri15 commented 2025-02-28 18:31:11 -08:00 (Migrated from github.com)
Review

Thanks for the feedback! I’ll keep that in mind for future improvements.

Thanks for the feedback! I’ll keep that in mind for future improvements.
}
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="$" />
djwesty commented 2025-02-28 16:57:24 -08:00 (Migrated from github.com)
Review

One thing about currencies is that multiple currencies sometimes use the same symbol (example, Mexican Peso also uses the $ sign).
I think what you have is good for our app though and you do not need to change anything. This is just an interesting fact.

One thing about currencies is that multiple currencies sometimes use the same symbol (example, Mexican Peso also uses the `$` sign). I think what you have is good for our app though and you do not need to change anything. This is just an interesting fact.
Vutukuri15 commented 2025-02-28 18:52:34 -08:00 (Migrated from github.com)
Review

Yes, good to know! I think we're good with the current approach, as you said

Yes, good to know! I think we're good with the current approach, as you said
<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: "$",
});
});
});

103
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",
@ -33,6 +34,7 @@
"react-native-reanimated": "3.16.7",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.4.0",
"react-native-vector-icons": "^10.2.0",
"react-native-web": "0.19.13",
"react-native-webview": "13.12.5"
},
@ -3926,6 +3928,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",
@ -14431,6 +14446,92 @@
"react-native": "*"
}
},
"node_modules/react-native-vector-icons": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.2.0.tgz",
"integrity": "sha512-n5HGcxUuVaTf9QJPs/W22xQpC2Z9u0nb0KgLPnVltP8vdUvOp6+R26gF55kilP/fV4eL4vsAHUqUjewppJMBOQ==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2",
"yargs": "^16.1.1"
},
"bin": {
"fa-upgrade.sh": "bin/fa-upgrade.sh",
"fa5-upgrade": "bin/fa5-upgrade.sh",
"fa6-upgrade": "bin/fa6-upgrade.sh",
"generate-icon": "bin/generate-icon.js"
}
},
"node_modules/react-native-vector-icons/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/react-native-vector-icons/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/react-native-vector-icons/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vector-icons/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"license": "MIT",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-vector-icons/node_modules/yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-web": {
"version": "0.19.13",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.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",