Skip to content

Commit af48603

Browse files
committed
chore(docs): add custom data reference tests
Signed-off-by: John Yanarella <jyanarella@nvidia.com>
1 parent 75f42f5 commit af48603

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { readFile, readdir } from 'node:fs/promises';
5+
import { dirname, join, relative, resolve } from 'node:path';
6+
import { describe, expect, it } from 'vitest';
7+
8+
const DOCUMENTATION_BASE_URL = 'https://nvidia.github.io/elements/';
9+
const CUSTOM_DATA_SCHEMA_URL =
10+
'https://raw.githubusercontent.com/microsoft/vscode-html-languageservice/main/docs/customData.schema.json';
11+
const DOCUMENTATION_ROOT = import.meta.dirname;
12+
const PROJECTS_ROOT = resolve(DOCUMENTATION_ROOT, '../../..');
13+
14+
interface ComponentDocumentation {
15+
tag: string;
16+
url: string;
17+
}
18+
19+
interface CustomDataReference {
20+
name: string;
21+
url: string;
22+
}
23+
24+
interface CustomDataTag {
25+
name: string;
26+
references?: CustomDataReference[];
27+
}
28+
29+
interface HtmlCustomData {
30+
$schema: string;
31+
tags?: CustomDataTag[];
32+
version: number;
33+
}
34+
35+
interface CustomElementDeclaration {
36+
metadata?: Record<string, unknown>;
37+
tagName?: string;
38+
}
39+
40+
interface CustomElementsManifest {
41+
modules: {
42+
declarations?: CustomElementDeclaration[];
43+
}[];
44+
}
45+
46+
interface ProjectPackage {
47+
customElements?: string;
48+
name?: string;
49+
}
50+
51+
describe('HTML Custom Data references', () => {
52+
it('uses the V1 reference shape with valid HTTPS URLs', async () => {
53+
for (const customData of await getCustomData()) {
54+
expect(customData.$schema).toBe(CUSTOM_DATA_SCHEMA_URL);
55+
expect(customData.version).toBe(1.1);
56+
57+
for (const tag of customData.tags ?? []) {
58+
for (const reference of tag.references ?? []) {
59+
expect(reference).toMatchObject({
60+
name: expect.any(String),
61+
url: expect.any(String)
62+
});
63+
expect(new URL(reference.url).protocol).toBe('https:');
64+
}
65+
}
66+
}
67+
});
68+
69+
it('maps CEM ARIA metadata to the WAI-ARIA Reference label', async () => {
70+
const declarationsByTag = await getManifestDeclarations();
71+
const customDataTagsByName = new Map(
72+
(await getCustomData()).flatMap(customData => (customData.tags ?? []).map(tag => [tag.name, tag]))
73+
);
74+
75+
for (const [tagName, declaration] of declarationsByTag) {
76+
const url = declaration.metadata?.aria;
77+
if (!isHttpsUrl(url)) {
78+
continue;
79+
}
80+
81+
expect(customDataTagsByName.get(tagName)?.references).toContainEqual({ name: 'WAI-ARIA Reference', url });
82+
}
83+
});
84+
85+
it('generates canonical references for every documented public tag', async () => {
86+
const documentation = await getComponentDocumentation();
87+
const declarationsByTag = await getManifestDeclarations();
88+
const customDataTagsByName = new Map(
89+
(await getCustomData()).flatMap(customData => (customData.tags ?? []).map(tag => [tag.name, tag]))
90+
);
91+
92+
expect(documentation.length).toBeGreaterThan(0);
93+
94+
for (const component of documentation) {
95+
expect(declarationsByTag.get(component.tag)?.metadata?.documentation).toBe(component.url);
96+
97+
const tag = customDataTagsByName.get(component.tag);
98+
expect(tag?.references).toContainEqual({ name: 'Documentation', url: component.url });
99+
100+
const url = new URL(component.url);
101+
expect(url.origin).toBe('https://nvidia.github.io');
102+
expect(url.pathname).toMatch(/^\/elements\/docs\//);
103+
}
104+
});
105+
});
106+
107+
async function getComponentDocumentation(): Promise<ComponentDocumentation[]> {
108+
const files = [
109+
...(await getTopLevelMarkdownFiles(join(DOCUMENTATION_ROOT, 'elements'))),
110+
join(DOCUMENTATION_ROOT, 'elements/data-grid/index.md'),
111+
...(await getTopLevelMarkdownFiles(join(DOCUMENTATION_ROOT, 'code'))),
112+
...(await getTopLevelMarkdownFiles(join(DOCUMENTATION_ROOT, 'monaco'))),
113+
...(await getTopLevelMarkdownFiles(join(DOCUMENTATION_ROOT, 'media'))),
114+
join(DOCUMENTATION_ROOT, 'markdown/index.md')
115+
];
116+
117+
const documentation = await Promise.all(
118+
files.map(async file => {
119+
const content = await readFile(file, 'utf8');
120+
const tags = [
121+
content.match(/tag:\s*'([^']+)'/)?.[1],
122+
...[...(content.match(/associatedElements:\s*\[([\s\S]*?)\]/)?.[1]?.matchAll(/'([^']+)'/g) ?? [])].map(
123+
match => match[1]
124+
)
125+
].filter((tag): tag is string => !!tag);
126+
127+
return tags.map(tag => ({ tag, url: getDocumentationUrl(file) }));
128+
})
129+
);
130+
131+
return [...new Map(documentation.flat().map(component => [component.tag, component])).values()];
132+
}
133+
134+
async function getCustomData(): Promise<HtmlCustomData[]> {
135+
const customData = await Promise.all(
136+
(await getElementsPackages()).map(async ({ customElements, directory }) => {
137+
try {
138+
const dataPath = join(directory, dirname(customElements), 'data.html.json');
139+
return JSON.parse(await readFile(dataPath, 'utf8')) as HtmlCustomData;
140+
} catch {
141+
return null;
142+
}
143+
})
144+
);
145+
146+
return customData.filter((data): data is HtmlCustomData => data !== null);
147+
}
148+
149+
async function getManifestDeclarations(): Promise<Map<string, CustomElementDeclaration>> {
150+
const manifests = await Promise.all(
151+
(await getElementsPackages()).map(async ({ customElements, directory }) => {
152+
return JSON.parse(await readFile(join(directory, customElements), 'utf8')) as CustomElementsManifest;
153+
})
154+
);
155+
156+
return new Map(
157+
manifests.flatMap(manifest =>
158+
manifest.modules.flatMap(module =>
159+
(module.declarations ?? [])
160+
.filter((declaration): declaration is CustomElementDeclaration & { tagName: string } => !!declaration.tagName)
161+
.map(declaration => [declaration.tagName, declaration])
162+
)
163+
)
164+
);
165+
}
166+
167+
async function getElementsPackages(): Promise<{ customElements: string; directory: string }[]> {
168+
const directories = await readdir(PROJECTS_ROOT, { withFileTypes: true });
169+
const packages = await Promise.all(
170+
directories
171+
.filter(directory => directory.isDirectory())
172+
.map(async directory => {
173+
const projectDirectory = join(PROJECTS_ROOT, directory.name);
174+
try {
175+
const packageJson = JSON.parse(
176+
await readFile(join(projectDirectory, 'package.json'), 'utf8')
177+
) as ProjectPackage;
178+
return packageJson.name?.startsWith('@nvidia-elements/') && packageJson.customElements
179+
? { customElements: packageJson.customElements, directory: projectDirectory }
180+
: null;
181+
} catch {
182+
return null;
183+
}
184+
})
185+
);
186+
187+
return packages.filter((project): project is { customElements: string; directory: string } => project !== null);
188+
}
189+
190+
async function getTopLevelMarkdownFiles(directory: string): Promise<string[]> {
191+
try {
192+
const entries = await readdir(directory, { withFileTypes: true });
193+
return entries
194+
.filter(entry => entry.isFile() && entry.name.endsWith('.md'))
195+
.map(entry => join(directory, entry.name));
196+
} catch {
197+
return [];
198+
}
199+
}
200+
201+
function getDocumentationUrl(file: string): string {
202+
const filePath = relative(DOCUMENTATION_ROOT, file).replace(/\.md$/, '');
203+
const route = filePath.endsWith('/index') ? filePath.slice(0, -'/index'.length) : filePath;
204+
return new URL(`docs/${route}/`, DOCUMENTATION_BASE_URL).href;
205+
}
206+
207+
function isHttpsUrl(value: unknown): value is string {
208+
if (typeof value !== 'string') {
209+
return false;
210+
}
211+
212+
try {
213+
return new URL(value).protocol === 'https:';
214+
} catch {
215+
return false;
216+
}
217+
}

0 commit comments

Comments
 (0)