- 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
567 lines
20 KiB
Go
567 lines
20 KiB
Go
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
|
|
form: ~/plugins/acme/demo/models/widget/fields.yaml
|
|
modelClass: Widget
|
|
defaultRedirect: acme/demo/widgets
|
|
create:
|
|
redirect: acme/demo/widgets/update/:id
|
|
redirectClose: acme/demo/widgets
|
|
update:
|
|
redirect: acme/demo/widgets
|
|
redirectClose: acme/demo/widgets
|
|
`
|
|
|
|
const allFieldsYAML = `fields:
|
|
title:
|
|
label: demo.lang.title
|
|
comment: demo.lang.title_comment
|
|
span: left
|
|
type: text
|
|
required: true
|
|
tab: demo.lang.main
|
|
context: create
|
|
default: Untitled
|
|
body:
|
|
label: demo.lang.body
|
|
span: full
|
|
type: textarea
|
|
size: small
|
|
count:
|
|
label: demo.lang.count
|
|
span: right
|
|
type: number
|
|
default: 3
|
|
attributes:
|
|
min: 0
|
|
readonly: false
|
|
step: 1
|
|
active:
|
|
label: demo.lang.active
|
|
span: left
|
|
type: checkbox
|
|
default: false
|
|
enabled:
|
|
label: demo.lang.enabled
|
|
type: switch
|
|
default: 0
|
|
format:
|
|
label: demo.lang.format
|
|
type: dropdown
|
|
required: true
|
|
emptyOption: demo.lang.none
|
|
options:
|
|
vinyl: Vinyl
|
|
2: Two
|
|
true: "Yes"
|
|
artist:
|
|
label: demo.lang.artist
|
|
span: right
|
|
type: relation
|
|
nameFrom: name
|
|
emptyOption: demo.lang.pick
|
|
editors:
|
|
label: demo.lang.editors
|
|
type: relation-manager
|
|
relation: editors
|
|
tab: demo.lang.editors_tab
|
|
span: full
|
|
context:
|
|
- update
|
|
- create
|
|
`
|
|
|
|
const allFieldsJSON = `{"name":"demo.form","modelClass":"Widget","fields":[` +
|
|
`{"name":"title","type":"text","label":"demo.lang.title","comment":"demo.lang.title_comment","span":"left","tab":"demo.lang.main","context":"create","required":true,"default":"Untitled"},` +
|
|
`{"name":"body","type":"textarea","label":"demo.lang.body","span":"full","size":"small"},` +
|
|
`{"name":"count","type":"number","label":"demo.lang.count","span":"right","default":3,"attributes":{"min":0,"readonly":false,"step":1}},` +
|
|
`{"name":"active","type":"checkbox","label":"demo.lang.active","span":"left","default":false},` +
|
|
`{"name":"enabled","type":"switch","label":"demo.lang.enabled","default":0},` +
|
|
`{"name":"format","type":"dropdown","label":"demo.lang.format","emptyOption":"demo.lang.none","required":true,"options":[{"value":"vinyl","label":"Vinyl"},{"value":2,"label":"Two"},{"value":true,"label":"Yes"}]},` +
|
|
`{"name":"artist","type":"relation","label":"demo.lang.artist","span":"right","nameFrom":"name","emptyOption":"demo.lang.pick"},` +
|
|
`{"name":"editors","type":"relation-manager","label":"demo.lang.editors","span":"full","tab":"demo.lang.editors_tab","context":["update","create"],"relation":"editors"}]}`
|
|
|
|
func TestFormSchemaCompile(t *testing.T) {
|
|
got := compileFormJSON(t, schemaController{model: "Widget"}, formConfig, allFieldsYAML)
|
|
if got != allFieldsJSON {
|
|
t.Fatalf("compiled form =\n%s\nwant\n%s", got, allFieldsJSON)
|
|
}
|
|
again := compileFormJSON(t, schemaController{model: "Widget"}, formConfig, allFieldsYAML)
|
|
if again != got {
|
|
t.Fatalf("recompilation changed JSON:\n%s\n%s", got, again)
|
|
}
|
|
for _, want := range []string{`"default":false`, `"default":0`, `"value":2`, `"value":true`, `"value":"vinyl"`} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("compiled form missing scalar %s:\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, `"value":"2"`) || strings.Contains(got, `"value":"true"`) || strings.Contains(got, `"fields":null`) {
|
|
t.Fatalf("scalar type or null fields drifted:\n%s", got)
|
|
}
|
|
|
|
t.Run("list-only controller still activates", func(t *testing.T) {
|
|
_, err := compileRegistry([]controllerRef{{
|
|
plugin: formPlugin{fsys: fstest.MapFS{
|
|
"controllers/widgets/config_list.yaml": &fstest.MapFile{Data: []byte("list: ~/plugins/acme/demo/models/widget/columns.yaml\nmodelClass: Widget\nrecordsPerPage: 20\nshowSearch: true\n")},
|
|
"models/widget/columns.yaml": &fstest.MapFile{Data: []byte("columns:\n name:\n label: Name\n searchable: true\n")},
|
|
}},
|
|
ctl: schemaController{model: "Widget"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("list-only registry: %v", err)
|
|
}
|
|
})
|
|
t.Run("field name case is exact", func(t *testing.T) {
|
|
raw := compileFormJSON(t, schemaController{model: "Widget"}, formConfig, "fields:\n Name:\n label: Name\n type: text\n")
|
|
if !strings.Contains(raw, `"name":"Name"`) || strings.Contains(raw, `"name":"name"`) {
|
|
t.Fatalf("field name was folded: %s", raw)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFormSchemaEmpty(t *testing.T) {
|
|
cfg := strings.Replace(formConfig, "name: demo.form", "name: empty", 1)
|
|
got := compileFormJSON(t, schemaController{model: "Widget"}, cfg, "fields: {}\n")
|
|
if got != `{"name":"empty","modelClass":"Widget","fields":[]}` {
|
|
t.Fatalf("empty form = %s", got)
|
|
}
|
|
if strings.Contains(got, `"fields":null`) {
|
|
t.Fatal("empty fields serialized as null")
|
|
}
|
|
}
|
|
|
|
func TestFormSchemaSingle(t *testing.T) {
|
|
cfg := strings.Replace(formConfig, "name: demo.form", "name: one", 1)
|
|
got := compileFormJSON(t, schemaController{model: "Widget"}, cfg, "fields:\n title:\n label: Title\n type: text\n")
|
|
want := `{"name":"one","modelClass":"Widget","fields":[{"name":"title","type":"text","label":"Title"}]}`
|
|
if got != want {
|
|
t.Fatalf("single form =\n%s\nwant\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestFormSchemaOrdering(t *testing.T) {
|
|
fields := `fields:
|
|
zeta:
|
|
label: Z
|
|
type: text
|
|
alpha:
|
|
label: A
|
|
type: text
|
|
mid:
|
|
label: M
|
|
type: number
|
|
`
|
|
first := compileFormJSON(t, schemaController{model: "Widget"}, formConfig, fields)
|
|
second := compileFormJSON(t, schemaController{model: "Widget"}, formConfig, fields)
|
|
if first != second {
|
|
t.Fatalf("order changed between compiles:\n%s\n%s", first, second)
|
|
}
|
|
zeta := strings.Index(first, `"name":"zeta"`)
|
|
alpha := strings.Index(first, `"name":"alpha"`)
|
|
mid := strings.Index(first, `"name":"mid"`)
|
|
if zeta < 0 || alpha < 0 || mid < 0 || !(zeta < alpha && alpha < mid) {
|
|
t.Fatalf("source order lost: %s", first)
|
|
}
|
|
}
|
|
|
|
func TestFormSchemaRejects(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
model string
|
|
config string
|
|
fields string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "unknown key",
|
|
config: formConfig,
|
|
fields: "fields:\n title:\n type: text\n cssClass: bad\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/fields.yaml", "cssClass"},
|
|
},
|
|
{
|
|
name: "unknown type",
|
|
config: formConfig,
|
|
fields: "fields:\n title:\n type: widget\n label: Title\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/fields.yaml", "widget"},
|
|
},
|
|
{
|
|
name: "duplicate name",
|
|
config: formConfig,
|
|
fields: "fields:\n title:\n type: text\n label: A\n title:\n type: text\n label: B\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/fields.yaml", "duplicate"},
|
|
},
|
|
{
|
|
name: "path escape",
|
|
config: strings.Replace(formConfig, "form: ~/plugins/acme/demo/models/widget/fields.yaml", "form: ../../etc/passwd", 1),
|
|
fields: "fields: {}\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_form.yaml", "escape"},
|
|
},
|
|
{
|
|
name: "mismatched modelClass",
|
|
model: "Widget",
|
|
config: strings.Replace(formConfig, "modelClass: Widget", "modelClass: widget", 1),
|
|
fields: "fields: {}\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "config_form.yaml", "modelClass"},
|
|
},
|
|
{
|
|
name: "partial",
|
|
config: formConfig,
|
|
fields: "fields:\n editors:\n type: partial\n tab: Editors\n span: full\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/fields.yaml", "partial"},
|
|
},
|
|
{
|
|
name: "partial path",
|
|
config: formConfig,
|
|
fields: "fields:\n editors:\n type: partial\n path: $/golem15/fonoteka/controllers/collections/_editors.htm\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "models/widget/fields.yaml", "path"},
|
|
},
|
|
{
|
|
name: "missing fields",
|
|
config: strings.Replace(formConfig, "models/widget/fields.yaml", "models/widget/missing.yaml", 1),
|
|
fields: "fields: {}\n",
|
|
want: []string{"acme.demo", "acme.demo.widgets", "missing.yaml"},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
model := tc.model
|
|
if model == "" {
|
|
model = "Widget"
|
|
}
|
|
_, err := CompileForm("acme.demo", schemaController{model: model}, formFS(tc.config, tc.fields))
|
|
if err == nil {
|
|
t.Fatal("expected boot error")
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range tc.want {
|
|
if want == "duplicate" {
|
|
if strings.Contains(msg, "duplicate") || strings.Contains(msg, "already defined") {
|
|
continue
|
|
}
|
|
t.Fatalf("error %q missing duplicate", msg)
|
|
}
|
|
if want == "escape" {
|
|
if strings.Contains(msg, "escape") || strings.Contains(msg, "..") {
|
|
continue
|
|
}
|
|
t.Fatalf("error %q missing escape", msg)
|
|
}
|
|
if !strings.Contains(msg, want) {
|
|
t.Fatalf("error %q missing %q", msg, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("bad form fails activation", func(t *testing.T) {
|
|
fsys := formFS(formConfig, "fields:\n editors:\n type: partial\n tab: Editors\n")
|
|
fsys = withList(fsys)
|
|
_, err := compileRegistry([]controllerRef{{
|
|
plugin: &formPlugin{fsys: fsys},
|
|
ctl: schemaController{model: "Widget"},
|
|
}})
|
|
if err == nil || !strings.Contains(err.Error(), "partial") {
|
|
t.Fatalf("activation err = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
type schemaController struct{ model string }
|
|
|
|
func (c schemaController) ID() string { return "acme.demo.widgets" }
|
|
func (c schemaController) ModelName() string {
|
|
if c.model == "" {
|
|
return "Widget"
|
|
}
|
|
return c.model
|
|
}
|
|
func (c schemaController) ConfigDir() string { return "controllers/widgets" }
|
|
|
|
type formPlugin struct{ fsys fs.FS }
|
|
|
|
func (p formPlugin) ID() string { return "acme.demo" }
|
|
func (p formPlugin) Requires() []string { return nil }
|
|
func (p formPlugin) Register(*backpack.App) error { return nil }
|
|
func (p formPlugin) Boot(*backpack.App) error { return nil }
|
|
func (p formPlugin) AdminFS() fs.FS { return p.fsys }
|
|
|
|
func formFS(config, fields string) fs.FS {
|
|
return fstest.MapFS{
|
|
"controllers/widgets/config_form.yaml": &fstest.MapFile{Data: []byte(config)},
|
|
"models/widget/fields.yaml": &fstest.MapFile{Data: []byte(fields)},
|
|
}
|
|
}
|
|
|
|
func withList(fsys fs.FS) fs.FS {
|
|
base, ok := fsys.(fstest.MapFS)
|
|
if !ok {
|
|
return fsys
|
|
}
|
|
base["controllers/widgets/config_list.yaml"] = &fstest.MapFile{Data: []byte("list: ~/plugins/acme/demo/models/widget/columns.yaml\nmodelClass: Widget\nrecordsPerPage: 20\nshowSearch: true\n")}
|
|
base["models/widget/columns.yaml"] = &fstest.MapFile{Data: []byte("columns:\n name:\n label: Name\n searchable: true\n")}
|
|
return base
|
|
}
|
|
|
|
func compileFormJSON(t *testing.T, ctl schemaController, config, fields string) string {
|
|
t.Helper()
|
|
schema, err := CompileForm("acme.demo", ctl, formFS(config, fields))
|
|
if err != nil {
|
|
t.Fatalf("compile: %v", err)
|
|
}
|
|
raw, err := schema.CanonicalJSON()
|
|
if err != nil {
|
|
t.Fatalf("json: %v", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := json.Compact(&buf, raw); err != nil {
|
|
t.Fatalf("compact: %v\n%s", err, raw)
|
|
}
|
|
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{}
|