test(09-03): add failing tests for form locale and dropdown options
- The same cached schema must localize pl and en independently, including Accept-Language parent fallback and raw keys - YAML option maps keep order and scalar type, and a method provider is required at boot
This commit is contained in:
@@ -2,14 +2,19 @@ package cabana
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"git.golem15.com/golem15/summercms/backpack"
|
||||
"git.golem15.com/golem15/summercms/pact"
|
||||
"git.golem15.com/golem15/summercms/party"
|
||||
"git.golem15.com/golem15/summercms/phrasebook"
|
||||
"git.golem15.com/golem15/summercms/towel"
|
||||
)
|
||||
|
||||
const formConfig = `name: demo.form
|
||||
@@ -331,4 +336,231 @@ func compileFormJSON(t *testing.T, ctl schemaController, config, fields string)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestFormSchemaLocalization(t *testing.T) {
|
||||
schema := mustCompileForm(t, schemaController{model: "Widget"}, strings.Replace(formConfig, "name: demo.form", "name: acme.demo::lang.form", 1), localizedFieldsYAML)
|
||||
before := mustCanonical(t, schema)
|
||||
en := phrasebook.NewTranslator(formCatalog(t), phrasebook.Options{Locale: "en", Fallback: "en"})
|
||||
pl := phrasebook.NewTranslator(formCatalog(t), phrasebook.Options{Locale: "pl", Fallback: "en"})
|
||||
|
||||
enJSON := localizeJSON(t, schema, towel.WithLocale(context.Background(), "en"), en, nil)
|
||||
plJSON := localizeJSON(t, schema, towel.WithLocale(context.Background(), "pl"), pl, nil)
|
||||
if enJSON == plJSON {
|
||||
t.Fatalf("locales collapsed:\n%s", enJSON)
|
||||
}
|
||||
for _, want := range []string{`"locale":"en"`, `"label":"Title"`, `"comment":"Comment"`, `"tab":"Tab"`, `"emptyOption":"None"`, `"name":"Form"`} {
|
||||
if !strings.Contains(enJSON, want) {
|
||||
t.Fatalf("en JSON missing %s:\n%s", want, enJSON)
|
||||
}
|
||||
}
|
||||
for _, want := range []string{`"locale":"pl"`, `"label":"Tytuł"`, `"comment":"Komentarz"`, `"tab":"Zakładka"`, `"emptyOption":"Brak"`, `"name":"Formularz"`} {
|
||||
if !strings.Contains(plJSON, want) {
|
||||
t.Fatalf("pl JSON missing %s:\n%s", want, plJSON)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(enJSON, `"label":"missing.label"`) || !strings.Contains(plJSON, `"label":"missing.label"`) {
|
||||
t.Fatalf("raw key fallback lost:\nen %s\npl %s", enJSON, plJSON)
|
||||
}
|
||||
after := mustCanonical(t, schema)
|
||||
if before != after {
|
||||
t.Fatalf("localization mutated the cached schema:\n%s\n%s", before, after)
|
||||
}
|
||||
|
||||
t.Run("parent and app locale", func(t *testing.T) {
|
||||
app := phrasebook.NewTranslator(formCatalog(t), phrasebook.Options{Locale: "pl", Fallback: "en"})
|
||||
parent := localizeJSON(t, schema, towel.WithLocale(context.Background(), "pl-PL,en;q=0.5"), app, nil)
|
||||
if !strings.Contains(parent, `"locale":"pl-PL"`) || !strings.Contains(parent, `"label":"Tytuł"`) {
|
||||
t.Fatalf("Accept-Language parent fallback = %s", parent)
|
||||
}
|
||||
fallback := localizeJSON(t, schema, context.Background(), app, nil)
|
||||
if !strings.Contains(fallback, `"locale":"pl"`) || !strings.Contains(fallback, `"name":"Formularz"`) {
|
||||
t.Fatalf("app.locale fallback = %s", fallback)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormSchemaDropdownOptions(t *testing.T) {
|
||||
schema := mustCompileForm(t, schemaController{model: "Widget"}, formConfig, optionMapYAML)
|
||||
tr := phrasebook.NewTranslator(formCatalog(t), phrasebook.Options{Locale: "pl", Fallback: "en"})
|
||||
got := localizeJSON(t, schema, towel.WithLocale(context.Background(), "pl"), tr, nil)
|
||||
vinyl := strings.Index(got, `{"value":"vinyl","label":"Winyl"}`)
|
||||
two := strings.Index(got, `{"value":2,"label":"Dwa"}`)
|
||||
yes := strings.Index(got, `{"value":true,"label":"Yes"}`)
|
||||
if vinyl < 0 || two < 0 || yes < 0 || !(vinyl < two && two < yes) {
|
||||
t.Fatalf("option order or scalar type drifted: %s", got)
|
||||
}
|
||||
if strings.Contains(got, `"value":"2"`) || strings.Contains(got, `"value":"true"`) {
|
||||
t.Fatalf("option values became strings: %s", got)
|
||||
}
|
||||
|
||||
methodSchema := mustCompileForm(t, providerController{schemaController: schemaController{model: "Widget"}}, formConfig, methodFieldsYAML)
|
||||
methodJSON := localizeJSON(t, methodSchema, towel.WithLocale(context.Background(), "pl"), tr, providerController{})
|
||||
if !strings.Contains(methodJSON, `{"value":"vinyl","label":"Winyl"}`) || !strings.Contains(methodJSON, `{"value":"cd","label":"Płyta"}`) {
|
||||
t.Fatalf("method options = %s", methodJSON)
|
||||
}
|
||||
if strings.Contains(methodJSON, "getFormatOptions") {
|
||||
t.Fatalf("method name leaked into the response: %s", methodJSON)
|
||||
}
|
||||
source := mustCanonical(t, methodSchema)
|
||||
if strings.Contains(source, "getFormatOptions") {
|
||||
t.Fatalf("cached schema stored the method name in JSON: %s", source)
|
||||
}
|
||||
|
||||
modelSchema := mustCompileForm(t, sourceController{schemaController: schemaController{model: "Widget"}, rec: modelOptions{}}, formConfig, "fields:\n format:\n type: dropdown\n options: getFormatOptions\n")
|
||||
modelJSON := localizeJSON(t, modelSchema, towel.WithLocale(context.Background(), "pl"), tr, modelOptions{})
|
||||
if !strings.Contains(modelJSON, `{"value":"lp","label":"Winyl"}`) {
|
||||
t.Fatalf("model provider options = %s", modelJSON)
|
||||
}
|
||||
|
||||
for _, method := range []string{"getFormatOptions", "GetFormatOptions"} {
|
||||
_, err := CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, "fields:\n format:\n type: dropdown\n options: "+method+"\n"))
|
||||
if err == nil || !strings.Contains(err.Error(), method) || !strings.Contains(err.Error(), "acme.demo.widgets") || !strings.Contains(err.Error(), "models/widget/fields.yaml") {
|
||||
t.Fatalf("method %s err = %v", method, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormSchemaLocaleIsolation(t *testing.T) {
|
||||
schema := mustCompileForm(t, schemaController{model: "Widget"}, strings.Replace(formConfig, "name: demo.form", "name: acme.demo::lang.form", 1), localizedFieldsYAML)
|
||||
before := mustCanonical(t, schema)
|
||||
tr := phrasebook.NewTranslator(formCatalog(t), phrasebook.Options{Locale: "en", Fallback: "en"})
|
||||
enWant := localizeJSON(t, schema, towel.WithLocale(context.Background(), "en"), tr, nil)
|
||||
plWant := localizeJSON(t, schema, towel.WithLocale(context.Background(), "pl"), tr, nil)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan string, 32)
|
||||
for i := 0; i < 16; i++ {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
got := localizeJSON(t, schema, towel.WithLocale(context.Background(), "en"), tr, nil)
|
||||
if got != enWant {
|
||||
errCh <- "en drifted: " + got
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
got := localizeJSON(t, schema, towel.WithLocale(context.Background(), "pl"), tr, nil)
|
||||
if got != plWant {
|
||||
errCh <- "pl drifted: " + got
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errCh)
|
||||
for msg := range errCh {
|
||||
t.Fatal(msg)
|
||||
}
|
||||
if after := mustCanonical(t, schema); after != before {
|
||||
t.Fatal("concurrent localization mutated the cache")
|
||||
}
|
||||
}
|
||||
|
||||
const localizedFieldsYAML = `fields:
|
||||
title:
|
||||
label: acme.demo::lang.title
|
||||
comment: acme.demo::lang.comment
|
||||
tab: acme.demo::lang.tab
|
||||
emptyOption: acme.demo::lang.empty
|
||||
type: dropdown
|
||||
options:
|
||||
vinyl: acme.demo::lang.vinyl
|
||||
note:
|
||||
label: missing.label
|
||||
type: text
|
||||
`
|
||||
|
||||
const optionMapYAML = `fields:
|
||||
format:
|
||||
label: acme.demo::lang.title
|
||||
type: dropdown
|
||||
emptyOption: acme.demo::lang.empty
|
||||
options:
|
||||
vinyl: acme.demo::lang.vinyl
|
||||
2: acme.demo::lang.two
|
||||
true: "Yes"
|
||||
`
|
||||
|
||||
const methodFieldsYAML = `fields:
|
||||
Format:
|
||||
label: acme.demo::lang.title
|
||||
type: dropdown
|
||||
options: getFormatOptions
|
||||
`
|
||||
|
||||
func formCatalog(t *testing.T) *phrasebook.Catalog {
|
||||
t.Helper()
|
||||
cat := phrasebook.NewCatalog()
|
||||
err := cat.Load("acme.demo", fstest.MapFS{
|
||||
"lang/en/lang.yaml": &fstest.MapFile{Data: []byte("form: Form\ntitle: Title\ncomment: Comment\ntab: Tab\nempty: None\nvinyl: Vinyl\ncd: CD\ntwo: Two\n")},
|
||||
"lang/pl/lang.yaml": &fstest.MapFile{Data: []byte("form: Formularz\ntitle: Tytuł\ncomment: Komentarz\ntab: Zakładka\nempty: Brak\nvinyl: Winyl\ncd: Płyta\ntwo: Dwa\n")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("catalog: %v", err)
|
||||
}
|
||||
return cat
|
||||
}
|
||||
|
||||
func mustCompileForm(t *testing.T, ctl pact.AdminController, config, fields string) *FormSchema {
|
||||
t.Helper()
|
||||
schema, err := CompileForm("acme.demo", ctl, formFS(config, fields))
|
||||
if err != nil {
|
||||
t.Fatalf("compile: %v", err)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
func mustCanonical(t *testing.T, schema *FormSchema) string {
|
||||
t.Helper()
|
||||
raw, err := schema.CanonicalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func localizeJSON(t *testing.T, schema *FormSchema, ctx context.Context, tr *phrasebook.Translator, provider pact.DropdownOptionsProvider) string {
|
||||
t.Helper()
|
||||
view, err := schema.Localize(ctx, tr, provider)
|
||||
if err != nil {
|
||||
t.Fatalf("localize: %v", err)
|
||||
}
|
||||
raw, err := view.CanonicalJSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := json.Compact(&buf, raw); err != nil {
|
||||
t.Fatalf("compact: %v\n%s", err, raw)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type providerController struct{ schemaController }
|
||||
|
||||
func (providerController) DropdownOptions(field string) []pact.Option {
|
||||
if field != "Format" {
|
||||
return nil
|
||||
}
|
||||
return []pact.Option{
|
||||
{Value: "vinyl", Label: "acme.demo::lang.vinyl"},
|
||||
{Value: "cd", Label: "acme.demo::lang.cd"},
|
||||
}
|
||||
}
|
||||
|
||||
type sourceController struct {
|
||||
schemaController
|
||||
rec any
|
||||
}
|
||||
|
||||
func (s sourceController) NewRecord() any { return s.rec }
|
||||
|
||||
type modelOptions struct{}
|
||||
|
||||
func (modelOptions) DropdownOptions(field string) []pact.Option {
|
||||
if field != "format" {
|
||||
return nil
|
||||
}
|
||||
return []pact.Option{{Value: "lp", Label: "acme.demo::lang.vinyl"}}
|
||||
}
|
||||
|
||||
var _ party.Plugin = formPlugin{}
|
||||
|
||||
Reference in New Issue
Block a user