docusaurus-plugin-generate-schema-docs 1.8.3 → 1.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +12 -0
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
  3. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof-multi.json +12 -0
  4. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof.json +30 -0
  5. package/__tests__/__fixtures__/validateSchemas/schema-with-not-edge-cases.json +24 -0
  6. package/__tests__/__fixtures__/validateSchemas/schema-with-not-non-object.json +15 -0
  7. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +6 -0
  8. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +6 -0
  9. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +15 -0
  10. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +6 -0
  11. package/__tests__/components/PropertiesTable.test.js +66 -0
  12. package/__tests__/components/PropertyRow.test.js +85 -4
  13. package/__tests__/components/SchemaJsonViewer.test.js +118 -0
  14. package/__tests__/generateEventDocs.anchor.test.js +1 -1
  15. package/__tests__/generateEventDocs.nested.test.js +1 -1
  16. package/__tests__/generateEventDocs.partials.test.js +1 -1
  17. package/__tests__/generateEventDocs.test.js +506 -1
  18. package/__tests__/generateEventDocs.versioned.test.js +1 -1
  19. package/__tests__/helpers/buildExampleFromSchema.test.js +240 -0
  20. package/__tests__/helpers/constraintSchemaPaths.test.js +208 -0
  21. package/__tests__/helpers/continuingLinesStyle.test.js +492 -0
  22. package/__tests__/helpers/example-helper.test.js +12 -0
  23. package/__tests__/helpers/exampleModel.test.js +209 -0
  24. package/__tests__/helpers/file-system.test.js +73 -1
  25. package/__tests__/helpers/getConstraints.test.js +43 -0
  26. package/__tests__/helpers/mergeSchema.test.js +94 -0
  27. package/__tests__/helpers/processSchema.test.js +309 -1
  28. package/__tests__/helpers/schema-doc-template.test.js +54 -0
  29. package/__tests__/helpers/schema-processing.test.js +122 -2
  30. package/__tests__/helpers/schemaToExamples.test.js +1007 -0
  31. package/__tests__/helpers/schemaToTableData.mutations.test.js +970 -0
  32. package/__tests__/helpers/schemaToTableData.test.js +157 -0
  33. package/__tests__/helpers/schemaTraversal.test.js +110 -0
  34. package/__tests__/helpers/snippetTargets.test.js +432 -0
  35. package/__tests__/helpers/trackingTargets.test.js +319 -0
  36. package/__tests__/helpers/validator.test.js +385 -1
  37. package/__tests__/index.test.js +436 -0
  38. package/__tests__/syncGtm.test.js +366 -6
  39. package/__tests__/update-schema-ids.test.js +70 -1
  40. package/__tests__/validateSchemas-integration.test.js +2 -2
  41. package/__tests__/validateSchemas.test.js +192 -1
  42. package/components/PropertiesTable.js +32 -2
  43. package/components/PropertyRow.js +29 -2
  44. package/components/SchemaJsonViewer.js +234 -131
  45. package/components/SchemaRows.css +40 -0
  46. package/components/SchemaViewer.js +11 -2
  47. package/generateEventDocs.js +21 -1
  48. package/helpers/constraintSchemaPaths.js +10 -14
  49. package/helpers/example-helper.js +2 -2
  50. package/helpers/getConstraints.js +20 -0
  51. package/helpers/processSchema.js +32 -1
  52. package/helpers/schema-doc-template.js +4 -0
  53. package/helpers/schemaToExamples.js +29 -35
  54. package/helpers/schemaToTableData.js +538 -492
  55. package/helpers/schemaTraversal.cjs +148 -0
  56. package/helpers/trackingTargets.js +26 -3
  57. package/helpers/validator.js +18 -4
  58. package/index.js +1 -2
  59. package/package.json +1 -1
  60. package/scripts/sync-gtm.js +65 -34
  61. package/test-data/payloadContracts.js +35 -0
  62. package/validateSchemas.js +1 -1
