Files
kjol/go/webui/icons_test.go

117 lines
3.3 KiB
Go

package webui
import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
"kjol/vdom"
)
// An unregistered icon name renders as an empty box: right size, no glyph. Nothing
// errors, nothing logs — the icons just quietly aren't there. That is exactly how
// this broke, so scan our own source for every name we ask for and assert the
// registry has it.
func TestEveryIconTheKitUsesIsRegistered(t *testing.T) {
// Icon("name", …), IconInline("name", …), and the Icon: "name" struct field.
calls := regexp.MustCompile(`\bIcon(?:Inline|Success|Error)?\("([a-z0-9-]+)"`)
fields := regexp.MustCompile(`\bIcon:\s*"([a-z0-9-]+)"`)
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
used := map[string][]string{} // icon name -> files that ask for it
for _, f := range files {
if strings.HasSuffix(f, "_test.go") || f == "icons.go" {
continue // icons.go is the registry itself
}
src, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
for _, re := range []*regexp.Regexp{calls, fields} {
for _, m := range re.FindAllStringSubmatch(string(src), -1) {
if m[1] == "" {
continue
}
used[m[1]] = append(used[m[1]], f)
}
}
}
// Icon names also reach Icon() through lookup tables. Those cannot be found by
// scanning call sites, so add them from the tables themselves.
for _, name := range toastTypeIcons {
if name != "" {
used[name] = append(used[name], "toast.go (toastTypeIcons)")
}
}
if len(used) == 0 {
t.Fatal("scanned no icon usages — the regexes have rotted")
}
var missing []string
for name, where := range used {
if _, ok := iconRegistry[name]; !ok {
missing = append(missing, name+" (used in "+strings.Join(dedupe(where), ", ")+")")
}
}
sort.Strings(missing)
for _, m := range missing {
t.Errorf("icon not registered, renders as an empty box: %s", m)
}
}
// A registered icon must actually draw something. An entry with empty content is
// the same silent failure as a missing one.
func TestRegisteredIconsHaveContent(t *testing.T) {
for name, def := range iconRegistry {
if strings.TrimSpace(def.content) == "" {
t.Errorf("icon %q is registered with no SVG content", name)
}
if !strings.Contains(def.content, "<path") {
t.Errorf("icon %q has no <path>: %q", name, def.content)
}
if def.viewBox == "" {
t.Errorf("icon %q has no viewBox", name)
}
}
}
func TestIconRendersRegisteredContent(t *testing.T) {
html := renderNode(Icon("check", 20, "text-red-500"))
if !strings.Contains(html, "<path") {
t.Errorf("Icon did not render its path: %s", html)
}
for _, want := range []string{`width="20"`, `height="20"`, "text-red-500", `viewBox="0 0 24 24"`} {
if !strings.Contains(html, want) {
t.Errorf("Icon output missing %s: %s", want, html)
}
}
// An unknown name still renders a correctly sized, empty box rather than
// collapsing the layout.
if empty := renderNode(Icon("no-such-icon", 16, "")); strings.Contains(empty, "<path") {
t.Errorf("unknown icon drew something: %s", empty)
}
}
// renderNode serializes a component's VNode the way the server does.
func renderNode(n *vdom.VNode) string { return vdom.RenderHTML(n) }
func dedupe(xs []string) []string {
seen := map[string]bool{}
out := []string{}
for _, x := range xs {
if !seen[x] {
seen[x] = true
out = append(out, x)
}
}
sort.Strings(out)
return out
}