This commit is contained in:
Cayden Schwechten 2026-10-31 20:44:29 -05:00
commit c19b910c36
80 changed files with 16134 additions and 0 deletions

93
app/lib/plan.test.ts Normal file
View file

@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test";
import type { MealPlanState } from "@/app/types";
import { activeRecipeIds, mealKindFor, mealLabel, visibleMeals } from "./plan";
import { DEFAULT_STATE, migrateV1Slots, sanitizeState } from "./sanitize";
import { RECIPES } from "./recipes";
import { planReducer } from "./usePlanState";
// ---------------------------------------------------------------------------
// Meal slots. Regression tests for the "change meals per day and your
// breakfast turns into lunch" bug. Meal numbers are fixed:
// 1 = Breakfast, 2 = Lunch, 3 = Dinner
// and "meals per day" only decides which ones you can see.
// ---------------------------------------------------------------------------
// Any real recipes will do; names are just for readability.
const PANCAKES = RECIPES.find((r) => r.kind === "breakfast")!.id;
const [SANDWICH, STEW] = RECIPES.filter((r) => r.kind === "main").map((r) => r.id);
/** Monday with all three meals planned. */
const fullMonday: MealPlanState = {
...DEFAULT_STATE,
daysCount: 1,
mealsPerDay: 3,
slots: { "0-1": PANCAKES, "0-2": SANDWICH, "0-3": STEW },
};
/** What the grid shows, as "Label: recipe". */
function visibleGrid(state: MealPlanState): string[] {
return visibleMeals(state.mealsPerDay).map((meal) => `${mealLabel(meal)}: ${state.slots[`0-${meal}`] ?? "—"}`);
}
describe("visible meals", () => {
test("fewer meals hides from the front of the day", () => {
expect(visibleMeals(3).map(mealLabel)).toEqual(["Breakfast", "Lunch", "Dinner"]);
expect(visibleMeals(2).map(mealLabel)).toEqual(["Lunch", "Dinner"]);
expect(visibleMeals(1).map(mealLabel)).toEqual(["Dinner"]);
});
test("only meal 1 is breakfast", () => {
expect([1, 2, 3].map(mealKindFor)).toEqual(["breakfast", "main", "main"]);
});
});
describe("changing meals per day", () => {
test("3 → 2 keeps lunch as lunch and dinner as dinner", () => {
const state = planReducer(fullMonday, { type: "setMealsPerDay", value: 2 });
expect(visibleGrid(state)).toEqual([`Lunch: ${SANDWICH}`, `Dinner: ${STEW}`]);
});
test("3 → 1 → 3 brings breakfast and lunch back (nothing deleted)", () => {
const down = planReducer(fullMonday, { type: "setMealsPerDay", value: 1 });
expect(visibleGrid(down)).toEqual([`Dinner: ${STEW}`]);
const backUp = planReducer(down, { type: "setMealsPerDay", value: 3 });
expect(visibleGrid(backUp)).toEqual([`Breakfast: ${PANCAKES}`, `Lunch: ${SANDWICH}`, `Dinner: ${STEW}`]);
});
test("hidden meals don't count toward the cart", () => {
expect(activeRecipeIds({ ...fullMonday, mealsPerDay: 1 })).toEqual([STEW]);
expect(activeRecipeIds({ ...fullMonday, mealsPerDay: 2 })).toEqual([SANDWICH, STEW]);
});
});
describe("Clear", () => {
test("empties the visible grid but keeps hidden meals", () => {
const lunchAndDinner = { ...fullMonday, mealsPerDay: 2 };
const cleared = planReducer(lunchAndDinner, { type: "clearSlots" });
expect(visibleGrid(cleared)).toEqual(["Lunch: —", "Dinner: —"]);
expect(cleared.slots).toEqual({ "0-1": PANCAKES }); // breakfast was hidden, so untouched
});
});
describe("migrating plans saved before meal numbers were fixed", () => {
test("2 meals/day: old 1/2 (Lunch/Dinner) become 2/3", () => {
const old = { mealsPerDay: 2, slots: { "0-1": SANDWICH, "0-2": STEW } };
expect((migrateV1Slots(old) as MealPlanState).slots).toEqual({ "0-2": SANDWICH, "0-3": STEW });
});
test("1 meal/day: old 1 (Dinner) becomes 3, stale hidden keys are dropped", () => {
const old = { mealsPerDay: 1, slots: { "0-1": STEW, "0-2": SANDWICH } };
expect((migrateV1Slots(old) as MealPlanState).slots).toEqual({ "0-3": STEW });
});
test("3 meals/day: nothing moves", () => {
const old = { mealsPerDay: 3, slots: { "0-1": PANCAKES, "0-3": STEW } };
expect((migrateV1Slots(old) as MealPlanState).slots).toEqual(old.slots);
});
test("migrated plans still pass sanitizeState", () => {
expect(() => sanitizeState(migrateV1Slots({ mealsPerDay: 2, slots: { "0-1": "x" } }))).not.toThrow();
});
});