···7979 if !strings.Contains(html, "Shopping list") {
8080 t.Error("shopping list HTML should render the shopping list section for a single recipe")
8181 }
8282+ if !strings.Contains(html, `sm:grid-cols-[minmax(0,1fr)_10rem_5rem]`) {
8383+ t.Error("shopping list HTML should render ingredient rows with responsive aligned columns")
8484+ }
8585+ if strings.Contains(html, `flex flex-wrap items-center justify-between gap-2 rounded-lg bg-brand-50 px-3 py-2 text-sm`) {
8686+ t.Error("shopping list HTML should no longer use the old wrapped ingredient row layout")
8787+ }
8288 if !strings.Contains(html, `id="finalize-help"`) {
8389 t.Error("shopping list HTML should include helper text for disabled finalize state")
8490 }
···249255 if !strings.Contains(html, "Estimated cost:") {
250256 t.Error("recipe HTML should contain estimated cost")
251257 }
258258+ if !strings.Contains(html, `sm:grid-cols-[minmax(0,1fr)_10rem_5rem]`) {
259259+ t.Error("recipe HTML should render ingredient rows with responsive aligned columns")
260260+ }
261261+ if strings.Contains(html, `flex flex-wrap items-center justify-between gap-2 rounded-lg bg-brand-50 px-3 py-2 text-sm`) {
262262+ t.Error("recipe HTML should no longer use the old wrapped ingredient row layout")
263263+ }
252264 if !strings.Contains(html, `id="question-error"`) {
253265 t.Error("recipe HTML should contain question error surface")
254266 }
···372384 }
373385 if strings.Contains(html, "Choose a wine") {
374386 t.Error("recipe HTML should not render the wine picker when recommendation exists")
387387+ }
388388+}
389389+390390+func TestFormatRecipeHTML_AllowsIngredientWithoutPrice(t *testing.T) {
391391+ loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"}
392392+ p := DefaultParams(&loc, time.Now())
393393+ p.ConversationID = "convo123"
394394+ w := httptest.NewRecorder()
395395+ recipe := ai.Recipe{
396396+ Title: "Market Greens",
397397+ Description: "Simple salad",
398398+ CookTime: "10 minutes",
399399+ CostEstimate: "$8-10",
400400+ Ingredients: []ai.Ingredient{
401401+ {Name: "Little gem lettuce", Quantity: "2 heads", Price: ""},
402402+ },
403403+ Instructions: []string{"Wash and plate."},
404404+ Health: "Light",
405405+ DrinkPairing: "Sparkling water",
406406+ }
407407+408408+ FormatRecipeHTML(t.Context(), p, recipe, true, false, []RecipeThreadEntry{}, feedback.Feedback{}, nil, w)
409409+ html := w.Body.String()
410410+411411+ isValidHTML(t, html)
412412+ if !strings.Contains(html, "Little gem lettuce") {
413413+ t.Fatal("recipe HTML should include ingredient name when price is empty")
414414+ }
415415+ if !strings.Contains(html, "2 heads") {
416416+ t.Fatal("recipe HTML should include ingredient quantity when price is empty")
417417+ }
418418+ if !strings.Contains(html, `hidden sm:block`) {
419419+ t.Fatal("recipe HTML should reserve desktop alignment when ingredient price is empty")
420420+ }
421421+}
422422+423423+func TestFormatShoppingListHTML_AllowsIngredientWithoutPrice(t *testing.T) {
424424+ loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"}
425425+ p := DefaultParams(&loc, time.Now())
426426+ w := httptest.NewRecorder()
427427+ listWithoutPrice := ai.ShoppingList{
428428+ Recipes: []ai.Recipe{
429429+ {
430430+ Title: "Spring Pasta",
431431+ Description: "Bright and quick",
432432+ CookTime: "20 minutes",
433433+ CostEstimate: "$12-15",
434434+ Ingredients: []ai.Ingredient{
435435+ {Name: "English peas", Quantity: "1 cup", Price: ""},
436436+ },
437437+ Instructions: []string{"Boil and toss."},
438438+ Health: "Balanced",
439439+ DrinkPairing: "Lemon water",
440440+ },
441441+ },
442442+ }
443443+444444+ FormatShoppingListHTML(t.Context(), p, listWithoutPrice, true, w)
445445+ html := w.Body.String()
446446+447447+ isValidHTML(t, html)
448448+ if !strings.Contains(html, "English peas") {
449449+ t.Fatal("shopping list HTML should include ingredient name when price is empty")
450450+ }
451451+ if !strings.Contains(html, "1 cup") {
452452+ t.Fatal("shopping list HTML should include ingredient quantity when price is empty")
453453+ }
454454+ if !strings.Contains(html, `hidden sm:block`) {
455455+ t.Fatal("shopping list HTML should reserve desktop alignment when ingredient price is empty")
375456 }
376457}
377458