@@ -0,0 +1,970 @@
1
+ /**
2
+ * Targeted mutation-killing tests for schemaToTableData.js.
3
+ * Each describe block targets specific uncovered branches identified by
4
+ * surviving mutations.
5
+ */
6
+ import { schemaToTableData } from '../../helpers/schemaToTableData';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // materializeConditionalBranchSchema (L13-35)
10
+ // ---------------------------------------------------------------------------
11
+ describe('materializeConditionalBranchSchema early-return conditions', () => {
12
+ // L15: branchSchema is falsy — the function should just return branchSchema (falsy)
13
+ // This is exercised indirectly: if subSchema.then is undefined, buildConditionalRow
14
+ // skips it. We test nullish branch indirectly via a schema where else is undefined.
15
+ it('skips else branch entirely when there is no else', () => {
16
+ const schema = {
17
+ type: 'object',
18
+ properties: { status: { type: 'string' } },
19
+ if: { properties: { status: { const: 'active' } } },
20
+ then: { properties: { active_since: { type: 'string' } } },
21
+ };
22
+ const rows = schemaToTableData(schema);
23
+ const conditional = rows.find((r) => r.type === 'conditional');
24
+ expect(conditional).toBeDefined();
25
+ expect(conditional.branches).toHaveLength(1);
26
+ expect(conditional.branches[0].title).toBe('Then');
27
+ });
28
+
29
+ // L16: branchSchema.properties is set → return early (don't materialise)
30
+ it('does NOT add parent properties when branch already has .properties', () => {
31
+ const schema = {
32
+ type: 'object',
33
+ properties: {
34
+ foo: { type: 'string' },
35
+ bar: { type: 'number' },
36
+ },
37
+ if: { properties: { foo: { const: 'x' } } },
38
+ then: {
39
+ // branch has its own properties — must NOT be merged with parent props
40
+ properties: { bar: { type: 'number' } },
41
+ required: ['bar'],
42
+ },
43
+ };
44
+ const rows = schemaToTableData(schema);
45
+ const conditional = rows.find((r) => r.type === 'conditional');
46
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
47
+ // Only 'bar' (from branch.properties), not duplicated from parent
48
+ expect(thenBranch.rows).toHaveLength(1);
49
+ expect(thenBranch.rows[0].name).toBe('bar');
50
+ });
51
+
52
+ // L17: branchSchema.oneOf set → return early
53
+ it('returns branch as-is when branch has .oneOf', () => {
54
+ const schema = {
55
+ type: 'object',
56
+ properties: { x: { type: 'string' } },
57
+ if: { properties: { x: { const: 'val' } } },
58
+ then: {
59
+ oneOf: [{ title: 'A', properties: { a_prop: { type: 'string' } } }],
60
+ },
61
+ };
62
+ const rows = schemaToTableData(schema);
63
+ const conditional = rows.find((r) => r.type === 'conditional');
64
+ expect(conditional).toBeDefined();
65
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
66
+ expect(thenBranch).toBeDefined();
67
+ // Branch rendered via its own oneOf, not materialised from parent
68
+ const choiceRow = thenBranch.rows.find((r) => r.type === 'choice');
69
+ expect(choiceRow).toBeDefined();
70
+ });
71
+
72
+ // L18: branchSchema.anyOf set → return early
73
+ it('returns branch as-is when branch has .anyOf', () => {
74
+ const schema = {
75
+ type: 'object',
76
+ properties: { x: { type: 'string' } },
77
+ if: { properties: { x: { const: 'val' } } },
78
+ then: {
79
+ anyOf: [{ title: 'B', properties: { b_prop: { type: 'string' } } }],
80
+ },
81
+ };
82
+ const rows = schemaToTableData(schema);
83
+ const conditional = rows.find((r) => r.type === 'conditional');
84
+ expect(conditional).toBeDefined();
85
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
86
+ expect(thenBranch).toBeDefined();
87
+ const choiceRow = thenBranch.rows.find((r) => r.type === 'choice');
88
+ expect(choiceRow).toBeDefined();
89
+ });
90
+
91
+ // L19: branchSchema.if set → return early
92
+ it('returns branch as-is when branch itself has .if', () => {
93
+ const schema = {
94
+ type: 'object',
95
+ properties: { x: { type: 'string' }, y: { type: 'string' } },
96
+ if: { properties: { x: { const: 'val' } } },
97
+ then: {
98
+ // nested if inside a branch — must not be materialised
99
+ if: { properties: { y: { const: 'z' } } },
100
+ then: { properties: { z_prop: { type: 'number' } } },
101
+ },
102
+ };
103
+ const rows = schemaToTableData(schema);
104
+ const conditional = rows.find((r) => r.type === 'conditional');
105
+ expect(conditional).toBeDefined();
106
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
107
+ expect(thenBranch).toBeDefined();
108
+ // The branch's own if/then renders as a nested conditional row
109
+ const nestedConditional = thenBranch.rows.find(
110
+ (r) => r.type === 'conditional',
111
+ );
112
+ expect(nestedConditional).toBeDefined();
113
+ });
114
+
115
+ // L20: branchSchema.required is not an array → return early
116
+ it('returns branch as-is when branch.required is not an array', () => {
117
+ const schema = {
118
+ type: 'object',
119
+ properties: { foo: { type: 'string' } },
120
+ if: { properties: { foo: { const: 'x' } } },
121
+ then: {
122
+ // required is a string, not an array → should NOT materialise
123
+ required: 'foo',
124
+ description: 'a non-array required branch',
125
+ },
126
+ };
127
+ const rows = schemaToTableData(schema);
128
+ const conditional = rows.find((r) => r.type === 'conditional');
129
+ expect(conditional).toBeDefined();
130
+ // The then branch should render as-is (no property rows since
131
+ // no .properties / .type defined)
132
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
133
+ expect(thenBranch).toBeDefined();
134
+ // No property rows — branch passed through unchanged
135
+ expect(thenBranch.rows).toHaveLength(0);
136
+ });
137
+
138
+ // L21: OptionalChaining — parentSchema?.properties is falsy → return early
139
+ it('returns branch as-is when parentSchema has no .properties', () => {
140
+ // Calling materializeConditionalBranchSchema with a parent that has no
141
+ // properties. We test this via a schema where the conditional container
142
+ // itself has no properties keyword.
143
+ const schema = {
144
+ // No .properties on this object, just if/then
145
+ type: 'object',
146
+ if: { properties: { kind: { const: 'special' } } },
147
+ then: { required: ['kind'] },
148
+ };
149
+ const rows = schemaToTableData(schema);
150
+ const conditional = rows.find((r) => r.type === 'conditional');
151
+ expect(conditional).toBeDefined();
152
+ // then.required=['kind'] but parent has no .properties → branch as-is → 0 rows
153
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
154
+ expect(thenBranch).toBeDefined();
155
+ expect(thenBranch.rows).toHaveLength(0);
156
+ });
157
+
158
+ // L27: branchSchema.required.filter — required points to names NOT in parent
159
+ it('returns branchSchema unchanged when required names not present in parent properties', () => {
160
+ const schema = {
161
+ type: 'object',
162
+ properties: {
163
+ foo: { type: 'string' },
164
+ },
165
+ if: { properties: { foo: { const: 'x' } } },
166
+ then: {
167
+ // 'nonexistent' is not in parent.properties → branchProperties is empty
168
+ required: ['nonexistent'],
169
+ },
170
+ };
171
+ const rows = schemaToTableData(schema);
172
+ const conditional = rows.find((r) => r.type === 'conditional');
173
+ expect(conditional).toBeDefined();
174
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
175
+ // Empty branchProperties → returns branchSchema unchanged (no .properties) → 0 rows
176
+ expect(thenBranch.rows).toHaveLength(0);
177
+ });
178
+
179
+ // L32: ConditionalExpression — empty branchProperties returns unchanged schema
180
+ // Already covered above by "required names not present" — extra assertion:
181
+ it('materialises branch properties correctly when required matches parent', () => {
182
+ const schema = {
183
+ type: 'object',
184
+ properties: {
185
+ foo: { type: 'string' },
186
+ bar: { type: 'number' },
187
+ },
188
+ if: { properties: { foo: { const: 'x' } } },
189
+ then: { required: ['bar'] },
190
+ };
191
+ const rows = schemaToTableData(schema);
192
+ const conditional = rows.find((r) => r.type === 'conditional');
193
+ const thenBranch = conditional.branches.find((b) => b.title === 'Then');
194
+ expect(thenBranch.rows).toHaveLength(1);
195
+ expect(thenBranch.rows[0].name).toBe('bar');
196
+ expect(thenBranch.rows[0].required).toBe(true);
197
+ });
198
+ });
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // hasRenderableAdditionalProperties (L37-43)
202
+ // ---------------------------------------------------------------------------
203
+ describe('hasRenderableAdditionalProperties edge cases', () => {
204
+ // L39: schemaNode?.additionalProperties is falsy (undefined / false)
205
+ it('does not render additionalProperties when it is false', () => {
206
+ const schema = {
207
+ properties: {
208
+ obj: {
209
+ type: 'object',
210
+ additionalProperties: false,
211
+ properties: { x: { type: 'string' } },
212
+ },
213
+ },
214
+ };
215
+ const rows = schemaToTableData(schema);
216
+ const addlRow = rows.find((r) => r.name === 'additionalProperties');
217
+ expect(addlRow).toBeUndefined();
218
+ });
219
+
220
+ it('does not render additionalProperties when it is absent', () => {
221
+ const schema = {
222
+ properties: {
223
+ obj: { type: 'object', properties: { x: { type: 'string' } } },
224
+ },
225
+ };
226
+ const rows = schemaToTableData(schema);
227
+ const addlRow = rows.find((r) => r.name === 'additionalProperties');
228
+ expect(addlRow).toBeUndefined();
229
+ });
230
+
231
+ // L40: typeof check — additionalProperties must be object (not boolean true)
232
+ it('does not render additionalProperties when it is boolean true', () => {
233
+ const schema = {
234
+ properties: {
235
+ obj: {
236
+ type: 'object',
237
+ additionalProperties: true,
238
+ },
239
+ },
240
+ };
241
+ const rows = schemaToTableData(schema);
242
+ const addlRow = rows.find((r) => r.name === 'additionalProperties');
243
+ expect(addlRow).toBeUndefined();
244
+ });
245
+
246
+ // additionalProperties is an array — should not render
247
+ it('does not render additionalProperties when it is an array', () => {
248
+ const schema = {
249
+ properties: {
250
+ obj: {
251
+ type: 'object',
252
+ additionalProperties: [{ type: 'string' }],
253
+ },
254
+ },
255
+ };
256
+ const rows = schemaToTableData(schema);
257
+ const addlRow = rows.find((r) => r.name === 'additionalProperties');
258
+ expect(addlRow).toBeUndefined();
259
+ });
260
+
261
+ // Valid object additionalProperties renders correctly
262
+ it('renders additionalProperties when it is a schema object', () => {
263
+ const schema = {
264
+ properties: {
265
+ obj: {
266
+ type: 'object',
267
+ additionalProperties: { type: 'string' },
268
+ },
269
+ },
270
+ };
271
+ const rows = schemaToTableData(schema);
272
+ const objRow = rows.find((r) => r.name === 'obj');
273
+ expect(objRow.containerType).toBe('object');
274
+ const addlRow = rows.find((r) => r.name === 'additionalProperties');
275
+ expect(addlRow).toBeDefined();
276
+ expect(addlRow.propertyType).toBe('string');
277
+ });
278
+ });
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // getRenderablePatternProperties (L45-53)
282
+ // ---------------------------------------------------------------------------
283
+ describe('getRenderablePatternProperties edge cases', () => {
284
+ // L46: schemaNode?.patternProperties is absent → returns []
285
+ it('returns no pattern property rows when patternProperties is absent', () => {
286
+ const schema = { properties: { x: { type: 'string' } } };
287
+ const rows = schemaToTableData(schema);
288
+ const patternRows = rows.filter((r) =>
289
+ r.name?.startsWith('patternProperties'),
290
+ );
291
+ expect(patternRows).toHaveLength(0);
292
+ });
293
+
294
+ // L47: Object.entries filter — non-object pattern value is skipped
295
+ it('skips patternProperties entries that are not objects', () => {
296
+ const schema = {
297
+ properties: {
298
+ obj: {
299
+ type: 'object',
300
+ // Include a declared property so buildPropertyChildren enters buildRows for obj
301
+ properties: { declared: { type: 'string' } },
302
+ patternProperties: {
303
+ '^valid_': { type: 'string' },
304
+ '^null_pattern': null,
305
+ '^array_pattern': ['not', 'an', 'object'],
306
+ },
307
+ },
308
+ },
309
+ };
310
+ const rows = schemaToTableData(schema);
311
+ const patternRows = rows.filter((r) =>
312
+ r.name?.startsWith('patternProperties'),
313
+ );
314
+ // Only the valid object schema pattern renders (null and array are skipped)
315
+ expect(patternRows).toHaveLength(1);
316
+ expect(patternRows[0].name).toBe('patternProperties /^valid_/');
317
+ });
318
+
319
+ // L48: filter condition — null value excluded
320
+ it('skips patternProperties entry with null schema', () => {
321
+ const schema = {
322
+ properties: {
323
+ obj: {
324
+ type: 'object',
325
+ properties: { declared: { type: 'string' } },
326
+ patternProperties: {
327
+ '^foo': null,
328
+ },
329
+ },
330
+ },
331
+ };
332
+ const rows = schemaToTableData(schema);
333
+ const patternRows = rows.filter((r) =>
334
+ r.name?.startsWith('patternProperties'),
335
+ );
336
+ expect(patternRows).toHaveLength(0);
337
+ });
338
+ });
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // isEffectivelyEmpty (L55-66)
342
+ // ---------------------------------------------------------------------------
343
+ describe('isEffectivelyEmpty edge cases', () => {
344
+ // L57: type !== 'object' AND typeof properties === 'undefined'
345
+ // A non-object type with no properties → isEffectivelyEmpty returns false
346
+ // (i.e. it is NOT empty — should still render)
347
+ it('does not filter non-object typed property even when x-gtm-clear is set', () => {
348
+ const schema = {
349
+ properties: {
350
+ label: {
351
+ 'x-gtm-clear': true,
352
+ type: 'string', // non-object, no .properties
353
+ },
354
+ },
355
+ };
356
+ const rows = schemaToTableData(schema);
357
+ // isEffectivelyEmpty returns false for string type → not filtered
358
+ expect(rows).toHaveLength(1);
359
+ expect(rows[0].name).toBe('label');
360
+ });
361
+
362
+ // L57: type IS 'object' with no properties — effectively empty → filtered
363
+ it('filters x-gtm-clear object with no properties at all', () => {
364
+ const schema = {
365
+ properties: {
366
+ empty_obj: {
367
+ 'x-gtm-clear': true,
368
+ type: 'object',
369
+ // no .properties key
370
+ },
371
+ visible: { type: 'string' },
372
+ },
373
+ };
374
+ const rows = schemaToTableData(schema);
375
+ expect(rows).toHaveLength(1);
376
+ expect(rows[0].name).toBe('visible');
377
+ });
378
+
379
+ // L58: oneOf makes it NOT empty
380
+ it('does not filter x-gtm-clear object that has oneOf', () => {
381
+ const schema = {
382
+ properties: {
383
+ choice_obj: {
384
+ 'x-gtm-clear': true,
385
+ type: 'object',
386
+ properties: {},
387
+ oneOf: [{ title: 'A', properties: { a: { type: 'string' } } }],
388
+ },
389
+ },
390
+ };
391
+ const rows = schemaToTableData(schema);
392
+ const choiceObjRow = rows.find((r) => r.name === 'choice_obj');
393
+ expect(choiceObjRow).toBeDefined();
394
+ });
395
+
396
+ // L58: anyOf makes it NOT empty
397
+ it('does not filter x-gtm-clear object that has anyOf', () => {
398
+ const schema = {
399
+ properties: {
400
+ any_obj: {
401
+ 'x-gtm-clear': true,
402
+ type: 'object',
403
+ properties: {},
404
+ anyOf: [{ title: 'B', properties: { b: { type: 'string' } } }],
405
+ },
406
+ },
407
+ };
408
+ const rows = schemaToTableData(schema);
409
+ const anyObjRow = rows.find((r) => r.name === 'any_obj');
410
+ expect(anyObjRow).toBeDefined();
411
+ });
412
+
413
+ // L58: if makes it NOT empty
414
+ it('does not filter x-gtm-clear object that has if', () => {
415
+ const schema = {
416
+ properties: {
417
+ cond_obj: {
418
+ 'x-gtm-clear': true,
419
+ type: 'object',
420
+ properties: {},
421
+ if: { properties: { x: { const: 'y' } } },
422
+ then: { properties: { y: { type: 'string' } } },
423
+ },
424
+ },
425
+ };
426
+ const rows = schemaToTableData(schema);
427
+ const condObjRow = rows.find((r) => r.name === 'cond_obj');
428
+ expect(condObjRow).toBeDefined();
429
+ });
430
+
431
+ // L63-65: empty properties ({}) → effectively empty
432
+ it('filters x-gtm-clear object with empty properties object', () => {
433
+ const schema = {
434
+ properties: {
435
+ empty: { 'x-gtm-clear': true, type: 'object', properties: {} },
436
+ visible: { type: 'number' },
437
+ },
438
+ };
439
+ const rows = schemaToTableData(schema);
440
+ expect(rows).toHaveLength(1);
441
+ expect(rows[0].name).toBe('visible');
442
+ });
443
+
444
+ // L65: every() — object with all-empty children → effectively empty
445
+ it('filters x-gtm-clear object whose all children are themselves empty', () => {
446
+ const schema = {
447
+ properties: {
448
+ parent: {
449
+ 'x-gtm-clear': true,
450
+ type: 'object',
451
+ properties: {
452
+ child: { type: 'object', properties: {} },
453
+ },
454
+ },
455
+ visible: { type: 'string' },
456
+ },
457
+ };
458
+ const rows = schemaToTableData(schema);
459
+ // parent's only child is empty → parent is effectively empty → filtered
460
+ expect(rows).toHaveLength(1);
461
+ expect(rows[0].name).toBe('visible');
462
+ });
463
+
464
+ // L65: every() — NOT empty when at least one child has content
465
+ it('does NOT filter x-gtm-clear object that has at least one non-empty child', () => {
466
+ const schema = {
467
+ properties: {
468
+ parent: {
469
+ 'x-gtm-clear': true,
470
+ type: 'object',
471
+ properties: {
472
+ child: { type: 'string' }, // non-object type → isEffectivelyEmpty=false
473
+ },
474
+ },
475
+ },
476
+ };
477
+ const rows = schemaToTableData(schema);
478
+ const parentRow = rows.find((r) => r.name === 'parent');
479
+ expect(parentRow).toBeDefined();
480
+ });
481
+ });
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // resolveContainerType / getContainerInfo (L68-136)
485
+ // ---------------------------------------------------------------------------
486
+ describe('resolveContainerType and getContainerInfo edge cases', () => {
487
+ // L83: hasArrayItems requires type==='array' AND items with properties or if
488
+ it('does not set containerType=array when array has items without properties or if', () => {
489
+ const schema = {
490
+ properties: {
491
+ tags: {
492
+ type: 'array',
493
+ items: { type: 'string' }, // no .properties, no .if
494
+ },
495
+ },
496
+ };
497
+ const rows = schemaToTableData(schema);
498
+ const tagsRow = rows.find((r) => r.name === 'tags');
499
+ expect(tagsRow.containerType).toBeNull();
500
+ expect(tagsRow.hasChildren).toBe(false);
501
+ });
502
+
503
+ it('sets containerType=array when array items have properties', () => {
504
+ const schema = {
505
+ properties: {
506
+ orders: {
507
+ type: 'array',
508
+ items: { type: 'object', properties: { id: { type: 'string' } } },
509
+ },
510
+ },
511
+ };
512
+ const rows = schemaToTableData(schema);
513
+ const ordersRow = rows.find((r) => r.name === 'orders');
514
+ expect(ordersRow.containerType).toBe('array');
515
+ expect(ordersRow.hasChildren).toBe(true);
516
+ });
517
+
518
+ // L83: array items with .if (no .properties) also triggers hasArrayItems
519
+ it('sets containerType=array when array items have if but no properties', () => {
520
+ const schema = {
521
+ properties: {
522
+ events: {
523
+ type: 'array',
524
+ items: {
525
+ if: { properties: { kind: { const: 'special' } } },
526
+ then: { properties: { special_field: { type: 'string' } } },
527
+ },
528
+ },
529
+ },
530
+ };
531
+ const rows = schemaToTableData(schema);
532
+ const eventsRow = rows.find((r) => r.name === 'events');
533
+ expect(eventsRow.containerType).toBe('array');
534
+ expect(eventsRow.hasChildren).toBe(true);
535
+ });
536
+
537
+ // L86: isConditionalWrapper with type==='object' → containerType='object'
538
+ it('sets containerType=object for conditional wrapper with type object', () => {
539
+ const schema = {
540
+ properties: {
541
+ shipping: {
542
+ type: 'object',
543
+ if: { properties: { method: { const: 'express' } } },
544
+ then: { properties: { priority: { type: 'string' } } },
545
+ },
546
+ },
547
+ };
548
+ const rows = schemaToTableData(schema);
549
+ const shippingRow = rows.find((r) => r.name === 'shipping');
550
+ expect(shippingRow.containerType).toBe('object');
551
+ expect(shippingRow.hasChildren).toBe(true);
552
+ });
553
+
554
+ // L86: isConditionalWrapper WITHOUT type==='object' → should NOT be 'object'
555
+ it('does not set containerType=object for conditional wrapper without type object', () => {
556
+ const schema = {
557
+ properties: {
558
+ cond_prop: {
559
+ // no type specified
560
+ if: { properties: { x: { const: 'y' } } },
561
+ then: { properties: { y_prop: { type: 'string' } } },
562
+ },
563
+ },
564
+ };
565
+ const rows = schemaToTableData(schema);
566
+ const condRow = rows.find((r) => r.name === 'cond_prop');
567
+ // isConditionalWrapper=true but type !== 'object' → resolveContainerType returns null
568
+ expect(condRow.containerType).toBeNull();
569
+ // hasChildren is still true
570
+ expect(condRow.hasChildren).toBe(true);
571
+ });
572
+
573
+ // L82-85: isChoiceWrapper with type==='object' → containerType='object'
574
+ it('sets containerType=object for choice wrapper with type object', () => {
575
+ const schema = {
576
+ properties: {
577
+ payment: {
578
+ type: 'object',
579
+ oneOf: [
580
+ { title: 'Card', properties: { card_num: { type: 'string' } } },
581
+ ],
582
+ },
583
+ },
584
+ };
585
+ const rows = schemaToTableData(schema);
586
+ const paymentRow = rows.find(
587
+ (r) => r.name === 'payment' && r.type === 'property',
588
+ );
589
+ expect(paymentRow.containerType).toBe('object');
590
+ expect(paymentRow.hasChildren).toBe(true);
591
+ });
592
+
593
+ // L82-85: isChoiceWrapper with choiceOptionsAreObjects (no type) → containerType='object'
594
+ it('sets containerType=object for choice wrapper where options have properties', () => {
595
+ const schema = {
596
+ properties: {
597
+ method: {
598
+ // no type keyword — but options have object properties
599
+ anyOf: [
600
+ { title: 'A', properties: { a_field: { type: 'string' } } },
601
+ { title: 'B', properties: { b_field: { type: 'number' } } },
602
+ ],
603
+ },
604
+ },
605
+ };
606
+ const rows = schemaToTableData(schema);
607
+ const methodRow = rows.find(
608
+ (r) => r.name === 'method' && r.type === 'property',
609
+ );
610
+ expect(methodRow).toBeDefined();
611
+ expect(methodRow.containerType).toBe('object');
612
+ });
613
+
614
+ // choiceOptionsAreObjects: options with type==='object' (no .properties)
615
+ it('sets containerType=object for choice wrapper where options have type object', () => {
616
+ const schema = {
617
+ properties: {
618
+ payload: {
619
+ oneOf: [
620
+ { title: 'X', type: 'object' },
621
+ { title: 'Y', type: 'string' },
622
+ ],
623
+ },
624
+ },
625
+ };
626
+ const rows = schemaToTableData(schema);
627
+ const payloadRow = rows.find(
628
+ (r) => r.name === 'payload' && r.type === 'property',
629
+ );
630
+ expect(payloadRow).toBeDefined();
631
+ expect(payloadRow.containerType).toBe('object');
632
+ });
633
+
634
+ // L104-105: hasArrayItems — items?.properties check (optional chaining)
635
+ it('does not crash and returns hasChildren=false when items is absent', () => {
636
+ const schema = {
637
+ properties: {
638
+ list: { type: 'array' }, // no items at all
639
+ },
640
+ };
641
+ const rows = schemaToTableData(schema);
642
+ const listRow = rows.find((r) => r.name === 'list');
643
+ expect(listRow.hasChildren).toBe(false);
644
+ expect(listRow.containerType).toBeNull();
645
+ });
646
+ });
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // buildPropertyRows / buildRows (L419+)
650
+ // ---------------------------------------------------------------------------
651
+ describe('buildPropertyRows edge cases', () => {
652
+ // L469-470: continuingLevels for non-last properties includes currentLevel
653
+ it('includes currentLevel in continuingLevels for non-last property children', () => {
654
+ const schema = {
655
+ properties: {
656
+ parent: {
657
+ type: 'object',
658
+ properties: {
659
+ first: { type: 'string' },
660
+ last: { type: 'string' },
661
+ },
662
+ },
663
+ sibling: { type: 'number' },
664
+ },
665
+ };
666
+ const rows = schemaToTableData(schema);
667
+ // 'parent' is NOT last (sibling follows) → children should have level 0 in continuingLevels
668
+ const firstRow = rows.find((r) => r.name === 'first');
669
+ expect(firstRow.continuingLevels).toContain(0);
670
+ const lastRow = rows.find((r) => r.name === 'last');
671
+ expect(lastRow.continuingLevels).toContain(0);
672
+ });
673
+
674
+ it('does NOT include currentLevel in continuingLevels for last property children', () => {
675
+ const schema = {
676
+ properties: {
677
+ parent: {
678
+ type: 'object',
679
+ properties: {
680
+ child: { type: 'string' },
681
+ },
682
+ },
683
+ // no sibling — parent is last
684
+ },
685
+ };
686
+ const rows = schemaToTableData(schema);
687
+ const childRow = rows.find((r) => r.name === 'child');
688
+ expect(childRow.continuingLevels).not.toContain(0);
689
+ });
690
+
691
+ // L496: propertyType fallback — no type, has enum → 'enum'
692
+ it('uses "enum" as propertyType when schema has enum but no type', () => {
693
+ const schema = {
694
+ properties: {
695
+ status: {
696
+ enum: ['active', 'inactive'],
697
+ description: 'Status value',
698
+ },
699
+ },
700
+ };
701
+ const rows = schemaToTableData(schema);
702
+ const statusRow = rows.find((r) => r.name === 'status');
703
+ expect(statusRow.propertyType).toBe('enum');
704
+ });
705
+
706
+ // L496: propertyType fallback — no type, no enum → 'object'
707
+ it('uses "object" as propertyType when schema has neither type nor enum', () => {
708
+ const schema = {
709
+ properties: {
710
+ mystery: {
711
+ description: 'Unknown type property',
712
+ properties: { x: { type: 'string' } },
713
+ },
714
+ },
715
+ };
716
+ const rows = schemaToTableData(schema);
717
+ const mysteryRow = rows.find((r) => r.name === 'mystery');
718
+ expect(mysteryRow.propertyType).toBe('object');
719
+ });
720
+
721
+ // L506: isLastInGroup — last property that is also last option
722
+ it('marks last property in group as isLastInGroup=true', () => {
723
+ const schema = {
724
+ properties: {
725
+ first: { type: 'string' },
726
+ second: { type: 'number' },
727
+ },
728
+ };
729
+ const rows = schemaToTableData(schema);
730
+ const firstRow = rows.find((r) => r.name === 'first');
731
+ const secondRow = rows.find((r) => r.name === 'second');
732
+ expect(firstRow.isLastInGroup).toBe(false);
733
+ expect(secondRow.isLastInGroup).toBe(true);
734
+ });
735
+
736
+ // L506: isLastInGroup=false when hasSiblingChoices (sibling oneOf/if exists)
737
+ it('marks all property rows as isLastInGroup=false when sibling choices exist', () => {
738
+ const schema = {
739
+ properties: {
740
+ prop_a: { type: 'string' },
741
+ },
742
+ oneOf: [{ title: 'Opt', properties: { opt_prop: { type: 'string' } } }],
743
+ };
744
+ const rows = schemaToTableData(schema);
745
+ const propARow = rows.find((r) => r.name === 'prop_a');
746
+ // prop_a is the only property but hasSiblingChoices=true → not last
747
+ expect(propARow.isLastInGroup).toBe(false);
748
+ });
749
+
750
+ // L532-534: groupBrackets and continuingLevels in deeply nested scenario
751
+ it('propagates groupBrackets correctly through nested levels', () => {
752
+ const schema = {
753
+ properties: {
754
+ level0: {
755
+ type: 'object',
756
+ properties: {
757
+ level1: {
758
+ type: 'object',
759
+ properties: {
760
+ level2: { type: 'string' },
761
+ },
762
+ },
763
+ },
764
+ },
765
+ },
766
+ };
767
+ const rows = schemaToTableData(schema);
768
+ const level2Row = rows.find((r) => r.name === 'level2');
769
+ expect(level2Row).toBeDefined();
770
+ expect(level2Row.level).toBe(2);
771
+ // groupBrackets stays empty at all levels when no choice/conditional wrappers
772
+ expect(level2Row.groupBrackets).toEqual([]);
773
+ });
774
+ });
775
+
776
+ // ---------------------------------------------------------------------------
777
+ // Root-level primitive schema (L611-625)
778
+ // ---------------------------------------------------------------------------
779
+ describe('root-level primitive schema handling', () => {
780
+ // L615: title || '<value>' — name uses title when present
781
+ it('uses schema title as name for root-level primitive', () => {
782
+ const schema = {
783
+ type: 'string',
784
+ title: 'My Value',
785
+ description: 'A root string value',
786
+ };
787
+ const rows = schemaToTableData(schema);
788
+ expect(rows).toHaveLength(1);
789
+ expect(rows[0].name).toBe('My Value');
790
+ expect(rows[0].type).toBe('property');
791
+ expect(rows[0].propertyType).toBe('string');
792
+ });
793
+
794
+ // L615: title absent → '<value>'
795
+ it('uses "<value>" as name when root-level primitive has no title', () => {
796
+ const schema = {
797
+ type: 'number',
798
+ description: 'A root number',
799
+ };
800
+ const rows = schemaToTableData(schema);
801
+ expect(rows).toHaveLength(1);
802
+ expect(rows[0].name).toBe('<value>');
803
+ expect(rows[0].propertyType).toBe('number');
804
+ });
805
+
806
+ // L618: required: false for root-level primitive
807
+ it('sets required=false for root-level primitive schema', () => {
808
+ const schema = { type: 'boolean' };
809
+ const rows = schemaToTableData(schema);
810
+ expect(rows[0].required).toBe(false);
811
+ });
812
+
813
+ // L623: isLastInGroup: true for root-level primitive
814
+ it('sets isLastInGroup=true for root-level primitive schema', () => {
815
+ const schema = { type: 'string', title: 'Val' };
816
+ const rows = schemaToTableData(schema);
817
+ expect(rows[0].isLastInGroup).toBe(true);
818
+ });
819
+
820
+ // L624: continuingLevels is empty array for root-level primitive
821
+ it('sets continuingLevels=[] for root-level primitive schema', () => {
822
+ const schema = { type: 'integer' };
823
+ const rows = schemaToTableData(schema);
824
+ expect(rows[0].continuingLevels).toEqual([]);
825
+ });
826
+
827
+ // L625: groupBrackets is empty array for root-level primitive
828
+ it('sets groupBrackets=[] for root-level primitive schema', () => {
829
+ const schema = { type: 'string' };
830
+ const rows = schemaToTableData(schema);
831
+ expect(rows[0].groupBrackets).toEqual([]);
832
+ });
833
+
834
+ // L614: returns a property row (not choice or conditional)
835
+ it('root-level primitive is type=property', () => {
836
+ const schema = { type: 'string' };
837
+ const rows = schemaToTableData(schema);
838
+ expect(rows[0].type).toBe('property');
839
+ });
840
+
841
+ // Root-level primitive with parentContinuingLevels passed in
842
+ it('uses parentContinuingLevels when passed for root-level primitive', () => {
843
+ const schema = { type: 'string' };
844
+ const rows = schemaToTableData(schema, 1, ['some', 'path'], [0], false, []);
845
+ expect(rows[0].continuingLevels).toEqual([0]);
846
+ });
847
+
848
+ // Root-level primitive does NOT render when schema also has properties
849
+ it('does not render primitive row when schema has both type and properties', () => {
850
+ const schema = {
851
+ type: 'object',
852
+ properties: { x: { type: 'string' } },
853
+ };
854
+ const rows = schemaToTableData(schema);
855
+ // Only property row for 'x', not a primitive row
856
+ expect(rows).toHaveLength(1);
857
+ expect(rows[0].name).toBe('x');
858
+ });
859
+ });
860
+
861
+ // ---------------------------------------------------------------------------
862
+ // Additional logical combination coverage
863
+ // ---------------------------------------------------------------------------
864
+ describe('additional logical branch coverage', () => {
865
+ // hasChildren: true when isConditionalWrapper (if+then but no properties)
866
+ it('sets hasChildren=true when prop has if/then but no properties', () => {
867
+ const schema = {
868
+ properties: {
869
+ cond: {
870
+ type: 'object',
871
+ if: { properties: { x: { const: 'y' } } },
872
+ then: { properties: { y: { type: 'string' } } },
873
+ },
874
+ },
875
+ };
876
+ const rows = schemaToTableData(schema);
877
+ const condRow = rows.find((r) => r.name === 'cond');
878
+ expect(condRow.hasChildren).toBe(true);
879
+ });
880
+
881
+ // isChoiceWrapper false when neither oneOf nor anyOf
882
+ it('sets hasChildren=false for plain primitive property', () => {
883
+ const schema = {
884
+ properties: { plain: { type: 'string' } },
885
+ };
886
+ const rows = schemaToTableData(schema);
887
+ expect(rows[0].hasChildren).toBe(false);
888
+ expect(rows[0].containerType).toBeNull();
889
+ });
890
+
891
+ // Verify required constraint bubbles into constraints array
892
+ it('prepends "required" to constraints when property is required', () => {
893
+ const schema = {
894
+ type: 'object',
895
+ required: ['name'],
896
+ properties: {
897
+ name: { type: 'string' },
898
+ age: { type: 'number' },
899
+ },
900
+ };
901
+ const rows = schemaToTableData(schema);
902
+ const nameRow = rows.find((r) => r.name === 'name');
903
+ const ageRow = rows.find((r) => r.name === 'age');
904
+ expect(nameRow.required).toBe(true);
905
+ expect(nameRow.constraints[0]).toBe('required');
906
+ expect(ageRow.required).toBe(false);
907
+ });
908
+
909
+ // isConditionalWrapper requires both if AND (then OR else)
910
+ it('does not treat schema with if but no then/else as conditional wrapper', () => {
911
+ const schema = {
912
+ properties: {
913
+ ambiguous: {
914
+ type: 'object',
915
+ if: { properties: { x: { const: 'y' } } },
916
+ // no then, no else
917
+ properties: { x: { type: 'string' } },
918
+ },
919
+ },
920
+ };
921
+ const rows = schemaToTableData(schema);
922
+ const ambiguousRow = rows.find((r) => r.name === 'ambiguous');
923
+ expect(ambiguousRow).toBeDefined();
924
+ // hasChildren from properties, not from conditional
925
+ expect(ambiguousRow.hasChildren).toBe(true);
926
+ // No conditional row emitted for this property
927
+ const conditional = rows.find((r) => r.type === 'conditional');
928
+ expect(conditional).toBeUndefined();
929
+ });
930
+
931
+ // Both then and else present → two branches
932
+ it('creates two branches when schema has both then and else', () => {
933
+ const schema = {
934
+ type: 'object',
935
+ properties: { flag: { type: 'boolean' } },
936
+ if: { properties: { flag: { const: true } } },
937
+ then: { properties: { when_true: { type: 'string' } } },
938
+ else: { properties: { when_false: { type: 'number' } } },
939
+ };
940
+ const rows = schemaToTableData(schema);
941
+ const conditional = rows.find((r) => r.type === 'conditional');
942
+ expect(conditional.branches).toHaveLength(2);
943
+ expect(conditional.branches[0].title).toBe('Then');
944
+ expect(conditional.branches[1].title).toBe('Else');
945
+ });
946
+
947
+ // schemaToTableData with no properties and no type — returns empty
948
+ it('returns empty array for empty schema object', () => {
949
+ const rows = schemaToTableData({});
950
+ expect(rows).toHaveLength(0);
951
+ });
952
+
953
+ // additionalProperties on an object without .properties (type=object, no .properties)
954
+ it('renders additionalProperties even when there are no declared properties', () => {
955
+ const schema = {
956
+ properties: {
957
+ free_form: {
958
+ type: 'object',
959
+ additionalProperties: { type: 'string' },
960
+ // no .properties keyword
961
+ },
962
+ },
963
+ };
964
+ const rows = schemaToTableData(schema);
965
+ const freeFormRow = rows.find((r) => r.name === 'free_form');
966
+ expect(freeFormRow.containerType).toBe('object');
967
+ const addlRow = rows.find((r) => r.name === 'additionalProperties');
968
+ expect(addlRow).toBeDefined();
969
+ });
970
+ });