@adobe/spectrum-component-api-schemas 5.0.1 → 6.1.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/CHANGELOG.md +187 -184
- package/index.js +28 -12
- package/moon.yml +1 -4
- package/package.json +6 -5
- package/scripts/convert-to-spec-format.mjs +136 -0
- package/test/index.test.js +64 -34
- package/test/integration.test.js +4 -6
- package/test/performance.test.js +10 -10
- package/test/schema-validation.test.js +64 -105
- package/test/snapshots/index.test.js.md +36 -34
- package/schemas/component.json +0 -29
- package/schemas/components/accordion.json +0 -74
- package/schemas/components/action-bar.json +0 -18
- package/schemas/components/action-button.json +0 -62
- package/schemas/components/action-group.json +0 -69
- package/schemas/components/alert-banner.json +0 -29
- package/schemas/components/alert-dialog.json +0 -42
- package/schemas/components/avatar-group.json +0 -22
- package/schemas/components/avatar.json +0 -45
- package/schemas/components/badge.json +0 -69
- package/schemas/components/body.json +0 -25
- package/schemas/components/bottom-navigation-android.json +0 -37
- package/schemas/components/breadcrumbs.json +0 -72
- package/schemas/components/button-group.json +0 -33
- package/schemas/components/button.json +0 -61
- package/schemas/components/calendar.json +0 -109
- package/schemas/components/cards.json +0 -238
- package/schemas/components/checkbox-group.json +0 -49
- package/schemas/components/checkbox.json +0 -49
- package/schemas/components/close-button.json +0 -35
- package/schemas/components/coach-indicator.json +0 -34
- package/schemas/components/coach-mark.json +0 -51
- package/schemas/components/code.json +0 -21
- package/schemas/components/color-area.json +0 -64
- package/schemas/components/color-handle.json +0 -31
- package/schemas/components/color-loupe.json +0 -21
- package/schemas/components/color-slider.json +0 -66
- package/schemas/components/color-wheel.json +0 -47
- package/schemas/components/combo-box.json +0 -80
- package/schemas/components/contextual-help.json +0 -67
- package/schemas/components/date-picker.json +0 -157
- package/schemas/components/detail.json +0 -26
- package/schemas/components/divider.json +0 -23
- package/schemas/components/drop-zone.json +0 -41
- package/schemas/components/field-label.json +0 -39
- package/schemas/components/heading.json +0 -29
- package/schemas/components/help-text.json +0 -36
- package/schemas/components/illustrated-message.json +0 -52
- package/schemas/components/in-field-progress-button.json +0 -31
- package/schemas/components/in-field-progress-circle.json +0 -22
- package/schemas/components/in-line-alert.json +0 -43
- package/schemas/components/link.json +0 -32
- package/schemas/components/list-view.json +0 -106
- package/schemas/components/menu.json +0 -63
- package/schemas/components/meter.json +0 -44
- package/schemas/components/number-field.json +0 -57
- package/schemas/components/opacity-checkerboard.json +0 -21
- package/schemas/components/picker.json +0 -95
- package/schemas/components/popover.json +0 -65
- package/schemas/components/progress-bar.json +0 -64
- package/schemas/components/progress-circle.json +0 -41
- package/schemas/components/radio-button.json +0 -30
- package/schemas/components/radio-group.json +0 -58
- package/schemas/components/rating.json +0 -34
- package/schemas/components/scroll-zoom-bar.json +0 -44
- package/schemas/components/search-field.json +0 -58
- package/schemas/components/segmented-control.json +0 -72
- package/schemas/components/select-box.json +0 -50
- package/schemas/components/side-navigation.json +0 -77
- package/schemas/components/slider.json +0 -79
- package/schemas/components/standard-dialog.json +0 -53
- package/schemas/components/standard-panel.json +0 -49
- package/schemas/components/status-light.json +0 -51
- package/schemas/components/steplist.json +0 -52
- package/schemas/components/swatch-group.json +0 -44
- package/schemas/components/swatch.json +0 -46
- package/schemas/components/switch.json +0 -42
- package/schemas/components/tab-bar-ios.json +0 -37
- package/schemas/components/table.json +0 -144
- package/schemas/components/tabs.json +0 -75
- package/schemas/components/tag-field.json +0 -42
- package/schemas/components/tag-group.json +0 -31
- package/schemas/components/tag.json +0 -41
- package/schemas/components/takeover-dialog.json +0 -60
- package/schemas/components/text-area.json +0 -92
- package/schemas/components/text-field.json +0 -80
- package/schemas/components/thumbnail.json +0 -22
- package/schemas/components/toast.json +0 -29
- package/schemas/components/tooltip.json +0 -50
- package/schemas/components/tray.json +0 -17
- package/schemas/components/tree-view.json +0 -50
- package/schemas/types/hex-color.json +0 -8
- package/schemas/types/typography-classification.json +0 -9
- package/schemas/types/typography-script.json +0 -9
- package/schemas/types/workflow-icon.json +0 -901
- package/test/componentSchemaValidator.test.js +0 -74
package/moon.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2026 Adobe. All rights reserved.
|
|
2
2
|
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License. You may obtain a copy
|
|
4
4
|
# of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
$schema: "https://moonrepo.dev/schemas/project.json"
|
|
11
11
|
layer: library
|
|
12
12
|
fileGroups:
|
|
13
|
-
schemas:
|
|
14
|
-
- "schemas/**/*.{yaml,yml,json}"
|
|
15
13
|
tests:
|
|
16
14
|
- "test/**/*"
|
|
17
15
|
tasks:
|
|
@@ -20,7 +18,6 @@ tasks:
|
|
|
20
18
|
- pnpm
|
|
21
19
|
- ava
|
|
22
20
|
inputs:
|
|
23
|
-
- "@globs(schemas)"
|
|
24
21
|
- "@globs(tests)"
|
|
25
22
|
- "index.js"
|
|
26
23
|
- "ava.config.js"
|
package/package.json
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spectrum-component-api-schemas",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/adobe/spectrum-
|
|
9
|
+
"url": "git+https://github.com/adobe/spectrum-design-data.git"
|
|
10
10
|
},
|
|
11
11
|
"author": "Garth Braithwaite <garthdb@gmail.com> (https://garthdb.com/)",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/adobe/spectrum-
|
|
13
|
+
"url": "https://github.com/adobe/spectrum-design-data/issues"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"provenance": true
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://github.com/adobe/spectrum-
|
|
18
|
+
"homepage": "https://github.com/adobe/spectrum-design-data/tree/main/packages/component-schemas#readme",
|
|
19
19
|
"license": "Apache-2.0",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"glob": "^10.3.12"
|
|
21
|
+
"glob": "^10.3.12",
|
|
22
|
+
"@adobe/design-data-spec": "0.4.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"ajv": "^8.12.0",
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One-time conversion script: old component-schemas format → new design-data-spec format.
|
|
15
|
+
*
|
|
16
|
+
* Reads packages/component-schemas/schemas/components/*.json (old format)
|
|
17
|
+
* and writes packages/design-data-spec/components/{name}.json (new format).
|
|
18
|
+
*
|
|
19
|
+
* Skips button.json (hand-crafted in Phase 6.4).
|
|
20
|
+
* Run: node packages/component-schemas/scripts/convert-to-spec-format.mjs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
24
|
+
import { glob } from "glob";
|
|
25
|
+
import { resolve, dirname } from "path";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const repoRoot = resolve(__dirname, "../../..");
|
|
30
|
+
const sourceDir = resolve(repoRoot, "packages/component-schemas/schemas/components");
|
|
31
|
+
const destDir = resolve(repoRoot, "packages/design-data-spec/components");
|
|
32
|
+
|
|
33
|
+
const SKIP = new Set(["button.json"]);
|
|
34
|
+
|
|
35
|
+
// States that are driven by runtime interaction (vs persistent prop).
|
|
36
|
+
const INTERACTION_STATES = new Set([
|
|
37
|
+
"hover",
|
|
38
|
+
"focus",
|
|
39
|
+
"focus-visible",
|
|
40
|
+
"active",
|
|
41
|
+
"pressed",
|
|
42
|
+
"down",
|
|
43
|
+
"keyboard-focus",
|
|
44
|
+
"dragging",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
function toKebabCase(str) {
|
|
48
|
+
return str.trim().toLowerCase().replace(/\s+/g, "-");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getSlugFromDocumentationUrl(url) {
|
|
52
|
+
return url
|
|
53
|
+
.split("/")
|
|
54
|
+
.filter((p) => p !== "")
|
|
55
|
+
.pop();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function convertStateEnum(stateEnum) {
|
|
59
|
+
if (!Array.isArray(stateEnum)) return [];
|
|
60
|
+
return stateEnum
|
|
61
|
+
.filter((v) => v !== "default")
|
|
62
|
+
.map((v) => {
|
|
63
|
+
const name = toKebabCase(v);
|
|
64
|
+
const trigger = INTERACTION_STATES.has(name) ? "interaction" : "prop";
|
|
65
|
+
return { name, trigger };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function convertFile(srcPath, fileName) {
|
|
70
|
+
const raw = await readFile(srcPath, "utf8");
|
|
71
|
+
const old = JSON.parse(raw);
|
|
72
|
+
|
|
73
|
+
const name = getSlugFromDocumentationUrl(old.meta.documentationUrl);
|
|
74
|
+
const displayName = old.title;
|
|
75
|
+
|
|
76
|
+
// properties → options (remove state; keep everything else)
|
|
77
|
+
const { state: stateProperty, ...remainingProperties } = old.properties ?? {};
|
|
78
|
+
const options = Object.keys(remainingProperties).length > 0 ? remainingProperties : undefined;
|
|
79
|
+
|
|
80
|
+
// Derive states from old state.enum
|
|
81
|
+
const stateList = convertStateEnum(stateProperty?.enum);
|
|
82
|
+
const states = stateList.length > 0 ? stateList : undefined;
|
|
83
|
+
|
|
84
|
+
// Build new $id and $schema
|
|
85
|
+
const newId = (old.$id ?? "").replace(
|
|
86
|
+
"https://opensource.adobe.com/spectrum-design-data/schemas/components/",
|
|
87
|
+
"https://opensource.adobe.com/spectrum-design-data/schemas/v0/components/",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const newDoc = {
|
|
91
|
+
$schema:
|
|
92
|
+
"https://opensource.adobe.com/spectrum-design-data/schemas/v0/component.schema.json",
|
|
93
|
+
$id: newId,
|
|
94
|
+
specVersion: "1.0.0-draft",
|
|
95
|
+
name,
|
|
96
|
+
displayName,
|
|
97
|
+
...(old.description ? { description: old.description } : {}),
|
|
98
|
+
meta: old.meta,
|
|
99
|
+
...(options ? { options } : {}),
|
|
100
|
+
...(states ? { states } : {}),
|
|
101
|
+
lifecycle: { introduced: "1.0.0-draft" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return newDoc;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
await mkdir(destDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const srcFiles = (await glob(`${sourceDir}/*.json`)).sort();
|
|
111
|
+
let converted = 0;
|
|
112
|
+
let skipped = 0;
|
|
113
|
+
|
|
114
|
+
for (const srcPath of srcFiles) {
|
|
115
|
+
const fileName = srcPath.split("/").pop();
|
|
116
|
+
|
|
117
|
+
if (SKIP.has(fileName)) {
|
|
118
|
+
console.log(` skip ${fileName} (hand-crafted)`);
|
|
119
|
+
skipped++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newDoc = await convertFile(srcPath, fileName);
|
|
124
|
+
const destPath = resolve(destDir, fileName);
|
|
125
|
+
await writeFile(destPath, JSON.stringify(newDoc, null, 2) + "\n", "utf8");
|
|
126
|
+
console.log(` write ${fileName} → ${newDoc.name}`);
|
|
127
|
+
converted++;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`\nDone: ${converted} converted, ${skipped} skipped.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main().catch((err) => {
|
|
134
|
+
console.error(err);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|
package/test/index.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Copyright
|
|
2
|
+
Copyright 2026 Adobe. All rights reserved.
|
|
3
3
|
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
5
|
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
@@ -16,63 +16,93 @@ import {
|
|
|
16
16
|
getAllSchemas,
|
|
17
17
|
getSlugFromDocumentationUrl,
|
|
18
18
|
getAllSlugs,
|
|
19
|
-
|
|
19
|
+
componentsDir,
|
|
20
20
|
getSchemaBySlug,
|
|
21
21
|
} from "../index.js";
|
|
22
22
|
import test from "ava";
|
|
23
23
|
import { glob } from "glob";
|
|
24
24
|
import { resolve, parse } from "path";
|
|
25
25
|
|
|
26
|
-
test("the number of
|
|
26
|
+
test("the number of schemas returned by getAllSchemas should match the number of schemaFileNames", async (t) => {
|
|
27
27
|
const allSchemas = await getAllSchemas();
|
|
28
28
|
t.is(schemaFileNames.length, allSchemas.length);
|
|
29
29
|
});
|
|
30
|
-
test("getSchemaFile should fetch schema data", async (t) => {
|
|
31
|
-
const relativeSchemaComponent = await getSchemaFile("component.json");
|
|
32
|
-
t.is(relativeSchemaComponent.title, "Component");
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
t.is(
|
|
31
|
+
test("getSchemaFile should fetch schema data using a relative filename", async (t) => {
|
|
32
|
+
const schema = await getSchemaFile("action-bar.json");
|
|
33
|
+
// new-format field
|
|
34
|
+
t.is(schema.displayName, "Action bar");
|
|
35
|
+
// backward-compat alias
|
|
36
|
+
t.is(schema.title, "Action bar");
|
|
37
|
+
t.truthy(schema.name);
|
|
38
|
+
t.truthy(schema.meta?.documentationUrl);
|
|
39
|
+
});
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
42
|
-
t.is(
|
|
41
|
+
test("getSchemaFile should fetch schema data using an absolute path", async (t) => {
|
|
42
|
+
const schema = await getSchemaFile(resolve(componentsDir, "action-bar.json"));
|
|
43
|
+
t.is(schema.displayName, "Action bar");
|
|
44
|
+
t.is(schema.title, "Action bar");
|
|
45
|
+
});
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
t.
|
|
48
|
-
//
|
|
47
|
+
test("getSchemaFile returned objects include backward-compat properties alias", async (t) => {
|
|
48
|
+
const schema = await getSchemaFile("checkbox.json");
|
|
49
|
+
// options is the new-format field
|
|
50
|
+
t.truthy(schema.options);
|
|
51
|
+
// properties is the compat alias
|
|
52
|
+
t.is(schema.properties, schema.options);
|
|
49
53
|
});
|
|
54
|
+
|
|
50
55
|
test("getSlugFromDocumentationUrl should return last part of documentationUrl", (t) => {
|
|
51
56
|
t.is(
|
|
52
57
|
getSlugFromDocumentationUrl("https://spectrum.adobe.com/page/tooltip/"),
|
|
53
58
|
"tooltip",
|
|
54
59
|
);
|
|
55
60
|
});
|
|
61
|
+
|
|
56
62
|
test("getSlugFromDocumentationUrl should return last part of documentationUrl even without trailing slash", (t) => {
|
|
57
63
|
t.is(
|
|
58
64
|
getSlugFromDocumentationUrl("https://spectrum.adobe.com/page/tooltip"),
|
|
59
65
|
"tooltip",
|
|
60
66
|
);
|
|
61
67
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
68
|
+
|
|
69
|
+
test("getAllSlugs should return all component slugs sorted", async (t) => {
|
|
70
|
+
// getAllSlugs() extracts slugs from documentationUrl. This asserts that
|
|
71
|
+
// URL slugs match filename stems — a required invariant for all components.
|
|
72
|
+
const files = await glob(`${componentsDir}/*.json`);
|
|
73
|
+
const expected = files.map((f) => parse(f).name).sort();
|
|
74
|
+
t.deepEqual(await getAllSlugs(), expected);
|
|
70
75
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
|
|
77
|
+
test("getAllSchemas should include slug, title, displayName, properties, and options on each schema", async (t) => {
|
|
78
|
+
const schemas = await getAllSchemas();
|
|
79
|
+
for (const schema of schemas) {
|
|
80
|
+
if (
|
|
81
|
+
Object.hasOwn(schema, "meta") &&
|
|
82
|
+
Object.hasOwn(schema.meta, "documentationUrl")
|
|
83
|
+
) {
|
|
84
|
+
t.truthy(schema.slug, `${schema.name}: missing slug`);
|
|
85
|
+
t.truthy(schema.displayName, `${schema.name}: missing displayName`);
|
|
86
|
+
t.is(
|
|
87
|
+
schema.title,
|
|
88
|
+
schema.displayName,
|
|
89
|
+
`${schema.name}: title alias mismatch`,
|
|
90
|
+
);
|
|
91
|
+
t.truthy(schema.name, `${schema.name}: missing name`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("getSchemaBySlug should return a schema without a slug field", async (t) => {
|
|
97
|
+
const schema = await getSchemaBySlug("tooltip");
|
|
98
|
+
t.is(schema.displayName, "Tooltip");
|
|
99
|
+
t.is(schema.title, "Tooltip");
|
|
100
|
+
t.is(schema.name, "tooltip");
|
|
101
|
+
t.false(Object.hasOwn(schema, "slug"));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("getSchemaBySlug should throw for unknown slug", async (t) => {
|
|
105
|
+
await t.throwsAsync(async () => getSchemaBySlug("not-a-real-component"), {
|
|
106
|
+
message: /Schema not found for slug: not-a-real-component/,
|
|
107
|
+
});
|
|
78
108
|
});
|
package/test/integration.test.js
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
getAllSlugs,
|
|
6
6
|
getSlugFromDocumentationUrl,
|
|
7
7
|
schemaFileNames,
|
|
8
|
+
componentsDir,
|
|
8
9
|
} from "../index.js";
|
|
9
|
-
import {
|
|
10
|
-
import { relative } from "path";
|
|
10
|
+
import { glob } from "glob";
|
|
11
11
|
|
|
12
12
|
test("getAllSchemas should return all schemas with slugs", async (t) => {
|
|
13
13
|
const schemas = await getAllSchemas();
|
|
@@ -83,15 +83,13 @@ test("getSlugFromDocumentationUrl should handle various URL formats", (t) => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
test("schemaFileNames should match actual files on disk", async (t) => {
|
|
86
|
-
const actualFiles = await
|
|
86
|
+
const actualFiles = await glob(`${componentsDir}/*.json`);
|
|
87
87
|
|
|
88
88
|
t.is(schemaFileNames.length, actualFiles.length);
|
|
89
89
|
|
|
90
|
-
// Verify all files in schemaFileNames exist
|
|
91
90
|
for (const fileName of schemaFileNames) {
|
|
92
|
-
const relativePath = relative(process.cwd(), fileName);
|
|
93
91
|
t.true(
|
|
94
|
-
actualFiles.includes(
|
|
92
|
+
actualFiles.includes(fileName),
|
|
95
93
|
`File ${fileName} not found on disk`,
|
|
96
94
|
);
|
|
97
95
|
}
|
package/test/performance.test.js
CHANGED
|
@@ -49,24 +49,24 @@ test("getSchemaBySlug should complete within reasonable time", async (t) => {
|
|
|
49
49
|
const end = performance.now();
|
|
50
50
|
const duration = end - start;
|
|
51
51
|
|
|
52
|
-
// Should complete within
|
|
52
|
+
// Should complete within 250ms (increased for CI environments)
|
|
53
53
|
t.true(
|
|
54
|
-
duration <
|
|
55
|
-
`getSchemaBySlug took ${duration.toFixed(2)}ms, expected <
|
|
54
|
+
duration < 250,
|
|
55
|
+
`getSchemaBySlug took ${duration.toFixed(2)}ms, expected < 250ms`,
|
|
56
56
|
);
|
|
57
57
|
t.truthy(schema);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("getSchemaFile should complete within reasonable time", async (t) => {
|
|
61
61
|
const start = performance.now();
|
|
62
|
-
const schema = await getSchemaFile("
|
|
62
|
+
const schema = await getSchemaFile("button.json");
|
|
63
63
|
const end = performance.now();
|
|
64
64
|
const duration = end - start;
|
|
65
65
|
|
|
66
|
-
// Should complete within
|
|
66
|
+
// Should complete within 500ms (generous for CI runner variability)
|
|
67
67
|
t.true(
|
|
68
|
-
duration <
|
|
69
|
-
`getSchemaFile took ${duration.toFixed(2)}ms, expected <
|
|
68
|
+
duration < 500,
|
|
69
|
+
`getSchemaFile took ${duration.toFixed(2)}ms, expected < 500ms`,
|
|
70
70
|
);
|
|
71
71
|
t.truthy(schema);
|
|
72
72
|
});
|
|
@@ -89,10 +89,10 @@ test("multiple concurrent getSchemaBySlug calls should complete efficiently", as
|
|
|
89
89
|
const end = performance.now();
|
|
90
90
|
const duration = end - start;
|
|
91
91
|
|
|
92
|
-
// Should complete within
|
|
92
|
+
// Should complete within 500ms for 3 concurrent calls (generous for CI runner variability)
|
|
93
93
|
t.true(
|
|
94
|
-
duration <
|
|
95
|
-
`Concurrent getSchemaBySlug calls took ${duration.toFixed(2)}ms, expected <
|
|
94
|
+
duration < 500,
|
|
95
|
+
`Concurrent getSchemaBySlug calls took ${duration.toFixed(2)}ms, expected < 500ms`,
|
|
96
96
|
);
|
|
97
97
|
t.is(results.length, 3);
|
|
98
98
|
t.true(results.every((schema) => schema !== null));
|
|
@@ -1,131 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
1
12
|
import test from "ava";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
import { readFile } from "fs/promises";
|
|
14
|
+
import { resolve } from "path";
|
|
15
|
+
import { glob } from "glob";
|
|
16
|
+
import { createRequire } from "module";
|
|
17
|
+
import Ajv from "ajv/dist/2020.js";
|
|
18
|
+
import addFormats from "ajv-formats";
|
|
19
|
+
import * as url from "url";
|
|
9
20
|
|
|
10
|
-
|
|
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);
|
|
21
|
+
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const specPkgPath = createRequire(import.meta.url).resolve(
|
|
24
|
+
"@adobe/design-data-spec/package.json",
|
|
25
|
+
);
|
|
26
|
+
const specPkgDir = resolve(specPkgPath, "..");
|
|
27
|
+
const componentsDir = resolve(specPkgDir, "components");
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const validationResults = [];
|
|
29
|
+
const readJSON = async (filePath) =>
|
|
30
|
+
JSON.parse(await readFile(filePath, "utf8"));
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const result = validateSchema(schema, ajv);
|
|
34
|
-
|
|
35
|
-
if (!result.valid) {
|
|
36
|
-
validationResults.push({
|
|
37
|
-
file: filePath,
|
|
38
|
-
errors: result.errors,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
32
|
+
let ajv;
|
|
33
|
+
let componentSchema;
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`Schema validation failed for:\n${validationResults
|
|
47
|
-
.map((r) => `${r.file}:\n${JSON.stringify(r.errors, null, 2)}`)
|
|
48
|
-
.join("\n\n")}`,
|
|
35
|
+
test.before(async () => {
|
|
36
|
+
componentSchema = await readJSON(
|
|
37
|
+
resolve(specPkgDir, "schemas/component.schema.json"),
|
|
49
38
|
);
|
|
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
39
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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")}`,
|
|
40
|
+
ajv = new Ajv({ allErrors: true, strict: false });
|
|
41
|
+
addFormats(ajv);
|
|
42
|
+
ajv.addSchema(
|
|
43
|
+
await readJSON(resolve(specPkgDir, "schemas/anatomy-part.schema.json")),
|
|
44
|
+
);
|
|
45
|
+
ajv.addSchema(
|
|
46
|
+
await readJSON(
|
|
47
|
+
resolve(specPkgDir, "schemas/state-declaration.schema.json"),
|
|
48
|
+
),
|
|
74
49
|
);
|
|
75
50
|
});
|
|
76
51
|
|
|
77
|
-
test("all
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
for (const filePath of typeFiles) {
|
|
82
|
-
const schema = await readJSON(filePath);
|
|
83
|
-
const result = validateSchema(schema, ajv);
|
|
52
|
+
test("all component files should validate against component.schema.json", async (t) => {
|
|
53
|
+
const componentFiles = (await glob(`${componentsDir}/*.json`)).sort();
|
|
54
|
+
const validate = ajv.compile(componentSchema);
|
|
55
|
+
const failures = [];
|
|
84
56
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
57
|
+
for (const filePath of componentFiles) {
|
|
58
|
+
const data = await readJSON(filePath);
|
|
59
|
+
const valid = validate(data);
|
|
60
|
+
if (!valid) {
|
|
61
|
+
failures.push({ file: filePath, errors: validate.errors });
|
|
90
62
|
}
|
|
91
63
|
}
|
|
92
64
|
|
|
93
65
|
t.is(
|
|
94
|
-
|
|
66
|
+
failures.length,
|
|
95
67
|
0,
|
|
96
|
-
`
|
|
97
|
-
.map((
|
|
68
|
+
`Schema validation failed:\n${failures
|
|
69
|
+
.map((f) => `${f.file}:\n${JSON.stringify(f.errors, null, 2)}`)
|
|
98
70
|
.join("\n\n")}`,
|
|
99
71
|
);
|
|
100
72
|
});
|
|
101
73
|
|
|
102
|
-
test("component
|
|
103
|
-
const componentFiles = await
|
|
104
|
-
const
|
|
74
|
+
test("all component files should have required metadata", async (t) => {
|
|
75
|
+
const componentFiles = (await glob(`${componentsDir}/*.json`)).sort();
|
|
76
|
+
const missing = [];
|
|
105
77
|
|
|
106
78
|
for (const filePath of componentFiles) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
if (!
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!
|
|
114
|
-
|
|
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
|
-
}
|
|
79
|
+
const data = await readJSON(filePath);
|
|
80
|
+
|
|
81
|
+
if (!data.name) missing.push(`${filePath}: missing name`);
|
|
82
|
+
if (!data.displayName) missing.push(`${filePath}: missing displayName`);
|
|
83
|
+
if (!data.meta?.category)
|
|
84
|
+
missing.push(`${filePath}: missing meta.category`);
|
|
85
|
+
if (!data.meta?.documentationUrl)
|
|
86
|
+
missing.push(`${filePath}: missing meta.documentationUrl`);
|
|
124
87
|
}
|
|
125
88
|
|
|
126
|
-
t.is(
|
|
127
|
-
missingMetadata.length,
|
|
128
|
-
0,
|
|
129
|
-
`Missing required metadata:\n${missingMetadata.join("\n")}`,
|
|
130
|
-
);
|
|
89
|
+
t.is(missing.length, 0, `Missing required metadata:\n${missing.join("\n")}`);
|
|
131
90
|
});
|