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.
- package/README.md +10 -0
- package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof-multi.json +12 -0
- package/__tests__/__fixtures__/validateSchemas/schema-with-not-anyof.json +30 -0
- package/__tests__/__fixtures__/validateSchemas/schema-with-not-edge-cases.json +24 -0
- package/__tests__/__fixtures__/validateSchemas/schema-with-not-non-object.json +15 -0
- package/__tests__/generateEventDocs.anchor.test.js +1 -1
- package/__tests__/generateEventDocs.nested.test.js +1 -1
- package/__tests__/generateEventDocs.partials.test.js +1 -1
- package/__tests__/generateEventDocs.test.js +506 -1
- package/__tests__/generateEventDocs.versioned.test.js +1 -1
- package/__tests__/helpers/buildExampleFromSchema.test.js +240 -0
- package/__tests__/helpers/constraintSchemaPaths.test.js +208 -0
- package/__tests__/helpers/continuingLinesStyle.test.js +492 -0
- package/__tests__/helpers/exampleModel.test.js +209 -0
- package/__tests__/helpers/file-system.test.js +73 -1
- package/__tests__/helpers/getConstraints.test.js +27 -0
- package/__tests__/helpers/mergeSchema.test.js +94 -0
- package/__tests__/helpers/processSchema.test.js +291 -1
- package/__tests__/helpers/schema-doc-template.test.js +54 -0
- package/__tests__/helpers/schema-processing.test.js +122 -2
- package/__tests__/helpers/schemaToExamples.test.js +1007 -0
- package/__tests__/helpers/schemaToTableData.mutations.test.js +970 -0
- package/__tests__/helpers/schemaToTableData.test.js +157 -0
- package/__tests__/helpers/snippetTargets.test.js +432 -0
- package/__tests__/helpers/trackingTargets.test.js +319 -0
- package/__tests__/helpers/validator.test.js +385 -1
- package/__tests__/index.test.js +436 -0
- package/__tests__/syncGtm.test.js +139 -3
- package/__tests__/update-schema-ids.test.js +70 -1
- package/__tests__/validateSchemas-integration.test.js +2 -2
- package/__tests__/validateSchemas.test.js +142 -1
- package/generateEventDocs.js +21 -1
- package/helpers/constraintSchemaPaths.js +10 -14
- package/helpers/schemaToTableData.js +538 -492
- package/helpers/trackingTargets.js +26 -3
- package/helpers/validator.js +18 -4
- package/index.js +1 -2
- package/package.json +1 -1
- 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);
|