From 2a207b65815801c97a34bfe648f4c5f196af369d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Sp=C3=B6ttel?= <1682504+fspoettel@users.noreply.github.com> Date: Sun, 16 Jun 2024 11:41:19 +0200 Subject: [PATCH] test: cover card quantity updates --- src/components/card-list/card-list.tsx | 4 +- .../card-modal/card-modal-quantities.tsx | 12 +- src/components/decklist/decklist-groups.tsx | 4 +- src/store/slices/data.spec.ts | 5 + src/store/slices/data.ts | 4 +- src/store/slices/deck-view.spec.ts | 119 +++++++ src/store/slices/deck-view.ts | 295 +++++++++--------- src/store/slices/deck-view.types.ts | 10 +- 8 files changed, 289 insertions(+), 164 deletions(-) create mode 100644 src/store/slices/deck-view.spec.ts diff --git a/src/components/card-list/card-list.tsx b/src/components/card-list/card-list.tsx index 5463e8c1..d3fd5527 100644 --- a/src/components/card-list/card-list.tsx +++ b/src/components/card-list/card-list.tsx @@ -34,7 +34,7 @@ export function CardList({ slotLeft, slotRight }: Props) { const data = useStore(selectListCards); const canEdit = useStore(selectCanEditDeck); - const changeCardQuantity = useStore((state) => state.changeCardQuantity); + const updateCardQuantity = useStore((state) => state.updateCardQuantity); const quantities = useStore(selectCardQuantities); const search = useStore(selectActiveListSearch); const metadata = useStore((state) => state.metadata); @@ -204,7 +204,7 @@ export function CardList({ slotLeft, slotRight }: Props) { isActive={index === currentTop} key={data.cards[index].code} onChangeCardQuantity={ - canEdit ? changeCardQuantity : undefined + canEdit ? updateCardQuantity : undefined } quantities={quantities} /> diff --git a/src/components/card-modal/card-modal-quantities.tsx b/src/components/card-modal/card-modal-quantities.tsx index b9c7497f..6a22d917 100644 --- a/src/components/card-modal/card-modal-quantities.tsx +++ b/src/components/card-modal/card-modal-quantities.tsx @@ -36,7 +36,7 @@ export function CardModalQuantities({ [onClickBackground], ); - const changeCardQuantity = useStore((state) => state.changeCardQuantity); + const updateCardQuantity = useStore((state) => state.updateCardQuantity); useEffect(() => { if (!canEdit) return; @@ -44,13 +44,13 @@ export function CardModalQuantities({ function onKeyDown(evt: KeyboardEvent) { if (evt.key === "ArrowRight") { evt.preventDefault(); - changeCardQuantity(card.code, 1, "slots"); + updateCardQuantity(card.code, 1, "slots"); } else if (evt.key === "ArrowLeft") { evt.preventDefault(); - changeCardQuantity(card.code, -1, "slots"); + updateCardQuantity(card.code, -1, "slots"); } else if (Number.parseInt(evt.key) >= 0) { evt.preventDefault(); - changeCardQuantity(card.code, Number.parseInt(evt.key), "slots", "set"); + updateCardQuantity(card.code, Number.parseInt(evt.key), "slots", "set"); onClickBackground?.(); } } @@ -59,7 +59,7 @@ export function CardModalQuantities({ return () => { window.removeEventListener("keydown", onKeyDown); }; - }, [canEdit, card.code, changeCardQuantity, onClickBackground]); + }, [canEdit, card.code, updateCardQuantity, onClickBackground]); const quantities = useStore((state) => selectCardQuantitiesForSlot(state, "slots"), @@ -82,7 +82,7 @@ export function CardModalQuantities({ ); const onChangeQuantity = (quantity: number, slot: Slot) => { - changeCardQuantity(card.code, quantity, slot); + updateCardQuantity(card.code, quantity, slot); }; const showIgnoreDeckLimitSlots = useStore((state) => diff --git a/src/components/decklist/decklist-groups.tsx b/src/components/decklist/decklist-groups.tsx index 61e45ea1..894171ff 100644 --- a/src/components/decklist/decklist-groups.tsx +++ b/src/components/decklist/decklist-groups.tsx @@ -112,7 +112,7 @@ export function DecklistGroup({ const forbiddenCards = useStore(selectForbiddenCards); const canEdit = useStore(selectCanEditDeck) && mapping !== "bonded"; const canCheckOwnership = useStore(selectCanCheckOwnership); - const changeCardQuantity = useStore((state) => state.changeCardQuantity); + const updateCardQuantity = useStore((state) => state.updateCardQuantity); return (
    @@ -130,7 +130,7 @@ export function DecklistGroup({ isIgnored={ignoredCounts?.[card.code]} key={card.code} omitBorders - onChangeCardQuantity={canEdit ? changeCardQuantity : undefined} + onChangeCardQuantity={canEdit ? updateCardQuantity : undefined} owned={ownershipCounts[card.code]} quantities={quantities} size="sm" diff --git a/src/store/slices/data.spec.ts b/src/store/slices/data.spec.ts index 8429c6c6..98ba5c2b 100644 --- a/src/store/slices/data.spec.ts +++ b/src/store/slices/data.spec.ts @@ -1,3 +1,4 @@ +import { afterEach } from "node:test"; import { beforeAll, describe, expect, it } from "vitest"; import type { StoreApi } from "zustand"; @@ -29,6 +30,10 @@ describe("data slice", () => { }, }; + afterEach(async () => { + store = await getMockStore(); + }); + it("does not delete decks with upgrades", () => { store.setState(mockState); diff --git a/src/store/slices/data.ts b/src/store/slices/data.ts index ddf10e05..f6e07969 100644 --- a/src/store/slices/data.ts +++ b/src/store/slices/data.ts @@ -31,9 +31,7 @@ export const createDataSlice: StateCreator = ( deck.tags = deck.tags.replaceAll(", ", " "); } - if (state.data.decks[deck.id]) { - throw new Error(`Deck ${deck.id} already exists.`); - } + assert(!state.data.decks[deck.id], `Deck ${deck.id} already exists.`); const deckHistory = type === "deck" && deck.previous_deck diff --git a/src/store/slices/deck-view.spec.ts b/src/store/slices/deck-view.spec.ts new file mode 100644 index 00000000..68c02e4a --- /dev/null +++ b/src/store/slices/deck-view.spec.ts @@ -0,0 +1,119 @@ +import { afterEach } from "node:test"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import type { StoreApi } from "zustand"; + +import deckExtraSlots from "@/test/fixtures/decks/extra_slots.json"; +import { getMockStore } from "@/test/get-mock-store"; + +import type { StoreState } from "."; +import { selectActiveDeck } from "../selectors/decks"; + +describe("deck-view slice", () => { + let store: StoreApi; + + beforeAll(async () => { + store = await getMockStore(); + }); + + describe("updateCardQuantity", () => { + beforeEach(() => { + store.setState({ + data: { + decks: { + "deck-id": deckExtraSlots, + }, + history: { + "deck-id": [], + }, + }, + }); + }); + + afterEach(async () => { + store = await getMockStore(); + }); + + it("throws an error if there is no active deck", () => { + store.setState({ + deckView: null, + }); + + expect(() => { + store.getState().updateCardQuantity("01000", 1, "slots"); + }).toThrowErrorMatchingInlineSnapshot( + `[Error: assertion failed: trying to edit deck, but state does not have an active deck.]`, + ); + }); + + it("throws an error if the active deck is not in edit mode", () => { + store.setState({ + deckView: { + id: "deck-id", + mode: "view", + }, + }); + + expect(() => { + store.getState().updateCardQuantity("01000", 1, "slots"); + }).toThrowErrorMatchingInlineSnapshot( + `[Error: assertion failed: trying to edit deck, but not in edit mode.]`, + ); + }); + + it("throws an error if the active deck does not exist", () => { + store.getState().setActiveDeck("non-existent-deck", "edit"); + + expect(() => { + store.getState().updateCardQuantity("01000", 1, "slots"); + }).toThrowErrorMatchingInlineSnapshot( + `[Error: assertion failed: trying to edit deck, but deck does not exist.]`, + ); + }); + + it("increments the quantity of a card", () => { + const state = store.getState(); + state.setActiveDeck("deck-id", "edit"); + state.updateCardQuantity("01000", 1, "slots", "increment"); + expect(selectActiveDeck(store.getState())?.slots["01000"]).toEqual(2); + }); + + it("decrements the quantity of a card", () => { + const state = store.getState(); + state.setActiveDeck("deck-id", "edit"); + state.updateCardQuantity("01000", -1, "slots", "increment"); + expect(selectActiveDeck(store.getState())?.slots["01000"]).toEqual(0); + }); + + it("sets the quantity of a card", () => { + const state = store.getState(); + state.setActiveDeck("deck-id", "edit"); + state.updateCardQuantity("01000", 5, "slots", "set"); + expect(selectActiveDeck(store.getState())?.slots["01000"]).toEqual(5); + }); + + it("does not set the quantity of a card to a negative value", () => { + const state = store.getState(); + state.setActiveDeck("deck-id", "edit"); + state.updateCardQuantity("01000", -5, "slots", "set"); + state.updateCardQuantity("01000", -5, "slots", "increment"); + expect(selectActiveDeck(store.getState())?.slots["01000"]).toEqual(0); + }); + + it("does not set the quantity of a card exceeding the limit", () => { + const state = store.getState(); + state.setActiveDeck("deck-id", "edit"); + state.updateCardQuantity("06021", 5, "slots", "set"); + state.updateCardQuantity("06021", 5, "slots", "increment"); + expect(selectActiveDeck(store.getState())?.slots["06021"]).toEqual(3); + }); + + it("adjusts cards in side slots", () => { + const state = store.getState(); + state.setActiveDeck("deck-id", "edit"); + state.updateCardQuantity("06021", 1, "sideSlots", "increment"); + expect(selectActiveDeck(store.getState())?.sideSlots?.["06021"]).toEqual( + 1, + ); + }); + }); +}); diff --git a/src/store/slices/deck-view.ts b/src/store/slices/deck-view.ts index edb73639..3155c029 100644 --- a/src/store/slices/deck-view.ts +++ b/src/store/slices/deck-view.ts @@ -4,6 +4,7 @@ import { assert } from "@/utils/assert"; import type { StoreState } from "."; import { resolveDeck } from "../lib/resolve-deck"; +import type { EditState } from "./deck-view.types"; import { type DeckViewSlice, isTab, mapTabToSlot } from "./deck-view.types"; export const createDeckViewSlice: StateCreator< @@ -14,21 +15,55 @@ export const createDeckViewSlice: StateCreator< > = (set, get) => ({ deckView: null, - changeCardQuantity(code, quantity, tab, mode = "increment") { + setActiveDeck(activeDeckId, mode) { + if (!activeDeckId || !mode) { + set({ deckView: null }); + return; + } + + if (mode === "view") { + set({ + deckView: { + id: activeDeckId, + mode, + }, + }); + return; + } + + set({ + deckView: { + activeTab: "slots", + showUnusableCards: false, + id: activeDeckId, + dirty: false, + edits: { + meta: {}, + quantities: {}, + customizations: {}, + }, + mode, + }, + }); + }, + + updateActiveTab(value) { const state = get(); - assert( - state.deckView, - "trying to edit deck, but state does not have an active deck.", - ); - assert( - state.deckView.mode === "edit", - "trying to edit deck, but not in edit mode.", - ); - assert( - state.data.decks[state.deckView.id], - `trying to edit deck, but deck does not exist.`, - ); + assertInEditMode(state); + assert(isTab(value), `invalid tab value: ${value}`); + + set({ + deckView: { + ...state.deckView, + activeTab: value, + }, + }); + }, + + updateCardQuantity(code, quantity, tab, mode = "increment") { + const state = get(); + assertInEditMode(state); const targetTab = tab || state.deckView.activeTab || "slots"; @@ -52,7 +87,7 @@ export const createDeckViewSlice: StateCreator< const newValue = mode === "increment" ? Math.max(value + quantity, 0) - : Math.min(quantity, limit); + : Math.max(Math.min(quantity, limit), 0); if (mode === "increment" && value + quantity > limit) return; @@ -74,183 +109,132 @@ export const createDeckViewSlice: StateCreator< }); }, - setActiveDeck(activeDeckId, mode) { - if (!activeDeckId || !mode) { - set({ deckView: null }); - return; - } - - if (mode === "view") { - set({ - deckView: { - id: activeDeckId, - mode, - }, - }); - return; - } + updateTabooId(value) { + const state = get(); + assertInEditMode(state); set({ deckView: { - activeTab: "slots", - showUnusableCards: false, - id: activeDeckId, - dirty: false, + ...state.deckView, + dirty: true, edits: { - meta: {}, - quantities: {}, - customizations: {}, + ...state.deckView.edits, + tabooId: value, }, - mode, }, }); }, - - updateActiveTab(value) { - const state = get(); - - if (state.deckView && state.deckView.mode === "edit" && isTab(value)) { - set({ - deckView: { - ...state.deckView, - activeTab: value, - }, - }); - } - }, - - updateTabooId(value) { - const state = get(); - - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - tabooId: value, - }, - }, - }); - } - }, updateDescription(value) { const state = get(); + assertInEditMode(state); - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - description_md: value, - }, + set({ + deckView: { + ...state.deckView, + dirty: true, + edits: { + ...state.deckView.edits, + description_md: value, }, - }); - } + }, + }); }, updateName(value) { const state = get(); + assertInEditMode(state); - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - name: value, - }, + set({ + deckView: { + ...state.deckView, + dirty: true, + edits: { + ...state.deckView.edits, + name: value, }, - }); - } + }, + }); }, updateMetaProperty(key, value) { const state = get(); + assertInEditMode(state); - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - meta: { - ...state.deckView.edits.meta, - [key]: value, - }, + set({ + deckView: { + ...state.deckView, + dirty: true, + edits: { + ...state.deckView.edits, + meta: { + ...state.deckView.edits.meta, + [key]: value, }, }, - }); - } + }, + }); }, updateInvestigatorSide(side, code) { const state = get(); - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - investigatorBack: - side === "back" ? code : state.deckView.edits.investigatorBack, - investigatorFront: - side === "front" ? code : state.deckView.edits.investigatorFront, - }, + assertInEditMode(state); + + set({ + deckView: { + ...state.deckView, + dirty: true, + edits: { + ...state.deckView.edits, + investigatorBack: + side === "back" ? code : state.deckView.edits.investigatorBack, + investigatorFront: + side === "front" ? code : state.deckView.edits.investigatorFront, }, - }); - } + }, + }); }, updateCustomization(code, index, edit) { const state = get(); - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - customizations: { - ...state.deckView.edits.customizations, - [code]: { - ...state.deckView.edits.customizations[code], - [index]: { - ...state.deckView.edits.customizations[code]?.[index], - ...edit, - }, + assertInEditMode(state); + + set({ + deckView: { + ...state.deckView, + dirty: true, + edits: { + ...state.deckView.edits, + customizations: { + ...state.deckView.edits.customizations, + [code]: { + ...state.deckView.edits.customizations[code], + [index]: { + ...state.deckView.edits.customizations[code]?.[index], + ...edit, }, }, }, }, - }); - } + }, + }); }, updateTags(value) { const state = get(); - if (state.deckView && state.deckView.mode === "edit") { - set({ - deckView: { - ...state.deckView, - dirty: true, - edits: { - ...state.deckView.edits, - tags: value, - }, + assertInEditMode(state); + set({ + deckView: { + ...state.deckView, + dirty: true, + edits: { + ...state.deckView.edits, + tags: value, }, - }); - } + }, + }); }, updateShowUnusableCards(showUnusableCards) { const state = get(); - - if (state.deckView?.mode !== "edit") return; + assertInEditMode(state); set({ deckView: { @@ -260,3 +244,22 @@ export const createDeckViewSlice: StateCreator< }); }, }); + +function assertInEditMode(state: StoreState): asserts state is StoreState & { + deckView: EditState; +} { + assert( + state.deckView, + "trying to edit deck, but state does not have an active deck.", + ); + + assert( + state.deckView.mode === "edit", + "trying to edit deck, but not in edit mode.", + ); + + assert( + state.data.decks[state.deckView.id], + `trying to edit deck, but deck does not exist.`, + ); +} diff --git a/src/store/slices/deck-view.types.ts b/src/store/slices/deck-view.types.ts index 668cd995..9b0fdf3c 100644 --- a/src/store/slices/deck-view.types.ts +++ b/src/store/slices/deck-view.types.ts @@ -79,17 +79,17 @@ export type DeckViewState = { export type DeckViewSlice = { deckView: DeckViewState | null; - changeCardQuantity( + setActiveDeck(id?: string, type?: "view" | "edit"): void; + + updateActiveTab(value: string): void; + + updateCardQuantity( code: string, quantity: number, slot?: Slot, mode?: "increment" | "set", ): void; - setActiveDeck(id?: string, type?: "view" | "edit"): void; - - updateActiveTab(value: string): void; - updateTabooId(value: number | null): void; updateInvestigatorSide(side: string, code: string): void;