Skip to content

Commit 377f23f

Browse files
akoclaude
andcommitted
test: cover object-list extraction (Phase 1 Layer 1+2)
Refs: mendixlabs#538 Five new tests guarding the object-list extraction work in commit 4add8627: cmd/mxcli/cmd_widget_test.go: - TestDeriveObjectListKeyword: plural→singular keyword derivation, including the irregular override map (basicItems→ITEM, series→SERIES, dynamicMarkers→DYNAMICMARKER, etc.) - TestGenerateDefJSON_ObjectList: end-to-end mapping of an Accordion-style groups property — sub-properties split correctly between ItemProperties (5 non-widgets) and ItemSlots (2 widgets); object without IsList=true still skipped - TestGenerateDefJSON_ObjectListPrimitiveDefaults: primitive item properties carry their MPK default values into the mapping sdk/widgets/mpk/mpk_test.go: - TestObjectListExtraction_GroupedShape: covers the propertyGroup-wrapped XML nesting (Accordion, DataGrid) - TestObjectListExtraction_FlatShape: covers the direct-property XML nesting (PopupMenu, Maps) — without the NestedDirectProps field these children silently drop Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 753c346 commit 377f23f

2 files changed

Lines changed: 286 additions & 0 deletions

File tree

cmd/mxcli/cmd_widget_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,192 @@ func findMapping(mappings []executor.PropertyMapping, key string) *executor.Prop
196196
}
197197
return nil
198198
}
199+
200+
// TestDeriveObjectListKeyword verifies plural→singular keyword derivation
201+
// for object-list properties, including the override map for irregular cases.
202+
func TestDeriveObjectListKeyword(t *testing.T) {
203+
tests := []struct {
204+
propertyKey string
205+
expected string
206+
}{
207+
// Regular plurals (strip trailing 's', uppercase)
208+
{"groups", "GROUP"},
209+
{"columns", "COLUMN"},
210+
{"markers", "MARKER"},
211+
// Override map (irregular cases)
212+
{"basicItems", "ITEM"},
213+
{"customItems", "CUSTOMITEM"},
214+
{"dynamicMarkers", "DYNAMICMARKER"},
215+
{"attributesList", "ATTR"},
216+
{"filterOptions", "OPTION"},
217+
{"series", "SERIES"}, // Latin singular == plural
218+
}
219+
220+
for _, tc := range tests {
221+
t.Run(tc.propertyKey, func(t *testing.T) {
222+
got := deriveObjectListKeyword(tc.propertyKey)
223+
if got != tc.expected {
224+
t.Errorf("deriveObjectListKeyword(%q) = %q, want %q",
225+
tc.propertyKey, got, tc.expected)
226+
}
227+
})
228+
}
229+
}
230+
231+
// TestGenerateDefJSON_ObjectList covers extraction of Type:"object"+IsList:true
232+
// properties (e.g. Accordion groups, DataGrid columns, PopupMenu basicItems).
233+
// Each list item's sub-property tree should be split between ItemProperties
234+
// (scalar/datasource/attribute/etc.) and ItemSlots (widgets-typed).
235+
func TestGenerateDefJSON_ObjectList(t *testing.T) {
236+
// Synthesize an Accordion-style "groups" property with mixed sub-property kinds.
237+
mpkDef := &mpk.WidgetDefinition{
238+
ID: "com.example.widget.Accordion",
239+
Name: "Accordion",
240+
Properties: []mpk.PropertyDef{
241+
{Key: "advancedMode", Type: "boolean", DefaultValue: "false"},
242+
{
243+
Key: "groups",
244+
Type: "object",
245+
IsList: true,
246+
Children: []mpk.PropertyDef{
247+
{Key: "headerRenderMode", Type: "enumeration", DefaultValue: "text"},
248+
{Key: "headerText", Type: "textTemplate"},
249+
{Key: "visible", Type: "expression"},
250+
{Key: "collapsed", Type: "attribute"},
251+
{Key: "onToggleCollapsed", Type: "action"},
252+
{Key: "headerContent", Type: "widgets"},
253+
{Key: "content", Type: "widgets"},
254+
},
255+
},
256+
},
257+
}
258+
259+
def := generateDefJSON(mpkDef, "ACCORDION")
260+
261+
// Top-level primitive should still land in PropertyMappings.
262+
if len(def.PropertyMappings) != 1 {
263+
t.Fatalf("PropertyMappings count = %d, want 1", len(def.PropertyMappings))
264+
}
265+
266+
// Object-list goes to ObjectLists, not to ChildSlots or PropertyMappings.
267+
if len(def.ObjectLists) != 1 {
268+
t.Fatalf("ObjectLists count = %d, want 1", len(def.ObjectLists))
269+
}
270+
ol := def.ObjectLists[0]
271+
if ol.PropertyKey != "groups" {
272+
t.Errorf("ObjectLists[0].PropertyKey = %q, want %q", ol.PropertyKey, "groups")
273+
}
274+
if ol.MDLContainer != "GROUP" {
275+
t.Errorf("ObjectLists[0].MDLContainer = %q, want %q", ol.MDLContainer, "GROUP")
276+
}
277+
278+
// 5 non-widgets items should be ItemProperties; 2 widgets items should be ItemSlots.
279+
if len(ol.ItemProperties) != 5 {
280+
t.Errorf("ItemProperties count = %d, want 5", len(ol.ItemProperties))
281+
}
282+
if len(ol.ItemSlots) != 2 {
283+
t.Errorf("ItemSlots count = %d, want 2", len(ol.ItemSlots))
284+
}
285+
286+
// Spot-check operation kinds for sub-properties.
287+
wantOps := map[string]string{
288+
"headerRenderMode": "primitive",
289+
"headerText": "texttemplate",
290+
"visible": "expression",
291+
"collapsed": "attribute",
292+
"onToggleCollapsed": "action",
293+
}
294+
for _, ip := range ol.ItemProperties {
295+
want, ok := wantOps[ip.PropertyKey]
296+
if !ok {
297+
t.Errorf("unexpected ItemProperty key %q", ip.PropertyKey)
298+
continue
299+
}
300+
if ip.Operation != want {
301+
t.Errorf("ItemProperty %q: Operation = %q, want %q",
302+
ip.PropertyKey, ip.Operation, want)
303+
}
304+
}
305+
306+
// ItemSlots should map widgets-typed sub-properties to their MDLContainer.
307+
wantSlots := map[string]string{
308+
"headerContent": "HEADERCONTENT",
309+
"content": "CONTENT",
310+
}
311+
for _, slot := range ol.ItemSlots {
312+
want, ok := wantSlots[slot.PropertyKey]
313+
if !ok {
314+
t.Errorf("unexpected ItemSlot key %q", slot.PropertyKey)
315+
continue
316+
}
317+
if slot.MDLContainer != want {
318+
t.Errorf("ItemSlot %q: MDLContainer = %q, want %q",
319+
slot.PropertyKey, slot.MDLContainer, want)
320+
}
321+
if slot.Operation != "widgets" {
322+
t.Errorf("ItemSlot %q: Operation = %q, want %q",
323+
slot.PropertyKey, slot.Operation, "widgets")
324+
}
325+
}
326+
327+
// IsList=false on an object property should still skip (not extracted).
328+
mpkDef2 := &mpk.WidgetDefinition{
329+
ID: "com.example.widget.NotAList",
330+
Name: "NotAList",
331+
Properties: []mpk.PropertyDef{
332+
{Key: "myObj", Type: "object", IsList: false},
333+
},
334+
}
335+
def2 := generateDefJSON(mpkDef2, "NOTALIST")
336+
if len(def2.ObjectLists) != 0 {
337+
t.Errorf("ObjectLists for non-list object property = %d, want 0",
338+
len(def2.ObjectLists))
339+
}
340+
}
341+
342+
// TestGenerateDefJSON_ObjectListPrimitiveDefaults verifies that primitive
343+
// item properties carry their MPK default values into the ItemPropertyMapping.
344+
func TestGenerateDefJSON_ObjectListPrimitiveDefaults(t *testing.T) {
345+
mpkDef := &mpk.WidgetDefinition{
346+
ID: "com.example.widget.Sized",
347+
Name: "Sized",
348+
Properties: []mpk.PropertyDef{
349+
{
350+
Key: "items",
351+
Type: "object",
352+
IsList: true,
353+
Children: []mpk.PropertyDef{
354+
{Key: "size", Type: "integer", DefaultValue: "10"},
355+
{Key: "label", Type: "string", DefaultValue: ""},
356+
{Key: "kind", Type: "enumeration", DefaultValue: "default"},
357+
},
358+
},
359+
},
360+
}
361+
362+
def := generateDefJSON(mpkDef, "SIZED")
363+
if len(def.ObjectLists) != 1 {
364+
t.Fatalf("ObjectLists count = %d, want 1", len(def.ObjectLists))
365+
}
366+
props := def.ObjectLists[0].ItemProperties
367+
if len(props) != 3 {
368+
t.Fatalf("ItemProperties count = %d, want 3", len(props))
369+
}
370+
371+
wantValues := map[string]string{
372+
"size": "10",
373+
"label": "", // empty default → no Value set
374+
"kind": "default",
375+
}
376+
for _, ip := range props {
377+
want := wantValues[ip.PropertyKey]
378+
if ip.Value != want {
379+
t.Errorf("ItemProperty %q Value = %q, want %q",
380+
ip.PropertyKey, ip.Value, want)
381+
}
382+
if ip.Operation != "primitive" {
383+
t.Errorf("ItemProperty %q Operation = %q, want primitive",
384+
ip.PropertyKey, ip.Operation)
385+
}
386+
}
387+
}

