diff --git a/package.json b/package.json index d9fb09d..0cbf597 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@eslint/js": "^9.19.0", "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", "@types/react": "^19.0.8", diff --git a/src/App.spec.tsx b/src/App.spec.tsx index 42fff82..1ee9910 100644 --- a/src/App.spec.tsx +++ b/src/App.spec.tsx @@ -1,6 +1,60 @@ -import { render } from "@testing-library/react"; +import { render, screen, fireEvent } from "@testing-library/react"; import App from "./App"; +import "@testing-library/jest-dom"; + +window.prompt = jest.fn(); +window.localStorage.__proto__.getItem = jest.fn(); +window.localStorage.__proto__.setItem = jest.fn(); test("renders", () => { render(); }); + +test("creates a new deck with a unique name", () => { + render(); + const addButton = screen.getByText("Add New Deck"); + fireEvent.click(addButton); + const promptSpy = jest.spyOn(window, "prompt").mockReturnValue("Unique Deck"); + fireEvent.click(addButton); + expect(screen.getByText("Unique Deck")).toBeInTheDocument(); + promptSpy.mockRestore(); +}); + +test("saves the deck to local storage", () => { + render(); + const addButton = screen.getByText("Add New Deck"); + fireEvent.click(addButton); + const promptSpy = jest.spyOn(window, "prompt").mockReturnValue("Saved Deck"); + fireEvent.click(addButton); + expect(localStorage.setItem).toHaveBeenCalledWith( + "decks", + JSON.stringify([{ deckName: "Saved Deck", factions: expect.any(Array) }]), + ); + promptSpy.mockRestore(); +}); + +test("loads decks from local storage on component mount", () => { + const savedDecks = [{ deckName: "Loaded Deck", factions: [] }]; + jest + .spyOn(localStorage, "getItem") + .mockReturnValue(JSON.stringify(savedDecks)); + render(); + expect(screen.getByText("Loaded Deck")).toBeInTheDocument(); +}); + +test("shows only one deck per tab", () => { + const savedDecks = [ + { deckName: "Deck 1", factions: [] }, + { deckName: "Deck 2", factions: [] }, + ]; + jest + .spyOn(localStorage, "getItem") + .mockReturnValue(JSON.stringify(savedDecks)); + render(); + expect(screen.getByText("Deck 1")).toBeInTheDocument(); + expect(screen.getByText("Deck 2")).toBeInTheDocument(); + const tab1 = screen.getByRole("tabpanel", { hidden: true, name: "Deck 1" }); + const tab2 = screen.getByRole("tabpanel", { hidden: true, name: "Deck 2" }); + expect(tab1).toBeInTheDocument(); + expect(tab2).toBeInTheDocument(); +}); diff --git a/src/App.tsx b/src/App.tsx index f72c273..23a9ede 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,33 @@ -// src/App.tsx -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Deck } from "./models/types"; import { deepCopyMasterTable } from "./models/masterData"; import DeckTab from "./components/DeckTab"; -import { Container, Typography, Button, Box } from "@mui/material"; +import { Container, Typography, Button, Box, Tabs, Tab } from "@mui/material"; const App: React.FC = () => { const [decks, setDecks] = useState([]); + const [selectedTab, setSelectedTab] = useState(0); + + useEffect(() => { + const savedDecks = localStorage.getItem("decks"); + if (savedDecks) { + setDecks(JSON.parse(savedDecks)); + } + }, []); const handleAddDeck = () => { + const deckName = prompt("Enter deck name:", "New Deck") || "New Deck"; const newDeck: Deck = { - deckName: "New Deck", + deckName, factions: deepCopyMasterTable(), }; - setDecks([...decks, newDeck]); + const updatedDecks = [...decks, newDeck]; + setDecks(updatedDecks); + localStorage.setItem("decks", JSON.stringify(updatedDecks)); + }; + + const handleChange = (_event: React.SyntheticEvent, newValue: number) => { + setSelectedTab(newValue); }; return ( @@ -25,9 +39,23 @@ const App: React.FC = () => { Add New Deck + + {decks.map((deck, index) => ( + + ))} + + {decks.map((deck, index) => ( - + ))} diff --git a/src/components/DeckTab.tsx b/src/components/DeckTab.tsx index 9c71eee..62f044e 100644 --- a/src/components/DeckTab.tsx +++ b/src/components/DeckTab.tsx @@ -8,8 +8,9 @@ import { TableRow, Paper, Typography, + Box, } from "@mui/material"; -import { Deck, Faction, Warlord } from "../models/types"; +import { Deck, Warlord, Faction } from "../models/types"; // Helper function to compute derived stats function getWarlordStats(w: Warlord) { @@ -30,115 +31,113 @@ const DeckTab: React.FC = ({ deck }) => { {deck.deckName} - - - - - - Faction - - - Warlord - - - Matches - - - Off. Wins - - - Off. Losses - - - Def. Wins - - - Def. Losses - - - Total Wins - - - Total Losses - - - Win Rate - - - - - {deck.factions.map((faction: Faction) => { - const numberOfWarlords = faction.warlords.length; - - return faction.warlords.map((warlord: Warlord, idx: number) => { - const { matches, totalWins, totalLosses, winRate } = - getWarlordStats(warlord); - - // Color-coded column styles - const offWinStyle = { backgroundColor: "#bfb" }; - const offLossStyle = { backgroundColor: "#fbb" }; - const defWinStyle = { backgroundColor: "#bfb" }; - const defLossStyle = { backgroundColor: "#fbb" }; - const totalWinStyle = { backgroundColor: "#dfd" }; - const totalLossStyle = { backgroundColor: "#fdd" }; - const rateStyle = { backgroundColor: "#eee" }; - - return ( - - {/* - Only render the Faction cell for the first warlord in this faction - so it spans multiple rows. - */} - {idx === 0 && ( - - {faction.factionName} - - )} - - {/* Warlord Name */} - - {warlord.warlordName} + + +
+ + + + Warlord + + + Matches + + + Off. Wins + + + Off. Losses + + + Def. Wins + + + Def. Losses + + + Total Wins + + + Total Losses + + + Win Rate + + + + + {deck.factions.map((faction: Faction) => ( + + + + {faction.factionName} - - {/* Matches */} - {matches} - - {/* Off. Wins */} - {warlord.offWins} - - {/* Off. Losses */} - {warlord.offLosses} - - {/* Def. Wins */} - {warlord.defWins} - - {/* Def. Losses */} - {warlord.defLosses} - - {/* Total Wins */} - {totalWins} - - {/* Total Losses */} - {totalLosses} - - {/* Win Rate */} - {winRate.toFixed(1)}% - ); - }); - })} - -
-
+ {faction.warlords.map((warlord: Warlord) => { + const { matches, totalWins, totalLosses, winRate } = + getWarlordStats(warlord); + + // Color-coded column styles + const offWinStyle = { backgroundColor: "#bfb" }; + const offLossStyle = { backgroundColor: "#fbb" }; + const defWinStyle = { backgroundColor: "#bfb" }; + const defLossStyle = { backgroundColor: "#fbb" }; + const totalWinStyle = { backgroundColor: "#dfd" }; + const totalLossStyle = { backgroundColor: "#fdd" }; + const rateStyle = { backgroundColor: "#eee" }; + + return ( + + {/* Warlord Name */} + + {warlord.warlordName} + + + {/* Matches */} + {matches} + + {/* Off. Wins */} + + {warlord.offWins} + + + {/* Off. Losses */} + + {warlord.offLosses} + + + {/* Def. Wins */} + + {warlord.defWins} + + + {/* Def. Losses */} + + {warlord.defLosses} + + + {/* Total Wins */} + {totalWins} + + {/* Total Losses */} + {totalLosses} + + {/* Win Rate */} + + {winRate.toFixed(1)}% + + + ); + })} + + ))} + + + + ); }; diff --git a/yarn.lock b/yarn.lock index f799688..8d81106 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.4.0": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" + integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -1134,6 +1139,19 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" + redent "^3.0.0" + "@testing-library/react@^16.2.0": version "16.2.0" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.2.0.tgz#c96126ee01a49cdb47175721911b4a9432afc601" @@ -1521,6 +1539,11 @@ aria-query@5.3.0: dependencies: dequal "^2.0.3" +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" @@ -1798,6 +1821,14 @@ caniuse-lite@^1.0.30001688: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz#26cd429cf09b4fd4e745daf4916039c794d720f6" integrity sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ== +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1931,6 +1962,11 @@ css-to-react-native@3.2.0: css-color-keywords "^1.0.0" postcss-value-parser "^4.0.2" +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" @@ -2071,6 +2107,11 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -2842,6 +2883,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -3674,6 +3720,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -3752,6 +3803,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -4157,6 +4213,14 @@ react@^19.0.0: resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" @@ -4564,6 +4628,13 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"