core/templates_test.go

303 lines
8.2 KiB
Go

// Copyright (c) 2026 Zeni Kim <zenik@smarteching.com>
// 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))
}
}