package cabana import ( "context" "strings" "testing" "testing/fstest" "git.golem15.com/golem15/summercms/modules/pact" ) // TestFieldTriggerBoot (D-21): every refusal of compileTrigger and // checkTriggers stops boot with an error naming the controller, the field // and the trigger key; valid disable and hide triggers reach the localized // schema unchanged. func TestFieldTriggerBoot(t *testing.T) { base := "fields:\n is_admin:\n type: switch\n agree:\n type: checkbox\n title:\n type: text\n" notes := func(trigger string) string { return base + " notes:\n type: textarea\n trigger:\n" + trigger } full := func(action, field, condition string) string { return notes(" action: " + action + "\n field: " + field + "\n condition: " + condition + "\n") } for _, tc := range []struct { name, fields string want []string }{ {"missing action", notes(" field: is_admin\n condition: checked\n"), []string{"action is required"}}, {"missing field", notes(" action: hide\n condition: checked\n"), []string{"field is required"}}, {"missing condition", notes(" action: hide\n field: is_admin\n"), []string{"condition is required"}}, {"extra sub-key", notes(" action: hide\n field: is_admin\n condition: checked\n value: 1\n"), []string{"unknown key value"}}, {"not a mapping", base + " notes:\n type: textarea\n trigger: is_admin\n", []string{"trigger must be a mapping"}}, {"unknown action", full("show", "is_admin", "checked"), []string{"action show is not supported"}}, {"unknown condition", full("hide", "is_admin", "value[1]"), []string{"condition value[1] is not supported"}}, {"field is not a name", full("hide", "is-admin", "checked"), []string{`field "is-admin" is not a field name`}}, {"self reference", base + " notes:\n type: checkbox\n trigger:\n action: hide\n field: notes\n condition: checked\n", []string{"names the field itself"}}, {"unknown controlling field", full("hide", "active", "checked"), []string{"trigger field active is not a field of this form"}}, {"text controlling field", full("disable", "title", "unchecked"), []string{"trigger field title must be a switch or checkbox", "type text"}}, {"in a repeater row", base + " rows:\n type: repeater\n form:\n fields:\n flag:\n type: checkbox\n notes:\n type: text\n trigger:\n action: hide\n field: flag\n condition: checked\n", []string{"trigger is not supported in a repeater row"}}, } { t.Run(tc.name, func(t *testing.T) { _, err := compileTriggerForm(tc.fields) if err == nil { t.Fatalf("compiled; want an error containing %v", tc.want) } msg := err.Error() for _, want := range append([]string{"acme.trig.items", "notes", "trigger"}, tc.want...) { if !strings.Contains(msg, want) { t.Fatalf("err = %v, want it to contain %q", err, want) } } }) } t.Run("duplicate sub-key", func(t *testing.T) { // The YAML decoder refuses a duplicate key before compileTrigger. _, err := compileTriggerForm(notes(" action: hide\n action: disable\n field: is_admin\n condition: checked\n")) if err == nil || !strings.Contains(err.Error(), "acme.trig.items") || !strings.Contains(err.Error(), `"action" already defined`) { t.Fatalf("duplicate sub-key = %v", err) } }) t.Run("valid triggers reach the localized schema", func(t *testing.T) { fields := base + " notes:\n type: textarea\n trigger:\n action: disable\n field: is_admin\n condition: checked\n" + " summary:\n type: text\n trigger:\n action: hide\n field: agree\n condition: unchecked\n" schema, err := compileTriggerForm(fields) if err != nil { t.Fatal(err) } view, err := schema.Localize(context.Background(), nil, nil) if err != nil { t.Fatal(err) } want := map[string]FieldTrigger{ "notes": {Action: "disable", Field: "is_admin", Condition: "checked"}, "summary": {Action: "hide", Field: "agree", Condition: "unchecked"}, } for _, field := range view.Fields { expected, ok := want[field.Name] switch { case ok && (field.Trigger == nil || *field.Trigger != expected): t.Fatalf("%s trigger = %+v, want %+v", field.Name, field.Trigger, expected) case !ok && field.Trigger != nil: t.Fatalf("%s has trigger %+v", field.Name, field.Trigger) } delete(want, field.Name) } if len(want) != 0 { t.Fatalf("fields missing from the view: %v", want) } }) t.Run("a settings form refuses trigger at boot", func(t *testing.T) { fsys := fstest.MapFS{"models/settings/fields.yaml": &fstest.MapFile{Data: []byte( "fields:\n enabled:\n type: switch\n extra:\n type: text\n trigger:\n action: hide\n field: enabled\n condition: checked\n")}} item := pact.SettingsItem{Code: "demo", Label: "Demo", Form: "models/settings/fields.yaml", Model: "DemoSettings", NewModel: func() any { return &ucSettings{} }} _, err := compileSetting("acme.unit", item, fsys) if err == nil || !strings.Contains(err.Error(), "extra") || !strings.Contains(err.Error(), "trigger") { t.Fatalf("settings form with a trigger = %v", err) } }) t.Run("a relation form refuses trigger", func(t *testing.T) { field := FormField{Name: "notes", Type: "text", Trigger: &FieldTrigger{Action: "hide", Field: "flag", Condition: "checked"}} if err := controllerFormOnly(field); err == nil || !strings.Contains(err.Error(), "trigger") { t.Fatalf("controllerFormOnly = %v", err) } field.Trigger = nil if err := controllerFormOnly(field); err != nil { t.Fatalf("controllerFormOnly without a trigger = %v", err) } }) }