test(17-06): framework and admin SPA unit tests for the blog companions
- signpost: activation refusals, host overrides, D-02 pattern layers incl. PatternFrom, ReferenceOptions depths - seaglass: removal report, bluemonday bypass corpus, Check - pact Option.Depth omitempty and the menu item type contracts; party activates menu item types after Register and before the first Boot - cabana: repeater compile and decode, dependsOn compile and recheck, richeditor keys and notices, reorder and singleton boot checks (Docker-free); repeater save, dependent recheck, richeditor save, reorder and singleton routes on Postgres; conformance cases for the documented 404, 409 and 422 answers of the new routes - admin: richeditor value contract, link popover, notices and chunk failure; repeater bounds, remove and reorder, collapsed rows; dependent dropdown states and last request wins; reorder announcements, partition switch and tree rules; SingletonForm spec - gate: required names in the go, admin and postgres stages
This commit is contained in:
117
modules/pact/menu_item_types_test.go
Normal file
117
modules/pact/menu_item_types_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package pact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestOptionDepthOmitEmpty pins that Option.Depth leaves every flat option
|
||||
// payload unchanged: depth 0 has no JSON key, a nested option carries it.
|
||||
func TestOptionDepthOmitEmpty(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
opt Option
|
||||
want string
|
||||
}{
|
||||
{Option{Value: "a", Label: "A"}, `{"value":"a","label":"A"}`},
|
||||
{Option{Value: "", Label: ""}, `{"value":"","label":""}`},
|
||||
{Option{Value: "b", Label: "B", Depth: 1}, `{"value":"b","label":"B","depth":1}`},
|
||||
{Option{Value: "c", Label: "C", Depth: 3}, `{"value":"c","label":"C","depth":3}`},
|
||||
} {
|
||||
raw, err := json.Marshal(tc.opt)
|
||||
if err != nil || string(raw) != tc.want {
|
||||
t.Fatalf("Marshal(%+v) = %s, %v; want %s", tc.opt, raw, err, tc.want)
|
||||
}
|
||||
var back Option
|
||||
if err := json.Unmarshal(raw, &back); err != nil || back != tc.opt {
|
||||
t.Fatalf("round trip of %s = %+v, %v", raw, back, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// menuTypesPlugin is a plugin-shaped value with menu item types.
|
||||
type menuTypesPlugin struct{ types []MenuItemType }
|
||||
|
||||
func (p menuTypesPlugin) MenuItemTypes() []MenuItemType { return p.types }
|
||||
|
||||
// singletonController is a controller-shaped value that edits one record.
|
||||
type singletonController struct{}
|
||||
|
||||
func (singletonController) SingletonRecord(context.Context, *gorm.DB) (any, error) { return 1, nil }
|
||||
|
||||
// dependentController answers the options of a dependent dropdown.
|
||||
type dependentController struct{}
|
||||
|
||||
func (dependentController) DependentOptions(_ context.Context, field string, values map[string]any) (DependentOptionSet, error) {
|
||||
if values["type"] == nil {
|
||||
return DependentOptionSet{Applies: false}, nil
|
||||
}
|
||||
return DependentOptionSet{Applies: true, Options: []Option{{Value: field, Label: "x"}}}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ HasMenuItemTypes = menuTypesPlugin{}
|
||||
_ AdminSingleton = singletonController{}
|
||||
_ DependentOptionsProvider = dependentController{}
|
||||
)
|
||||
|
||||
// TestMenuItemTypeContracts pins the shape of the menu item type contracts:
|
||||
// a type marshals without its functions, a resolved item keeps its
|
||||
// alternates in the order given (never a map), list types carry their
|
||||
// children in Items, and the capability interfaces are satisfied by plain
|
||||
// values.
|
||||
func TestMenuItemTypeContracts(t *testing.T) {
|
||||
typ := MenuItemType{
|
||||
Key: "acme-post", Label: "acme.blog::lang.post", DefaultPattern: "/blog/:slug", PatternFrom: "",
|
||||
Placeholders: []string{":slug", ":id"}, Nesting: true, DynamicItems: false,
|
||||
References: func(context.Context) ([]MenuItemReference, error) { return nil, nil },
|
||||
Resolve: func(_ context.Context, in MenuItemResolve) (*ResolvedMenuItem, error) {
|
||||
return &ResolvedMenuItem{Title: in.Reference, URL: in.Pattern}, nil
|
||||
},
|
||||
}
|
||||
raw, err := json.Marshal(typ)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal(MenuItemType) = %v", err)
|
||||
}
|
||||
if strings.Contains(string(raw), "Resolve") || strings.Contains(string(raw), "References") {
|
||||
t.Fatalf("functions leaked into JSON: %s", raw)
|
||||
}
|
||||
got, err := typ.Resolve(context.Background(), MenuItemResolve{Type: "acme-post", Reference: "7", Pattern: "/blog/:slug"})
|
||||
if err != nil || got.Title != "7" || got.URL != "/blog/:slug" {
|
||||
t.Fatalf("Resolve = %+v, %v", got, err)
|
||||
}
|
||||
|
||||
when := time.Date(2026, 10, 12, 0, 0, 0, 0, time.UTC)
|
||||
item := ResolvedMenuItem{
|
||||
Title: "All posts", URL: "", MTime: &when,
|
||||
Alternates: []MenuItemAlternate{{Locale: "pl", URL: "/a"}, {Locale: "en", URL: "/en/a"}, {Locale: "de", URL: "/de/a"}},
|
||||
Items: []ResolvedMenuItem{{Title: "One", URL: "/blog/one"}, {Title: "Two", URL: "/blog/two"}},
|
||||
}
|
||||
var locales []string
|
||||
for _, alt := range item.Alternates {
|
||||
locales = append(locales, alt.Locale)
|
||||
}
|
||||
if strings.Join(locales, ",") != "pl,en,de" || item.URL != "" || len(item.Items) != 2 {
|
||||
t.Fatalf("resolved list item = %+v", item)
|
||||
}
|
||||
|
||||
var source any = menuTypesPlugin{types: []MenuItemType{typ}}
|
||||
if p, ok := source.(HasMenuItemTypes); !ok || len(p.MenuItemTypes()) != 1 {
|
||||
t.Fatal("HasMenuItemTypes not satisfied")
|
||||
}
|
||||
set, err := dependentController{}.DependentOptions(context.Background(), "reference", map[string]any{"type": nil})
|
||||
if err != nil || set.Applies || set.Options != nil {
|
||||
t.Fatalf("not-applying set = %+v, %v", set, err)
|
||||
}
|
||||
set, err = dependentController{}.DependentOptions(context.Background(), "rows.reference", map[string]any{"type": "acme-post"})
|
||||
if err != nil || !set.Applies || set.Options[0].Value != "rows.reference" {
|
||||
t.Fatalf("applying set = %+v, %v", set, err)
|
||||
}
|
||||
if rec, err := (singletonController{}).SingletonRecord(context.Background(), nil); err != nil || rec != 1 {
|
||||
t.Fatalf("SingletonRecord = %v, %v", rec, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user