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) } }) } }