Files
summercms/modules/cabana/ml_textarea_test.go
Jakub Zych 3596df4ff5 feat(cabana): add mltextarea field type
- mltextarea compiles (with size), binds as a writable scalar and goes
  through the mltext lift, translation write and hydrate paths
- preset on or from an mltextarea field stays refused
- tests: compile, Postgres round trip over a nullable host column with
  multi-line text, invalid locale maps, preset refusals
2026-10-06 21:30:56 +02:00

182 lines
5.1 KiB
Go

package cabana
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"testing/fstest"
)
const mlTextareaFields = `fields:
title:
label: Title
type: mltext
required: true
description:
label: Description
type: mltextarea
size: small
`
func mlTextareaCompiled(t *testing.T) *CompiledController {
t.Helper()
fsys := mlFS()
fsys["models/post/fields.yaml"] = &fstest.MapFile{Data: []byte(mlTextareaFields)}
reg, err := compileRegistry([]controllerRef{{
plugin: formPlugin{fsys: fsys},
ctl: mlNullableController{},
}})
if err != nil {
t.Fatalf("registry: %v", err)
}
cc, ok := reg.Get("acme.demo.posts")
if !ok || cc.Form == nil {
t.Fatalf("compiled controller missing form: %+v", cc)
}
return cc
}
func TestMLTextareaFieldType(t *testing.T) {
accepted := `fields:
title:
type: mltext
label: Title
excerpt:
type: mltextarea
label: Excerpt
size: small
`
schema, err := CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, accepted))
if err != nil {
t.Fatalf("compile mltextarea: %v", err)
}
var excerpt *FormField
for i := range schema.Fields {
if schema.Fields[i].Name == "excerpt" {
excerpt = &schema.Fields[i]
}
}
if excerpt == nil {
t.Fatal("excerpt field missing")
}
if excerpt.Type != "mltextarea" || excerpt.Size != "small" {
t.Fatalf("excerpt = type %q size %q, want mltextarea small", excerpt.Type, excerpt.Size)
}
raw, err := json.Marshal(excerpt)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{`"type":"mltextarea"`, `"size":"small"`} {
if !strings.Contains(string(raw), want) {
t.Fatalf("json %s lacks %s", raw, want)
}
}
_, err = CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, "fields:\n excerpt:\n type: mltextareas\n label: Excerpt\n"))
if err == nil || !strings.Contains(err.Error(), "unsupported type mltextareas") {
t.Fatalf("unknown type err = %v", err)
}
if !mlFieldType("mltextarea") {
t.Fatal("mlFieldType(mltextarea) = false")
}
if mlFieldType("textarea") {
t.Fatal("mlFieldType(textarea) = true")
}
if !scalarFormField("mltextarea") {
t.Fatal("scalarFormField(mltextarea) = false")
}
}
func TestMLTextareaRoundTrip(t *testing.T) {
_, db := newListService(t)
if err := db.Migrator().DropTable(&mlNullablePost{}); err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&mlNullablePost{}); err != nil {
t.Fatal(err)
}
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
svc := CRUDService{DB: db, writer: writer}
cc := mlTextareaCompiled(t)
ctx := context.Background()
row := mlNullablePost{Title: "Hello"}
if err := db.Create(&row).Error; err != nil {
t.Fatal(err)
}
title := map[string]any{"en": "Hello", "pl": "Witaj"}
t.Run("nil host hydrates to empty strings", func(t *testing.T) {
shown, err := svc.ShowRecord(ctx, cc, row.ID)
if err != nil {
t.Fatalf("show: %v", err)
}
got := mlLocaleMap(t, "show", shown.Data, "description")
if got["en"] != "" || got["pl"] != "" || len(got) != 2 {
t.Fatalf("nil description = %#v, want en and pl empty", got)
}
})
t.Run("multi-line text saves per locale", func(t *testing.T) {
en := "Line one\nLine two"
pl := "Linia pierwsza\nLinia druga"
saved, err := svc.UpdateRecord(ctx, cc, row.ID, RecordInput{Body: map[string]any{
"title": title,
"description": map[string]any{"en": en, "pl": pl},
}})
if err != nil {
t.Fatalf("update: %v", err)
}
if got := mlLocaleMap(t, "save", saved.Data, "description"); got["en"] != en || got["pl"] != pl {
t.Fatalf("saved description = %#v", got)
}
shown, err := svc.ShowRecord(ctx, cc, row.ID)
if err != nil {
t.Fatalf("show: %v", err)
}
if got := mlLocaleMap(t, "show", shown.Data, "description"); got["en"] != en || got["pl"] != pl || len(got) != 2 {
t.Fatalf("shown description = %#v", got)
}
var stored mlNullablePost
if err := db.First(&stored, row.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Description == nil || *stored.Description != en {
t.Fatalf("stored description = %v, want %q", stored.Description, en)
}
if writer.attrs["pl"]["description"] != pl {
t.Fatalf("Polish attributes = %#v", writer.attrs)
}
if _, dup := writer.attrs["en"]["description"]; dup {
t.Fatalf("default locale written to the translation store: %#v", writer.attrs)
}
})
invalid := []struct {
name string
value map[string]any
}{
{"undeclared locale", map[string]any{"en": "A", "de": "B"}},
{"non-string value", map[string]any{"en": "A", "pl": 3}},
{"missing default locale", map[string]any{"pl": "B"}},
}
for _, tc := range invalid {
t.Run(tc.name+" is a validation error", func(t *testing.T) {
_, err := svc.UpdateRecord(ctx, cc, row.ID, RecordInput{Body: map[string]any{
"title": title,
"description": tc.value,
}})
var verr *ValidationError
if !errors.As(err, &verr) {
t.Fatalf("err = %v, want *ValidationError", err)
}
if _, ok := verr.Details["description"]; !ok {
t.Fatalf("details = %#v, want description key", verr.Details)
}
})
}
}