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:
2026-10-12 19:29:37 +02:00
parent 490a4e3e6a
commit 91593da1f7
17 changed files with 3136 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
package party
import (
"context"
"fmt"
"io/fs"
"os"
@@ -15,6 +16,7 @@ import (
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/phrasebook"
"git.golem15.com/golem15/summercms/modules/postcard"
"git.golem15.com/golem15/summercms/modules/signpost"
)
type tracePlugin struct {
@@ -346,7 +348,6 @@ func TestActivatePublishesActivatedOrderAndRefusesSecond(t *testing.T) {
}
}
func TestRegisterNilIsNoop(t *testing.T) {
Register(nil)
}
@@ -527,12 +528,12 @@ type mailPlugin struct {
sawMailer *bool
}
func (p *mailPlugin) ID() string { return p.id }
func (p *mailPlugin) Requires() []string { return nil }
func (p *mailPlugin) Register(*backpack.App) error { return nil }
func (p *mailPlugin) MailTemplatesFS() fs.FS { return p.fsys }
func (p *mailPlugin) MailTemplates() []string { return p.templates }
func (p *mailPlugin) MailLayouts() map[string]string { return p.layouts }
func (p *mailPlugin) ID() string { return p.id }
func (p *mailPlugin) Requires() []string { return nil }
func (p *mailPlugin) Register(*backpack.App) error { return nil }
func (p *mailPlugin) MailTemplatesFS() fs.FS { return p.fsys }
func (p *mailPlugin) MailTemplates() []string { return p.templates }
func (p *mailPlugin) MailLayouts() map[string]string { return p.layouts }
func (p *mailPlugin) Boot(app *backpack.App) error {
if p.sawMailer == nil {
@@ -551,7 +552,7 @@ func TestActivatePublishesMailerBeforeBoot(t *testing.T) {
templates: []string{"golem15.hello::mail.hello"},
layouts: map[string]string{"hello": "golem15.hello::mail.layouts.hello"},
fsys: fstest.MapFS{
"views/mail/hello.htm": {Data: []byte("subject = \"Hi\"\nlayout = \"hello\"\n==\nHi.\n")},
"views/mail/hello.htm": {Data: []byte("subject = \"Hi\"\nlayout = \"hello\"\n==\nHi.\n")},
"views/mail/layouts/hello.htm": {Data: []byte("name = \"Hello\"\n==\n{{ .Content }}\n==\n<div>{{ .Content }}</div>\n")},
},
}
@@ -580,3 +581,80 @@ func TestActivateMissingMailTemplateFailsBootWithName(t *testing.T) {
t.Fatalf("error %q should name the boot plugin and missing template", msg)
}
}
// menuTypesPlugin registers one menu item type and records whether the
// signpost registry was published at Register and at Boot.
type menuTypesPlugin struct {
id string
key string
log *[]string
atRegister *bool
atBoot *bool
typesAtBoot *int
}
func (p *menuTypesPlugin) ID() string { return p.id }
func (p *menuTypesPlugin) Requires() []string { return nil }
func (p *menuTypesPlugin) MenuItemTypes() []pact.MenuItemType {
*p.log = append(*p.log, p.id+":types")
return []pact.MenuItemType{{Key: p.key, DefaultPattern: "/:slug", Resolve: func(context.Context, pact.MenuItemResolve) (*pact.ResolvedMenuItem, error) {
return &pact.ResolvedMenuItem{}, nil
}}}
}
func (p *menuTypesPlugin) Register(app *backpack.App) error {
*p.log = append(*p.log, p.id+":register")
_, ok := app.Lookup[*signpost.Registry]()
*p.atRegister = ok
return nil
}
func (p *menuTypesPlugin) Boot(app *backpack.App) error {
*p.log = append(*p.log, p.id+":boot")
reg, ok := app.Lookup[*signpost.Registry]()
*p.atBoot = ok
if ok {
*p.typesAtBoot = len(reg.Types())
}
return nil
}
// TestActivateMenuItemTypesOrder pins the party hook: menu item types are
// collected after every Register and before the first Boot, so a Boot sees
// the full registry; a duplicate key fails activation naming both plugins and
// no plugin boots.
func TestActivateMenuItemTypesOrder(t *testing.T) {
var log []string
var reg1, boot1, reg2, boot2 bool
var n1, n2 int
a := &menuTypesPlugin{id: "acme.a", key: "acme-a", log: &log, atRegister: &reg1, atBoot: &boot1, typesAtBoot: &n1}
b := &menuTypesPlugin{id: "acme.b", key: "acme-b", log: &log, atRegister: &reg2, atBoot: &boot2, typesAtBoot: &n2}
app := backpack.New(&compass.Config{})
if _, err := activate([]Plugin{a, b}, app, []string{"acme.a", "acme.b"}); err != nil {
t.Fatalf("activate: %v", err)
}
if reg1 || reg2 {
t.Fatal("the registry existed during Register")
}
if !boot1 || !boot2 || n1 != 2 || n2 != 2 {
t.Fatalf("Boot saw registry %v/%v with %d/%d types, want both and 2", boot1, boot2, n1, n2)
}
want := "acme.a:register,acme.b:register,acme.a:types,acme.b:types,acme.a:boot,acme.b:boot"
if got := strings.Join(log, ","); got != want {
t.Fatalf("order = %s\nwant %s", got, want)
}
log = nil
dupA := &menuTypesPlugin{id: "acme.a", key: "acme-same", log: &log, atRegister: &reg1, atBoot: &boot1, typesAtBoot: &n1}
dupB := &menuTypesPlugin{id: "acme.b", key: "acme-same", log: &log, atRegister: &reg2, atBoot: &boot2, typesAtBoot: &n2}
_, err := activate([]Plugin{dupA, dupB}, backpack.New(&compass.Config{}), []string{"acme.a", "acme.b"})
if err == nil || !strings.Contains(err.Error(), "party: menu item types") || !strings.Contains(err.Error(), "acme.a") || !strings.Contains(err.Error(), "acme.b") {
t.Fatalf("duplicate error = %v", err)
}
for _, entry := range log {
if strings.HasSuffix(entry, ":boot") {
t.Fatalf("a plugin booted after the duplicate: %v", log)
}
}
}