@@ -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+ }
0 commit comments