docusaurus-plugin-generate-schema-docs 1.8.2 → 1.8.4

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 (43) hide show
  1. package/README.md +2 -0
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-not-allof.json +11 -0
  3. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +21 -3
  4. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +26 -4
  5. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +45 -6
  6. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +16 -2
  7. package/__tests__/components/ConditionalRows.test.js +28 -0
  8. package/__tests__/components/FoldableRows.test.js +31 -290
  9. package/__tests__/components/PropertiesTable.test.js +66 -0
  10. package/__tests__/components/PropertyRow.test.js +297 -0
  11. package/__tests__/components/SchemaJsonViewer.test.js +194 -10
  12. package/__tests__/components/SchemaRows.test.js +62 -12
  13. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
  14. package/__tests__/generateEventDocs.test.js +3 -0
  15. package/__tests__/helpers/example-helper.test.js +12 -0
  16. package/__tests__/helpers/getConstraints.test.js +16 -0
  17. package/__tests__/helpers/processSchema.test.js +18 -0
  18. package/__tests__/helpers/schemaToTableData.test.js +112 -0
  19. package/__tests__/helpers/schemaTraversal.test.js +110 -0
  20. package/__tests__/syncGtm.test.js +227 -3
  21. package/__tests__/validateSchemas.test.js +50 -0
  22. package/components/ConditionalRows.js +6 -3
  23. package/components/FoldableRows.js +9 -3
  24. package/components/PropertiesTable.js +34 -3
  25. package/components/PropertyRow.js +118 -6
  26. package/components/SchemaJsonViewer.js +324 -4
  27. package/components/SchemaRows.css +138 -7
  28. package/components/SchemaRows.js +11 -1
  29. package/components/SchemaViewer.js +11 -2
  30. package/generateEventDocs.js +87 -1
  31. package/helpers/choice-index-template.js +6 -2
  32. package/helpers/example-helper.js +2 -2
  33. package/helpers/file-system.js +28 -0
  34. package/helpers/getConstraints.js +20 -0
  35. package/helpers/processSchema.js +32 -1
  36. package/helpers/schema-doc-template.js +11 -1
  37. package/helpers/schemaToExamples.js +29 -35
  38. package/helpers/schemaToTableData.js +68 -7
  39. package/helpers/schemaTraversal.cjs +148 -0
  40. package/package.json +1 -1
  41. package/scripts/sync-gtm.js +41 -28
  42. package/test-data/payloadContracts.js +35 -0
  43. package/validateSchemas.js +1 -1
@@ -56,6 +56,9 @@ describe('generateEventDocs (non-versioned)', () => {
56
56
  'utf-8',
57
57
  );
58
58
  expect(addToCart).toMatchSnapshot();
59
+ expect(addToCart).toContain('sourcePath={"add-to-cart-event.json"}');
60
+ expect(addToCart).toContain(JSON.stringify('components/product.json'));
61
+ expect(addToCart).toContain('"$ref":"./components/product.json"');
59
62
 
60
63
  const choiceEvent = fs.readFileSync(
61
64
  path.join(outputDir, 'choice-event.mdx'),
@@ -34,6 +34,12 @@ describe('example-helper', () => {
34
34
  };
35
35
  expect(getExamples(schema)).toEqual(['example1']);
36
36
  });
37
+
38
+ it('preserves falsy "example" values', () => {
39
+ expect(getExamples({ example: false })).toEqual([false]);
40
+ expect(getExamples({ example: 0 })).toEqual([0]);
41
+ expect(getExamples({ example: '' })).toEqual(['']);
42
+ });
37
43
  });
38
44
 
39
45
  describe('getSingleExampleValue', () => {
@@ -67,5 +73,11 @@ describe('example-helper', () => {
67
73
  };
68
74
  expect(getSingleExampleValue(schema)).toBe('default-value');
69
75
  });
76
+
77
+ it('returns falsy "example" values when present', () => {
78
+ expect(getSingleExampleValue({ example: false })).toBe(false);
79
+ expect(getSingleExampleValue({ example: 0 })).toBe(0);
80
+ expect(getSingleExampleValue({ example: '' })).toBe('');
81
+ });
70
82
  });
