summaryrefslogtreecommitdiff
path: root/internal/handlers/shopping_template_test.go
blob: 06f63b3e46e031e935b2e2b460a78be95a70ce46 (plain)
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")
	}
}