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;