Phase 1: mobile-first pantry redesign with In/Low/Out state
Track presence (In/Low/Out) as the primary signal instead of exact quantities; quantity becomes an optional integer, unit optional too (migration 0003 backfills state from quantity: 0 -> out, else in). - Pantry rebuilt as a phone-first card list: colored state rail, segmented In/Low/Out switch (server-driven HTMX), greyed out-items, live summary counts. - Fast add: bottom add bar with type-ahead autocomplete, location picker, and quick-add chips for things you've run out of. - Per-item menu (move / set expiry / delete); expiry is now an optional quiet pill, never required. - New endpoints: pantry search + set-state; dropped the old edit-expiry/cancel flow and its partial. - Recipes matcher made presence-based (have-it beats have-enough); state added to admin. Tests cover state, add/restock, search, presence matching, and page rendering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
25a09b08fc
commit
ce61a0a86f
+104
-2
@@ -1,3 +1,105 @@
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your tests here.
|
||||
from kitchen.models import Ingredient, PantryItem, MetaRecipe, Slot, SlotOption
|
||||
|
||||
|
||||
class PantryStateTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.eggs = Ingredient.objects.create(name="eggs", default_unit="items")
|
||||
self.item = PantryItem.objects.create(
|
||||
ingredient=self.eggs, location="fridge", state="in"
|
||||
)
|
||||
|
||||
def test_state_defaults_to_in(self):
|
||||
self.assertEqual(self.item.state, "in")
|
||||
|
||||
def test_quantity_is_optional(self):
|
||||
self.assertIsNone(self.item.quantity)
|
||||
|
||||
def test_set_state_endpoint(self):
|
||||
resp = self.client.post(
|
||||
reverse("app-pantry-set-state", args=[self.item.id]), {"to": "low"}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.item.refresh_from_db()
|
||||
self.assertEqual(self.item.state, "low")
|
||||
|
||||
def test_set_state_rejects_unknown_value(self):
|
||||
self.client.post(
|
||||
reverse("app-pantry-set-state", args=[self.item.id]), {"to": "bogus"}
|
||||
)
|
||||
self.item.refresh_from_db()
|
||||
self.assertEqual(self.item.state, "in") # unchanged
|
||||
|
||||
def test_out_items_still_listed(self):
|
||||
self.item.state = "out"
|
||||
self.item.save()
|
||||
resp = self.client.get(reverse("app-pantry"))
|
||||
self.assertContains(resp, "eggs")
|
||||
|
||||
def test_add_creates_in_stock_item(self):
|
||||
resp = self.client.post(
|
||||
reverse("app-pantry-add"),
|
||||
{"ingredient_name": "milk", "location": "fridge"},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(
|
||||
PantryItem.objects.filter(ingredient__name="milk", state="in").exists()
|
||||
)
|
||||
|
||||
def test_add_restocks_existing_to_in(self):
|
||||
self.item.state = "out"
|
||||
self.item.save()
|
||||
self.client.post(
|
||||
reverse("app-pantry-add"),
|
||||
{"ingredient_name": "eggs", "location": "fridge"},
|
||||
)
|
||||
# No duplicate row, and the existing one is back in stock.
|
||||
items = PantryItem.objects.filter(ingredient=self.eggs, location="fridge")
|
||||
self.assertEqual(items.count(), 1)
|
||||
self.assertEqual(items.first().state, "in")
|
||||
|
||||
def test_search_matches_by_name(self):
|
||||
resp = self.client.get(
|
||||
reverse("app-pantry-search"), {"ingredient_name": "egg"}
|
||||
)
|
||||
self.assertContains(resp, "eggs")
|
||||
|
||||
|
||||
class RecipesPresenceTests(TestCase):
|
||||
"""The recipes matcher is presence-based now: having an ingredient (not
|
||||
'out') satisfies a slot regardless of quantity."""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.noodles = Ingredient.objects.create(name="noodles", default_unit="nests")
|
||||
self.mr = MetaRecipe.objects.create(name="Noodles", method="boil")
|
||||
slot = Slot.objects.create(meta_recipe=self.mr, name="carb", required=True)
|
||||
SlotOption.objects.create(
|
||||
slot=slot, ingredient=self.noodles, quantity_per_serving=2, unit="nests"
|
||||
)
|
||||
|
||||
def test_present_ingredient_no_quantity_is_available(self):
|
||||
# 'in' with no quantity should still count as available.
|
||||
PantryItem.objects.create(
|
||||
ingredient=self.noodles, location="cupboard", state="in"
|
||||
)
|
||||
resp = self.client.get(reverse("app-recipes"))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, "Noodles")
|
||||
|
||||
def test_out_ingredient_renders_without_error(self):
|
||||
PantryItem.objects.create(
|
||||
ingredient=self.noodles, location="cupboard", state="out"
|
||||
)
|
||||
resp = self.client.get(reverse("app-recipes"))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
class PageSmokeTests(TestCase):
|
||||
def test_pages_render(self):
|
||||
for name in ("app-pantry", "app-recipes", "app-shopping", "app-log"):
|
||||
with self.subTest(page=name):
|
||||
self.assertEqual(self.client.get(reverse(name)).status_code, 200)
|
||||
|
||||
Reference in New Issue
Block a user