docusaurus-plugin-generate-schema-docs 1.8.4 → 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 (39) hide show
  1. package/README.md +10 -0
  2. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof-multi.json +12 -0
  3. package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof.json +30 -0
  4. package/__tests__/__fixtures__/validateSchemas/schema-with-not-edge-cases.json +24 -0
  5. package/__tests__/__fixtures__/validateSchemas/schema-with-not-non-object.json +15 -0
  6. package/__tests__/generateEventDocs.anchor.test.js +1 -1
  7. package/__tests__/generateEventDocs.nested.test.js +1 -1
  8. package/__tests__/generateEventDocs.partials.test.js +1 -1
  9. package/__tests__/generateEventDocs.test.js +506 -1
  10. package/__tests__/generateEventDocs.versioned.test.js +1 -1
  11. package/__tests__/helpers/buildExampleFromSchema.test.js +240 -0
  12. package/__tests__/helpers/constraintSchemaPaths.test.js +208 -0
  13. package/__tests__/helpers/continuingLinesStyle.test.js +492 -0
  14. package/__tests__/helpers/exampleModel.test.js +209 -0
  15. package/__tests__/helpers/file-system.test.js +73 -1
  16. package/__tests__/helpers/getConstraints.test.js +27 -0
  17. package/__tests__/helpers/mergeSchema.test.js +94 -0
  18. package/__tests__/helpers/processSchema.test.js +291 -1
  19. package/__tests__/helpers/schema-doc-template.test.js +54 -0
  20. package/__tests__/helpers/schema-processing.test.js +122 -2
  21. package/__tests__/helpers/schemaToExamples.test.js +1007 -0
  22. package/__tests__/helpers/schemaToTableData.mutations.test.js +970 -0
  23. package/__tests__/helpers/schemaToTableData.test.js +157 -0
  24. package/__tests__/helpers/snippetTargets.test.js +432 -0
  25. package/__tests__/helpers/trackingTargets.test.js +319 -0
  26. package/__tests__/helpers/validator.test.js +385 -1
  27. package/__tests__/index.test.js +436 -0
  28. package/__tests__/syncGtm.test.js +139 -3
  29. package/__tests__/update-schema-ids.test.js +70 -1
  30. package/__tests__/validateSchemas-integration.test.js +2 -2
  31. package/__tests__/validateSchemas.test.js +142 -1
  32. package/generateEventDocs.js +21 -1
  33. package/helpers/constraintSchemaPaths.js +10 -14
  34. package/helpers/schemaToTableData.js +538 -492
  35. package/helpers/trackingTargets.js +26 -3
  36. package/helpers/validator.js +18 -4
  37. package/index.js +1 -2
  38. package/package.json +1 -1
  39. package/scripts/sync-gtm.js +25 -7
@@ -56,6 +56,1013 @@ describe('schemaToExamples', () => {
56
56
  expect(userIdIntegerOption.example).toHaveProperty('payment_method');
57
57
  });
58
58
 
