303 lines
8.2 KiB
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))
|
|
}
|
|
}
|