@adobe/spectrum-component-api-schemas 1.0.0 → 1.0.1
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/CHANGELOG.md +17 -4
- package/ava.config.js +9 -0
- package/index.js +3 -0
- package/moon.yml +26 -1
- package/package.json +1 -1
- package/schemas/components/select-box.json +3 -0
- package/schemas/components/text-area.json +3 -8
- package/schemas/components/text-field.json +3 -8
- package/test/README.md +97 -0
- package/test/integration.test.js +118 -0
- package/test/performance.test.js +121 -0
- package/test/schema-validation.test.js +131 -0
- package/test/utils/test-helpers.js +89 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @adobe/spectrum-component-api-schemas
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#523](https://github.com/adobe/spectrum-tokens/pull/523) [`9c5a2ac`](https://github.com/adobe/spectrum-tokens/commit/9c5a2ac5fccb29b6f106396b21d91aab949043d4) Thanks [@GarthDB](https://github.com/GarthDB)! - S2 components batch 1 (part 2)
|
|
8
|
+
|
|
9
|
+
## Changes
|
|
10
|
+
|
|
11
|
+
### Properties added
|
|
12
|
+
- component: select-box
|
|
13
|
+
- `body`
|
|
14
|
+
|
|
15
|
+
### Properties updated
|
|
16
|
+
- component: text-area
|
|
17
|
+
- `errorMessage`
|
|
18
|
+
- removed: `"default": null`
|
|
19
|
+
|
|
3
20
|
## 1.0.0
|
|
4
21
|
|
|
5
22
|
### Major Changes
|
|
@@ -9,7 +26,6 @@
|
|
|
9
26
|
## Changes
|
|
10
27
|
|
|
11
28
|
### Properties Added
|
|
12
|
-
|
|
13
29
|
- component: search-field
|
|
14
30
|
- `helpText`
|
|
15
31
|
- `placeholder`
|
|
@@ -29,7 +45,6 @@
|
|
|
29
45
|
- `helpText`
|
|
30
46
|
|
|
31
47
|
### Properties removed
|
|
32
|
-
|
|
33
48
|
- component: search-field
|
|
34
49
|
- `isQuiet`
|
|
35
50
|
- component: text-area
|
|
@@ -40,7 +55,6 @@
|
|
|
40
55
|
- `isReadOnly`
|
|
41
56
|
|
|
42
57
|
### Properties updated
|
|
43
|
-
|
|
44
58
|
- component: meter
|
|
45
59
|
- `size`:
|
|
46
60
|
- `enum`: `["small", "large"]` -> `["s", "m", "l", "xl"]`
|
|
@@ -54,7 +68,6 @@
|
|
|
54
68
|
- `default`: `6` -> `8`
|
|
55
69
|
|
|
56
70
|
### New Component
|
|
57
|
-
|
|
58
71
|
- select-box
|
|
59
72
|
|
|
60
73
|
## 0.0.0
|
package/ava.config.js
ADDED
package/index.js
CHANGED
|
@@ -87,6 +87,9 @@ export const getSchemaBySlug = async (slug) => {
|
|
|
87
87
|
return Object.hasOwn(schema, "slug") && schema.slug === slug;
|
|
88
88
|
}),
|
|
89
89
|
);
|
|
90
|
+
if (schema === undefined) {
|
|
91
|
+
throw new Error(`Schema not found for slug: ${slug}`);
|
|
92
|
+
}
|
|
90
93
|
delete schema.slug;
|
|
91
94
|
return schema;
|
|
92
95
|
};
|
package/moon.yml
CHANGED
|
@@ -22,7 +22,8 @@ tasks:
|
|
|
22
22
|
inputs:
|
|
23
23
|
- "@globs(schemas)"
|
|
24
24
|
- "@globs(tests)"
|
|
25
|
-
- index.js
|
|
25
|
+
- "index.js"
|
|
26
|
+
- "ava.config.js"
|
|
26
27
|
platform: node
|
|
27
28
|
test-watch:
|
|
28
29
|
command:
|
|
@@ -30,3 +31,27 @@ tasks:
|
|
|
30
31
|
- --watch
|
|
31
32
|
local: true
|
|
32
33
|
platform: node
|
|
34
|
+
test-coverage:
|
|
35
|
+
command:
|
|
36
|
+
- c8
|
|
37
|
+
- ava
|
|
38
|
+
local: true
|
|
39
|
+
platform: node
|
|
40
|
+
test-schema-validation:
|
|
41
|
+
command:
|
|
42
|
+
- ava
|
|
43
|
+
- test/schema-validation.test.js
|
|
44
|
+
local: true
|
|
45
|
+
platform: node
|
|
46
|
+
test-integration:
|
|
47
|
+
command:
|
|
48
|
+
- ava
|
|
49
|
+
- test/integration.test.js
|
|
50
|
+
local: true
|
|
51
|
+
platform: node
|
|
52
|
+
test-performance:
|
|
53
|
+
command:
|
|
54
|
+
- ava
|
|
55
|
+
- test/performance.test.js
|
|
56
|
+
local: true
|
|
57
|
+
platform: node
|
package/package.json
CHANGED
|
@@ -34,9 +34,6 @@
|
|
|
34
34
|
"enum": ["text", "icon"],
|
|
35
35
|
"default": "icon"
|
|
36
36
|
},
|
|
37
|
-
"helpText": {
|
|
38
|
-
"type": "string"
|
|
39
|
-
},
|
|
40
37
|
"isRequired": {
|
|
41
38
|
"type": "boolean",
|
|
42
39
|
"default": false
|
|
@@ -65,13 +62,11 @@
|
|
|
65
62
|
"type": "number",
|
|
66
63
|
"description": "If undefined, height is dynamic and grows with input text."
|
|
67
64
|
},
|
|
68
|
-
"
|
|
69
|
-
"type": "string"
|
|
70
|
-
"default": null
|
|
65
|
+
"helpText": {
|
|
66
|
+
"type": "string"
|
|
71
67
|
},
|
|
72
68
|
"errorMessage": {
|
|
73
|
-
"type": "string"
|
|
74
|
-
"default": null
|
|
69
|
+
"type": "string"
|
|
75
70
|
},
|
|
76
71
|
"inputType": {
|
|
77
72
|
"type": "string",
|
|
@@ -34,9 +34,6 @@
|
|
|
34
34
|
"enum": ["text", "icon"],
|
|
35
35
|
"default": "icon"
|
|
36
36
|
},
|
|
37
|
-
"helpText": {
|
|
38
|
-
"type": "string"
|
|
39
|
-
},
|
|
40
37
|
"isRequired": {
|
|
41
38
|
"type": "boolean",
|
|
42
39
|
"default": false
|
|
@@ -58,13 +55,11 @@
|
|
|
58
55
|
"type": "boolean",
|
|
59
56
|
"default": false
|
|
60
57
|
},
|
|
61
|
-
"
|
|
62
|
-
"type": "string"
|
|
63
|
-
"default": null
|
|
58
|
+
"helpText": {
|
|
59
|
+
"type": "string"
|
|
64
60
|
},
|
|
65
61
|
"errorMessage": {
|
|
66
|
-
"type": "string"
|
|
67
|
-
"default": null
|
|
62
|
+
"type": "string"
|
|
68
63
|
},
|
|
69
64
|
"state": {
|
|
70
65
|
"type": "string",
|
package/test/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Component Schemas Testing
|
|
2
|
+
|
|
3
|
+
This directory contains comprehensive tests for the component-schemas package.
|
|
4
|
+
|
|
5
|
+
## Test Structure
|
|
6
|
+
|
|
7
|
+
### Core Tests
|
|
8
|
+
|
|
9
|
+
- `index.test.js` - Tests for the main API functions
|
|
10
|
+
- `componentSchemaValidator.test.js` - Legacy schema validation tests
|
|
11
|
+
|
|
12
|
+
### New Test Suite
|
|
13
|
+
|
|
14
|
+
- `schema-validation.test.js` - Comprehensive schema validation using test utilities
|
|
15
|
+
- `integration.test.js` - Integration tests for full workflow
|
|
16
|
+
- `performance.test.js` - Performance benchmarks
|
|
17
|
+
- `utils/test-helpers.js` - Shared test utilities
|
|
18
|
+
|
|
19
|
+
## Running Tests
|
|
20
|
+
|
|
21
|
+
### Using Moon (Recommended)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Run all tests
|
|
25
|
+
moon run component-schemas:test
|
|
26
|
+
|
|
27
|
+
# Run specific test suites
|
|
28
|
+
moon run component-schemas:test-schema-validation
|
|
29
|
+
moon run component-schemas:test-integration
|
|
30
|
+
moon run component-schemas:test-performance
|
|
31
|
+
|
|
32
|
+
# Run with coverage
|
|
33
|
+
moon run component-schemas:test-coverage
|
|
34
|
+
|
|
35
|
+
# Watch mode
|
|
36
|
+
moon run component-schemas:test-watch
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Test Categories
|
|
40
|
+
|
|
41
|
+
### 1. Schema Validation Tests
|
|
42
|
+
|
|
43
|
+
- Validates all component schemas against the base component definition
|
|
44
|
+
- Ensures all examples in schemas are valid
|
|
45
|
+
- Checks that type schemas are valid JSON Schema
|
|
46
|
+
- Verifies required metadata is present
|
|
47
|
+
|
|
48
|
+
### 2. Integration Tests
|
|
49
|
+
|
|
50
|
+
- Tests the full workflow of schema loading and processing
|
|
51
|
+
- Validates API function behavior with real data
|
|
52
|
+
- Ensures data consistency across functions
|
|
53
|
+
- Tests error handling and edge cases
|
|
54
|
+
|
|
55
|
+
### 3. Performance Tests
|
|
56
|
+
|
|
57
|
+
- Benchmarks function execution times
|
|
58
|
+
- Monitors memory usage
|
|
59
|
+
- Tests concurrent operations
|
|
60
|
+
- Ensures performance meets requirements
|
|
61
|
+
|
|
62
|
+
## Test Utilities
|
|
63
|
+
|
|
64
|
+
The `utils/test-helpers.js` file provides common utilities:
|
|
65
|
+
|
|
66
|
+
- `readJSON()` - Read and parse JSON files
|
|
67
|
+
- `getSchemaFiles()` - Get schema files using glob patterns
|
|
68
|
+
- `createAjvInstance()` - Create configured Ajv instance
|
|
69
|
+
- `validateSchema()` - Validate schemas with detailed error reporting
|
|
70
|
+
- `validateExamples()` - Validate examples in schemas
|
|
71
|
+
|
|
72
|
+
## Coverage
|
|
73
|
+
|
|
74
|
+
Test coverage includes:
|
|
75
|
+
|
|
76
|
+
- ✅ All exported functions
|
|
77
|
+
- ✅ Schema validation logic
|
|
78
|
+
- ✅ Error handling
|
|
79
|
+
- ✅ Edge cases
|
|
80
|
+
- ✅ Performance benchmarks
|
|
81
|
+
|
|
82
|
+
## Adding New Tests
|
|
83
|
+
|
|
84
|
+
1. Create test file in the appropriate category
|
|
85
|
+
2. Use test utilities from `utils/test-helpers.js`
|
|
86
|
+
3. Add descriptive test names and assertions
|
|
87
|
+
4. Update this README if adding new test categories
|
|
88
|
+
5. Consider adding performance tests for new functions
|
|
89
|
+
|
|
90
|
+
## Best Practices
|
|
91
|
+
|
|
92
|
+
- Use descriptive test names that explain the expected behavior
|
|
93
|
+
- Group related tests together
|
|
94
|
+
- Use test utilities for common operations
|
|
95
|
+
- Include both positive and negative test cases
|
|
96
|
+
- Add performance tests for new functions
|
|
97
|
+
- Keep tests focused and independent
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import test from "ava";
|
|
2
|
+
import {
|
|
3
|
+
getAllSchemas,
|
|
4
|
+
getSchemaBySlug,
|
|
5
|
+
getAllSlugs,
|
|
6
|
+
getSlugFromDocumentationUrl,
|
|
7
|
+
schemaFileNames,
|
|
8
|
+
} from "../index.js";
|
|
9
|
+
import { getSchemaFiles } from "./utils/test-helpers.js";
|
|
10
|
+
import { relative } from "path";
|
|
11
|
+
|
|
12
|
+
test("getAllSchemas should return all schemas with slugs", async (t) => {
|
|
13
|
+
const schemas = await getAllSchemas();
|
|
14
|
+
|
|
15
|
+
t.true(Array.isArray(schemas));
|
|
16
|
+
t.true(schemas.length > 0);
|
|
17
|
+
|
|
18
|
+
// Check that schemas with documentation URLs have slugs
|
|
19
|
+
const schemasWithSlugs = schemas.filter((schema) => schema.slug);
|
|
20
|
+
t.true(schemasWithSlugs.length > 0);
|
|
21
|
+
|
|
22
|
+
// Verify slug format
|
|
23
|
+
for (const schema of schemasWithSlugs) {
|
|
24
|
+
t.true(typeof schema.slug === "string");
|
|
25
|
+
t.true(schema.slug.length > 0);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("getAllSlugs should return all component slugs", async (t) => {
|
|
30
|
+
const slugs = await getAllSlugs();
|
|
31
|
+
|
|
32
|
+
t.true(Array.isArray(slugs));
|
|
33
|
+
t.true(slugs.length > 0);
|
|
34
|
+
|
|
35
|
+
// Verify all slugs are unique
|
|
36
|
+
const uniqueSlugs = new Set(slugs);
|
|
37
|
+
t.is(uniqueSlugs.size, slugs.length, "All slugs should be unique");
|
|
38
|
+
|
|
39
|
+
// Verify slug format (should be kebab-case)
|
|
40
|
+
for (const slug of slugs) {
|
|
41
|
+
t.true(/^[a-z0-9-]+$/.test(slug), `Slug "${slug}" should be kebab-case`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("getSchemaBySlug should return correct schema", async (t) => {
|
|
46
|
+
const slugs = await getAllSlugs();
|
|
47
|
+
|
|
48
|
+
if (slugs.length === 0) {
|
|
49
|
+
t.skip("No slugs available for testing");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const testSlug = slugs[0];
|
|
54
|
+
const schema = await getSchemaBySlug(testSlug);
|
|
55
|
+
|
|
56
|
+
t.truthy(schema);
|
|
57
|
+
t.true(typeof schema === "object");
|
|
58
|
+
t.true(Object.hasOwn(schema.meta, "documentationUrl"));
|
|
59
|
+
|
|
60
|
+
// Verify the slug was extracted correctly
|
|
61
|
+
const expectedSlug = getSlugFromDocumentationUrl(
|
|
62
|
+
schema.meta.documentationUrl,
|
|
63
|
+
);
|
|
64
|
+
t.is(expectedSlug, testSlug);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("getSlugFromDocumentationUrl should handle various URL formats", (t) => {
|
|
68
|
+
const testCases = [
|
|
69
|
+
{ url: "https://spectrum.adobe.com/page/tooltip/", expected: "tooltip" },
|
|
70
|
+
{ url: "https://spectrum.adobe.com/page/tooltip", expected: "tooltip" },
|
|
71
|
+
{
|
|
72
|
+
url: "https://spectrum.adobe.com/page/action-bar/",
|
|
73
|
+
expected: "action-bar",
|
|
74
|
+
},
|
|
75
|
+
{ url: "https://spectrum.adobe.com/page/button/", expected: "button" },
|
|
76
|
+
{ url: "https://spectrum.adobe.com/page/button", expected: "button" },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
for (const { url, expected } of testCases) {
|
|
80
|
+
const result = getSlugFromDocumentationUrl(url);
|
|
81
|
+
t.is(result, expected, `Failed for URL: ${url}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("schemaFileNames should match actual files on disk", async (t) => {
|
|
86
|
+
const actualFiles = await getSchemaFiles();
|
|
87
|
+
|
|
88
|
+
t.is(schemaFileNames.length, actualFiles.length);
|
|
89
|
+
|
|
90
|
+
// Verify all files in schemaFileNames exist
|
|
91
|
+
for (const fileName of schemaFileNames) {
|
|
92
|
+
const relativePath = relative(process.cwd(), fileName);
|
|
93
|
+
t.true(
|
|
94
|
+
actualFiles.includes(relativePath),
|
|
95
|
+
`File ${fileName} not found on disk`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("getAllSchemas should return same number as schemaFileNames", async (t) => {
|
|
101
|
+
const allSchemas = await getAllSchemas();
|
|
102
|
+
|
|
103
|
+
t.is(schemaFileNames.length, allSchemas.length);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("getSchemaBySlug should throw for non-existent slug", async (t) => {
|
|
107
|
+
const nonExistentSlug = "non-existent-component";
|
|
108
|
+
|
|
109
|
+
await t.throwsAsync(
|
|
110
|
+
async () => {
|
|
111
|
+
const schema = await getSchemaBySlug(nonExistentSlug);
|
|
112
|
+
if (!schema) {
|
|
113
|
+
throw new Error("Schema not found");
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{ message: /Schema not found/ },
|
|
117
|
+
);
|
|
118
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import test from "ava";
|
|
2
|
+
import { performance } from "perf_hooks";
|
|
3
|
+
import {
|
|
4
|
+
getAllSchemas,
|
|
5
|
+
getSchemaBySlug,
|
|
6
|
+
getAllSlugs,
|
|
7
|
+
getSchemaFile,
|
|
8
|
+
} from "../index.js";
|
|
9
|
+
|
|
10
|
+
test("getAllSchemas should complete within reasonable time", async (t) => {
|
|
11
|
+
const start = performance.now();
|
|
12
|
+
const schemas = await getAllSchemas();
|
|
13
|
+
const end = performance.now();
|
|
14
|
+
const duration = end - start;
|
|
15
|
+
|
|
16
|
+
// Should complete within 1 second
|
|
17
|
+
t.true(
|
|
18
|
+
duration < 1000,
|
|
19
|
+
`getAllSchemas took ${duration.toFixed(2)}ms, expected < 1000ms`,
|
|
20
|
+
);
|
|
21
|
+
t.true(schemas.length > 0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("getAllSlugs should complete within reasonable time", async (t) => {
|
|
25
|
+
const start = performance.now();
|
|
26
|
+
const slugs = await getAllSlugs();
|
|
27
|
+
const end = performance.now();
|
|
28
|
+
const duration = end - start;
|
|
29
|
+
|
|
30
|
+
// Should complete within 500ms
|
|
31
|
+
t.true(
|
|
32
|
+
duration < 500,
|
|
33
|
+
`getAllSlugs took ${duration.toFixed(2)}ms, expected < 500ms`,
|
|
34
|
+
);
|
|
35
|
+
t.true(slugs.length > 0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("getSchemaBySlug should complete within reasonable time", async (t) => {
|
|
39
|
+
const slugs = await getAllSlugs();
|
|
40
|
+
|
|
41
|
+
if (slugs.length === 0) {
|
|
42
|
+
t.skip("No slugs available for testing");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const testSlug = slugs[0];
|
|
47
|
+
const start = performance.now();
|
|
48
|
+
const schema = await getSchemaBySlug(testSlug);
|
|
49
|
+
const end = performance.now();
|
|
50
|
+
const duration = end - start;
|
|
51
|
+
|
|
52
|
+
// Should complete within 100ms
|
|
53
|
+
t.true(
|
|
54
|
+
duration < 100,
|
|
55
|
+
`getSchemaBySlug took ${duration.toFixed(2)}ms, expected < 100ms`,
|
|
56
|
+
);
|
|
57
|
+
t.truthy(schema);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("getSchemaFile should complete within reasonable time", async (t) => {
|
|
61
|
+
const start = performance.now();
|
|
62
|
+
const schema = await getSchemaFile("component.json");
|
|
63
|
+
const end = performance.now();
|
|
64
|
+
const duration = end - start;
|
|
65
|
+
|
|
66
|
+
// Should complete within 50ms
|
|
67
|
+
t.true(
|
|
68
|
+
duration < 50,
|
|
69
|
+
`getSchemaFile took ${duration.toFixed(2)}ms, expected < 100ms`,
|
|
70
|
+
);
|
|
71
|
+
t.truthy(schema);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("multiple concurrent getSchemaBySlug calls should complete efficiently", async (t) => {
|
|
75
|
+
const slugs = await getAllSlugs();
|
|
76
|
+
|
|
77
|
+
if (slugs.length < 3) {
|
|
78
|
+
t.skip("Not enough slugs for concurrent testing");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const testSlugs = slugs.slice(0, 3);
|
|
83
|
+
const start = performance.now();
|
|
84
|
+
|
|
85
|
+
const results = await Promise.all(
|
|
86
|
+
testSlugs.map((slug) => getSchemaBySlug(slug)),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const end = performance.now();
|
|
90
|
+
const duration = end - start;
|
|
91
|
+
|
|
92
|
+
// Should complete within 200ms for 3 concurrent calls
|
|
93
|
+
t.true(
|
|
94
|
+
duration < 200,
|
|
95
|
+
`Concurrent getSchemaBySlug calls took ${duration.toFixed(2)}ms, expected < 200ms`,
|
|
96
|
+
);
|
|
97
|
+
t.is(results.length, 3);
|
|
98
|
+
t.true(results.every((schema) => schema !== null));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("memory usage should be reasonable", async (t) => {
|
|
102
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
103
|
+
|
|
104
|
+
// Perform multiple operations
|
|
105
|
+
const schemas = await getAllSchemas();
|
|
106
|
+
const slugs = await getAllSlugs();
|
|
107
|
+
|
|
108
|
+
if (slugs.length > 0) {
|
|
109
|
+
await getSchemaBySlug(slugs[0]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
113
|
+
const memoryIncrease = finalMemory - initialMemory;
|
|
114
|
+
|
|
115
|
+
// Memory increase should be less than 10MB
|
|
116
|
+
const memoryIncreaseMB = memoryIncrease / 1024 / 1024;
|
|
117
|
+
t.true(
|
|
118
|
+
memoryIncreaseMB < 10,
|
|
119
|
+
`Memory usage increased by ${memoryIncreaseMB.toFixed(2)}MB, expected < 10MB`,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import test from "ava";
|
|
2
|
+
import {
|
|
3
|
+
readJSON,
|
|
4
|
+
getSchemaFiles,
|
|
5
|
+
createAjvInstance,
|
|
6
|
+
validateSchema,
|
|
7
|
+
validateExamples,
|
|
8
|
+
} from "./utils/test-helpers.js";
|
|
9
|
+
|
|
10
|
+
// Setup Ajv instance once for all tests
|
|
11
|
+
let ajv;
|
|
12
|
+
|
|
13
|
+
test.before(async () => {
|
|
14
|
+
ajv = await createAjvInstance();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("component schema should be valid", async (t) => {
|
|
18
|
+
const componentSchema = await readJSON("schemas/component.json");
|
|
19
|
+
const result = validateSchema(componentSchema, ajv);
|
|
20
|
+
|
|
21
|
+
t.true(
|
|
22
|
+
result.valid,
|
|
23
|
+
`Component schema validation failed: ${JSON.stringify(result.errors, null, 2)}`,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("all component schemas should validate against the definition", async (t) => {
|
|
28
|
+
const componentFiles = await getSchemaFiles("schemas/components/*.json");
|
|
29
|
+
const validationResults = [];
|
|
30
|
+
|
|
31
|
+
for (const filePath of componentFiles) {
|
|
32
|
+
const schema = await readJSON(filePath);
|
|
33
|
+
const result = validateSchema(schema, ajv);
|
|
34
|
+
|
|
35
|
+
if (!result.valid) {
|
|
36
|
+
validationResults.push({
|
|
37
|
+
file: filePath,
|
|
38
|
+
errors: result.errors,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
t.is(
|
|
44
|
+
validationResults.length,
|
|
45
|
+
0,
|
|
46
|
+
`Schema validation failed for:\n${validationResults
|
|
47
|
+
.map((r) => `${r.file}:\n${JSON.stringify(r.errors, null, 2)}`)
|
|
48
|
+
.join("\n\n")}`,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("all component examples should validate against their schemas", async (t) => {
|
|
53
|
+
const componentFiles = await getSchemaFiles("schemas/components/*.json");
|
|
54
|
+
const validationResults = [];
|
|
55
|
+
|
|
56
|
+
for (const filePath of componentFiles) {
|
|
57
|
+
const schema = await readJSON(filePath);
|
|
58
|
+
const result = validateExamples(schema, ajv);
|
|
59
|
+
|
|
60
|
+
if (!result.valid) {
|
|
61
|
+
validationResults.push({
|
|
62
|
+
file: filePath,
|
|
63
|
+
errors: result.errors,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
t.is(
|
|
69
|
+
validationResults.length,
|
|
70
|
+
0,
|
|
71
|
+
`Example validation failed for:\n${validationResults
|
|
72
|
+
.map((r) => `${r.file}:\n${JSON.stringify(r.errors, null, 2)}`)
|
|
73
|
+
.join("\n\n")}`,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("all type schemas should be valid JSON Schema", async (t) => {
|
|
78
|
+
const typeFiles = await getSchemaFiles("schemas/types/*.json");
|
|
79
|
+
const validationResults = [];
|
|
80
|
+
|
|
81
|
+
for (const filePath of typeFiles) {
|
|
82
|
+
const schema = await readJSON(filePath);
|
|
83
|
+
const result = validateSchema(schema, ajv);
|
|
84
|
+
|
|
85
|
+
if (!result.valid) {
|
|
86
|
+
validationResults.push({
|
|
87
|
+
file: filePath,
|
|
88
|
+
errors: result.errors,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
t.is(
|
|
94
|
+
validationResults.length,
|
|
95
|
+
0,
|
|
96
|
+
`Type schema validation failed for:\n${validationResults
|
|
97
|
+
.map((r) => `${r.file}:\n${JSON.stringify(r.errors, null, 2)}`)
|
|
98
|
+
.join("\n\n")}`,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("component schemas should have required metadata", async (t) => {
|
|
103
|
+
const componentFiles = await getSchemaFiles("schemas/components/*.json");
|
|
104
|
+
const missingMetadata = [];
|
|
105
|
+
|
|
106
|
+
for (const filePath of componentFiles) {
|
|
107
|
+
const schema = await readJSON(filePath);
|
|
108
|
+
|
|
109
|
+
if (!schema.meta?.category) {
|
|
110
|
+
missingMetadata.push(`${filePath}: missing category`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!schema.meta?.documentationUrl) {
|
|
114
|
+
missingMetadata.push(`${filePath}: missing documentationUrl`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!schema.title) {
|
|
118
|
+
missingMetadata.push(`${filePath}: missing title`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!Object.hasOwn(schema, "description")) {
|
|
122
|
+
missingMetadata.push(`${filePath}: missing description`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
t.is(
|
|
127
|
+
missingMetadata.length,
|
|
128
|
+
0,
|
|
129
|
+
`Missing required metadata:\n${missingMetadata.join("\n")}`,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import Ajv from "ajv/dist/2020.js";
|
|
5
|
+
import addFormats from "ajv-formats";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read and parse a JSON file
|
|
9
|
+
* @param {string} filePath - Path to the JSON file
|
|
10
|
+
* @returns {Promise<Object>} Parsed JSON object
|
|
11
|
+
*/
|
|
12
|
+
export const readJSON = async (filePath) =>
|
|
13
|
+
JSON.parse(await readFile(filePath, "utf8"));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get all schema files matching a pattern
|
|
17
|
+
* @param {string} pattern - Glob pattern for schema files
|
|
18
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
19
|
+
*/
|
|
20
|
+
export const getSchemaFiles = async (pattern = "schemas/**/*.json") =>
|
|
21
|
+
await glob(pattern);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a configured Ajv instance with common schemas
|
|
25
|
+
* @returns {Promise<Ajv>} Configured Ajv instance
|
|
26
|
+
*/
|
|
27
|
+
export const createAjvInstance = async () => {
|
|
28
|
+
const ajv = new Ajv({
|
|
29
|
+
allErrors: true,
|
|
30
|
+
verbose: true,
|
|
31
|
+
strict: false,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
addFormats(ajv);
|
|
35
|
+
|
|
36
|
+
// Add component schema as meta schema
|
|
37
|
+
const componentSchema = await readJSON("schemas/component.json");
|
|
38
|
+
ajv.addMetaSchema(componentSchema);
|
|
39
|
+
|
|
40
|
+
// Add type schemas
|
|
41
|
+
const typeSchemaFiles = await glob("schemas/types/*.json");
|
|
42
|
+
for (const schemaFile of typeSchemaFiles) {
|
|
43
|
+
const schema = await readJSON(schemaFile);
|
|
44
|
+
ajv.addSchema(schema);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return ajv;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate a schema against the component definition
|
|
52
|
+
* @param {Object} schema - Schema to validate
|
|
53
|
+
* @param {Ajv} ajv - Ajv instance
|
|
54
|
+
* @returns {Object} Validation result with errors if any
|
|
55
|
+
*/
|
|
56
|
+
export const validateSchema = (schema, ajv) => {
|
|
57
|
+
const valid = ajv.validateSchema(schema);
|
|
58
|
+
return {
|
|
59
|
+
valid,
|
|
60
|
+
errors: ajv.errors || [],
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate examples in a schema
|
|
66
|
+
* @param {Object} schema - Schema containing examples
|
|
67
|
+
* @param {Ajv} ajv - Ajv instance
|
|
68
|
+
* @returns {Object} Validation result with errors if any
|
|
69
|
+
*/
|
|
70
|
+
export const validateExamples = (schema, ajv) => {
|
|
71
|
+
const validate = ajv.compile(schema);
|
|
72
|
+
const examples = schema.examples || [];
|
|
73
|
+
const errors = [];
|
|
74
|
+
|
|
75
|
+
for (const example of examples) {
|
|
76
|
+
const valid = validate(example);
|
|
77
|
+
if (!valid) {
|
|
78
|
+
errors.push({
|
|
79
|
+
example,
|
|
80
|
+
errors: validate.errors,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
valid: errors.length === 0,
|
|
87
|
+
errors,
|
|
88
|
+
};
|
|
89
|
+
};
|