Implemented Currency Selector # 34 #46
131
app/index.tsx
131
app/index.tsx
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
46
components/CurrencySelector.tsx
Normal file
46
components/CurrencySelector.tsx
Normal 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="$" />
|
||||
![]() One thing about currencies is that multiple currencies sometimes use the same symbol (example, Mexican Peso also uses the 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.
![]() 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;
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
49
components/__tests__/CurrencySelector.test.tsx
Normal file
49
components/__tests__/CurrencySelector.test.tsx
Normal 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("$");
|
||||
});
|
||||
});
|
@ -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
103
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user
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.
Thanks for the feedback! I’ll keep that in mind for future improvements.