package handlers import ( "bytes" "html/template" "path/filepath" "strings" "testing" "task-dashboard/internal/models" ) // renderShoppingTab renders the real shopping-tab template with the real func // map, so these assertions cover what actually ships rather than a stand-in. func renderShoppingTab(t *testing.T, data any) string { t.Helper() dir := filepath.Join("..", "..", "web", "templates") tmpl, err := template.New("").Funcs(templateFuncMap()).ParseGlob(filepath.Join(dir, "*.html")) if err != nil { t.Fatalf("parsing top-level templates: %v", err) } if _, err := tmpl.ParseGlob(filepath.Join(dir, "partials", "*.html")); err != nil { t.Fatalf("parsing partials: %v", err) } var buf bytes.Buffer if err := tmpl.ExecuteTemplate(&buf, "shopping-tab", data); err != nil { t.Fatalf("executing shopping-tab: %v", err) } return buf.String() } // renderShoppingItemsContent renders just the swapped-in portion of the tab. func renderShoppingItemsContent(t *testing.T, data any) string { t.Helper() dir := filepath.Join("..", "..", "web", "templates") tmpl, err := template.New("").Funcs(templateFuncMap()).ParseGlob(filepath.Join(dir, "*.html")) if err != nil { t.Fatalf("parsing top-level templates: %v", err) } if _, err := tmpl.ParseGlob(filepath.Join(dir, "partials", "*.html")); err != nil { t.Fatalf("parsing partials: %v", err) } var buf bytes.Buffer if err := tmpl.ExecuteTemplate(&buf, "shopping-items-content", data); err != nil { t.Fatalf("executing shopping-items-content: %v", err) } return buf.String() } func twoStoreTabData() any { return struct { Stores []models.ShoppingStore Grouped bool }{ Stores: []models.ShoppingStore{ {Name: "Costco", Categories: []models.ShoppingCategory{ {Name: "", Items: []models.UnifiedShoppingItem{ {ID: "user-1", Name: "Milk", Store: "Costco", Source: "user"}, }}, }}, {Name: "Hardware Store", Categories: []models.ShoppingCategory{ {Name: "", Items: []models.UnifiedShoppingItem{ {ID: "user-2", Name: "Screws", Store: "Hardware Store", Source: "user"}, }}, }}, }, Grouped: true, } } // The inline per-store add form used to submit with a bare // hx-swap="innerHTML". Because that form lives *inside* the swap target // (#shopping-items-list), submitting it destroys the form mid-request and the // page ends up scrolled to the bottom instead of staying where the user was. // The quick-add form at the top already pinned this with an explicit // scroll: modifier; the per-store forms did not. func TestShoppingTab_InlineAddScrollsToItsOwnStore(t *testing.T) { html := renderShoppingTab(t, twoStoreTabData()) for _, want := range []string{ `id="store-costco"`, `id="store-hardware-store"`, `scroll:#store-costco:top`, `scroll:#store-hardware-store:top`, } { if !strings.Contains(html, want) { t.Errorf("rendered shopping tab is missing %q", want) } } } // Every form that lives *inside* the swap target must carry a scroll modifier. // Those forms are destroyed by their own swap, and without an explicit target // the page lands at the bottom. Forms outside the target (the store-create // form) do not have this problem and are excluded by rendering only the items // content here. func TestShoppingItemsContent_EveryFormInsideTheSwapTargetScrolls(t *testing.T) { html := renderShoppingItemsContent(t, twoStoreTabData()) forms := strings.Count(html, `hx-target="#shopping-items-list"`) scrolls := strings.Count(html, "scroll:#store-") if forms == 0 { t.Fatal("expected forms inside the items content") } if forms != scrolls { t.Errorf("%d forms target the items list but only %d carry a scroll target", forms, scrolls) } } func TestSlug(t *testing.T) { slug, ok := templateFuncMap()["slug"].(func(string) string) if !ok { t.Fatal("templateFuncMap has no slug function of type func(string) string") } cases := []struct{ in, want string }{ {"Costco", "costco"}, {"Hardware Store", "hardware-store"}, {"Trader Joe's", "trader-joe-s"}, {" Spaced Out ", "spaced-out"}, {"Multiple---Dashes", "multiple-dashes"}, // Non-ASCII is treated as a separator rather than transliterated. That // keeps the id valid without pulling in a transliteration table; the // result only has to be stable and unique, not pretty. {"Ünïcode Störe", "n-code-st-re"}, {"", "unnamed"}, {"!!!", "unnamed"}, } for _, c := range cases { if got := slug(c.in); got != c.want { t.Errorf("slug(%q) = %q, want %q", c.in, got, c.want) } } } // Two stores whose names differ only by punctuation must not collide into the // same element id, or the scroll target becomes ambiguous. func TestSlug_DistinguishesSimilarNames(t *testing.T) { slug := templateFuncMap()["slug"].(func(string) string) if slug("Store A") == slug("Store B") { t.Error("distinct store names produced the same slug") } }