- Adopt BoardUI (Pro init): theme tokens, Inter, .dark mode, Button, SegmentedControl, Meter, Skeleton and ThemeToggle; remap the app's color aliases onto BoardUI tokens and swap lucide for Remix icons - Retint BoardUI's neutral ramp to cool slate with a salmon accent; drop the dotted background; raise muted text contrast - Simplify the landing page and remove the overlap explorer - Planner: explicit "I have this" toggle on grocery lines, pantry panel above the menu with the staples list spelled out, sticky aisle header fix, and a "Download PDF" plan export (jsPDF, lazy-loaded) - Recipes: replace the family cabbage soup with Cade's Cabbage Soup (adds Bone-In Ham Steak to the catalog); remove Ham, Bean & Cabbage Soup - Remove unused BoardUI components, dependencies and exports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
2 KiB
TypeScript
50 lines
2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import { jsPDF } from "jspdf";
|
||
import { buildPlanModel, pdfSafe, renderPlanPdf } from "./exportPlan";
|
||
import { RECIPES } from "./recipes";
|
||
|
||
const breakfast = RECIPES.find((r) => r.kind === "breakfast")!;
|
||
const dinner = RECIPES.find((r) => r.kind === "main")!;
|
||
|
||
const state = {
|
||
daysCount: 2,
|
||
mealsPerDay: 3,
|
||
householdSize: 4,
|
||
// Monday breakfast + dinner, Tuesday dinner; "6-3" is outside daysCount and must not export.
|
||
slots: { "0-1": breakfast.id, "0-3": dinner.id, "1-3": dinner.id, "6-3": dinner.id },
|
||
};
|
||
|
||
describe("plan export", () => {
|
||
const model = buildPlanModel(state);
|
||
|
||
test("organizes meals under each planned day, in meal order", () => {
|
||
expect(model.days.map((d) => d.name)).toEqual(["Monday", "Tuesday"]);
|
||
const [monday, tuesday] = model.days;
|
||
expect(monday.meals.map((m) => ("empty" in m ? `${m.label}: -` : `${m.label}: ${m.title}`))).toEqual([
|
||
`Breakfast: ${breakfast.title}`,
|
||
"Lunch: -",
|
||
`Dinner: ${dinner.title}`,
|
||
]);
|
||
expect(tuesday.meals.filter((m) => !("empty" in m))).toHaveLength(1);
|
||
});
|
||
|
||
test("each meal carries its full recipe, scaled to the household", () => {
|
||
const meal = model.days[0].meals[0];
|
||
if ("empty" in meal) throw new Error("expected a planned meal");
|
||
expect(meal.steps).toEqual(breakfast.steps);
|
||
expect(meal.ingredients).toHaveLength(breakfast.ingredients.length);
|
||
expect(meal.meta).toContain("serves 4");
|
||
});
|
||
|
||
test("text is made printable in the base PDF font", () => {
|
||
expect(pdfSafe("1⅓ cup flour")).toBe("1 1/3 cup flour");
|
||
expect(pdfSafe("⅛ tsp salt")).toBe("1/8 tsp salt");
|
||
expect(pdfSafe("½ cup · 350°F — Rakı")).toBe("½ cup · 350°F — Raki");
|
||
});
|
||
|
||
test("renders a real multi-page PDF (one day per page)", () => {
|
||
const bytes = new Uint8Array(renderPlanPdf(model, jsPDF).output("arraybuffer"));
|
||
expect(new TextDecoder().decode(bytes.slice(0, 5))).toBe("%PDF-");
|
||
expect(renderPlanPdf(model, jsPDF).getNumberOfPages()).toBeGreaterThanOrEqual(2);
|
||
});
|
||
});
|