docusaurus-plugin-generate-schema-docs 1.8.1 → 1.8.3

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 (37) hide show
  1. package/README.md +17 -7
  2. package/__tests__/__fixtures__/validateSchemas/main-schema-with-constraints-ref.json +20 -0
  3. package/__tests__/__snapshots__/generateEventDocs.anchor.test.js.snap +15 -3
  4. package/__tests__/__snapshots__/generateEventDocs.nested.test.js.snap +20 -4
  5. package/__tests__/__snapshots__/generateEventDocs.test.js.snap +30 -6
  6. package/__tests__/__snapshots__/generateEventDocs.versioned.test.js.snap +10 -2
  7. package/__tests__/components/ConditionalRows.test.js +28 -0
  8. package/__tests__/components/FoldableRows.test.js +31 -290
  9. package/__tests__/components/PropertyRow.test.js +216 -0
  10. package/__tests__/components/SchemaJsonViewer.test.js +76 -10
  11. package/__tests__/components/SchemaRows.test.js +62 -12
  12. package/__tests__/components/__snapshots__/ConnectorLines.visualRegression.test.js.snap +3 -3
  13. package/__tests__/generateEventDocs.partials.test.js +95 -0
  14. package/__tests__/generateEventDocs.test.js +3 -0
  15. package/__tests__/helpers/processSchema.test.js +29 -0
  16. package/__tests__/helpers/schemaToTableData.test.js +112 -0
  17. package/__tests__/helpers/validator.test.js +32 -0
  18. package/components/ConditionalRows.js +6 -3
  19. package/components/FoldableRows.js +9 -3
  20. package/components/PropertiesTable.js +2 -1
  21. package/components/PropertyRow.js +90 -5
  22. package/components/SchemaJsonViewer.js +221 -4
  23. package/components/SchemaRows.css +98 -7
  24. package/components/SchemaRows.js +11 -1
  25. package/generateEventDocs.js +184 -18
  26. package/helpers/buildExampleFromSchema.js +3 -3
  27. package/helpers/choice-index-template.js +6 -2
  28. package/helpers/constraintSchemaPaths.js +46 -0
  29. package/helpers/file-system.js +28 -0
  30. package/helpers/mergeSchema.js +16 -0
  31. package/helpers/processSchema.js +19 -6
  32. package/helpers/schema-doc-template.js +7 -1
  33. package/helpers/schema-processing.js +4 -11
  34. package/helpers/schemaToExamples.js +4 -4
  35. package/helpers/schemaToTableData.js +68 -7
  36. package/helpers/validator.js +7 -0
  37. package/package.json +1 -1
@@ -1,17 +1,21 @@
1
+ /* eslint-disable @docusaurus/no-html-links */
1
2
  import '@testing-library/jest-dom';
2
3
  import React from 'react';
3
- import { render } from '@testing-library/react';
4
+ import { fireEvent, render, screen } from '@testing-library/react';
4
5
  import SchemaJsonViewer from '../../components/SchemaJsonViewer';
5
6
 
6
- // Mock CodeBlock as it's an external theme component
7
- jest.mock('@theme/CodeBlock', () => {
8
- return function DummyCodeBlock({ children, language }) {
9
- return <pre data-language={language}>{children}</pre>;
10
- };
11
- });
7
+ jest.mock(
8
+ '@docusaurus/Link',
9
+ () => {
10
+ return function DocusaurusLink({ children, ...props }) {
11
+ return <a {...props}>{children}</a>;
12
+ };
13
+ },
14
+ { virtual: true },
15
+ );
12
16
 
