docusaurus-plugin-generate-schema-docs 1.5.4 → 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.
- package/__tests__/__fixtures__/static/schemas/battle-test-event.json +771 -0
- package/__tests__/__fixtures__/static/schemas/conditional-event.json +52 -0
- package/__tests__/__fixtures__/static/schemas/nested-conditional-event.json +50 -0
- package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +3 -2
- package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +4 -2
- package/__tests__/__snapshots__/generateEventDocs.test.js.snap +3 -2
- package/__tests__/components/ConditionalRows.test.js +150 -0
- package/__tests__/components/ConnectorLines.visualRegression.test.js +93 -0
- package/__tests__/components/FoldableRows.test.js +7 -4
- package/__tests__/components/SchemaRows.test.js +31 -0
- package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +7 -0
- package/__tests__/generateEventDocs.anchor.test.js +7 -0
- package/__tests__/generateEventDocs.nested.test.js +7 -0
- package/__tests__/generateEventDocs.partials.test.js +134 -0
- package/__tests__/generateEventDocs.test.js +7 -0
- package/__tests__/helpers/buildExampleFromSchema.test.js +49 -0
- package/__tests__/helpers/schemaToExamples.test.js +75 -0
- package/__tests__/helpers/schemaToTableData.battleTest.test.js +704 -0
- package/__tests__/helpers/schemaToTableData.hierarchicalLines.test.js +190 -7
- package/__tests__/helpers/schemaToTableData.test.js +263 -2
- package/__tests__/helpers/validator.test.js +6 -6
- package/components/ConditionalRows.js +156 -0
- package/components/FoldableRows.js +88 -61
- package/components/PropertiesTable.js +1 -1
- package/components/PropertyRow.js +24 -8
- package/components/SchemaRows.css +115 -0
- package/components/SchemaRows.js +31 -4
- package/generateEventDocs.js +55 -37
- package/helpers/buildExampleFromSchema.js +11 -0
- package/helpers/choice-index-template.js +2 -1
- package/helpers/continuingLinesStyle.js +169 -0
- package/helpers/schema-doc-template.js +2 -5
- package/helpers/schema-processing.js +3 -0
- package/helpers/schemaToExamples.js +75 -2
- package/helpers/schemaToTableData.js +252 -26
- package/helpers/update-schema-ids.js +3 -3
- package/helpers/validator.js +7 -19
- package/package.json +3 -2
|
@@ -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
|
+
});
|