Merge pull request #46 from djwesty/vutukuri15/34
Implemented Currency Selector # 34
This commit is contained in:
commit
8cc8f006df
129
app/index.tsx
129
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>
|
||||
);
|
||||
};
|
||||
|
||||
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="$" />
|
||||
<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: "$",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
101
package-lock.json
generated
101
package-lock.json
generated
@ -10,6 +10,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",
|
||||
@ -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