open-design/e2e/tests/localized-content.test.ts
chaoxiaoche a75d9938c7
feat(design-systems): add structured tokens.css schema (default + kami) (#1231)
* feat(design-systems): add structured tokens.css schema (default + kami)

Compile each brand's DESIGN.md prose into a machine-readable :root
block agents paste verbatim, removing the "Primary → --accent"
translation step where most token misuse happens. Daemon prompt
injection lands in a follow-up; lint-artifact already enforces the
shared token vocabulary so no rule changes needed.

Schema validated across two contrasting aesthetics:
- default (sans-serif, cobalt, B2B utility) — stress test the
  shallow form, 2-level fg / 2-level surface
- kami (serif, parchment, ink-blue, print-first) — stress test the
  rich form, 4-level fg ramp, 3-level surface, ring elevation, i18n
  font stacks, and solid-hex tag tints (print renderers double-paint
  alpha)

Schema growth from kami's stress test (5 new optional slots, all
backward-compatible — default aliases via var() to existing tokens):
- --fg-2 / --meta (4-level fg ramp)
- --surface-warm (3-level surface)
- --border-soft (2-level border)
- --elev-ring (ring elevation as first-class level)

Brand-specific extensions live in tokens.css with explicit "NOT in
shared schema" labels and a documented promotion path (≥2 brands
need it → promote to schema slot).

components.html in each brand is a self-contained reference fixture
that exercises every token through real layouts. Both fixtures lint
clean against apps/daemon/src/lint-artifact.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(design-systems): add token-fixture drift guard

Each design system in design-systems/<brand>/ ships two files agents
consume in tandem: tokens.css (canonical token bindings) and
components.html (a self-contained fixture whose first <style> embeds
the same :root paste so the file renders standalone). The fixture's
:root block is a copy of tokens.css's :root block, kept in sync only
by an inline comment.

This adds scripts/check-tokens-fixture-sync.ts and registers it in
pnpm guard. The check pairs each brand's tokens.css with its
components.html and asserts the unscoped :root block is byte-equivalent
after canonical normalization (CSS comments stripped, whitespace
collapsed, separator spacing normalized). Brands missing one half of
the pair, or with no :root rule in either file, fail the guard.