71
83
  });
@@ -59,6 +59,7 @@ describe('getConstraints', () => {
59
59
  contains: { type: 'string' },
60
60
  enum: ['a', 'b', 'c'],
61
61
  const: 'hello',
62
+ not: { const: 'US' },
62
63
  };
63
64
  const expected = [
64
65
  'pattern: /^[a-z]+$/',
@@ -69,6 +70,7 @@ describe('getConstraints', () => {
69
70
  'contains: {"type":"string"}',
70
71
  'enum: [a, b, c]',
71
72
  'const: "hello"',
73
+ 'not: { const: "US" }',
72
74
  ];
73
75
  expect(getConstraints(prop, false)).toEqual(
74
76
  expect.arrayContaining(expected),
@@ -100,4 +102,18 @@ describe('getConstraints', () => {
100
102
  expect.arrayContaining(['required', ...expected]),
101
103
  );
102
104
  });
105
+
106
+ it('should render "not" constraint in JSON schema syntax for const', () => {
107
+ const prop = {
108
+ not: { const: 'US' },
109
+ };
110
+ expect(getConstraints(prop, false)).toEqual(['not: { const: "US" }']);
111
+ });
112
+
113
+ it('should render "not" constraint in JSON schema syntax for type', () => {
114
+ const prop = {
115
+ not: { type: 'string' },
116
+ };
117
+ expect(getConstraints(prop, false)).toEqual(['not: { type: "string" }']);
118
+ });
103
119
  });
@@ -82,4 +82,22 @@ describe('processSchema', () => {
82
82
  }),
83
83
  );
84
84
  });
85
+
86
+ it('keeps simple not const constraints readable after allOf merge', async () => {
87
+ const filePath = path.join(
88
+ __dirname,
89
+ '..',
90
+ '__fixtures__',
91
+ 'validateSchemas',
92
+ 'main-schema-with-not-allof.json',
93
+ );
94
+ const mergedSchema = await processSchema(filePath);
95
+
96
+ expect(mergedSchema.properties.country).toEqual(
97
+ expect.objectContaining({
98
+ type: 'string',
99
+ not: { const: 'US' },
100
+ }),
101
+ );
102
+ });
85
103
  });
@@ -217,6 +217,118 @@ describe('schemaToTableData', () => {
217
217
  expect(tableData[0].examples).toEqual(['default-value']);
218
218
  });
219
219
 
