1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
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()
}
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 inline add form must carry a scroll target. A form without one is the
// bug, and it is easy to reintroduce by adding a new store section by copy.
func TestShoppingTab_EveryInlineAddFormHasAScrollTarget(t *testing.T) {
html := renderShoppingTab(t, twoStoreTabData())
// Count the class attribute specifically — a bare "inline-add-form" also
// matches the string literal in the page's own JS handler.
forms := strings.Count(html, `class="inline-add-form`)
scrolls := strings.Count(html, "scroll:#store-")
if forms == 0 {
t.Fatal("expected at least one inline add form in the grouped view")
}
if forms != scrolls {
t.Errorf("found %d inline add forms but %d scroll targets; every form needs one",
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")
}
}
|