// Copyright (c) 2026 Zeni Kim // Use of this source code is governed by MIT-style // license that can be found in the LICENSE file. package core import ( "embed" "strings" "testing" "time" ) type templateFieldSample struct { Name string Age int } func TestHasField(t *testing.T) { s := templateFieldSample{Name: "test", Age: 3} if !hasField(s, "Name") { t.Errorf("expected struct to have field 'Name'") } if !hasField(&s, "Age") { t.Errorf("expected pointer to struct to have field 'Age'") } if hasField(s, "Missing") { t.Errorf("expected struct NOT to have field 'Missing'") } if hasField(42, "Name") { t.Errorf("expected non-struct to have no fields") } } func TestCapitalize(t *testing.T) { tests := []struct { in string want string }{ {"hello world", "Hello world"}, {"HELLO", "Hello"}, {"", ""}, {"a", "A"}, } for _, tt := range tests { if got := capitalize(tt.in); got != tt.want { t.Errorf("capitalize(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestPrepend(t *testing.T) { if got := prepend("World", "Hello, "); got != "Hello, World" { t.Errorf("prepend returned %q", got) } } func TestStrAppend(t *testing.T) { if got := strAppend("Hello", "!"); got != "Hello!" { t.Errorf("strAppend returned %q", got) } } func TestSplitHelper(t *testing.T) { got := split("a,b,c", ",") if len(got) != 3 { t.Fatalf("expected 3 parts, got %d", len(got)) } if got[0] != "a" || got[2] != "c" { t.Errorf("unexpected split result: %v", got) } } func TestTruncate(t *testing.T) { if got := truncate(5, "hello world"); got != "hello…" { t.Errorf("truncate returned %q", got) } if got := truncate(20, "short"); got != "short" { t.Errorf("truncate should not modify short strings, got %q", got) } // Multibyte safety. if got := truncate(3, "héllo"); got != "hél…" { t.Errorf("truncate multibyte returned %q", got) } } func TestFmtNumber(t *testing.T) { if got := fmtNumber(1000000); got != "1,000,000" { t.Errorf("fmtNumber(int) = %q, want %q", got, "1,000,000") } if got := fmtNumber(int64(1500)); got != "1,500" { t.Errorf("fmtNumber(int64) = %q, want %q", got, "1,500") } if got := fmtNumber(12.5); got != "12.50" { t.Errorf("fmtNumber(float64) = %q, want %q", got, "12.50") } // Fallback for unsupported types. if got := fmtNumber("abc"); got != "abc" { t.Errorf("fmtNumber(string) = %q, want %q", got, "abc") } } func TestFmtDate(t *testing.T) { d := time.Date(2023, time.May, 2, 15, 4, 0, 0, time.UTC) if got := fmtDate(d, "short"); got != "02 May 2023" { t.Errorf("fmtDate(short) = %q", got) } if got := fmtDate(d, "long"); got != "02 May 2023" { t.Errorf("fmtDate(long) = %q", got) } if got := fmtDate(d, "iso"); got != "2023-05-02" { t.Errorf("fmtDate(iso) = %q", got) } if got := fmtDate(d, "datetime"); got != "02 May 2023 15:04" { t.Errorf("fmtDate(datetime) = %q", got) } if got := fmtDate(d, "02/01/2006"); got != "02/05/2023" { t.Errorf("fmtDate(custom) = %q", got) } } func TestTimeAgo(t *testing.T) { tests := []struct { name string at time.Time want string }{ {"just now", time.Now().Add(-10 * time.Second), "just now"}, {"minutes", time.Now().Add(-5 * time.Minute), "5 minutes ago"}, {"one minute", time.Now().Add(-1 * time.Minute), "1 minute ago"}, {"hours", time.Now().Add(-3 * time.Hour), "3 hours ago"}, {"one hour", time.Now().Add(-1 * time.Hour), "1 hour ago"}, {"days", time.Now().Add(-48 * time.Hour), "2 days ago"}, {"one day", time.Now().Add(-24 * time.Hour), "1 day ago"}, } for _, tt := range tests { if got := timeAgo(tt.at); got != tt.want { t.Errorf("timeAgo(%s) = %q, want %q", tt.name, got, tt.want) } } } func TestPlural(t *testing.T) { if got := plural(1, "item"); got != "1 item" { t.Errorf("plural(1) = %q", got) } if got := plural(3, "item"); got != "3 items" { t.Errorf("plural(3) = %q", got) } } func TestFirstAndLast(t *testing.T) { items := []int{10, 20, 30} if got := first(items); got != 10 { t.Errorf("first = %v, want 10", got) } if got := last(items); got != 30 { t.Errorf("last = %v, want 30", got) } if got := first([]int{}); got != nil { t.Errorf("first of empty slice should be nil, got %v", got) } if got := last([]int{}); got != nil { t.Errorf("last of empty slice should be nil, got %v", got) } if got := first("not a slice"); got != nil { t.Errorf("first of non-slice should be nil, got %v", got) } } func TestSliceOf(t *testing.T) { items := []int{1, 2, 3, 4, 5} got, ok := sliceOf(items, 1, 4).([]int) if !ok { t.Fatalf("sliceOf should return a slice") } if len(got) != 3 || got[0] != 2 || got[2] != 4 { t.Errorf("sliceOf returned %v", got) } // Negative start should be clamped to 0. got, _ = sliceOf(items, -5, 2).([]int) if len(got) != 2 || got[0] != 1 { t.Errorf("sliceOf with negative start returned %v", got) } // End beyond length should be clamped. got, _ = sliceOf(items, 3, 100).([]int) if len(got) != 2 || got[1] != 5 { t.Errorf("sliceOf with large end returned %v", got) } if got := sliceOf("not slice", 0, 1); got != nil { t.Errorf("sliceOf of non-slice should be nil, got %v", got) } } func TestContainsHelper(t *testing.T) { if !contains("hello world", "world") { t.Errorf("expected substring match") } if contains("hello", "xyz") { t.Errorf("expected no substring match") } items := []string{"a", "b", "c"} if !contains(items, "b") { t.Errorf("expected element in slice") } if contains(items, "z") { t.Errorf("expected element not in slice") } if contains(42, "x") { t.Errorf("expected no match for non-string/non-slice") } } func TestJoinHelper(t *testing.T) { if got := join([]string{"a", "b", "c"}, ", "); got != "a, b, c" { t.Errorf("join returned %q", got) } } func TestDefaultVal(t *testing.T) { if got := defaultVal("N/A", ""); got != "N/A" { t.Errorf("defaultVal for empty string = %v, want N/A", got) } if got := defaultVal("N/A", nil); got != "N/A" { t.Errorf("defaultVal for nil = %v, want N/A", got) } if got := defaultVal("N/A", 0); got != "N/A" { t.Errorf("defaultVal for zero = %v, want N/A", got) } if got := defaultVal("N/A", "value"); got != "value" { t.Errorf("defaultVal for present value = %v, want value", got) } } func TestTernary(t *testing.T) { if got := ternary("yes", "no", true); got != "yes" { t.Errorf("ternary(true) = %v", got) } if got := ternary("yes", "no", false); got != "no" { t.Errorf("ternary(false) = %v", got) } } func TestCoalesce(t *testing.T) { if got := coalesce(nil, "", "first", "second"); got != "first" { t.Errorf("coalesce = %v, want first", got) } if got := coalesce(nil, ""); got != nil { t.Errorf("coalesce of all empty should be nil, got %v", got) } } func TestFuncMap(t *testing.T) { fm := funcMap() expected := []string{ "hasField", "capitalize", "prepend", "strAppend", "split", "truncate", "fmtNumber", "fmtDate", "timeAgo", "first", "last", "sliceOf", "contains", "join", "defaultVal", "ternary", "coalesce", } for _, name := range expected { if _, ok := fm[name]; !ok { t.Errorf("expected funcMap to contain %q", name) } } } //go:embed all:testingdata/templates var testTemplatesFS embed.FS func TestNewTemplatesAndRenderNamed(t *testing.T) { // Register the built-in components plus our test templates. NewTemplates(components_resources, testTemplatesFS) if tmpl == nil { t.Fatalf("expected templates to be initialized") } out, err := RenderNamedTemplate("test_greeting", map[string]interface{}{"Name": "Goffee"}) if err != nil { t.Fatalf("failed rendering named template: %v", err) } if !strings.Contains(string(out), "Hello, Goffee!") { t.Errorf("unexpected rendered output: %q", string(out)) } } func TestRenderNamedTemplateMissing(t *testing.T) { NewTemplates(components_resources, testTemplatesFS) _, err := RenderNamedTemplate("does_not_exist", nil) if err == nil { t.Errorf("expected error rendering a missing template") } } func TestRenderNamedTemplateUsesFuncMap(t *testing.T) { NewTemplates(components_resources, testTemplatesFS) out, err := RenderNamedTemplate("test_funcmap", map[string]interface{}{"Raw": "hello world"}) if err != nil { t.Fatalf("failed rendering named template: %v", err) } if !strings.Contains(string(out), "Hello world") { t.Errorf("expected capitalize helper to be applied, got %q", string(out)) } }