220
+ it('renders schema-valued additionalProperties as a synthetic row', () => {
221
+ const schema = {
222
+ properties: {
223
+ metadata: {
224
+ type: 'object',
225
+ description: 'Free-form metadata.',
226
+ additionalProperties: {
227
+ type: 'string',
228
+ description: 'Metadata value.',
229
+ pattern: '^[a-z]+$',
230
+ },
231
+ },
232
+ },
233
+ };
234
+
235
+ const tableData = schemaToTableData(schema);
236
+
237
+ const metadataRow = tableData.find((row) => row.name === 'metadata');
238
+ expect(metadataRow).toBeDefined();
239
+ expect(metadataRow.containerType).toBe('object');
240
+
241
+ const additionalPropertiesRow = tableData.find(
242
+ (row) =>
243
+ row.type === 'property' &&
244
+ row.name === 'additionalProperties' &&
245
+ row.level === 1,
246
+ );
247
+
248
+ expect(additionalPropertiesRow).toBeDefined();
249
+ expect(additionalPropertiesRow.propertyType).toBe('string');
250
+ expect(additionalPropertiesRow.description).toBe('Metadata value.');
251
+ expect(additionalPropertiesRow.constraints).toContain(
252
+ 'pattern: /^[a-z]+$/',
253
+ );
254
+ expect(additionalPropertiesRow.isSchemaKeywordRow).toBe(true);
255
+ expect(additionalPropertiesRow.keepConnectorOpen).toBe(false);
256
+ expect(additionalPropertiesRow.isLastInGroup).toBe(true);
257
+ });
258
+
259
+ it('treats additionalProperties as the last sibling after declared properties', () => {
260
+ const schema = {
261
+ properties: {
262
+ user_properties: {
263
+ type: 'object',
264
+ properties: {
265
+ allow_ad_personalization_signals: {
266
+ type: ['string', 'null'],
267
+ },
268
+ },
269
+ additionalProperties: {
270
+ type: ['string', 'null'],
271
+ },
272
+ },
273
+ },
274
+ };
275
+
276
+ const tableData = schemaToTableData(schema);
277
+
278
+ const explicitPropertyRow = tableData.find(
279
+ (row) =>
280
+ row.name === 'allow_ad_personalization_signals' && row.level === 1,
281
+ );
282
+ const additionalPropertiesRow = tableData.find(
283
+ (row) => row.name === 'additionalProperties' && row.level === 1,
284
+ );
285
+
286
+ expect(explicitPropertyRow).toBeDefined();
287
+ expect(additionalPropertiesRow).toBeDefined();
288
+ expect(explicitPropertyRow.isLastInGroup).toBe(false);
289
+ expect(additionalPropertiesRow.isLastInGroup).toBe(true);
290
+ });
291
+
292
+ it('renders schema-valued patternProperties as synthetic keyword rows', () => {
293
+ const schema = {
294
+ properties: {
295
+ attributes: {
296
+ type: 'object',
297
+ properties: {
298
+ segment: { type: 'string' },
299
+ },
300
+ patternProperties: {
301
+ '^custom_': {
302
+ type: 'number',
303
+ description: 'Numeric custom attribute values.',
304
+ minimum: 0,
305
+ },
306
+ },
307
+ },
308
+ },
309
+ };
310
+
311
+ const tableData = schemaToTableData(schema);
312
+
313
+ const explicitPropertyRow = tableData.find(
314
+ (row) => row.name === 'segment' && row.level === 1,
315
+ );
316
+ const patternPropertyRow = tableData.find(
317
+ (row) => row.name === 'patternProperties /^custom_/' && row.level === 1,
318
+ );
319
+
320
+ expect(explicitPropertyRow).toBeDefined();
321
+ expect(patternPropertyRow).toBeDefined();
322
+ expect(explicitPropertyRow.isLastInGroup).toBe(false);
323
+ expect(patternPropertyRow.isLastInGroup).toBe(true);
324
+ expect(patternPropertyRow.propertyType).toBe('number');
325
+ expect(patternPropertyRow.description).toBe(
326
+ 'Numeric custom attribute values.',
327
+ );
328
+ expect(patternPropertyRow.constraints).toContain('minimum: 0');
329
+ expect(patternPropertyRow.isSchemaKeywordRow).toBe(true);
330
+ });
331
+
220
332
  describe('if/then/else conditional support', () => {
221
333
  it('creates a conditional row for schema with if/then/else at root level', () => {
222
334
  const tableData = schemaToTableData(conditionalEventSchema);
@@ -0,0 +1,110 @@
1
+ const {
2
+ visitSchemaNodes,
3
+ visitSchemaPropertyEntries,
4
+ } = require('../../helpers/schemaTraversal.cjs');
5
+
6
+ describe('schemaTraversal', () => {
7
+ it('visits nested choice and conditional schema nodes', () => {
8
+ const schema = {
9
+ type: 'object',
10
+ properties: {
11
+ payment_method: {
12
+ oneOf: [
13
+ {
14
+ properties: {
15
+ card_number: { type: 'string' },
16
+ },
17
+ },
18
+ ],
19
+ },
20
+ },
21
+ if: {
22
+ properties: {
23
+ platform: { const: 'ios' },
24
+ },
25
+ },
26
+ then: {
27
+ properties: {
28
+ att_status: { type: 'string' },
29
+ },
30
+ },
31
+ };
32
+
33
+ const visitedPaths = [];
34
+ visitSchemaNodes(schema, (_node, context) => {
35
+ visitedPaths.push(context.path.join('.'));
36
+ });
37
+
38
+ expect(visitedPaths).toEqual(
39
+ expect.arrayContaining([
40
+ '',
41
+ 'properties.payment_method',
42
+ 'properties.payment_method.oneOf.0',
43
+ 'properties.payment_method.oneOf.0.properties.card_number',
44
+ 'if',
45
+ 'then',
46
+ 'then.properties.att_status',
47
+ ]),
48
+ );
49
+ });
50
+
51
+ it('collects property entries through object, array, choice, and conditional branches', () => {
52
+ const schema = {
53
+ type: 'object',
54
+ properties: {
55
+ event: { type: 'string' },
56
+ items: {
57
+ type: 'array',
58
+ items: {
59
+ properties: {
60
+ sku: { type: 'string' },
61
+ },
62
+ },
63
+ },
64
+ contact_method: {
65
+ type: 'object',
66
+ oneOf: [
67
+ {
68
+ properties: {
69
+ email: { type: 'string' },
70
+ },
71
+ },
72
+ {
73
+ properties: {
74
+ phone_number: { type: 'string' },
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ then: {
81
+ properties: {
82
+ att_status: { type: 'string' },
83
+ },
84
+ },
85
+ else: {
86
+ properties: {
87
+ ad_personalization_enabled: { type: 'boolean' },
88
+ },
89
+ },
90
+ };
91
+
92
+ const variableNames = [];
93
+ visitSchemaPropertyEntries(schema, (_property, context) => {
94
+ variableNames.push(context.name);
95
+ });
96
+
97
+ expect(variableNames).toEqual(
98
+ expect.arrayContaining([
99
+ 'event',
100
+ 'items',
101
+ 'items.0.sku',
102
+ 'contact_method',
103
+ 'contact_method.email',
104
+ 'contact_method.phone_number',
105
+ 'att_status',
106
+ 'ad_personalization_enabled',
107
+ ]),
108
+ );
109
+ });
110
+ });
@@ -79,7 +79,7 @@ describe('getVariablesFromSchemas', () => {
79
79
 
80
80
  const SCHEMA_PATH = '/fake/schemas';
81
81
  const mockFiles = {
82
- [SCHEMA_PATH]: ['complex-event.json', 'components'],
82
+ [SCHEMA_PATH]: ['complex-event.json', 'mobile-event.json', 'components'],
83
83
  [path.join(SCHEMA_PATH, 'components')]: ['address.json'],
84
84
  };
85
85
  const addressSchema = {
@@ -92,6 +92,7 @@ describe('getVariablesFromSchemas', () => {
92
92
  };
93
93
  const complexEventSchema = {
94
94
  title: 'Complex Event',
95
+ 'x-tracking-targets': ['web-datalayer-js'],
95
96
  type: 'object',
96
97
  properties: {
97
98
  $schema: { type: 'string', description: 'Should now be included.' },
@@ -107,12 +108,79 @@ describe('getVariablesFromSchemas', () => {
107
108
  },
108
109
  },
109
110
  },
111
+ contact_method: {
112
+ type: 'object',
113
+ oneOf: [
114
+ {
115
+ title: 'Email Contact',
116
+ properties: {
117
+ email: {
118
+ type: 'string',
119
+ description: 'Email address.',
120
+ },
121
+ },
122
+ },
123
+ {
124
+ title: 'Phone Contact',
125
+ properties: {
126
+ phone_number: {
127
+ type: 'string',
128
+ description: 'Phone number.',
129
+ },
130
+ },
131
+ },
132
+ ],
133
+ },
134
+ platform: {
135
+ type: 'string',
136
+ description: 'Target platform.',
137
+ },
110
138
  timestamp: { type: 'number', description: 'Event timestamp.' },
111
139
  },
140
+ if: {
141
+ properties: {
142
+ platform: { const: 'ios' },
143
+ },
144
+ },
145
+ then: {
146
+ properties: {
147
+ att_status: {
148
+ type: 'string',
149
+ description: 'App Tracking Transparency status.',
150
+ },
151
+ },
152
+ },
153
+ else: {
154
+ properties: {
155
+ ad_personalization_enabled: {
156
+ type: 'boolean',
157
+ description: 'Whether ad personalization is enabled.',
158
+ },
159
+ },
160
+ },
161
+ };
162
+ const mobileEventSchema = {
163
+ title: 'Mobile Event',
164
+ 'x-tracking-targets': ['android-firebase-kotlin-sdk'],
165
+ type: 'object',
166
+ properties: {
167
+ event: { type: 'string', const: 'screen_view' },
168
+ screen_name: { type: 'string', description: 'Screen name.' },
169
+ },
170
+ };
171
+ const untaggedEventSchema = {
172
+ title: 'Untagged Event',
173
+ type: 'object',
174
+ properties: {
175
+ event: { type: 'string', const: 'legacy_event' },
176
+ legacy_field: { type: 'string', description: 'Legacy field.' },
177
+ },
112
178
  };
113
179
  const mockFileContents = {
114
180
  [path.join(SCHEMA_PATH, 'complex-event.json')]:
115
181
  JSON.stringify(complexEventSchema),
182
+ [path.join(SCHEMA_PATH, 'mobile-event.json')]:
183
+ JSON.stringify(mobileEventSchema),
116
184
  [path.join(SCHEMA_PATH, 'components', 'address.json')]:
117
185
  JSON.stringify(addressSchema),
118
186
  };
@@ -148,7 +216,7 @@ describe('getVariablesFromSchemas', () => {
148
216
  expect.objectContaining({ name: 'timestamp' }),
149
217
  ]);
150
218
 
151
- expect(result.length).toBe(8);
219
+ expect(result.length).toBe(14);
152
220
  expect(result).toEqual(expected);
153
221
  });
154
222
 
@@ -172,7 +240,163 @@ describe('getVariablesFromSchemas', () => {
172
240
  ];
173
241
 
174
242
  expect(result.map((r) => r.name)).toEqual(expect.arrayContaining(expected));
175
- expect(result.length).toBe(expected.length);
243
+ expect(result.length).toBe(12);
244
+ });
245
+
246
+ it('should include variables from oneOf choices and conditional branches', async () => {
247
+ const bundledSchema = JSON.parse(JSON.stringify(complexEventSchema));
248
+ bundledSchema.properties.user_data.properties.addresses.items =
249
+ addressSchema;
250
+ RefParser.bundle.mockResolvedValue(bundledSchema);
251
+
252
+ const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
253
+ const variableNames = result.map((variable) => variable.name);
254
+
255
+ expect(variableNames).toEqual(
256
+ expect.arrayContaining([
257
+ 'contact_method.email',
258
+ 'contact_method.phone_number',
259
+ 'att_status',
260
+ 'ad_personalization_enabled',
261
+ ]),
262
+ );
263
+ });
264
+
265
+ it('should only include schemas explicitly targeted to web-datalayer-js', async () => {
266
+ const untaggedEventPath = path.join(SCHEMA_PATH, 'legacy-event.json');
267
+ mockFiles[SCHEMA_PATH].push('legacy-event.json');
268
+ mockFileContents[untaggedEventPath] = JSON.stringify(untaggedEventSchema);
269
+
270
+ const bundledWebSchema = JSON.parse(JSON.stringify(complexEventSchema));
271
+ bundledWebSchema.properties.user_data.properties.addresses.items =
272
+ addressSchema;
273
+
274
+ RefParser.bundle.mockImplementation(async (filePath) => {
275
+ if (filePath.endsWith('complex-event.json')) {
276
+ return bundledWebSchema;
277
+ }
278
+ if (filePath.endsWith('mobile-event.json')) {
279
+ return mobileEventSchema;
280
+ }
281
+ if (filePath.endsWith('legacy-event.json')) {
282
+ return untaggedEventSchema;
283
+ }
284
+ if (filePath.endsWith('address.json')) {
285
+ return addressSchema;
286
+ }
287
+ throw new Error(`Unexpected schema file: ${filePath}`);
288
+ });
289
+
290
+ const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
291
+
292
+ expect(result.map((variable) => variable.name)).toEqual(
293
+ expect.arrayContaining([
294
+ '$schema',
295
+ 'event',
296
+ 'user_data',
297
+ 'user_data.user_id',
298
+ 'user_data.addresses',
299
+ 'user_data.addresses.0.street',
300
+ 'user_data.addresses.0.city',
301
+ 'timestamp',
302
+ ]),
303
+ );
304
+ expect(result.map((variable) => variable.name)).not.toContain(
305
+ 'screen_name',
306
+ );
307
+ expect(result.map((variable) => variable.name)).not.toContain(
308
+ 'legacy_field',
309
+ );
310
+ });
311
+
312
+ it('should include root tracking schemas based on content instead of path names', async () => {
313
+ const nestedSchemaDir = path.join(SCHEMA_PATH, 'event-components-demo');
314
+ const nestedSchemaPath = path.join(nestedSchemaDir, 'checkout-event.json');
315
+ const nestedSchema = {
316
+ title: 'Checkout Event',
317
+ 'x-tracking-targets': ['web-datalayer-js'],
318
+ type: 'object',
319
+ properties: {
320
+ event: { type: 'string', const: 'checkout' },
321
+ order_id: { type: 'string', description: 'Order identifier.' },
322
+ },
323
+ };
324
+
325
+ mockFiles[SCHEMA_PATH].push('event-components-demo');
326
+ mockFiles[nestedSchemaDir] = ['checkout-event.json'];
327
+ mockFileContents[nestedSchemaPath] = JSON.stringify(nestedSchema);
328
+
329
+ const bundledWebSchema = JSON.parse(JSON.stringify(complexEventSchema));
330
+ bundledWebSchema.properties.user_data.properties.addresses.items =
331
+ addressSchema;
332
+
333
+ RefParser.bundle.mockImplementation(async (filePath) => {
334
+ if (filePath.endsWith('complex-event.json')) {
335
+ return bundledWebSchema;
336
+ }
337
+ if (filePath.endsWith('mobile-event.json')) {
338
+ return mobileEventSchema;
339
+ }
340
+ if (filePath.endsWith('address.json')) {
341
+ return addressSchema;
342
+ }
343
+ if (filePath.endsWith('legacy-event.json')) {
344
+ return untaggedEventSchema;
345
+ }
346
+ if (filePath.endsWith('checkout-event.json')) {
347
+ return nestedSchema;
348
+ }
349
+ throw new Error(`Unexpected schema file: ${filePath}`);
350
+ });
351
+
352
+ const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
353
+ const variableNames = result.map((variable) => variable.name);
354
+
355
+ expect(variableNames).toContain('order_id');
356
+ expect(RefParser.bundle).toHaveBeenCalledWith(nestedSchemaPath);
357
+ });
358
+
359
+ it('should ignore component schemas even when scanning all json files', async () => {
360
+ const bundledWebSchema = JSON.parse(JSON.stringify(complexEventSchema));
361
+ bundledWebSchema.properties.user_data.properties.addresses.items =
362
+ addressSchema;
363
+ const nestedSchema = {
364
+ title: 'Checkout Event',
365
+ 'x-tracking-targets': ['web-datalayer-js'],
366
+ type: 'object',
367
+ properties: {
368
+ event: { type: 'string', const: 'checkout' },
369
+ order_id: { type: 'string', description: 'Order identifier.' },
370
+ },
371
+ };
372
+
373
+ RefParser.bundle.mockImplementation(async (filePath) => {
374
+ if (filePath.endsWith('complex-event.json')) {
375
+ return bundledWebSchema;
376
+ }
377
+ if (filePath.endsWith('mobile-event.json')) {
378
+ return mobileEventSchema;
379
+ }
380
+ if (filePath.endsWith('address.json')) {
381
+ return addressSchema;
382
+ }
383
+ if (filePath.endsWith('legacy-event.json')) {
384
+ return untaggedEventSchema;
385
+ }
386
+ if (filePath.endsWith('checkout-event.json')) {
387
+ return nestedSchema;
388
+ }
389
+ throw new Error(`Unexpected schema file: ${filePath}`);
390
+ });
391
+
392
+ const result = await gtmScript.getVariablesFromSchemas(SCHEMA_PATH, {});
393
+ const variableNames = result.map((variable) => variable.name);
394
+
395
+ expect(RefParser.bundle).toHaveBeenCalledWith(
396
+ path.join(SCHEMA_PATH, 'components', 'address.json'),
397
+ );
398
+ expect(variableNames).not.toContain('street');
399
+ expect(variableNames).not.toContain('city');
176
400
  });
177
401
  });
178
402
 
@@ -102,4 +102,54 @@ describe('validateSchemas', () => {
102
102
  expect(consoleErrorSpy).toHaveBeenCalled();
103
103
  expect(consoleErrorSpy.mock.calls[0][0]).toContain('x-tracking-targets');
104
104
  });
105
+
106
+ it('should treat falsy scalar examples as valid examples', async () => {
107
+ const schemaDir = path.join(tmpDir, 'schemas');
108
+ fs.mkdirSync(schemaDir, { recursive: true });
109
+ fs.writeFileSync(
110
+ path.join(schemaDir, 'event.json'),
111
+ JSON.stringify({
112
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
113
+ type: 'object',
114
+ properties: {
115
+ event: {
116
+ type: 'string',
117
+ const: 'test_event',
118
+ },
119
+ is_enabled: {
120
+ type: 'boolean',
121
+ example: false,
122
+ },
123
+ retry_count: {
124
+ type: 'integer',
125
+ example: 0,
126
+ },
127
+ note: {
128
+ type: 'string',
129
+ example: '',
130
+ },
131
+ },
132
+ required: ['event', 'is_enabled', 'retry_count', 'note'],
133
+ }),
134
+ );
135
+
136
+ const result = await validateSchemas(schemaDir);
137
+ expect(result).toBe(true);
138
+ });
139
+
140
+ it('should allow a top-level falsy example value', async () => {
141
+ const schemaDir = path.join(tmpDir, 'schemas');
142
+ fs.mkdirSync(schemaDir, { recursive: true });
143
+ fs.writeFileSync(
144
+ path.join(schemaDir, 'flag.json'),
145
+ JSON.stringify({
146
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
147
+ type: 'boolean',
148
+ example: false,
149
+ }),
150
+ );
151
+
152
+ const result = await validateSchemas(schemaDir);
153
+ expect(result).toBe(true);
154
+ });
105
155
  });
@@ -17,6 +17,8 @@ import {
17
17
  */
18
18
  export default function ConditionalRows({
19
19
  row,
20
+ stripeIndex = 0,
21
+ stripeState,
20
22
  bracketEnds: parentBracketEnds,
21
23
  }) {
22
24
  const {
@@ -91,7 +93,7 @@ export default function ConditionalRows({
91
93
  return (
92
94
  <>
93
95
  {/* Condition (if) section - always visible */}
94
- <tr className="conditional-condition-header">
96
+ <tr className="conditional-condition-header schema-row--control">
95
97
  <td colSpan={5} style={headerStyle}>
96
98
  <span className="conditional-condition-label">
97
99
  <span className="conditional-info-icon-wrapper">
@@ -111,7 +113,7 @@ export default function ConditionalRows({
111
113
  )}
112
114
  </td>
113
115
  </tr>
114
- <SchemaRows tableData={condition.rows} />
116
+ <SchemaRows tableData={condition.rows} stripeState={stripeState} />
115
117
 
116
118
  {/* Branch toggles (then / else) */}
117
119
  {branches.map((branch, index) => {
@@ -121,7 +123,7 @@ export default function ConditionalRows({
121
123
  isLastBranch && !isActive ? lastToggleStyle : middleStyle;
122
124
  return (
123
125
  <React.Fragment key={branch.title}>
124
- <tr className="choice-row">
126
+ <tr className="choice-row schema-row--control">
125
127
  <td colSpan={5} style={toggleStyle}>
126
128
  <label className="choice-row-header">
127
129
  <input
@@ -141,6 +143,7 @@ export default function ConditionalRows({
141
143
  {isActive && (
142
144
  <SchemaRows
143
145
  tableData={branch.rows}
146
+ stripeState={stripeState}
144
147
  bracketEnds={
145
148
  isLastBranch
146
149
  ? [ownBracket, ...(parentBracketEnds || [])]