Scoped overrides like :root[lang="zh-CN"] are not required to appear
in the fixture (per the kami fixture's inline comment they are pasted
only when an artifact's <html lang> matches), so the check only
compares the unscoped :root block.

Verified: pnpm guard passes for default + kami, fails on intentional
value drift, fails on missing token, tolerates whitespace-only
formatting differences.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(design-systems): point fixture CTAs to real files

Both default and kami components.html advertised in-page anchors
(#tokens, #spec, #surface, #accent, #type, #components) but defined
no matching ids, so every CTA was a no-op when the fixture was
opened locally — flagged by mrcfps in #1231.

Re-point each link to a real artifact in the same brand directory:

- "View tokens" / "Inspect tokens" / "Inspect typography" → ./tokens.css
- "Read the spec" / "Read the rule" → ./DESIGN.md

Browsers render these as raw source views, which is the desired UX
for a reference fixture: clicking the CTA shows the underlying
contract instead of jumping to nothing. Agents copying the fixture
also learn the pattern of "buttons link to actual sibling resources".

The :root token block is unchanged, so the token-fixture drift guard
still passes for both brands.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(design-systems): codify token schema (A1/A2/B/C layers)

The two-brand pilot (default + kami) settled the shape of the shared
token schema; this commit codifies it as a machine-readable contract
and enforces it in pnpm guard, addressing lefarcen's review on #1231:

> the optional-vs-required split won't generalize cleanly when brand
> #3 needs different Layer A tokens or when multiple brands converge
> on the same extension (promoting C→B→A). Consider surfacing that
> limitation in the PR narrative or in a future SCHEMA.md.

Schema lives under design-systems/_schema/ as three files:

- tokens.schema.ts   — TypeScript declaration of every shared token
                       with its layer (A1-identity / A1-structure /
                       A2 / B-slot), plus per-brand C-extension
                       allowlists and a global C-prefix allowlist
- defaults.css       — CSS mirror of A2 fallback values, used as the
                       human-readable contract reviewer's-eye copy
                       and the future input to the derive script
- AGENTS.md          — schema layer model, C → B-slot → A2 promotion
                       rules, when-not-to-add-a-token guidance

Layer model:

  A1-identity    8 tokens — bg/surface/fg/muted/border/accent +
                 font-display/font-body. The brand IS these values;
                 no fallback is defensible.

  A1-structure  18 tokens — type scale (8), leading (2), tracking
                 (1), section-y (3), container (4). Structural
                 decisions vary per brand by design and have no
                 cross-brand default.

  A2            26 tokens — accent states, semantic colors, motion,
                 base spacing scale, radius, elevation, focus,
                 font-mono. Required in every tokens.css; fallback
                 lives in defaults.css for the future derive script
                 to inline when DESIGN.md does not specify the value.

  B-slot         4 tokens — fg-2 / meta / surface-warm / border-soft.
                 Brand may bind independently or alias the named
                 sibling via var(...) for components that target the
                 richer ramp.

  C-extension    n tokens — brand-specific names (kami's tag-bg-*,
                 leading-display, accent-light, etc.). Allowlisted
                 per-brand in BRAND_EXTENSIONS or globally by prefix
                 in BRAND_EXTENSION_PREFIXES. Promote when a second
                 brand adopts the same name.

Why A2 fails the guard today:
  Artifacts are generated by agents pasting one brand's :root block
  into a single <style>; there is no global stylesheet that supplies
  fallbacks at runtime. A tokens.css missing an A2 declaration would
  silently break any var() reference in the fixture. Until the
  derive script (PR-B) lands and inlines defaults, every brand's
  tokens.css must declare every A2 token directly. The guard
  enforces this strictly.

Why --font-mono lands in A2 (not A1):
  149 brands' DESIGN.md files were surveyed: 87 (58%) declare a
  monospace stack, 62 (42%) do not — including major brands like
  bmw / nike / apple / notion / mastercard / meta. Agent paste
  cannot rely on the brand author having written it down; a
  defaultable A2 fallback (with CJK brands like kami overriding) is
  safer than forcing every brand author to add a field they may not
  realize their kbd / code-block components need.

Five guard checks, each registered as its own entry in scripts/guard.ts
so failures attribute to a specific contract:

  1. token-fixture sync       — components.html :root ↔ tokens.css :root
                                 byte-equivalent (existing)
  2. A1 required tokens       — every brand declares every A1 token
  3. A2 required tokens       — every brand declares every A2 token
  4. unknown token allowlist  — every declared token is in schema or
                                 brand-extension allowlist
  5. A2 defaults parity       — defaults.css ↔ tokens.schema.ts
                                 fallback byte-equivalent

Verified on default + kami:
  - 26 A1 tokens declared in both brands
  - 26 A2 tokens declared in both brands
  - 129 total declarations, all match shared schema or brand extensions
  - defaults.css ↔ tokens.schema.ts parity holds
  - sanity test: drifting --motion-fast in defaults.css fails check 5
    with a clear divergence message

The PR description originally listed "Dedicated SCHEMA.md" as
explicitly NOT in this PR ("Once 3+ brands ship, extracting a single
source of truth becomes worthwhile"). That boundary moves: lefarcen's
review surfaced the schema-generalization risk, and the schema must
exist as a machine-enforced contract before the derive script can
read it. The TS file replaces the markdown that was deferred.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web/tests): pass missing designTemplates prop to ProjectView

Pre-existing typecheck regression on main: PR #955 (b5eb8c16,
"generic skills + split skills/design-templates + finalize-design
API") added required `designTemplates: SkillSummary[]` to ProjectView
Props but updated only two of the three test fixtures that render
ProjectView directly. The third — ProjectView.api-empty-response.test.tsx
— was missed, so `pnpm typecheck` (and CI on any PR merging into
main) fails on:

  apps/web/tests/components/ProjectView.api-empty-response.test.tsx
    (168,6): error TS2741: Property 'designTemplates' is missing in
    type ...

The other two ProjectView tests already pass `designTemplates={[]}`,
so this aligns this fixture with the existing pattern. Out of scope
for #1231 strictly, but the regression blocks the merged-state
typecheck CI runs that #1231 triggers, and the one-line fix here
restores main's typecheck health for everyone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(design-systems): enforce B-slot required tokens in pnpm guard

Closes mrcfps + lefarcen review comment thread on #1231:

> The guard validates A2 required tokens here, but there's no
> sibling check for B-slot aliases (--fg-2, --meta, --surface-warm,
> --border-soft). Per the schema docs, every brand must declare
> A1 + A2 + B-slot names so shared components can safely read
> var(--fg-2) etc. Without a B-slot guard, a brand can omit those
> aliases, pass pnpm guard, and break any artifact that references
> them.

Same artifact-paste constraint as A2: agents render artifacts by
pasting one brand's :root block into a single <style>; there is no
runtime cascade, so a missing B-slot makes any var(--fg-2) reference
resolve to nothing. Until now the schema narrative claimed B-slots
were optional with a var() default, but no machine check enforced
declaration — a contract gap reviewers reasonably refused to merge.

This commit closes the gap in three places so machine and narrative
agree:

1. scripts/check-tokens-fixture-sync.ts
   - Add checkDesignSystemBSlotRequiredTokens, mirroring the A2
     check but using getBSlotNames() from the schema.
   - Failure message names each missing slot AND the schema-suggested
     alias (--fg-2 (default alias: var(--fg))) so a brand author
     fixing the failure has a copy-pasteable resolution.
   - Renumber section comments: 5 checks → 6 checks.

2. scripts/guard.ts
   - Register the new check between A2 required and unknown
     allowlist so failures attribute to a specific contract.

3. design-systems/_schema/AGENTS.md
   - Update the layer table: B-slot row's "If omitted" column
     changes from "resolves via var() to a richer sibling" to
     "guard fails — brand must declare, either as var(--sibling)
     (collapsed) or independent value (richer)".
   - Add a "Why B-slot is required (and what the alias is for)"
     section that distinguishes the schema-suggested alias from a
     runtime fallback, with worked examples for default (alias) and
     kami (independent bind).

Verified on default + kami:
- pnpm guard passes all 6 design-system checks
- 4 B-slot tokens declared in both brands (default aliases via var(),
  kami binds independently — both forms satisfy the contract)
- pnpm typecheck clean across the workspace
- Sanity test: removing --fg-2 + --meta from default/tokens.css fires
  the new guard with a precise per-token alias hint:
    [default] design-systems/default/tokens.css is missing 2 B-slot
    tokens (alias the named sibling via var(...) or bind
    independently):
      --fg-2 (default alias: var(--fg)),
      --meta (default alias: var(--muted))

The schema contract is now machine-enforced end-to-end (A1 + A2 +
B-slot all required-with-fixed-form-of-fallback). The derive script
in PR-B can rely on every brand's tokens.css containing every shared
slot name.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(e2e): skip leading-underscore meta-directories under design-systems/

CI for #1231 went red on `Validate workspace` after merging origin/main.
Cause is a clean collision between two recently-landed changes:

- main #1270 (be77dc03 "Default English resource i18n fallback")
  tightened tests/localized-content.test.ts so every directory under
  design-systems/ is run through assertResourceId() with the strict
  RESOURCE_ID_PATTERN /^[a-z0-9][a-z0-9-]*$/.

- this branch #1231 introduced design-systems/_schema/ as the home
  of the shared token contract (tokens.schema.ts, defaults.css,
  AGENTS.md). The leading underscore signals "meta-directory, not
  brand" — the same convention SCSS partials, Jekyll, Hugo all use.

The two changes never met until CI built the merge commit, where
assertResourceId('_schema') deterministically failed:

  Error: Design system directory _schema has malformed resource id: _schema
    at invariant tests/localized-content.test.ts:66:11
    at assertResourceId tests/localized-content.test.ts:71:3
    at readDesignSystemResources tests/localized-content.test.ts:202:8

Fix tightens readDesignSystemResources's directory filter so the
leading-underscore convention is recognised explicitly:

    .filter((entry) => entry.isDirectory() && !entry.name.startsWith('_'))

This aligns with what apps/daemon/src/design-systems.ts:listDesignSystems
already does implicitly — it requires DESIGN.md per directory, so
_schema/ was always invisible at runtime; the test was the only place
that surfaced it.

Verified locally on the post-merge tree:
- pnpm test (e2e vitest) — tests/localized-content.test.ts: 4 passed
- pnpm guard — all 6 design-system checks pass on default + kami
- pnpm typecheck — clean across the workspace (after pnpm install
  to pull deps for tools/pr that arrived with main)

The fix is intentionally narrow (one filter line in one test) and
documents the convention inline so future meta-directories under
design-systems/ (e.g. _archive/, _drafts/) are covered for free.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@192.168.10.16>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 22:23:34 +08:00

412 lines
15 KiB
TypeScript

import { readdir, readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
declare global {
interface ImportMeta {
glob<T = unknown>(pattern: string, options: { eager: true }): Record<string, T>;
}
}
type LocalizedContentIds = {
skills: string[];
designSystems: string[];
designSystemCategories: string[];
promptTemplates: string[];
promptTemplateCategories: string[];
promptTemplateTags: string[];
};
type LocalizedContentModule = {
LOCALIZED_CONTENT_IDS: Record<string, LocalizedContentIds>;
localizeDesignSystemSummary: (locale: string, system: DesignSystemResource) => string;
localizePromptTemplateSummary: (
locale: string,
template: PromptTemplateResource,
) => PromptTemplateResource;
localizeSkillDescription: (locale: string, skill: SkillResource) => string;
};
type SkillResource = { id: string; description: string };
type DesignSystemResource = { id: string; category: string; summary: string | null };
type PromptTemplateResource = { id: string; category: string; tags: string[]; title: string; summary: string };
const repoRoot = fileURLToPath(new URL('../../', import.meta.url));
const webContentModules = import.meta.glob<LocalizedContentModule>(
'../../apps/web/src/i18n/content.ts',
{ eager: true },
);
const localizedContentModule = Object.values(webContentModules)[0];
if (localizedContentModule == null) {
throw new Error('Failed to load apps/web localized content ids');
}
const {
LOCALIZED_CONTENT_IDS,
localizeDesignSystemSummary,
localizePromptTemplateSummary,
localizeSkillDescription,
} = localizedContentModule;
const COVERAGE_LOCALES = ['de', 'fr', 'ru'] as const;
const RESOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
function sorted(values: Iterable<string>): string[] {
return [...values].sort((a, b) => a.localeCompare(b));
}
function uniqueSorted(values: Iterable<string>): string[] {
return sorted(new Set(values));
}
function invariant(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function assertResourceId(id: string, label: string): void {
invariant(RESOURCE_ID_PATTERN.test(id), `${label} has malformed resource id: ${id}`);
}
async function assertDirectory(root: string, label: string): Promise<void> {
let info;
try {
info = await stat(root);
} catch (error) {
throw new Error(`${label} root is missing: ${root}`, { cause: error });
}
invariant(info.isDirectory(), `${label} root is not a directory: ${root}`);
}
function normalizeText(value: string): string {
return value.replace(/\s+/g, ' ').trim();
}
function extractYamlScalar(frontmatter: string, key: string): string | null {
const lines = frontmatter.split(/\r?\n/);
const keyPattern = new RegExp(`^${key}:\\s*(.*?)\\s*$`);
const keyIndex = lines.findIndex((line) => keyPattern.test(line));
if (keyIndex === -1) return null;
const keyLine = lines[keyIndex];
invariant(keyLine, `YAML key ${key} is missing after lookup`);
const rawValue = keyPattern.exec(keyLine)?.[1]?.trim() ?? '';
invariant(
!rawValue.startsWith('|') || /^([|])[-+]?$/.test(rawValue),
`Skill frontmatter key ${key} has malformed block scalar marker: ${rawValue}`,
);
invariant(
!rawValue.startsWith('>') || /^([>])[-+]?$/.test(rawValue),
`Skill frontmatter key ${key} has malformed block scalar marker: ${rawValue}`,
);
const blockMarker = /^([|>])[-+]?$/.exec(rawValue)?.[1];
if (blockMarker) {
const blockLines: string[] = [];
for (const line of lines.slice(keyIndex + 1)) {
if (/^\S/.test(line)) break;
blockLines.push(line.replace(/^\s{2}/, ''));
}
const value = normalizeText(blockLines.join(blockMarker === '>' ? ' ' : '\n'));
return value || null;
}
invariant(
!rawValue.startsWith('"') || rawValue.endsWith('"'),
`Skill frontmatter key ${key} has malformed quoted scalar`,
);
invariant(
!rawValue.startsWith("'") || rawValue.endsWith("'"),
`Skill frontmatter key ${key} has malformed quoted scalar`,
);
invariant(
!rawValue.endsWith('"') || rawValue.startsWith('"'),
`Skill frontmatter key ${key} has malformed quoted scalar`,
);
invariant(
!rawValue.endsWith("'") || rawValue.startsWith("'"),
`Skill frontmatter key ${key} has malformed quoted scalar`,
);
const value = unquoteYamlScalar(rawValue);
return value ? normalizeText(value) : null;
}
function parseFrontmatter(filePath: string, src: string): string {
const text = src.replace(/^\uFEFF/, '');
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
invariant(match?.[1], `Skill frontmatter is missing: ${filePath}`);
return match[1];
}
function unquoteYamlScalar(value: string): string {
const trimmed = value.trim();
if (
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
) {
return trimmed.slice(1, -1);
}
return trimmed;
}
async function readSkillRootResources(rootName: 'skills' | 'design-templates'): Promise<SkillResource[]> {
const skillsRoot = path.join(repoRoot, rootName);
await assertDirectory(skillsRoot, rootName);
const entries = await readdir(skillsRoot, { withFileTypes: true });
const resources = await Promise.all(
entries
.filter((entry) => entry.isDirectory())
.map(async (entry) => {
const filePath = path.join(skillsRoot, entry.name, 'SKILL.md');
let raw: string;
try {
raw = await readFile(filePath, 'utf8');
} catch (error) {
throw new Error(`${rootName} resource is missing required file: ${filePath}`, { cause: error });
}
const frontmatter = parseFrontmatter(filePath, raw);
const id = extractYamlScalar(frontmatter, 'name') ?? entry.name;
assertResourceId(id, `${rootName} ${entry.name}`);
const description = extractYamlScalar(frontmatter, 'description');
invariant(
description,
`${rootName} ${id} is missing required English fallback field: description`,
);
return { id, description };
}),
);
return resources.sort((a, b) => a.id.localeCompare(b.id));
}
async function readSkillResources(): Promise<SkillResource[]> {
const [skills, designTemplates] = await Promise.all([
readSkillRootResources('skills'),
readSkillRootResources('design-templates'),
]);
return [...skills, ...designTemplates].sort((a, b) => a.id.localeCompare(b.id));
}
async function readDesignSystemResources(): Promise<DesignSystemResource[]> {
const systemsRoot = path.join(repoRoot, 'design-systems');
await assertDirectory(systemsRoot, 'design systems');
const entries = await readdir(systemsRoot, { withFileTypes: true });
const resources = await Promise.all(
entries
// Skip meta-directories whose names begin with `_` (e.g. `_schema/`,
// which holds the shared token contract — not a brand). This mirrors
// the leading-underscore-is-meta convention used by Jekyll, Hugo,
// SCSS partials, etc. The daemon's listDesignSystems already filters
// these out implicitly (it requires DESIGN.md); doing the same here
// keeps the localized-content guard aligned with the runtime registry.
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('_'))
.map(async (entry) => {
assertResourceId(entry.name, `Design system directory ${entry.name}`);
const filePath = path.join(systemsRoot, entry.name, 'DESIGN.md');
let raw: string;
try {
raw = await readFile(filePath, 'utf8');
} catch (error) {
throw new Error(`Design system resource is missing required file: ${filePath}`, {
cause: error,
});
}
const category = normalizeText(/^>\s*Category:\s*(.+?)\s*$/im.exec(raw)?.[1] ?? '');
invariant(
category,
`Design system ${entry.name} is missing required English fallback field: category`,
);
const summaryLine = raw
.split(/\r?\n/)
.find((line) => /^>\s*(?!Category:)(.+?)\s*$/i.test(line));
const summary = summaryLine ? normalizeText(summaryLine.replace(/^>\s*/, '')) : null;
invariant(
summary || category,
`Design system ${entry.name} is missing required English fallback field: summary or category fallback`,
);
return { id: entry.name, category, summary };
}),
);
return resources.sort((a, b) => a.id.localeCompare(b.id));
}
async function readPromptTemplateResources(): Promise<PromptTemplateResource[]> {
const templatesRoot = path.join(repoRoot, 'prompt-templates');
await assertDirectory(templatesRoot, 'prompt templates');
const resources: PromptTemplateResource[] = [];
for (const surface of ['image', 'video']) {
const dir = path.join(templatesRoot, surface);
await assertDirectory(dir, `prompt templates/${surface}`);
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
const filePath = path.join(dir, entry.name);
let rawText: string;
try {
rawText = await readFile(filePath, 'utf8');
} catch (error) {
throw new Error(`Prompt template resource is unreadable: ${filePath}`, { cause: error });
}
let raw: unknown;
try {
raw = JSON.parse(rawText);
} catch (error) {
throw new Error(`Prompt template JSON is malformed: ${filePath}`, { cause: error });
}
invariant(
Boolean(raw) && typeof raw === 'object' && !Array.isArray(raw),
`Prompt template ${filePath} must be a JSON object`,
);
const template = raw as Record<string, unknown>;
invariant(
typeof template.id === 'string' && template.id.trim().length > 0,
`Prompt template ${filePath} is missing or has malformed required id`,
);
const id = template.id.trim();
assertResourceId(id, `Prompt template ${filePath}`);
invariant(
template.surface === surface,
`Prompt template ${id} has mismatched surface metadata: expected ${surface}`,
);
invariant(
typeof template.title === 'string' && template.title.trim().length > 0,
`Prompt template ${id} is missing required English fallback field: title`,
);
invariant(
typeof template.prompt === 'string' && template.prompt.trim().length >= 20,
`Prompt template ${id} is missing or has malformed required prompt`,
);
const source = template.source;
invariant(
Boolean(source) && typeof source === 'object' && !Array.isArray(source),
`Prompt template ${id} is missing or has malformed source metadata`,
);
const sourceRecord = source as Record<string, unknown>;
invariant(
typeof sourceRecord.repo === 'string' && typeof sourceRecord.license === 'string',
`Prompt template ${id} is missing source.repo or source.license`,
);
const summary = typeof template.summary === 'string' ? normalizeText(template.summary) : '';
invariant(
summary,
`Prompt template ${id} is missing required English fallback field: summary`,
);
const category =
typeof template.category === 'string' ? normalizeText(template.category) || 'General' : 'General';
const tags = Array.isArray(template.tags)
? template.tags
.filter((tag): tag is string => typeof tag === 'string')
.map((tag) => normalizeText(tag))
.filter((tag) => tag.length > 0)
: [];
resources.push({
id,
title: normalizeText(template.title),
summary,
category,
tags,
});
}
}
return resources.sort((a, b) => a.id.localeCompare(b.id));
}
describe('localized display content coverage', () => {
it('derives displayable resources from discovered English fallback content', async () => {
const [skills, designSystems, promptTemplates] = await Promise.all([
readSkillResources(),
readDesignSystemResources(),
readPromptTemplateResources(),
]);
expect(uniqueSorted(skills.map((skill) => skill.id)), 'Expected discovered skills to be readable').not.toEqual([]);
expect(
uniqueSorted(designSystems.map((system) => system.id)),
'Expected discovered design systems to be readable',
).not.toEqual([]);
expect(
uniqueSorted(promptTemplates.map((template) => template.id)),
'Expected discovered prompt templates to be readable',
).not.toEqual([]);
for (const locale of COVERAGE_LOCALES) {
for (const skill of skills) {
expect(
normalizeText(localizeSkillDescription(locale, skill)),
`${locale} should display a skill description for ${skill.id}`,
).not.toEqual('');
}
for (const system of designSystems) {
expect(
normalizeText(localizeDesignSystemSummary(locale, system)),
`${locale} should display a design-system summary for ${system.id}`,
).not.toEqual('');
}
for (const template of promptTemplates) {
const localized = localizePromptTemplateSummary(locale, template);
expect(
normalizeText(localized.title),
`${locale} should display a prompt-template title for ${template.id}`,
).not.toEqual('');
expect(
normalizeText(localized.summary),
`${locale} should display a prompt-template summary for ${template.id}`,
).not.toEqual('');
}
}
});
for (const locale of COVERAGE_LOCALES) {
const ids = LOCALIZED_CONTENT_IDS[locale];
invariant(ids, `Localized content ids are missing for ${locale}`);
it(`covers every discovered design-system category and prompt tag for ${locale}`, async () => {
const [designSystems, promptTemplates] = await Promise.all([
readDesignSystemResources(),
readPromptTemplateResources(),
]);
const designSystemCategories = uniqueSorted(designSystems.map((system) => system.category));
const promptTemplateCategories = uniqueSorted(
promptTemplates.map((template) => template.category),
);
const promptTemplateTags = uniqueSorted(
promptTemplates.flatMap((template) => template.tags),
);
expect(
sorted(ids.designSystemCategories),
`${locale} is missing localized design-system category translations for: ${designSystemCategories.filter((category) => !ids.designSystemCategories.includes(category)).join(', ') || 'none'}`,
).toEqual(expect.arrayContaining(designSystemCategories));
expect(
sorted(ids.promptTemplateCategories),
`${locale} is missing localized prompt-template category translations for: ${promptTemplateCategories.filter((category) => !ids.promptTemplateCategories.includes(category)).join(', ') || 'none'}`,
).toEqual(expect.arrayContaining(promptTemplateCategories));
expect(
sorted(ids.promptTemplateTags),
`${locale} is missing localized prompt-template tag translations for: ${promptTemplateTags.filter((tag) => !ids.promptTemplateTags.includes(tag)).join(', ') || 'none'}`,
).toEqual(expect.arrayContaining(promptTemplateTags));
});
}
});