sdk/widgets/mpk/mpk_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package mpk
44

55
import (
6+
"encoding/xml"
67
"os"
78
"path/filepath"
89
"testing"
@@ -184,6 +185,102 @@ func TestNormalizeType(t *testing.T) {
184185
}
185186
}
186187

188+
// TestObjectListExtraction_GroupedShape covers the
189+
// <properties><propertyGroup><property>...</property></propertyGroup></properties>
190+
// nesting (Accordion, DataGrid).
191+
func TestObjectListExtraction_GroupedShape(t *testing.T) {
192+
xmlSrc := `<widget id="com.example.Acc" pluginWidget="true">
193+
<properties>
194+
<propertyGroup caption="Top">
195+
<property key="groups" type="object" isList="true">
196+
<caption>Groups</caption>
197+
<properties>
198+
<propertyGroup caption="Inner">
199+
<property key="headerText" type="textTemplate"><caption>Header</caption></property>
200+
<property key="content" type="widgets"><caption>Content</caption></property>
201+
</propertyGroup>
202+
</properties>
203+
</property>
204+
</propertyGroup>
205+
</properties>
206+
</widget>`
207+
208+
var w xmlWidget
209+
if err := xml.Unmarshal([]byte(xmlSrc), &w); err != nil {
210+
t.Fatalf("xml.Unmarshal: %v", err)
211+
}
212+
def := &WidgetDefinition{ID: "com.example.Acc"}
213+
for _, pg := range w.PropertyGroups {
214+
walkPropertyGroup(pg, "", def)
215+
}
216+
217+
if len(def.Properties) != 1 {
218+
t.Fatalf("Properties count = %d, want 1", len(def.Properties))
219+
}
220+
groups := def.Properties[0]
221+
if groups.Key != "groups" || groups.Type != "object" || !groups.IsList {
222+
t.Errorf("groups property has unexpected fields: %+v", groups)
223+
}
224+
if len(groups.Children) != 2 {
225+
t.Fatalf("groups.Children count = %d, want 2", len(groups.Children))
226+
}
227+
wantChildren := map[string]string{"headerText": "textTemplate", "content": "widgets"}
228+
for _, c := range groups.Children {
229+
if want, ok := wantChildren[c.Key]; !ok {
230+
t.Errorf("unexpected child key %q", c.Key)
231+
} else if c.Type != want {
232+
t.Errorf("child %q Type = %q, want %q", c.Key, c.Type, want)
233+
}
234+
}
235+
}
236+
237+
// TestObjectListExtraction_FlatShape covers the
238+
// <properties><property>...</property></properties> nesting (PopupMenu, Maps).
239+
// Without the NestedDirectProps field, the children would be silently dropped.
240+
func TestObjectListExtraction_FlatShape(t *testing.T) {
241+
xmlSrc := `<widget id="com.example.Menu" pluginWidget="true">
242+
<properties>
243+
<propertyGroup caption="Top">
244+
<property key="basicItems" type="object" isList="true">
245+
<caption>Items</caption>
246+
<properties>
247+
<property key="caption" type="textTemplate"><caption>Caption</caption></property>
248+
<property key="action" type="action"><caption>Action</caption></property>
249+
</properties>
250+
</property>
251+
</propertyGroup>
252+
</properties>
253+
</widget>`
254+
255+
var w xmlWidget
256+
if err := xml.Unmarshal([]byte(xmlSrc), &w); err != nil {
257+
t.Fatalf("xml.Unmarshal: %v", err)
258+
}
259+
def := &WidgetDefinition{ID: "com.example.Menu"}
260+
for _, pg := range w.PropertyGroups {
261+
walkPropertyGroup(pg, "", def)
262+
}
263+
264+
if len(def.Properties) != 1 {
265+
t.Fatalf("Properties count = %d, want 1", len(def.Properties))
266+
}
267+
items := def.Properties[0]
268+
if items.Key != "basicItems" || !items.IsList {
269+
t.Errorf("basicItems property has unexpected fields: %+v", items)
270+
}
271+
if len(items.Children) != 2 {
272+
t.Fatalf("basicItems.Children count = %d, want 2 (children dropped — flat XML shape unsupported?)", len(items.Children))
273+
}
274+
wantChildren := map[string]string{"caption": "textTemplate", "action": "action"}
275+
for _, c := range items.Children {
276+
if want, ok := wantChildren[c.Key]; !ok {
277+
t.Errorf("unexpected child key %q", c.Key)
278+
} else if c.Type != want {
279+
t.Errorf("child %q Type = %q, want %q", c.Key, c.Type, want)
280+
}
281+
}
282+
}
283+
187284
func fileExists(path string) bool {
188285
_, err := os.Stat(path)
189286
return err == nil

0 commit comments

Comments
 (0)