docusaurus-plugin-generate-schema-docs 1.6.0 → 1.7.0

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 (30) hide show
  1. package/__tests__/__fixtures__/static/schemas/battle-test-event.json +771 -0
  2. package/__tests__/__fixtures__/static/schemas/conditional-event.json +52 -0
  3. package/__tests__/__fixtures__/static/schemas/nested-conditional-event.json +50 -0
  4. package/__tests__/components/ConditionalRows.test.js +150 -0
  5. package/__tests__/components/ConnectorLines.visualRegression.test.js +93 -0
  6. package/__tests__/components/FoldableRows.test.js +7 -4
  7. package/__tests__/components/SchemaRows.test.js +31 -0
  8. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +7 -0
  9. package/__tests__/generateEventDocs.partials.test.js +134 -0
  10. package/__tests__/helpers/buildExampleFromSchema.test.js +49 -0
  11. package/__tests__/helpers/schemaToExamples.test.js +75 -0
  12. package/__tests__/helpers/schemaToTableData.battleTest.test.js +704 -0
  13. package/__tests__/helpers/schemaToTableData.hierarchicalLines.test.js +190 -7
  14. package/__tests__/helpers/schemaToTableData.test.js +263 -2
  15. package/__tests__/helpers/validator.test.js +6 -6
  16. package/components/ConditionalRows.js +156 -0
  17. package/components/FoldableRows.js +88 -61
  18. package/components/PropertiesTable.js +1 -1
  19. package/components/PropertyRow.js +24 -8
  20. package/components/SchemaRows.css +115 -0
  21. package/components/SchemaRows.js +31 -4
  22. package/generateEventDocs.js +41 -34
  23. package/helpers/buildExampleFromSchema.js +11 -0
  24. package/helpers/continuingLinesStyle.js +169 -0
  25. package/helpers/schema-doc-template.js +2 -5
  26. package/helpers/schemaToExamples.js +75 -2
  27. package/helpers/schemaToTableData.js +252 -26
  28. package/helpers/update-schema-ids.js +3 -3
  29. package/helpers/validator.js +7 -19
  30. package/package.json +1 -1