13
17
  describe('SchemaJsonViewer', () => {
14
- it('renders the schema in a CodeBlock', () => {
18
+ it('renders the schema in a syntax-highlighted pre block', () => {
15
19
  const schema = {
16
20
  type: 'object',
17
21
  properties: {
@@ -29,8 +33,70 @@ describe('SchemaJsonViewer', () => {
29
33
  const codeBlockElement = container.querySelector('pre');
30
34
  expect(codeBlockElement).toBeInTheDocument();
31
35
  expect(codeBlockElement).toHaveAttribute('data-language', 'json');
32
- expect(codeBlockElement.textContent).toEqual(
33
- JSON.stringify(schema, null, 2),
36
+ expect(codeBlockElement.textContent.replace(/\s+/g, '')).toEqual(
37
+ JSON.stringify(schema, null, 2).replace(/\s+/g, ''),
38
+ );
39
+ });
40
+
41
+ it('navigates local $ref values inside the viewer and resets to root', () => {
42
+ const rootSchema = {
43
+ type: 'object',
44
+ properties: {
45
+ component: { $ref: './components/referenced.json' },
46
+ },
47
+ };
48
+ const referencedSchema = {
49
+ title: 'Referenced Component',
50
+ type: 'object',
51
+ properties: {
52
+ prop: { type: 'string' },
53
+ },
54
+ };
55
+
56
+ render(
57
+ <SchemaJsonViewer
58
+ schema={rootSchema}
59
+ sourcePath="main-schema.json"
60
+ schemaSources={{
61
+ 'main-schema.json': rootSchema,
62
+ 'components/referenced.json': referencedSchema,
63
+ }}
64
+ />,
34
65
  );
66
+
67
+ fireEvent.click(screen.getByText('View Raw JSON Schema'));
68
+ fireEvent.click(
69
+ screen.getByRole('button', { name: '"./components/referenced.json"' }),
70
+ );
71
+
72
+ expect(screen.getAllByText(/Referenced Component/).length).toBeGreaterThan(
73
+ 0,
74
+ );
75
+ expect(
76
+ screen.getByRole('button', { name: 'Back to root' }),
77
+ ).toBeInTheDocument();
78
+
79
+ fireEvent.click(screen.getByRole('button', { name: 'Back to root' }));
80
+
81
+ expect(
82
+ screen.getByText('"./components/referenced.json"'),
83
+ ).toBeInTheDocument();
84
+ });
85
+
86
+ it('renders external $ref values as new-tab links', () => {
87
+ const schema = {
88
+ $ref: 'https://example.com/schema.json',
89
+ };
90
+
91
+ render(<SchemaJsonViewer schema={schema} sourcePath="root.json" />);
92
+
93
+ fireEvent.click(screen.getByText('View Raw JSON Schema'));
94
+ const refLink = screen.getByRole('link', {
95
+ name: '"https://example.com/schema.json"',
96
+ });
97
+
98
+ expect(refLink).toHaveAttribute('href', 'https://example.com/schema.json');
99
+ expect(refLink).toHaveAttribute('target', '_blank');
100
+ expect(refLink).toHaveAttribute('rel', 'noreferrer');
35
101
  });
36
102
  });
@@ -7,7 +7,9 @@ import SchemaRows from '../../components/SchemaRows';
7
7
  jest.mock('../../components/PropertyRow', () => {
8
8
  const MockPropertyRow = (props) => (
9
9
  <tr>
10
- <td>Mocked PropertyRow: {props.row.name}</td>
10
+ <td>
11
+ Mocked PropertyRow: {props.row.name} ({props.stripeIndex})
12
+ </td>
11
13
  </tr>
12
14
  );
13
15
  MockPropertyRow.displayName = 'MockPropertyRow';
@@ -17,7 +19,10 @@ jest.mock('../../components/PropertyRow', () => {
17
19
  jest.mock('../../components/FoldableRows', () => {
18
20
  const MockFoldableRows = (props) => (
19
21
  <tr>
20
- <td>Mocked FoldableRows: {props.row.choiceType}</td>
22
+ <td>
23
+ Mocked FoldableRows: {props.row.choiceType} ({props.stripeIndex},{' '}
24
+ {String(!!props.stripeState)})
25
+ </td>
21
26
  </tr>
22
27
  );
23
28
  MockFoldableRows.displayName = 'MockFoldableRows';
@@ -27,7 +32,10 @@ jest.mock('../../components/FoldableRows', () => {
27
32
  jest.mock('../../components/ConditionalRows', () => {
28
33
  const MockConditionalRows = (props) => (
29
34
  <tr>
30
- <td>Mocked ConditionalRows: {props.row.condition.title}</td>
35
+ <td>
36
+ Mocked ConditionalRows: {props.row.condition.title} ({props.stripeIndex}
37
+ , {String(!!props.stripeState)})
38
+ </td>
31
39
  </tr>
32
40
  );
33
41
  MockConditionalRows.displayName = 'MockConditionalRows';
@@ -49,8 +57,8 @@ describe('SchemaRows', () => {
49
57
  </table>,
50
58
  );
51
59
 
52
- expect(getByText('Mocked PropertyRow: name')).toBeInTheDocument();
53
- expect(getByText('Mocked PropertyRow: age')).toBeInTheDocument();
60
+ expect(getByText('Mocked PropertyRow: name (0)')).toBeInTheDocument();
61
+ expect(getByText('Mocked PropertyRow: age (1)')).toBeInTheDocument();
54
62
  });
55
63
 
56
64
  it('renders nested properties from a flat list', () => {
@@ -68,8 +76,8 @@ describe('SchemaRows', () => {
68
76
  );
69
77
 
70
78
  // It should render both the parent and child property from the flat list
71
- expect(getByText('Mocked PropertyRow: user')).toBeInTheDocument();
72
- expect(getByText('Mocked PropertyRow: id')).toBeInTheDocument();
79
+ expect(getByText('Mocked PropertyRow: user (0)')).toBeInTheDocument();
80
+ expect(getByText('Mocked PropertyRow: id (1)')).toBeInTheDocument();
73
81
  });
74
82
 
75
83
  it('renders a FoldableRows for choice type items in tableData', () => {
@@ -90,7 +98,9 @@ describe('SchemaRows', () => {
90
98
  </table>,
91
99
  );
92
100
 
93
- expect(getByText('Mocked FoldableRows: oneOf')).toBeInTheDocument();
101
+ expect(
102
+ getByText('Mocked FoldableRows: oneOf (0, true)'),
103
+ ).toBeInTheDocument();
94
104
  });
95
105
 
96
106
  it('renders a mix of properties and choices', () => {
@@ -113,9 +123,11 @@ describe('SchemaRows', () => {
113
123
  </table>,
114
124
  );
115
125
 
116
- expect(getByText('Mocked PropertyRow: prop1')).toBeInTheDocument();
117
- expect(getByText('Mocked FoldableRows: anyOf')).toBeInTheDocument();
118
- expect(getByText('Mocked PropertyRow: prop2')).toBeInTheDocument();
126
+ expect(getByText('Mocked PropertyRow: prop1 (0)')).toBeInTheDocument();
127
+ expect(
128
+ getByText('Mocked FoldableRows: anyOf (1, true)'),
129
+ ).toBeInTheDocument();
130
+ expect(getByText('Mocked PropertyRow: prop2 (2)')).toBeInTheDocument();
119
131
  });
120
132
 
121
133
  it('renders a ConditionalRows for conditional type items in tableData', () => {
@@ -136,6 +148,44 @@ describe('SchemaRows', () => {
136
148
  </table>,
137
149
  );
138
150
 
139
- expect(getByText('Mocked ConditionalRows: If')).toBeInTheDocument();
151
+ expect(
152
+ getByText('Mocked ConditionalRows: If (0, true)'),
153
+ ).toBeInTheDocument();
154
+ });
155
+
156
+ it('increments stripe indices across logical rows', () => {
157
+ const tableData = [
158
+ { type: 'property', name: 'prop1', path: ['prop1'] },
159
+ {
160
+ type: 'choice',
161
+ choiceType: 'anyOf',
162
+ path: ['choice'],
163
+ options: [],
164
+ },
165
+ {
166
+ type: 'conditional',
167
+ path: ['if/then/else'],
168
+ condition: { title: 'If', rows: [] },
169
+ branches: [],
170
+ },
171
+ { type: 'property', name: 'prop2', path: ['prop2'] },
172
+ ];
173
+
174
+ const { getByText } = render(
175
+ <table>
176
+ <tbody>
177
+ <SchemaRows tableData={tableData} />
178
+ </tbody>
179
+ </table>,
180
+ );
181
+
182
+ expect(getByText('Mocked PropertyRow: prop1 (0)')).toBeInTheDocument();
183
+ expect(
184
+ getByText('Mocked FoldableRows: anyOf (1, true)'),
185
+ ).toBeInTheDocument();
186
+ expect(
187
+ getByText('Mocked ConditionalRows: If (2, true)'),
188
+ ).toBeInTheDocument();
189
+ expect(getByText('Mocked PropertyRow: prop2 (3)')).toBeInTheDocument();
140
190
  });
141
191
  });
@@ -1,7 +1,7 @@
1
1
  // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
- exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
3
+ exports[`connector lines visual regressions keeps cvv row open when payment choice has following options 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell property-cell--tree level-1"><span class="property-name"><strong>cvv</strong></span></td>"`;
4
4
 
5
- exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
5
+ exports[`connector lines visual regressions keeps user_id option row open when user conditional follows 1`] = `"<td rowspan="1" style="padding-left: 1.75rem;" class="property-cell property-cell--tree level-1"><span class="property-name"><strong>user_id</strong></span></td>"`;
6
6
 
7
- exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
7
+ exports[`connector lines visual regressions keeps wallet_provider option row open when wallet_email follows 1`] = `"<td rowspan="2" style="padding-left: 1.75rem;" class="property-cell property-cell--required property-cell--tree level-1"><span class="property-name"><strong>wallet_provider</strong></span></td>"`;
@@ -45,6 +45,48 @@ describe('generateEventDocs (partials)', () => {
45
45
  readDirRecursive(fixturesDir);
46
46
  });
47
47
 
48
+ function writeDuplicateNameOneOfSchemas() {
49
+ const schemaA = {
50
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
51
+ $id: 'https://example.com/schemas/duplicate-a.json',
52
+ title: 'Duplicate A',
53
+ oneOf: [
54
+ {
55
+ title: 'Shared Option',
56
+ type: 'object',
57
+ properties: {
58
+ event: { type: 'string', const: 'dup_a' },
59
+ },
60
+ required: ['event'],
61
+ },
62
+ ],
63
+ };
64
+ const schemaB = {
65
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
66
+ $id: 'https://example.com/schemas/duplicate-b.json',
67
+ title: 'Duplicate B',
68
+ oneOf: [
69
+ {
70
+ title: 'Shared Option',
71
+ type: 'object',
72
+ properties: {
73
+ event: { type: 'string', const: 'dup_b' },
74
+ },
75
+ required: ['event'],
76
+ },
77
+ ],
78
+ };
79
+
80
+ fs.vol.writeFileSync(
81
+ path.join(fixturesDir, 'static', 'schemas', 'duplicate-a.json'),
82
+ JSON.stringify(schemaA, null, 2),
83
+ );
84
+ fs.vol.writeFileSync(
85
+ path.join(fixturesDir, 'static', 'schemas', 'duplicate-b.json'),
86
+ JSON.stringify(schemaB, null, 2),
87
+ );
88
+ }
89
+
48
90
  it('injects top partial when _<eventName>.mdx exists in partials dir', async () => {
49
91
  console.log = jest.fn();
50
92
  fs.vol.mkdirSync(partialsDir, { recursive: true });
@@ -131,4 +173,57 @@ describe('generateEventDocs (partials)', () => {
131
173
  expect(output).not.toContain('TopPartial');
132
174
  expect(output).not.toContain('BottomPartial');
133
175
  });
176
+
177
+ it('skips basename fallback partials when event names are ambiguous', async () => {
178
+ console.log = jest.fn();
179
+ writeDuplicateNameOneOfSchemas();
180
+ fs.vol.mkdirSync(partialsDir, { recursive: true });
181
+ fs.vol.writeFileSync(
182
+ path.join(partialsDir, '_shared-option.mdx'),
183
+ 'Shared',
184
+ );
185
+
186
+ await generateEventDocs(options);
187
+
188
+ const outputA = fs.readFileSync(
189
+ path.join(outputDir, 'duplicate-a', '01-shared-option.mdx'),
190
+ 'utf-8',
191
+ );
192
+ const outputB = fs.readFileSync(
193
+ path.join(outputDir, 'duplicate-b', '01-shared-option.mdx'),
194
+ 'utf-8',
195
+ );
196
+
197
+ expect(outputA).not.toContain('TopPartial');
198
+ expect(outputB).not.toContain('TopPartial');
199
+ });
200
+
201
+ it('uses scoped partials for ambiguous event names', async () => {
202
+ console.log = jest.fn();
203
+ writeDuplicateNameOneOfSchemas();
204
+ fs.vol.mkdirSync(path.join(partialsDir, 'duplicate-a'), {
205
+ recursive: true,
206
+ });
207
+ fs.vol.writeFileSync(
208
+ path.join(partialsDir, 'duplicate-a', '_shared-option.mdx'),
209
+ 'Scoped shared',
210
+ );
211
+
212
+ await generateEventDocs(options);
213
+
214
+ const outputA = fs.readFileSync(
215
+ path.join(outputDir, 'duplicate-a', '01-shared-option.mdx'),
216
+ 'utf-8',
217
+ );
218
+ const outputB = fs.readFileSync(
219
+ path.join(outputDir, 'duplicate-b', '01-shared-option.mdx'),
220
+ 'utf-8',
221
+ );
222
+
223
+ expect(outputA).toContain(
224
+ "import TopPartial from '@site/docs/partials/duplicate-a/_shared-option.mdx'",
225
+ );
226
+ expect(outputA).toContain('<TopPartial />');
227
+ expect(outputB).not.toContain('TopPartial');
228
+ });
134
229
  });
@@ -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'),
@@ -53,4 +53,33 @@ describe('processSchema', () => {
53
53
  expect(mergedSchema.properties.b.title).toBe('Schema B');
54
54
  expect(mergedSchema.properties.b.properties.a.$ref).toBe('#');
55
55
  });
56
+
57
+ it('resolves published /constraints refs when bundling and merging allOf', async () => {
58
+ const filePath = path.join(
59
+ __dirname,
60
+ '..',
61
+ '__fixtures__',
62
+ 'validateSchemas',
63
+ 'main-schema-with-constraints-ref.json',
64
+ );
65
+ const mergedSchema = await processSchema(filePath);
66
+
67
+ expect(mergedSchema.title).toBe('Main Schema with Constraints Ref');
68
+ expect(mergedSchema.additionalProperties).toEqual(
69
+ expect.objectContaining({
70
+ type: expect.arrayContaining([
71
+ 'string',
72
+ 'number',
73
+ 'integer',
74
+ 'boolean',
75
+ 'null',
76
+ ]),
77
+ }),
78
+ );
79
+ expect(mergedSchema.properties.items).toEqual(
80
+ expect.objectContaining({
81
+ type: 'array',
82
+ }),
83
+ );
84
+ });
56
85
  });
@@ -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);
@@ -1,4 +1,5 @@
1
1
  import { createValidator } from '../../helpers/validator';
2
+ import path from 'path';
2
3
 
3
4
  describe('createValidator', () => {
4
5
  it('creates a validator that returns true for valid data with no schema version (draft-07)', async () => {
@@ -112,4 +113,35 @@ describe('createValidator', () => {
112
113
  expect(result.valid).toBe(true);
113
114
  expect(result.errors).toEqual([]);
114
115
  });
116
+
117
+ it('resolves published /constraints refs to local constraint schemas', async () => {
118
+ const schema = {
119
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
120
+ type: 'object',
121
+ allOf: [
122
+ {
123
+ $ref: 'https://tracking-docs-demo.buchert.digital/constraints/schemas/firebase/v1/flat-event-params.json',
124
+ },
125
+ ],
126
+ };
127
+
128
+ const schemaPath = path.resolve(
129
+ __dirname,
130
+ '../../../../demo/static/schemas/next',
131
+ );
132
+ const validator = await createValidator([], schema, schemaPath);
133
+
134
+ const valid = validator({
135
+ event: 'screen_view',
136
+ screen_name: 'Checkout',
137
+ attempt: 1,
138
+ });
139
+ expect(valid.valid).toBe(true);
140
+
141
+ const invalid = validator({
142
+ event: 'screen_view',
143
+ nested: { disallowed: true },
144
+ });
145
+ expect(invalid.valid).toBe(false);
146
+ });
115
147
  });
@@ -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 || [])]
@@ -18,7 +18,7 @@ const ChoiceRow = ({
18
18
  name,
19
19
  continuingLinesStyle,
20
20
  }) => (
21
- <tr className="choice-row">
21
+ <tr className="choice-row schema-row--control">
22
22
  <td colSpan={5} style={continuingLinesStyle}>
23
23
  <label className="choice-row-header">
24
24
  <input
@@ -41,7 +41,12 @@ const ChoiceRow = ({
41
41
  * Renders 'oneOf' and 'anyOf' choices as a set of foldable `<tr>` elements
42
42
  * that integrate directly into the main table body.
43
43
  */
44
- export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
44
+ export default function FoldableRows({
45
+ row,
46
+ stripeIndex = 0,
47
+ stripeState,
48
+ bracketEnds: parentBracketEnds,
49
+ }) {
45
50
  const {
46
51
  choiceType,
47
52
  options,
@@ -136,7 +141,7 @@ export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
136
141
  return (
137
142
  <>
138
143
  {/* A header row for the entire choice block */}
139
- <tr>
144
+ <tr className="schema-row--control">
140
145
  <td colSpan={5} style={headerStyle}>
141
146
  <Heading as="h4" className="choice-row-header-headline">
142
147
  {header}
@@ -176,6 +181,7 @@ export default function FoldableRows({ row, bracketEnds: parentBracketEnds }) {
176
181
  {isActive && (
177
182
  <SchemaRows
178
183
  tableData={option.rows}
184
+ stripeState={stripeState}
179
185
  bracketEnds={
180
186
  isLastOption
181
187
  ? [ownBracket, ...(parentBracketEnds || [])]
@@ -8,6 +8,7 @@ import styles from './PropertiesTable.module.css';
8
8
  export default function PropertiesTable({ schema }) {
9
9
  const [isWordWrapOn, setIsWordWrapOn] = useState(true);
10
10
  const tableData = schemaToTableData(schema);
11
+ const stripeState = { current: 0 };
11
12
 
12
13
  return (
13
14
  <div
@@ -24,7 +25,7 @@ export default function PropertiesTable({ schema }) {
24
25
  <table className="schema-table">
25
26
  <TableHeader />
26
27
  <tbody>
27
- <SchemaRows tableData={tableData} />
28
+ <SchemaRows tableData={tableData} stripeState={stripeState} />
28
29
  </tbody>
29
30
  </table>
30
31
  </div>