59
+ // ── L23: findConditionalPoints only collects subschemas that have if AND (then OR else) ──
60
+ describe('findConditionalPoints filtering (L23)', () => {
61
+ it('does NOT generate a conditional group when schema has only if (no then/else)', () => {
62
+ const schema = {
63
+ type: 'object',
64
+ properties: {
65
+ status: { type: 'string', examples: ['active'] },
66
+ },
67
+ // `if` is present but no `then` or `else` — must NOT be treated as a conditional point
68
+ if: { properties: { status: { const: 'active' } } },
69
+ };
70
+ const groups = schemaToExamples(schema);
71
+ const conditionalGroup = groups.find((g) => g.property === 'conditional');
72
+ // No conditional group should be emitted
73
+ expect(conditionalGroup).toBeUndefined();
74
+ // The schema has no choice points either, so we should get a plain default example
75
+ expect(groups).toHaveLength(1);
76
+ expect(groups[0].property).toBe('default');
77
+ });
78
+
79
+ it('generates a conditional group when schema has if + else but no then', () => {
80
+ const schema = {
81
+ type: 'object',
82
+ properties: {
83
+ country: { type: 'string', examples: ['CA'] },
84
+ },
85
+ if: { properties: { country: { const: 'US' } } },
86
+ else: {
87
+ properties: {
88
+ postal_code: { type: 'string', examples: ['K1A 0B1'] },
89
+ },
90
+ },
91
+ };
92
+ const groups = schemaToExamples(schema);
93
+ const conditionalGroup = groups.find((g) => g.property === 'conditional');
94
+ expect(conditionalGroup).toBeDefined();
95
+ // Only the else branch option should exist
96
+ expect(conditionalGroup.options).toHaveLength(1);
97
+ expect(conditionalGroup.options[0].title).toBe(
98
+ 'When condition is not met',
99
+ );
100
+ });
101
+ });
102
+
103
+ // ── L34/L37/L38: root-level choice schema (path.length === 0) ──
104
+ describe('root-level oneOf choice (L34/L37/L38)', () => {
105
+ it('generates examples for a root-level oneOf schema', () => {
106
+ const schema = {
107
+ title: 'Root Choice',
108
+ oneOf: [
109
+ {
110
+ title: 'Option A',
111
+ type: 'object',
112
+ properties: { a: { type: 'string', examples: ['hello'] } },
113
+ },
114
+ {
115
+ title: 'Option B',
116
+ type: 'object',
117
+ properties: { b: { type: 'number', examples: [42] } },
118
+ },
119
+ ],
120
+ };
121
+ const groups = schemaToExamples(schema);
122
+ expect(groups).toHaveLength(1);
123
+ expect(groups[0].property).toBe('root');
124
+ expect(groups[0].options).toHaveLength(2);
125
+ const optA = groups[0].options.find((o) => o.title === 'Option A');
126
+ const optB = groups[0].options.find((o) => o.title === 'Option B');
127
+ expect(optA).toBeDefined();
128
+ expect(optB).toBeDefined();
129
+ // Each option should produce a different example shape
130
+ expect(optA.example).toHaveProperty('a');
131
+ expect(optB.example).toHaveProperty('b');
132
+ });
133
+
134
+ it('generates examples for a root-level anyOf schema', () => {
135
+ const schema = {
136
+ anyOf: [
137
+ { title: 'Numeric', type: 'number', examples: [1] },
138
+ { title: 'Text', type: 'string', examples: ['text'] },
139
+ ],
140
+ };
141
+ const groups = schemaToExamples(schema);
142
+ expect(groups).toHaveLength(1);
143
+ expect(groups[0].property).toBe('root');
144
+ expect(groups[0].options).toHaveLength(2);
145
+ });
146
+ });
147
+
148
+ // ── L57: default parameter for baseRequired ──
149
+ describe('pruneSiblingConditionalProperties baseRequired default (L57)', () => {
150
+ it('prunes inactive branch properties when baseRequired is omitted (default [])', () => {
151
+ // Conditional where both branches only have "required" (no properties of their own)
152
+ // The inactive branch requires a property that the base schema has;
153
+ // since it is not in activeRequired or baseRequired, it should be pruned.
154
+ const schema = {
155
+ type: 'object',
156
+ properties: {
157
+ event: { type: 'string', examples: ['purchase'] },
158
+ coupon_code: { type: 'string', examples: ['SAVE10'] },
159
+ },
160
+ required: ['event'],
161
+ if: { properties: { event: { const: 'purchase' } } },
162
+ then: {
163
+ // active branch — no properties, only required adjustment
164
+ required: ['event'],
165
+ },
166
+ else: {
167
+ // inactive branch — requires coupon_code (not in activeRequired)
168
+ required: ['coupon_code'],
169
+ },
170
+ };
171
+ const groups = schemaToExamples(schema);
172
+ const thenOption = groups
173
+ .find((g) => g.property === 'conditional')
174
+ ?.options.find((o) => o.title === 'When condition is met');
175
+ expect(thenOption).toBeDefined();
176
+ // coupon_code should have been pruned from the then example
177
+ expect(thenOption.example).not.toHaveProperty('coupon_code');
178
+ });
179
+ });
180
+
181
+ // ── L60: activeBranchSchema has no properties (branchesOnlyAdjustRequired) ──
182
+ describe('pruneSiblingConditionalProperties — activeBranchSchema has no properties (L60)', () => {
183
+ it('prunes inactive-branch-required property when active branch has no properties', () => {
184
+ const schema = {
185
+ type: 'object',
186
+ properties: {
187
+ event: { type: 'string', examples: ['click'] },
188
+ extra: { type: 'string', examples: ['value'] },
189
+ },
190
+ required: ['event'],
191
+ if: { properties: { event: { const: 'click' } } },
192
+ // active branch: no properties key — only adjusts required
193
+ then: { required: ['event'] },
194
+ // inactive branch: no properties key — requires extra
195
+ else: { required: ['extra'] },
196
+ };
197
+ const groups = schemaToExamples(schema);
198
+ const thenGroup = groups
199
+ .find((g) => g.property === 'conditional')
200
+ ?.options.find((o) => o.title === 'When condition is met');
201
+ expect(thenGroup).toBeDefined();
202
+ // 'extra' is in the inactive (else) required list but not in active (then) required
203
+ expect(thenGroup.example).not.toHaveProperty('extra');
204
+ });
205
+
206
+ it('does NOT prune when activeBranchSchema has properties (branchesOnlyAdjustRequired is false)', () => {
207
+ const schema = {
208
+ type: 'object',
209
+ properties: {
210
+ event: { type: 'string', examples: ['click'] },
211
+ },
212
+ required: ['event'],
213
+ if: { properties: { event: { const: 'click' } } },
214
+ // active branch: HAS properties — pruning should be skipped
215
+ then: {
216
+ properties: {
217
+ click_target: { type: 'string', examples: ['button'] },
218
+ },
219
+ required: ['click_target'],
220
+ },
221
+ else: {
222
+ properties: {
223
+ page_url: { type: 'string', examples: ['https://example.com'] },
224
+ },
225
+ required: ['page_url'],
226
+ },
227
+ };
228
+ const groups = schemaToExamples(schema);
229
+ const thenGroup = groups
230
+ .find((g) => g.property === 'conditional')
231
+ ?.options.find((o) => o.title === 'When condition is met');
232
+ expect(thenGroup).toBeDefined();
233
+ expect(thenGroup.example).toHaveProperty('click_target', 'button');
234
+ });
235
+ });
236
+
237
+ // ── L70: activeBranchSchema?.required fallback to [] ──
238
+ describe('pruneSiblingConditionalProperties — activeBranchSchema has no required (L70)', () => {
239
+ it('prunes inactive-required property even when active branch has no required array', () => {
240
+ const schema = {
241
+ type: 'object',
242
+ properties: {
243
+ event: { type: 'string', examples: ['view'] },
244
+ promo: { type: 'string', examples: ['PROMO1'] },
245
+ },
246
+ required: ['event'],
247
+ if: { properties: { event: { const: 'view' } } },
248
+ // active branch: no properties, no required
249
+ then: {},
250
+ // inactive branch: no properties, requires promo
251
+ else: { required: ['promo'] },
252
+ };
253
+ const groups = schemaToExamples(schema);
254
+ const thenGroup = groups
255
+ .find((g) => g.property === 'conditional')
256
+ ?.options.find((o) => o.title === 'When condition is met');
257
+ expect(thenGroup).toBeDefined();
258
+ // promo is required by else but NOT by then — should be pruned
259
+ expect(thenGroup.example).not.toHaveProperty('promo');
260
+ });
261
+ });
262
+
263
+ // ── L74: required intersection — property protected by baseRequired ──
264
+ describe('pruneSiblingConditionalProperties — required intersection (L74)', () => {
265
+ it('does not prune a property that is protected by baseRequired even if inactive branch requires it', () => {
266
+ const schema = {
267
+ type: 'object',
268
+ properties: {
269
+ event: { type: 'string', examples: ['buy'] },
270
+ user_id: { type: 'string', examples: ['u123'] },
271
+ },
272
+ // user_id is in the BASE required — should never be pruned
273
+ required: ['event', 'user_id'],
274
+ if: { properties: { event: { const: 'buy' } } },
275
+ then: {},
276
+ else: { required: ['user_id'] },
277
+ };
278
+ const groups = schemaToExamples(schema);
279
+ const thenGroup = groups
280
+ .find((g) => g.property === 'conditional')
281
+ ?.options.find((o) => o.title === 'When condition is met');
282
+ expect(thenGroup).toBeDefined();
283
+ // user_id is protected by baseRequired — must NOT be pruned
284
+ expect(thenGroup.example).toHaveProperty('user_id');
285
+ });
286
+
287
+ it('does not prune a property that is in both active and inactive required', () => {
288
+ const schema = {
289
+ type: 'object',
290
+ properties: {
291
+ event: { type: 'string', examples: ['buy'] },
292
+ shared: { type: 'string', examples: ['shared_value'] },
293
+ },
294
+ required: ['event'],
295
+ if: { properties: { event: { const: 'buy' } } },
296
+ // shared is required by BOTH branches — should NOT be pruned
297
+ then: { required: ['shared'] },
298
+ else: { required: ['shared'] },
299
+ };
300
+ const groups = schemaToExamples(schema);
301
+ const thenGroup = groups
302
+ .find((g) => g.property === 'conditional')
303
+ ?.options.find((o) => o.title === 'When condition is met');
304
+ expect(thenGroup).toBeDefined();
305
+ expect(thenGroup.example).toHaveProperty('shared');
306
+ });
307
+ });
308
+
309
+ // ── L79-81: pruneSiblingConditionalProperties filter chain — required array on mergedSchema ──
310
+ describe('pruneSiblingConditionalProperties — mergedSchema.required filter (L79-81)', () => {
311
+ it('removes the pruned property name from mergedSchema.required', () => {
312
+ // After pruning, mergedSchema.required should no longer contain the pruned name.
313
+ // scroll_depth must NOT be in baseRequired so it can be pruned.
314
+ const schema = {
315
+ type: 'object',
316
+ properties: {
317
+ event: { type: 'string', examples: ['scroll'] },
318
+ scroll_depth: { type: 'number', examples: [50] },
319
+ },
320
+ // Only event is in base required; scroll_depth is NOT protected by baseRequired
321
+ required: ['event'],
322
+ if: { properties: { event: { const: 'scroll' } } },
323
+ then: { required: ['event'] },
324
+ else: { required: ['scroll_depth'] },
325
+ };
326
+ const groups = schemaToExamples(schema);
327
+ const thenGroup = groups
328
+ .find((g) => g.property === 'conditional')
329
+ ?.options.find((o) => o.title === 'When condition is met');
330
+ expect(thenGroup).toBeDefined();
331
+ // scroll_depth was required by else but not by then, not in base — pruned
332
+ expect(thenGroup.example).not.toHaveProperty('scroll_depth');
333
+ });
334
+
335
+ it('does not throw when mergedSchema.required is not an array', () => {
336
+ // required is absent on the merged schema — the filter path is skipped
337
+ const schema = {
338
+ type: 'object',
339
+ properties: {
340
+ event: { type: 'string', examples: ['tap'] },
341
+ detail: { type: 'string', examples: ['some_detail'] },
342
+ },
343
+ // NO top-level required
344
+ if: { properties: { event: { const: 'tap' } } },
345
+ then: {},
346
+ else: { required: ['detail'] },
347
+ };
348
+ const groups = schemaToExamples(schema);
349
+ const thenGroup = groups
350
+ .find((g) => g.property === 'conditional')
351
+ ?.options.find((o) => o.title === 'When condition is met');
352
+ expect(thenGroup).toBeDefined();
353
+ expect(thenGroup.example).not.toHaveProperty('detail');
354
+ });
355
+ });
356
+
357
+ // ── L93/L96: generateConditionalExample — path.length === 0 branch, required fallback ──
358
+ describe('generateConditionalExample path.length === 0 (L93/L96)', () => {
359
+ it('handles a root-level conditional where baseSchema has no required array', () => {
360
+ const schema = {
361
+ type: 'object',
362
+ properties: {
363
+ mode: { type: 'string', examples: ['fast'] },
364
+ },
365
+ // no required at root
366
+ if: { properties: { mode: { const: 'fast' } } },
367
+ then: { properties: { turbo: { type: 'boolean', examples: [true] } } },
368
+ else: {
369
+ properties: { slow_mode: { type: 'boolean', examples: [false] } },
370
+ },
371
+ };
372
+ const groups = schemaToExamples(schema);
373
+ const conditional = groups.find((g) => g.property === 'conditional');
374
+ expect(conditional).toBeDefined();
375
+ const thenOpt = conditional.options.find(
376
+ (o) => o.title === 'When condition is met',
377
+ );
378
+ expect(thenOpt.example).toHaveProperty('turbo', true);
379
+ });
380
+
381
+ it('handles a root-level conditional where schemaVariant has required array', () => {
382
+ const schema = {
383
+ type: 'object',
384
+ properties: {
385
+ mode: { type: 'string', examples: ['fast'] },
386
+ },
387
+ required: ['mode'],
388
+ if: { properties: { mode: { const: 'fast' } } },
389
+ then: { properties: { turbo: { type: 'boolean', examples: [true] } } },
390
+ };
391
+ const groups = schemaToExamples(schema);
392
+ const conditional = groups.find((g) => g.property === 'conditional');
393
+ expect(conditional).toBeDefined();
394
+ expect(conditional.options[0].example).toHaveProperty('mode');
395
+ });
396
+ });
397
+
398
+ // ── L100/L122: branchSchema falsy — fallback path ──
399
+ describe('generateConditionalExample — missing branch schema (L100/L122)', () => {
400
+ it('returns base example when then branch is undefined at root level', () => {
401
+ // Manually trigger: else exists but then does not — when we call branch='then' it is falsy
402
+ // We achieve this via schemaToExamples by having only else
403
+ const schema = {
404
+ type: 'object',
405
+ properties: {
406
+ flag: { type: 'string', examples: ['off'] },
407
+ },
408
+ if: { properties: { flag: { const: 'on' } } },
409
+ else: {
410
+ properties: { detail: { type: 'string', examples: ['fallback'] } },
411
+ },
412
+ };
413
+ const groups = schemaToExamples(schema);
414
+ const conditional = groups.find((g) => g.property === 'conditional');
415
+ expect(conditional).toBeDefined();
416
+ expect(conditional.options).toHaveLength(1);
417
+ expect(conditional.options[0].title).toBe('When condition is not met');
418
+ expect(conditional.options[0].example).toHaveProperty('detail');
419
+ });
420
+ });
421
+
422
+ // ── L118/L122: nested conditional — target.required fallback (path.length > 0) ──
423
+ describe('generateConditionalExample nested — target.required fallback (L118/L122)', () => {
424
+ it('handles nested conditional where target has no required array', () => {
425
+ const schema = {
426
+ type: 'object',
427
+ properties: {
428
+ shipping: {
429
+ type: 'object',
430
+ properties: {
431
+ method: { type: 'string', examples: ['express'] },
432
+ },
433
+ // no required on the nested object
434
+ if: { properties: { method: { const: 'express' } } },
435
+ then: {
436
+ properties: {
437
+ priority_level: { type: 'string', examples: ['high'] },
438
+ },
439
+ },
440
+ else: {
441
+ properties: {
442
+ estimated_days: { type: 'number', examples: [5] },
443
+ },
444
+ },
445
+ },
446
+ },
447
+ };
448
+ const groups = schemaToExamples(schema);
449
+ const conditional = groups.find((g) => g.property === 'conditional');
450
+ expect(conditional).toBeDefined();
451
+ const thenOpt = conditional.options.find(
452
+ (o) => o.title === 'When condition is met',
453
+ );
454
+ expect(thenOpt.example.shipping).toHaveProperty('priority_level', 'high');
455
+ });
456
+
457
+ it('handles nested conditional where target has a required array', () => {
458
+ const schema = {
459
+ type: 'object',
460
+ properties: {
461
+ shipping: {
462
+ type: 'object',
463
+ properties: {
464
+ method: { type: 'string', examples: ['express'] },
465
+ },
466
+ required: ['method'],
467
+ if: { properties: { method: { const: 'express' } } },
468
+ then: {
469
+ properties: {
470
+ priority_level: { type: 'string', examples: ['high'] },
471
+ },
472
+ },
473
+ },
474
+ },
475
+ };
476
+ const groups = schemaToExamples(schema);
477
+ const conditional = groups.find((g) => g.property === 'conditional');
478
+ expect(conditional).toBeDefined();
479
+ const thenOpt = conditional.options.find(
480
+ (o) => o.title === 'When condition is met',
481
+ );
482
+ expect(thenOpt.example.shipping).toHaveProperty('method');
483
+ });
484
+ });
485
+
486
+ // ── L129: ArrowFunction — Object.keys(target).forEach deletion in nested path ──
487
+ describe('generateConditionalExample nested — target key deletion (L129)', () => {
488
+ it('merges branch schema into target for nested conditional with branchSchema present', () => {
489
+ const schema = {
490
+ type: 'object',
491
+ properties: {
492
+ delivery: {
493
+ type: 'object',
494
+ properties: {
495
+ speed: { type: 'string', examples: ['overnight'] },
496
+ cost: { type: 'number', examples: [9.99] },
497
+ },
498
+ if: { properties: { speed: { const: 'overnight' } } },
499
+ then: {
500
+ properties: {
501
+ surcharge: { type: 'number', examples: [5.0] },
502
+ },
503
+ },
504
+ },
505
+ },
506
+ };
507
+ const groups = schemaToExamples(schema);
508
+ const conditional = groups.find((g) => g.property === 'conditional');
509
+ const thenOpt = conditional?.options.find(
510
+ (o) => o.title === 'When condition is met',
511
+ );
512
+ expect(thenOpt).toBeDefined();
513
+ // The merged branch should include surcharge
514
+ expect(thenOpt.example.delivery).toHaveProperty('surcharge', 5.0);
515
+ // The original delivery properties should still be present
516
+ expect(thenOpt.example.delivery).toHaveProperty('speed');
517
+ });
518
+ });
519
+
520
+ // ── L143/L144/L145/L147/L149/L158/L161/L171: schemaToExamples simple branch (no choice/conditional) ──
521
+ describe('schemaToExamples — simple schema (no choice/conditional points) (L143-L171)', () => {
522
+ it('returns a default example group for a simple object schema', () => {
523
+ const schema = {
524
+ type: 'object',
525
+ properties: {
526
+ name: { type: 'string', examples: ['Alice'] },
527
+ },
528
+ };
529
+ const groups = schemaToExamples(schema);
530
+ expect(groups).toHaveLength(1);
531
+ expect(groups[0].property).toBe('default');
532
+ expect(groups[0].options).toHaveLength(1);
533
+ expect(groups[0].options[0].title).toBe('Example');
534
+ expect(groups[0].options[0].example).toHaveProperty('name', 'Alice');
535
+ });
536
+
537
+ it('returns empty array when buildExampleFromSchema returns undefined', () => {
538
+ // A schema that produces no example (empty type array or similar)
539
+ const schema = { not: {} };
540
+ const groups = schemaToExamples(schema);
541
+ // buildExampleFromSchema returns undefined for `not` schemas
542
+ expect(groups).toEqual([]);
543
+ });
544
+
545
+ it('returns empty array when buildExampleFromSchema returns an empty object (L144/L145)', () => {
546
+ // An object schema with no properties produces {} — should be filtered out
547
+ const schema = { type: 'object' };
548
+ const groups = schemaToExamples(schema);
549
+ expect(groups).toEqual([]);
550
+ });
551
+
552
+ it('includes null example (L144 — null is not filtered out)', () => {
553
+ // A schema with a top-level examples array that resolves to null
554
+ // null !== undefined, typeof null === 'object', null === null → shouldIncludeObjectExample = true
555
+ const schema = {
556
+ type: 'object',
557
+ examples: [null],
558
+ };
559
+ const groups = schemaToExamples(schema);
560
+ // buildExampleFromSchema picks the first entry from examples: [null] → null
561
+ // null passes the shouldIncludeObjectExample check (example === null branch)
562
+ expect(groups).toHaveLength(1);
563
+ expect(groups[0].options[0].example).toBeNull();
564
+ });
565
+
566
+ it('includes a primitive (string) example without empty-object filtering (L143/L147)', () => {
567
+ const schema = { type: 'string', examples: ['hello'] };
568
+ const groups = schemaToExamples(schema);
569
+ expect(groups).toHaveLength(1);
570
+ expect(groups[0].options[0].example).toBe('hello');
571
+ });
572
+
573
+ it('includes a non-empty object example (L145/L149)', () => {
574
+ const schema = {
575
+ type: 'object',
576
+ properties: {
577
+ id: { type: 'integer', examples: [1] },
578
+ },
579
+ };
580
+ const groups = schemaToExamples(schema);
581
+ expect(groups).toHaveLength(1);
582
+ expect(groups[0].options[0].example).toHaveProperty('id', 1);
583
+ });
584
+ });
585
+
586
+ // ── L158: path.length > 0 gives last path segment as propertyName ──
587
+ describe('choiceExamples — propertyName from path (L158)', () => {
588
+ it('uses "root" as property name when path is empty (root-level oneOf)', () => {
589
+ const schema = {
590
+ oneOf: [
591
+ { title: 'A', type: 'string', examples: ['a'] },
592
+ { title: 'B', type: 'string', examples: ['b'] },
593
+ ],
594
+ };
595
+ const groups = schemaToExamples(schema);
596
+ expect(groups[0].property).toBe('root');
597
+ });
598
+
599
+ it('uses last path segment as property name for nested oneOf', () => {
600
+ const schema = {
601
+ type: 'object',
602
+ properties: {
603
+ user_id: {
604
+ oneOf: [
605
+ { title: 'String ID', type: 'string', examples: ['u1'] },
606
+ { title: 'Numeric ID', type: 'integer', examples: [1] },
607
+ ],
608
+ },
609
+ },
610
+ };
611
+ const groups = schemaToExamples(schema);
612
+ // path = ['properties', 'user_id'] — last segment is 'user_id'
613
+ const group = groups.find((g) => g.property === 'user_id');
614
+ expect(group).toBeDefined();
615
+ expect(group.options).toHaveLength(2);
616
+ });
617
+ });
618
+
619
+ // ── L161: NoCoverage — options with no title fall back to 'Option' ──
620
+ describe('choiceExamples — option title fallback (L161)', () => {
621
+ it('uses "Option" as title when option has no title', () => {
622
+ const schema = {
623
+ type: 'object',
624
+ properties: {
625
+ value: {
626
+ oneOf: [
627
+ { type: 'string', examples: ['hello'] },
628
+ { type: 'number', examples: [42] },
629
+ ],
630
+ },
631
+ },
632
+ };
633
+ const groups = schemaToExamples(schema);
634
+ const group = groups.find((g) => g.property === 'value');
635
+ expect(group).toBeDefined();
636
+ group.options.forEach((opt) => {
637
+ expect(opt.title).toBe('Option');
638
+ });
639
+ });
640
+ });
641
+
642
+ // ── L171: conditionalExamples property is always 'conditional' ──
643
+ describe('conditionalExamples — property name (L171)', () => {
644
+ it('always uses "conditional" as property name for conditional groups', () => {
645
+ const schema = {
646
+ type: 'object',
647
+ properties: {
648
+ flag: { type: 'string', examples: ['yes'] },
649
+ },
650
+ if: { properties: { flag: { const: 'yes' } } },
651
+ then: {
652
+ properties: { bonus: { type: 'string', examples: ['gift'] } },
653
+ },
654
+ else: {
655
+ properties: { penalty: { type: 'string', examples: ['fee'] } },
656
+ },
657
+ };
658
+ const groups = schemaToExamples(schema);
659
+ const conditionalGroup = groups.find((g) => g.property === 'conditional');
660
+ expect(conditionalGroup).toBeDefined();
661
+ expect(conditionalGroup.property).toBe('conditional');
662
+ });
663
+ });
664
+
665
+ // ── Mutant killers: targeted tests for surviving Stryker mutants ──
666
+
667
+ describe('L34: root-level choice path.length === 0 must not be mutated to false', () => {
668
+ it('root-level oneOf deletes oneOf key and merges — result differs from nested path handling', () => {
669
+ const schema = {
670
+ type: 'object',
671
+ properties: { base: { type: 'string', examples: ['val'] } },
672
+ oneOf: [
673
+ {
674
+ title: 'WithExtra',
675
+ properties: { extra: { type: 'string', examples: ['e'] } },
676
+ },
677
+ ],
678
+ };
679
+ const groups = schemaToExamples(schema);
680
+ expect(groups).toHaveLength(1);
681
+ expect(groups[0].property).toBe('root');
682
+ // The merged example must contain BOTH base and extra
683
+ expect(groups[0].options[0].example).toHaveProperty('base', 'val');
684
+ expect(groups[0].options[0].example).toHaveProperty('extra', 'e');
685
+ });
686
+ });
687
+
688
+ describe('L60: branchesOnlyAdjustRequired — && vs || and optional chaining', () => {
689
+ it('does NOT prune when only inactiveBranchSchema has properties (one has, one does not)', () => {
690
+ // activeBranch has NO properties, inactiveBranch HAS properties
691
+ // branchesOnlyAdjustRequired = !undefined && !{...} = true && false = false
692
+ // With && -> false => skip pruning (correct)
693
+ // With || -> true || false = true => would incorrectly prune
694
+ const schema = {
695
+ type: 'object',
696
+ properties: {
697
+ event: { type: 'string', examples: ['test'] },
698
+ removable: { type: 'string', examples: ['keep_me'] },
699
+ },
700
+ required: ['event'],
701
+ if: { properties: { event: { const: 'test' } } },
702
+ then: { required: ['event'] },
703
+ else: {
704
+ properties: {
705
+ other: { type: 'string', examples: ['other_val'] },
706
+ },
707
+ required: ['removable'],
708
+ },
709
+ };
710
+ const groups = schemaToExamples(schema);
711
+ const thenOpt = groups
712
+ .find((g) => g.property === 'conditional')
713
+ ?.options.find((o) => o.title === 'When condition is met');
714
+ expect(thenOpt).toBeDefined();
715
+ // Since else has properties, branchesOnlyAdjustRequired is false,
716
+ // so pruning is skipped and removable should remain
717
+ expect(thenOpt.example).toHaveProperty('removable', 'keep_me');
718
+ });
719
+
720
+ it('does NOT prune when only activeBranchSchema has properties', () => {
721
+ const schema = {
722
+ type: 'object',
723
+ properties: {
724
+ event: { type: 'string', examples: ['test'] },
725
+ removable: { type: 'string', examples: ['keep_me'] },
726
+ },
727
+ required: ['event'],
728
+ if: { properties: { event: { const: 'test' } } },
729
+ then: {
730
+ properties: {
731
+ added: { type: 'string', examples: ['new_val'] },
732
+ },
733
+ required: ['event'],
734
+ },
735
+ else: { required: ['removable'] },
736
+ };
737
+ const groups = schemaToExamples(schema);
738
+ const thenOpt = groups
739
+ .find((g) => g.property === 'conditional')
740
+ ?.options.find((o) => o.title === 'When condition is met');
741
+ expect(thenOpt).toBeDefined();
742
+ // Since then has properties, branchesOnlyAdjustRequired is false,
743
+ // so pruning is skipped and removable should remain
744
+ expect(thenOpt.example).toHaveProperty('removable', 'keep_me');
745
+ });
746
+ });
747
+
748
+ describe('L79-83: mergedSchema.required array filtering', () => {
749
+ it('actually removes pruned property from mergedSchema.required array', () => {
750
+ // Need a scenario where:
751
+ // 1. mergedSchema has a required array containing the pruned property name
752
+ // 2. After pruning, that name should be gone from required
753
+ // We test this indirectly through the final example output
754
+ const schema = {
755
+ type: 'object',
756
+ properties: {
757
+ event: { type: 'string', examples: ['action'] },
758
+ temp_field: { type: 'string', examples: ['temp'] },
759
+ keep_field: { type: 'string', examples: ['keep'] },
760
+ },
761
+ required: ['event', 'keep_field'],
762
+ if: { properties: { event: { const: 'action' } } },
763
+ then: { required: ['event', 'keep_field'] },
764
+ else: { required: ['temp_field'] },
765
+ };
766
+ const groups = schemaToExamples(schema);
767
+ const thenOpt = groups
768
+ .find((g) => g.property === 'conditional')
769
+ ?.options.find((o) => o.title === 'When condition is met');
770
+ expect(thenOpt).toBeDefined();
771
+ // temp_field should be pruned (not in active required, not in base required)
772
+ expect(thenOpt.example).not.toHaveProperty('temp_field');
773
+ // keep_field is in base required — must stay
774
+ expect(thenOpt.example).toHaveProperty('keep_field', 'keep');
775
+ // event is in both — must stay
776
+ expect(thenOpt.example).toHaveProperty('event', 'action');
777
+ });
778
+
779
+ it('filter correctly excludes only the matching name from required (L81 !== vs ===)', () => {
780
+ // If the EqualityOperator is flipped (=== instead of !==), the filter would
781
+ // KEEP only the pruned name instead of removing it
782
+ const schema = {
783
+ type: 'object',
784
+ properties: {
785
+ event: { type: 'string', examples: ['go'] },
786
+ alpha: { type: 'string', examples: ['a'] },
787
+ beta: { type: 'string', examples: ['b'] },
788
+ },
789
+ required: ['event', 'alpha'],
790
+ if: { properties: { event: { const: 'go' } } },
791
+ then: { required: ['event'] },
792
+ else: { required: ['alpha', 'beta'] },
793
+ };
794
+ const groups = schemaToExamples(schema);
795
+ const thenOpt = groups
796
+ .find((g) => g.property === 'conditional')
797
+ ?.options.find((o) => o.title === 'When condition is met');
798
+ expect(thenOpt).toBeDefined();
799
+ // alpha is in base required — protected, must stay
800
+ expect(thenOpt.example).toHaveProperty('alpha', 'a');
801
+ // beta is NOT in active or base required — must be pruned
802
+ expect(thenOpt.example).not.toHaveProperty('beta');
803
+ // event is in active required — must stay
804
+ expect(thenOpt.example).toHaveProperty('event', 'go');
805
+ });
806
+ });
807
+
808
+ describe('L93: root conditional path.length === 0 block must execute', () => {
809
+ it('root conditional correctly deletes if/then/else and merges branch at root level', () => {
810
+ // If L93 block is emptied (BlockStatement -> {}), the root conditional
811
+ // would fall through to the nested path handling which would fail
812
+ const schema = {
813
+ type: 'object',
814
+ properties: {
815
+ color: { type: 'string', examples: ['red'] },
816
+ },
817
+ required: ['color'],
818
+ if: { properties: { color: { const: 'red' } } },
819
+ then: {
820
+ properties: { shade: { type: 'string', examples: ['crimson'] } },
821
+ },
822
+ else: {
823
+ properties: { shade: { type: 'string', examples: ['navy'] } },
824
+ },
825
+ };
826
+ const groups = schemaToExamples(schema);
827
+ const conditional = groups.find((g) => g.property === 'conditional');
828
+ expect(conditional).toBeDefined();
829
+
830
+ const thenOpt = conditional.options.find(
831
+ (o) => o.title === 'When condition is met',
832
+ );
833
+ expect(thenOpt).toBeDefined();
834
+ expect(thenOpt.example).toHaveProperty('color', 'red');
835
+ expect(thenOpt.example).toHaveProperty('shade', 'crimson');
836
+
837
+ const elseOpt = conditional.options.find(
838
+ (o) => o.title === 'When condition is not met',
839
+ );
840
+ expect(elseOpt).toBeDefined();
841
+ expect(elseOpt.example).toHaveProperty('color', 'red');
842
+ expect(elseOpt.example).toHaveProperty('shade', 'navy');
843
+ });
844
+ });
845
+
846
+ describe('L96: baseRequired fallback to [] when schema has no required (root conditional)', () => {
847
+ it('uses empty array as baseRequired fallback, allowing pruning of inactive-only properties', () => {
848
+ // If [] is mutated to ["Stryker was here"], then "Stryker was here" would
849
+ // be in protectedRequired and if any inactive required matches it wouldn't prune
850
+ // With the correct [] fallback, nothing is protected by baseRequired
851
+ const schema = {
852
+ type: 'object',
853
+ properties: {
854
+ event: { type: 'string', examples: ['click'] },
855
+ removable: { type: 'string', examples: ['bye'] },
856
+ },
857
+ // NO required array on schema root
858
+ if: { properties: { event: { const: 'click' } } },
859
+ then: {},
860
+ else: { required: ['removable'] },
861
+ };
862
+ const groups = schemaToExamples(schema);
863
+ const thenOpt = groups
864
+ .find((g) => g.property === 'conditional')
865
+ ?.options.find((o) => o.title === 'When condition is met');
866
+ expect(thenOpt).toBeDefined();
867
+ // removable should be pruned since it's only in else required and nothing protects it
868
+ expect(thenOpt.example).not.toHaveProperty('removable');
869
+ });
870
+ });
871
+
872
+ describe('L100: branchSchema truthiness check at root level', () => {
873
+ it('when branch is then and schema has then, produces merged example (not bare schema)', () => {
874
+ // If L100 is mutated to `true`, even a falsy branchSchema would enter the merge path
875
+ // and crash. We verify the correct path by checking the result is properly merged.
876
+ const schema = {
877
+ type: 'object',
878
+ properties: {
879
+ status: { type: 'string', examples: ['active'] },
880
+ },
881
+ if: { properties: { status: { const: 'active' } } },
882
+ then: {
883
+ properties: { detail: { type: 'string', examples: ['info'] } },
884
+ },
885
+ };
886
+ const groups = schemaToExamples(schema);
887
+ const conditional = groups.find((g) => g.property === 'conditional');
888
+ expect(conditional).toBeDefined();
889
+ // Only then branch, no else
890
+ expect(conditional.options).toHaveLength(1);
891
+ expect(conditional.options[0].example).toHaveProperty('detail', 'info');
892
+ expect(conditional.options[0].example).toHaveProperty('status', 'active');
893
+ });
894
+ });
895
+
896
+ describe('L118: nested conditional target.required fallback and branchSchema check', () => {
897
+ it('nested conditional with target.required present uses it as baseRequired', () => {
898
+ // If target.required || [] is mutated to target.required && [],
899
+ // when target.required exists, && would return [] instead of the actual array
900
+ const schema = {
901
+ type: 'object',
902
+ properties: {
903
+ wrapper: {
904
+ type: 'object',
905
+ properties: {
906
+ mode: { type: 'string', examples: ['fast'] },
907
+ protected_prop: { type: 'string', examples: ['safe'] },
908
+ removable_prop: { type: 'string', examples: ['gone'] },
909
+ },
910
+ required: ['mode', 'protected_prop'],
911
+ if: { properties: { mode: { const: 'fast' } } },
912
+ then: { required: ['mode'] },
913
+ else: { required: ['protected_prop', 'removable_prop'] },
914
+ },
915
+ },
916
+ };
917
+ const groups = schemaToExamples(schema);
918
+ const thenOpt = groups
919
+ .find((g) => g.property === 'conditional')
920
+ ?.options.find((o) => o.title === 'When condition is met');
921
+ expect(thenOpt).toBeDefined();
922
+ // protected_prop is in baseRequired (['mode', 'protected_prop']) — must NOT be pruned
923
+ expect(thenOpt.example.wrapper).toHaveProperty('protected_prop', 'safe');
924
+ // removable_prop is NOT in active required or base required — must be pruned
925
+ expect(thenOpt.example.wrapper).not.toHaveProperty('removable_prop');
926
+ });
927
+
928
+ it('nested conditional without branchSchema skips merge and returns base example', () => {
929
+ // If L122 is mutated to `true`, it would try to merge undefined branchSchema
930
+ const schema = {
931
+ type: 'object',
932
+ properties: {
933
+ wrapper: {
934
+ type: 'object',
935
+ properties: {
936
+ mode: { type: 'string', examples: ['slow'] },
937
+ },
938
+ if: { properties: { mode: { const: 'slow' } } },
939
+ else: {
940
+ properties: {
941
+ speed: { type: 'string', examples: ['fast'] },
942
+ },
943
+ },
944
+ },
945
+ },
946
+ };
947
+ const groups = schemaToExamples(schema);
948
+ const conditional = groups.find((g) => g.property === 'conditional');
949
+ expect(conditional).toBeDefined();
950
+ // Only else branch exists, so only one option
951
+ expect(conditional.options).toHaveLength(1);
952
+ expect(conditional.options[0].title).toBe('When condition is not met');
953
+ });
954
+ });
955
+
956
+ describe('L129: Object.keys(target).forEach deletion must execute', () => {
957
+ it('replaces all target keys with merged result in nested conditional', () => {
958
+ // If the arrow function is mutated to () => undefined, target keys won't be deleted
959
+ // and the result would have stale properties from the original target
960
+ const schema = {
961
+ type: 'object',
962
+ properties: {
963
+ nested: {
964
+ type: 'object',
965
+ properties: {
966
+ action: { type: 'string', examples: ['run'] },
967
+ },
968
+ required: ['action'],
969
+ if: { properties: { action: { const: 'run' } } },
970
+ then: {
971
+ properties: {
972
+ speed: { type: 'number', examples: [100] },
973
+ },
974
+ },
975
+ else: {
976
+ properties: {
977
+ reason: { type: 'string', examples: ['tired'] },
978
+ },
979
+ },
980
+ },
981
+ },
982
+ };
983
+ const groups = schemaToExamples(schema);
984
+ const conditional = groups.find((g) => g.property === 'conditional');
985
+ const thenOpt = conditional?.options.find(
986
+ (o) => o.title === 'When condition is met',
987
+ );
988
+ expect(thenOpt).toBeDefined();
989
+ expect(thenOpt.example.nested).toHaveProperty('action', 'run');
990
+ expect(thenOpt.example.nested).toHaveProperty('speed', 100);
991
+ // The nested object should be the merged result
992
+ expect(typeof thenOpt.example.nested).toBe('object');
993
+ expect(thenOpt.example.nested).not.toBeNull();
994
+ });
995
+ });
996
+
997
+ describe('L143-145: shouldIncludeObjectExample edge cases', () => {
998
+ it('excludes empty object example (Object.keys(example).length > 0 must be strict)', () => {
999
+ // If > 0 is mutated to >= 0, empty objects would pass through
1000
+ const schema = { type: 'object' };
1001
+ const groups = schemaToExamples(schema);
1002
+ // Empty object should be excluded
1003
+ expect(groups).toEqual([]);
1004
+ expect(groups).toHaveLength(0);
1005
+ });
1006
+
1007
+ it('includes object with exactly one property (boundary for > 0)', () => {
1008
+ const schema = {
1009
+ type: 'object',
1010
+ properties: {
1011
+ solo: { type: 'string', examples: ['only'] },
1012
+ },
1013
+ };
1014
+ const groups = schemaToExamples(schema);
1015
+ expect(groups).toHaveLength(1);
1016
+ expect(groups[0].options[0].example).toEqual({ solo: 'only' });
1017
+ });
1018
+
1019
+ it('typeof check distinguishes object from string correctly (L143)', () => {
1020
+ // If "object" is mutated to "", typeof example !== "" would be true for
1021
+ // everything, and the short-circuit would skip the Object.keys check
1022
+ const schema = { type: 'string', examples: ['test_string'] };
1023
+ const groups = schemaToExamples(schema);
1024
+ expect(groups).toHaveLength(1);
1025
+ expect(groups[0].options[0].example).toBe('test_string');
1026
+ });
1027
+
1028
+ it('number example is included (typeof !== object is true)', () => {
1029
+ const schema = { type: 'number', examples: [0] };
1030
+ const groups = schemaToExamples(schema);
1031
+ expect(groups).toHaveLength(1);
1032
+ expect(groups[0].options[0].example).toBe(0);
1033
+ });
1034
+
1035
+ it('boolean false example is included (typeof !== object is true)', () => {
1036
+ const schema = { type: 'boolean', examples: [false] };
1037
+ const groups = schemaToExamples(schema);
1038
+ expect(groups).toHaveLength(1);
1039
+ expect(groups[0].options[0].example).toBe(false);
1040
+ });
1041
+
1042
+ it('excludes empty object example provided via examples array (L147 falsy)', () => {
1043
+ const schema = { type: 'object', examples: [{}] };
1044
+ const groups = schemaToExamples(schema);
1045
+ expect(groups).toEqual([]);
1046
+ });
1047
+
1048
+ it('shouldIncludeObjectExample returns true when condition is met, wraps in expected structure (L147)', () => {
1049
+ const schema = {
1050
+ type: 'object',
1051
+ properties: {
1052
+ x: { type: 'integer', examples: [42] },
1053
+ },
1054
+ };
1055
+ const groups = schemaToExamples(schema);
1056
+ // Must be wrapped in exactly this structure
1057
+ expect(groups).toEqual([
1058
+ {
1059
+ property: 'default',
1060
+ options: [{ title: 'Example', example: { x: 42 } }],
1061
+ },
1062
+ ]);
1063
+ });
1064
+ });
1065
+
59
1066
  describe('if/then/else conditional examples', () => {
60
1067
  it('generates two examples for schema with if/then/else', () => {
61
1068
  const groups = schemaToExamples(conditionalEventSchema);