@@ -0,0 +1,704 @@
1
+ import { schemaToTableData } from '../../helpers/schemaToTableData';
2
+ import battleTestSchema from '../__fixtures__/static/schemas/battle-test-event.json';
3
+
4
+ /**
5
+ * Comprehensive integration test for the battle-test-event schema.
6
+ * This schema exercises every nesting combination:
7
+ * - oneOf/anyOf inside if/then/else
8
+ * - if/then/else inside oneOf/anyOf branches
9
+ * - nested conditionals inside conditionals
10
+ * - array items with conditionals and choices
11
+ * - deeply nested objects
12
+ *
13
+ * Tests verify: type, level, path, isLastInGroup, hasChildren,
14
+ * containerType, continuingLevels, and groupBrackets for every row.
15
+ */
16
+ describe('schemaToTableData – battle-test-event integration', () => {
17
+ let rows;
18
+
19
+ beforeAll(() => {
20
+ rows = schemaToTableData(battleTestSchema);
21
+ });
22
+
23
+ // Helper to find a row by name at a specific level in a flat array
24
+ const findRow = (data, name, level) =>
25
+ data.find((r) => r.name === name && r.level === level);
26
+ const findByPath = (data, path) =>
27
+ data.find((r) => JSON.stringify(r.path) === JSON.stringify(path));
28
+
29
+ // Shorthand bracket descriptor
30
+ const B = (level, bracketIndex) => ({ level, bracketIndex });
31
+
32
+ describe('top-level structure', () => {
33
+ it('produces 26 top-level rows', () => {
34
+ expect(rows).toHaveLength(26);
35
+ });
36
+
37
+ it('has the correct top-level row order', () => {
38
+ const topLevelNames = rows
39
+ .filter((r) => r.level === 0 && r.type === 'property')
40
+ .map((r) => r.name);
41
+ expect(topLevelNames).toEqual([
42
+ '$schema',
43
+ 'event',
44
+ 'user',
45
+ 'items',
46
+ 'payment',
47
+ 'fulfillment',
48
+ 'discount',
49
+ 'metadata',
50
+ ]);
51
+ });
52
+
53
+ it('marks the last top-level property as not-last (conditional follows)', () => {
54
+ const metadata = findRow(rows, 'metadata', 0);
55
+ expect(metadata.isLastInGroup).toBe(false);
56
+ });
57
+
58
+ it('has a root-level conditional as the last row', () => {
59
+ const lastRow = rows[rows.length - 1];
60
+ expect(lastRow.type).toBe('conditional');
61
+ expect(lastRow.level).toBe(0);
62
+ expect(lastRow.isLastInGroup).toBe(true);
63
+ });
64
+ });
65
+
66
+ describe('root-level property attributes', () => {
67
+ it.each([
68
+ ['$schema', { hasChildren: false, containerType: null, contLvls: [] }],
69
+ ['event', { hasChildren: false, containerType: null, contLvls: [] }],
70
+ ['user', { hasChildren: true, containerType: 'object', contLvls: [] }],
71
+ ['items', { hasChildren: true, containerType: 'array', contLvls: [] }],
72
+ ['payment', { hasChildren: true, containerType: 'object', contLvls: [] }],
73
+ [
74
+ 'fulfillment',
75
+ { hasChildren: true, containerType: 'object', contLvls: [] },
76
+ ],
77
+ [
78
+ 'discount',
79
+ { hasChildren: true, containerType: 'object', contLvls: [] },
80
+ ],
81
+ [
82
+ 'metadata',
83
+ { hasChildren: true, containerType: 'object', contLvls: [] },
84
+ ],
85
+ ])('%s has correct attributes', (name, expected) => {
86
+ const row = findRow(rows, name, 0);
87
+ expect(row.hasChildren).toBe(expected.hasChildren);
88
+ expect(row.containerType).toBe(expected.containerType);
89
+ expect(row.continuingLevels).toEqual(expected.contLvls);
90
+ expect(row.groupBrackets).toEqual([]);
91
+ });
92
+ });
93
+
94
+ describe('user section – if/then/else with oneOf inside then', () => {
95
+ it('user children: account_type, user_id (simple choice), conditional', () => {
96
+ const accountType = findByPath(rows, ['user', 'account_type']);
97
+ expect(accountType.type).toBe('property');
98
+ expect(accountType.level).toBe(1);
99
+ expect(accountType.isLastInGroup).toBe(false);
100
+ expect(accountType.continuingLevels).toEqual([0]);
101
+
102
+ const userId = findByPath(rows, ['user', 'user_id']);
103
+ expect(userId.type).toBe('choice');
104
+ expect(userId.choiceType).toBe('oneOf');
105
+ expect(userId.level).toBe(1);
106
+ expect(userId.isLastInGroup).toBe(false);
107
+ expect(userId.name).toBe('user_id');
108
+
109
+ const conditional = findByPath(rows, ['user', 'if/then/else']);
110
+ expect(conditional.type).toBe('conditional');
111
+ expect(conditional.level).toBe(1);
112
+ expect(conditional.isLastInGroup).toBe(true);
113
+ expect(conditional.continuingLevels).toEqual([0, 1]);
114
+ });
115
+
116
+ it('user_id simple choice has scalar options with correct brackets', () => {
117
+ const userId = findByPath(rows, ['user', 'user_id']);
118
+ expect(userId.options).toHaveLength(2);
119
+
120
+ const stringOpt = userId.options[0];
121
+ expect(stringOpt.title).toBe('String ID');
122
+ expect(stringOpt.rows).toHaveLength(1);
123
+ expect(stringOpt.rows[0].name).toBe('user_id');
124
+ expect(stringOpt.rows[0].isLastInGroup).toBe(false);
125
+ expect(stringOpt.rows[0].continuingLevels).toEqual([0, 1]);
126
+ expect(stringOpt.rows[0].groupBrackets).toEqual([B(1, 0)]);
127
+
128
+ const intOpt = userId.options[1];
129
+ expect(intOpt.title).toBe('Integer ID');
130
+ // Must keep the branch line going because user-level conditional follows
131
+ expect(intOpt.rows[0].isLastInGroup).toBe(false);
132
+ });
133
+
134
+ it('user conditional – condition rows', () => {
135
+ const conditional = findByPath(rows, ['user', 'if/then/else']);
136
+ const condRows = conditional.condition.rows;
137
+ expect(condRows).toHaveLength(1);
138
+ expect(condRows[0].name).toBe('account_type');
139
+ expect(condRows[0].isCondition).toBe(true);
140
+ expect(condRows[0].isLastInGroup).toBe(false); // branches always follow condition rows
141
+ expect(condRows[0].groupBrackets).toEqual([B(1, 0)]);
142
+ });
143
+
144
+ it('user conditional – Then branch with contact_method (complex choice)', () => {
145
+ const conditional = findByPath(rows, ['user', 'if/then/else']);
146
+ const thenBranch = conditional.branches[0];
147
+ expect(thenBranch.title).toBe('Then');
148
+
149
+ const loyalty = findRow(thenBranch.rows, 'loyalty_tier', 1);
150
+ expect(loyalty).toBeDefined();
151
+ expect(loyalty.isLastInGroup).toBe(false);
152
+ expect(loyalty.groupBrackets).toEqual([B(1, 0)]);
153
+
154
+ const contactMethod = findRow(thenBranch.rows, 'contact_method', 1);
155
+ expect(contactMethod.type).toBe('property');
156
+ expect(contactMethod.hasChildren).toBe(true);
157
+ expect(contactMethod.isLastInGroup).toBe(false);
158
+
159
+ // contact_method has a nested choice row at level 2
160
+ const contactChoice = thenBranch.rows.find(
161
+ (r) => r.type === 'choice' && r.path.includes('oneOf'),
162
+ );
163
+ expect(contactChoice.level).toBe(2);
164
+ expect(contactChoice.choiceType).toBe('oneOf');
165
+ expect(contactChoice.options).toHaveLength(2);
166
+ expect(contactChoice.groupBrackets).toEqual([B(1, 0)]);
167
+
168
+ // Email Contact option rows
169
+ const emailOpt = contactChoice.options[0];
170
+ expect(emailOpt.title).toBe('Email Contact');
171
+ const emailAddr = emailOpt.rows.find((r) => r.name === 'email_address');
172
+ expect(emailAddr.level).toBe(2);
173
+ expect(emailAddr.groupBrackets).toEqual([B(1, 0), B(2, 1)]);
174
+ expect(emailAddr.continuingLevels).toEqual([0, 1]);
175
+
176
+ // SMS Contact – phone_number is last in last option
177
+ const smsOpt = contactChoice.options[1];
178
+ const phoneNumber = smsOpt.rows.find((r) => r.name === 'phone_number');
179
+ expect(phoneNumber.isLastInGroup).toBe(true);
180
+ expect(phoneNumber.groupBrackets).toEqual([B(1, 0), B(2, 1)]);
181
+ });
182
+
183
+ it('user conditional – Then branch preferences with nested conditional', () => {
184
+ const conditional = findByPath(rows, ['user', 'if/then/else']);
185
+ const thenBranch = conditional.branches[0];
186
+
187
+ const preferences = findRow(thenBranch.rows, 'preferences', 1);
188
+ expect(preferences.isLastInGroup).toBe(false); // then branch has else, so last prop isn't truly last
189
+ expect(preferences.hasChildren).toBe(true);
190
+ expect(preferences.containerType).toBe('object');
191
+ expect(preferences.continuingLevels).toEqual([0]);
192
+
193
+ // Nested conditional inside preferences
194
+ const prefsConditional = thenBranch.rows.find(
195
+ (r) =>
196
+ r.type === 'conditional' && r.path.join('.').includes('preferences'),
197
+ );
198
+ expect(prefsConditional.level).toBe(2);
199
+ expect(prefsConditional.continuingLevels).toEqual([0, 1, 2]);
200
+ expect(prefsConditional.groupBrackets).toEqual([B(1, 0)]);
201
+
202
+ // Condition row for marketing_consent
203
+ expect(prefsConditional.condition.rows[0].name).toBe('marketing_consent');
204
+ expect(prefsConditional.condition.rows[0].groupBrackets).toEqual([
205
+ B(1, 0),
206
+ B(2, 1),
207
+ ]);
208
+
209
+ // Then branch: frequency + topics
210
+ const prefsThen = prefsConditional.branches[0];
211
+ expect(prefsThen.rows).toHaveLength(2);
212
+ expect(prefsThen.rows[0].name).toBe('frequency');
213
+ expect(prefsThen.rows[1].name).toBe('topics');
214
+
215
+ // Else branch: opt_out_reason
216
+ const prefsElse = prefsConditional.branches[1];
217
+ expect(prefsElse.rows).toHaveLength(1);
218
+ expect(prefsElse.rows[0].name).toBe('opt_out_reason');
219
+ });
220
+
221
+ it('user conditional – Else branch with session_id', () => {
222
+ const conditional = findByPath(rows, ['user', 'if/then/else']);
223
+ const elseBranch = conditional.branches[1];
224
+ expect(elseBranch.title).toBe('Else');
225
+ expect(elseBranch.rows).toHaveLength(1);
226
+ expect(elseBranch.rows[0].name).toBe('session_id');
227
+ expect(elseBranch.rows[0].groupBrackets).toEqual([B(1, 0)]);
228
+ });
229
+ });
230
+
231
+ describe('items section – array with if/then/else in items', () => {
232
+ it('items properties + conditional at correct levels', () => {
233
+ const itemType = findByPath(rows, ['items', '[n]', 'item_type']);
234
+ expect(itemType.level).toBe(1);
235
+ expect(itemType.isLastInGroup).toBe(false);
236
+ expect(itemType.continuingLevels).toEqual([0]);
237
+
238
+ const quantity = findByPath(rows, ['items', '[n]', 'quantity']);
239
+ expect(quantity.isLastInGroup).toBe(false);
240
+ expect(quantity.continuingLevels).toEqual([0]);
241
+
242
+ const itemsConditional = findByPath(rows, [
243
+ 'items',
244
+ '[n]',
245
+ 'if/then/else',
246
+ ]);
247
+ expect(itemsConditional.type).toBe('conditional');
248
+ expect(itemsConditional.level).toBe(1);
249
+ });
250
+
251
+ it('items Then branch – physical items with shipping_class (complex choice)', () => {
252
+ const itemsConditional = findByPath(rows, [
253
+ 'items',
254
+ '[n]',
255
+ 'if/then/else',
256
+ ]);
257
+ const thenBranch = itemsConditional.branches[0];
258
+ expect(thenBranch.title).toBe('Then');
259
+
260
+ const weightKg = thenBranch.rows.find((r) => r.name === 'weight_kg');
261
+ expect(weightKg.level).toBe(1);
262
+
263
+ const dimensions = thenBranch.rows.find((r) => r.name === 'dimensions');
264
+ expect(dimensions.hasChildren).toBe(true);
265
+ expect(dimensions.containerType).toBe('object');
266
+
267
+ // height is last child of dimensions
268
+ const height = thenBranch.rows.find((r) => r.name === 'height');
269
+ expect(height.level).toBe(2);
270
+ expect(height.isLastInGroup).toBe(true);
271
+
272
+ // shipping_class is a complex choice (property + nested choice row)
273
+ const shippingClass = thenBranch.rows.find(
274
+ (r) => r.name === 'shipping_class',
275
+ );
276
+ expect(shippingClass.type).toBe('property');
277
+ expect(shippingClass.hasChildren).toBe(true);
278
+
279
+ const shippingChoice = thenBranch.rows.find(
280
+ (r) => r.type === 'choice' && r.path.includes('oneOf'),
281
+ );
282
+ expect(shippingChoice.choiceType).toBe('oneOf');
283
+ expect(shippingChoice.options).toHaveLength(2);
284
+ });
285
+
286
+ it('items Else branch – digital items', () => {
287
+ const itemsConditional = findByPath(rows, [
288
+ 'items',
289
+ '[n]',
290
+ 'if/then/else',
291
+ ]);
292
+ const elseBranch = itemsConditional.branches[1];
293
+ expect(elseBranch.title).toBe('Else');
294
+
295
+ const downloadUrl = elseBranch.rows.find(
296
+ (r) => r.name === 'download_url',
297
+ );
298
+ expect(downloadUrl).toBeDefined();
299
+ const licenseType = elseBranch.rows.find(
300
+ (r) => r.name === 'license_type',
301
+ );
302
+ expect(licenseType.isLastInGroup).toBe(true);
303
+ });
304
+ });
305
+
306
+ describe('payment section – anyOf with if/then/else inside branch', () => {
307
+ it('payment has currency property + anyOf choice', () => {
308
+ const currency = findByPath(rows, ['payment', 'currency']);
309
+ expect(currency.level).toBe(1);
310
+ expect(currency.isLastInGroup).toBe(false);
311
+ expect(currency.continuingLevels).toEqual([0]);
312
+
313
+ const paymentChoice = rows.find(
314
+ (r) =>
315
+ r.type === 'choice' &&
316
+ r.path[0] === 'payment' &&
317
+ r.choiceType === 'anyOf',
318
+ );
319
+ expect(paymentChoice.level).toBe(1);
320
+ expect(paymentChoice.options).toHaveLength(3);
321
+ });
322
+
323
+ it('Credit Card option has if/then/else (conditional inside choice)', () => {
324
+ const paymentChoice = rows.find(
325
+ (r) =>
326
+ r.type === 'choice' &&
327
+ r.path[0] === 'payment' &&
328
+ r.choiceType === 'anyOf',
329
+ );
330
+ const creditCard = paymentChoice.options[0];
331
+ expect(creditCard.title).toBe('Credit Card');
332
+
333
+ // Credit card has properties + conditional
334
+ const cardBrand = creditCard.rows.find((r) => r.name === 'card_brand');
335
+ expect(cardBrand.level).toBe(1);
336
+
337
+ const cardConditional = creditCard.rows.find(
338
+ (r) => r.type === 'conditional',
339
+ );
340
+ expect(cardConditional).toBeDefined();
341
+ expect(cardConditional.level).toBe(1);
342
+
343
+ // Condition checks card_brand
344
+ expect(cardConditional.condition.rows[0].name).toBe('card_brand');
345
+ // Nested brackets: parent anyOf bracket + this conditional's bracket
346
+ expect(cardConditional.condition.rows[0].groupBrackets).toEqual([
347
+ B(1, 0),
348
+ B(1, 1),
349
+ ]);
350
+
351
+ // Then: cid for Amex
352
+ const thenBranch = cardConditional.branches[0];
353
+ expect(thenBranch.rows[0].name).toBe('cid');
354
+
355
+ // Else: cvv for non-Amex
356
+ const elseBranch = cardConditional.branches[1];
357
+ expect(elseBranch.rows[0].name).toBe('cvv');
358
+ });
359
+
360
+ it('Digital Wallet option has wallet_provider (simple choice)', () => {
361
+ const paymentChoice = rows.find(
362
+ (r) =>
363
+ r.type === 'choice' &&
364
+ r.path[0] === 'payment' &&
365
+ r.choiceType === 'anyOf',
366
+ );
367
+ const digitalWallet = paymentChoice.options[1];
368
+ expect(digitalWallet.title).toBe('Digital Wallet');
369
+
370
+ const walletProvider = digitalWallet.rows.find(
371
+ (r) => r.type === 'choice' && r.name === 'wallet_provider',
372
+ );
373
+ expect(walletProvider).toBeDefined();
374
+ expect(walletProvider.choiceType).toBe('oneOf');
375
+ // Simple choice: scalar options, no property row
376
+ expect(walletProvider.options).toHaveLength(2);
377
+
378
+ // Last wallet provider option must not terminate the line because
379
+ // wallet_email follows at the same level in this option.
380
+ const customProviderRow = walletProvider.options[1].rows[0];
381
+ expect(customProviderRow.name).toBe('wallet_provider');
382
+ expect(customProviderRow.isLastInGroup).toBe(false);
383
+ });
384
+
385
+ it('cvv in credit-card else branch keeps line continuity to following options', () => {
386
+ const paymentChoice = rows.find(
387
+ (r) =>
388
+ r.type === 'choice' &&
389
+ r.path[0] === 'payment' &&
390
+ r.choiceType === 'anyOf',
391
+ );
392
+ const creditCard = paymentChoice.options[0];
393
+ const cardConditional = creditCard.rows.find(
394
+ (r) => r.type === 'conditional',
395
+ );
396
+ const cvvRow = cardConditional.branches[1].rows.find(
397
+ (r) => r.name === 'cvv',
398
+ );
399
+
400
+ // Credit Card is not the last anyOf option, so cvv must not close the branch.
401
+ expect(cvvRow.isLastInGroup).toBe(false);
402
+ });
403
+ });
404
+
405
+ describe('fulfillment section – anyOf with nested conditional and nested anyOf', () => {
406
+ it('Home Delivery option has address with nested conditional', () => {
407
+ const fulfillmentChoice = rows.find(
408
+ (r) => r.type === 'choice' && r.path[0] === 'fulfillment',
409
+ );
410
+ const homeDelivery = fulfillmentChoice.options[0];
411
+ expect(homeDelivery.title).toBe('Home Delivery');
412
+
413
+ const address = homeDelivery.rows.find((r) => r.name === 'address');
414
+ expect(address.hasChildren).toBe(true);
415
+ expect(address.containerType).toBe('object');
416
+
417
+ // Address has country, city, street + conditional
418
+ const addressConditional = homeDelivery.rows.find(
419
+ (r) => r.type === 'conditional',
420
+ );
421
+ expect(addressConditional.level).toBe(2);
422
+ expect(addressConditional.groupBrackets).toEqual([B(1, 0)]);
423
+
424
+ // Condition checks country
425
+ expect(addressConditional.condition.rows[0].name).toBe('country');
426
+ expect(addressConditional.condition.rows[0].groupBrackets).toEqual([
427
+ B(1, 0),
428
+ B(2, 1),
429
+ ]);
430
+
431
+ // Then: customs_declaration with nested properties
432
+ const thenBranch = addressConditional.branches[0];
433
+ const customs = thenBranch.rows.find(
434
+ (r) => r.name === 'customs_declaration',
435
+ );
436
+ expect(customs.hasChildren).toBe(true);
437
+ expect(customs.containerType).toBe('object');
438
+
439
+ const declaredValue = thenBranch.rows.find(
440
+ (r) => r.name === 'declared_value',
441
+ );
442
+ expect(declaredValue.level).toBe(3);
443
+ expect(declaredValue.groupBrackets).toEqual([B(1, 0), B(2, 1)]);
444
+
445
+ // Else: zip_plus_four
446
+ const elseBranch = addressConditional.branches[1];
447
+ expect(elseBranch.rows[0].name).toBe('zip_plus_four');
448
+ });
449
+
450
+ it('Store Pickup option has pickup_time (complex choice with nested anyOf)', () => {
451
+ const fulfillmentChoice = rows.find(
452
+ (r) => r.type === 'choice' && r.path[0] === 'fulfillment',
453
+ );
454
+ const storePickup = fulfillmentChoice.options[1];
455
+ expect(storePickup.title).toBe('Store Pickup');
456
+
457
+ const pickupTime = storePickup.rows.find((r) => r.name === 'pickup_time');
458
+ expect(pickupTime.hasChildren).toBe(true);
459
+
460
+ // Nested anyOf choice inside pickup_time
461
+ const pickupChoice = storePickup.rows.find(
462
+ (r) => r.type === 'choice' && r.choiceType === 'anyOf',
463
+ );
464
+ expect(pickupChoice.level).toBe(2);
465
+ expect(pickupChoice.options).toHaveLength(2);
466
+ expect(pickupChoice.groupBrackets).toEqual([B(1, 0)]);
467
+
468
+ // Scheduled Time option
469
+ const scheduled = pickupChoice.options[0];
470
+ expect(scheduled.title).toBe('Scheduled Time');
471
+ const datetime = scheduled.rows.find((r) => r.name === 'datetime');
472
+ expect(datetime.groupBrackets).toEqual([B(1, 0), B(2, 1)]);
473
+ });
474
+ });
475
+
476
+ describe('discount section – complex choice (property + oneOf)', () => {
477
+ it('discount is a property with nested oneOf choice', () => {
478
+ const discount = findRow(rows, 'discount', 0);
479
+ expect(discount.type).toBe('property');
480
+ expect(discount.hasChildren).toBe(true);
481
+ // discount has no type in schema, but oneOf options are objects
482
+ expect(discount.containerType).toBe('object');
483
+
484
+ const discountChoice = rows.find(
485
+ (r) => r.type === 'choice' && r.path[0] === 'discount',
486
+ );
487
+ expect(discountChoice.level).toBe(1);
488
+ expect(discountChoice.choiceType).toBe('oneOf');
489
+ expect(discountChoice.options).toHaveLength(2);
490
+ expect(discountChoice.groupBrackets).toEqual([]);
491
+
492
+ // Percentage Discount option
493
+ const pctOpt = discountChoice.options[0];
494
+ expect(pctOpt.rows[0].groupBrackets).toEqual([B(1, 0)]);
495
+
496
+ // Fixed Discount – amount is last in last option
497
+ const fixedOpt = discountChoice.options[1];
498
+ const amount = fixedOpt.rows.find((r) => r.name === 'amount');
499
+ expect(amount.isLastInGroup).toBe(true);
500
+ });
501
+ });
502
+
503
+ describe('metadata section – oneOf with if/then/else inside branch', () => {
504
+ it('metadata has timestamp + source (complex choice)', () => {
505
+ const timestamp = findByPath(rows, ['metadata', 'timestamp']);
506
+ expect(timestamp.level).toBe(1);
507
+ expect(timestamp.isLastInGroup).toBe(false);
508
+
509
+ const source = findByPath(rows, ['metadata', 'source']);
510
+ expect(source.type).toBe('property');
511
+ expect(source.hasChildren).toBe(true);
512
+ expect(source.isLastInGroup).toBe(true);
513
+
514
+ const sourceChoice = rows.find(
515
+ (r) =>
516
+ r.type === 'choice' &&
517
+ r.path.includes('source') &&
518
+ r.path.includes('oneOf'),
519
+ );
520
+ expect(sourceChoice.level).toBe(2);
521
+ expect(sourceChoice.options).toHaveLength(2);
522
+ });
523
+
524
+ it('Web Source option has if/then/else (conditional inside choice)', () => {
525
+ const sourceChoice = rows.find(
526
+ (r) =>
527
+ r.type === 'choice' &&
528
+ r.path.includes('source') &&
529
+ r.path.includes('oneOf'),
530
+ );
531
+ const webSource = sourceChoice.options[0];
532
+ expect(webSource.title).toBe('Web Source');
533
+
534
+ const webConditional = webSource.rows.find(
535
+ (r) => r.type === 'conditional',
536
+ );
537
+ expect(webConditional).toBeDefined();
538
+ expect(webConditional.level).toBe(2);
539
+
540
+ // Condition: device_type with double bracket (parent oneOf + this conditional)
541
+ const condRow = webConditional.condition.rows[0];
542
+ expect(condRow.name).toBe('device_type');
543
+ expect(condRow.groupBrackets).toEqual([B(2, 0), B(2, 1)]);
544
+
545
+ // Then: screen_width + touch_capable
546
+ expect(webConditional.branches[0].rows).toHaveLength(2);
547
+ expect(webConditional.branches[0].rows[0].name).toBe('screen_width');
548
+ expect(webConditional.branches[0].rows[1].name).toBe('touch_capable');
549
+ expect(webConditional.branches[0].rows[1].isLastInGroup).toBe(false); // then branch has else
550
+
551
+ // Else: viewport_width + viewport_height
552
+ expect(webConditional.branches[1].rows).toHaveLength(2);
553
+ });
554
+
555
+ it('App Source option has no conditional', () => {
556
+ const sourceChoice = rows.find(
557
+ (r) =>
558
+ r.type === 'choice' &&
559
+ r.path.includes('source') &&
560
+ r.path.includes('oneOf'),
561
+ );
562
+ const appSource = sourceChoice.options[1];
563
+ expect(appSource.rows.every((r) => r.type === 'property')).toBe(true);
564
+ expect(appSource.rows).toHaveLength(3);
565
+
566
+ const os = appSource.rows.find((r) => r.name === 'os');
567
+ expect(os.isLastInGroup).toBe(true);
568
+ expect(os.groupBrackets).toEqual([B(2, 0)]);
569
+ });
570
+ });
571
+
572
+ describe('root-level if/then/else (payment.currency === USD)', () => {
573
+ let rootConditional;
574
+
575
+ beforeAll(() => {
576
+ rootConditional = rows[rows.length - 1];
577
+ });
578
+
579
+ it('root conditional has correct structure', () => {
580
+ expect(rootConditional.type).toBe('conditional');
581
+ expect(rootConditional.level).toBe(0);
582
+ expect(rootConditional.continuingLevels).toEqual([0]);
583
+ expect(rootConditional.groupBrackets).toEqual([]);
584
+ });
585
+
586
+ it('condition rows: payment > currency', () => {
587
+ const condRows = rootConditional.condition.rows;
588
+ expect(condRows).toHaveLength(2);
589
+
590
+ const payment = condRows[0];
591
+ expect(payment.name).toBe('payment');
592
+ expect(payment.isCondition).toBe(true);
593
+ expect(payment.isLastInGroup).toBe(false); // branches always follow condition rows
594
+ expect(payment.hasChildren).toBe(true);
595
+ expect(payment.groupBrackets).toEqual([B(0, 0)]);
596
+
597
+ const currency = condRows[1];
598
+ expect(currency.name).toBe('currency');
599
+ expect(currency.level).toBe(1);
600
+ expect(currency.isCondition).toBe(true);
601
+ expect(currency.isLastInGroup).toBe(true);
602
+ expect(currency.groupBrackets).toEqual([B(0, 0)]);
603
+ });
604
+
605
+ it('Then branch: tax + nested conditional (CA jurisdiction)', () => {
606
+ const thenBranch = rootConditional.branches[0];
607
+ expect(thenBranch.title).toBe('Then');
608
+
609
+ const tax = thenBranch.rows.find((r) => r.name === 'tax');
610
+ expect(tax.level).toBe(0);
611
+ expect(tax.hasChildren).toBe(true);
612
+ expect(tax.containerType).toBe('object');
613
+ expect(tax.isLastInGroup).toBe(false);
614
+ expect(tax.groupBrackets).toEqual([B(0, 0)]);
615
+
616
+ // Tax children: rate, amount, jurisdiction
617
+ const rate = thenBranch.rows.find((r) => r.name === 'rate');
618
+ expect(rate.level).toBe(1);
619
+ expect(rate.groupBrackets).toEqual([B(0, 0)]);
620
+
621
+ const jurisdiction = thenBranch.rows.find(
622
+ (r) => r.name === 'jurisdiction',
623
+ );
624
+ expect(jurisdiction.isLastInGroup).toBe(true);
625
+
626
+ // Nested conditional: if tax.jurisdiction === CA
627
+ const nestedConditional = thenBranch.rows.find(
628
+ (r) => r.type === 'conditional',
629
+ );
630
+ expect(nestedConditional).toBeDefined();
631
+ expect(nestedConditional.level).toBe(0);
632
+ expect(nestedConditional.groupBrackets).toEqual([B(0, 0)]);
633
+
634
+ // Nested condition: tax > jurisdiction with double bracket
635
+ const nestedCondRows = nestedConditional.condition.rows;
636
+ expect(nestedCondRows[0].name).toBe('tax');
637
+ expect(nestedCondRows[0].groupBrackets).toEqual([B(0, 0), B(0, 1)]);
638
+
639
+ // Nested Then: recycling_fee
640
+ const nestedThen = nestedConditional.branches[0];
641
+ const recyclingFee = nestedThen.rows.find(
642
+ (r) => r.name === 'recycling_fee',
643
+ );
644
+ expect(recyclingFee.hasChildren).toBe(true);
645
+ expect(recyclingFee.groupBrackets).toEqual([B(0, 0), B(0, 1)]);
646
+
647
+ // Nested Else: state_exemption_code
648
+ const nestedElse = nestedConditional.branches[1];
649
+ expect(nestedElse.rows[0].name).toBe('state_exemption_code');
650
+ expect(nestedElse.rows[0].groupBrackets).toEqual([B(0, 0), B(0, 1)]);
651
+ });
652
+
653
+ it('Else branch: vat_number', () => {
654
+ const elseBranch = rootConditional.branches[1];
655
+ expect(elseBranch.title).toBe('Else');
656
+ expect(elseBranch.rows).toHaveLength(1);
657
+ expect(elseBranch.rows[0].name).toBe('vat_number');
658
+ expect(elseBranch.rows[0].isLastInGroup).toBe(true);
659
+ expect(elseBranch.rows[0].groupBrackets).toEqual([B(0, 0)]);
660
+ });
661
+ });
662
+
663
+ describe('continuingLevels correctness across deep nesting', () => {
664
+ it('level-3 properties inside customs_declaration have correct ancestor lines', () => {
665
+ const fulfillmentChoice = rows.find(
666
+ (r) => r.type === 'choice' && r.path[0] === 'fulfillment',
667
+ );
668
+ const homeDelivery = fulfillmentChoice.options[0];
669
+ const addressConditional = homeDelivery.rows.find(
670
+ (r) => r.type === 'conditional',
671
+ );
672
+ const thenBranch = addressConditional.branches[0];
673
+ const declaredValue = thenBranch.rows.find(
674
+ (r) => r.name === 'declared_value',
675
+ );
676
+ // Level 3 inside: fulfillment(0) > address(1) > customs(2) > declared_value(3)
677
+ // fulfillment continues (not last root prop), address has more sibling rows,
678
+ // and customs_declaration is no longer "last" because else follows the then branch
679
+ expect(declaredValue.continuingLevels).toEqual([0, 1, 2]);
680
+ expect(declaredValue.isLastInGroup).toBe(false);
681
+
682
+ const hsCode = thenBranch.rows.find((r) => r.name === 'hs_code');
683
+ expect(hsCode.isLastInGroup).toBe(true);
684
+ expect(hsCode.continuingLevels).toEqual([0, 1, 2]);
685
+ });
686
+
687
+ it('nested conditional inside Then branch has accumulated brackets', () => {
688
+ const rootConditional = rows[rows.length - 1];
689
+ const thenBranch = rootConditional.branches[0];
690
+ const nestedConditional = thenBranch.rows.find(
691
+ (r) => r.type === 'conditional',
692
+ );
693
+
694
+ // Root conditional bracket + nested conditional bracket
695
+ const nestedCondRow = nestedConditional.condition.rows[0];
696
+ expect(nestedCondRow.groupBrackets).toEqual([B(0, 0), B(0, 1)]);
697
+
698
+ // Inner condition's child also has double bracket
699
+ const nestedJurisdiction = nestedConditional.condition.rows[1];
700
+ expect(nestedJurisdiction.name).toBe('jurisdiction');
701
+ expect(nestedJurisdiction.groupBrackets).toEqual([B(0, 0), B(0, 1)]);
702
+ });
703
+ });
704
+ });