114 lines
3.7 KiB
JavaScript
114 lines
3.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const sourceRoot = path.resolve("src");
|
|
|
|
function sourceFiles(directory) {
|
|
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const target = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) return sourceFiles(target);
|
|
return /\.(tsx|ts)$/.test(entry.name) ? [target] : [];
|
|
});
|
|
}
|
|
|
|
const sources = sourceFiles(sourceRoot).map((file) => ({
|
|
file,
|
|
value: fs.readFileSync(file, "utf8"),
|
|
}));
|
|
|
|
for (const { file, value } of sources) {
|
|
assert.ok(
|
|
!value.includes('className="content-pad workspace-data-page'),
|
|
`${path.relative(sourceRoot, file)} must use Core PageLayout instead of a raw page frame`,
|
|
);
|
|
assert.ok(
|
|
!/<div\s+className="workspace(?:\s|\")/.test(value),
|
|
`${path.relative(sourceRoot, file)} must use Core WorkspaceLayout instead of a raw workspace`,
|
|
);
|
|
}
|
|
|
|
for (const relativePath of [
|
|
"features/campaigns/CampaignWorkspace.tsx",
|
|
"features/campaigns/CampaignModulePage.tsx",
|
|
"features/templates/TemplatesPage.tsx",
|
|
]) {
|
|
const value = fs.readFileSync(path.join(sourceRoot, relativePath), "utf8");
|
|
assert.ok(value.includes("<WorkspaceLayout"), `${relativePath} must retain the shared workspace shell`);
|
|
}
|
|
|
|
const nonSemanticClicks = sources.flatMap(({ file, value }) =>
|
|
[...value.matchAll(/<(div|span|li|tr)\b[^>]*\bonClick\s*=/g)].map(
|
|
(match) => `${path.relative(sourceRoot, file)}:${match.index}`,
|
|
),
|
|
);
|
|
assert.deepEqual(
|
|
nonSemanticClicks,
|
|
[],
|
|
`Use a semantic button/link instead of clickable layout elements: ${nonSemanticClicks.join(", ")}`,
|
|
);
|
|
|
|
const preview = fs.readFileSync(
|
|
path.join(sourceRoot, "features/campaigns/components/MessagePreviewOverlay.tsx"),
|
|
"utf8",
|
|
);
|
|
|
|
const styles = fs.readFileSync(
|
|
path.join(sourceRoot, "styles/campaign-workspace.css"),
|
|
"utf8",
|
|
);
|
|
const jsonView = fs.readFileSync(
|
|
path.join(sourceRoot, "features/campaigns/CampaignJsonView.tsx"),
|
|
"utf8",
|
|
);
|
|
const auditView = fs.readFileSync(
|
|
path.join(sourceRoot, "features/campaigns/CampaignAuditPage.tsx"),
|
|
"utf8",
|
|
);
|
|
const attachmentPreview = fs.readFileSync(
|
|
path.join(sourceRoot, "features/campaigns/review/AttachmentLinkingPreview.tsx"),
|
|
"utf8",
|
|
);
|
|
assert.ok(
|
|
jsonView.includes('DismissibleAlert tone="warning" dismissible={false}'),
|
|
"The full Campaign JSON projection must retain an explicit privacy warning",
|
|
);
|
|
assert.ok(
|
|
auditView.includes("ActionBlockerHint"),
|
|
"Unavailable Campaign audit projection must retain an actionable shared blocker",
|
|
);
|
|
assert.ok(
|
|
attachmentPreview.includes('<DismissibleAlert tone="danger" compact dismissible={false}>'),
|
|
"Attachment-preview validation failures must use the compact shared alert",
|
|
);
|
|
assert.ok(
|
|
styles.includes("@media (prefers-reduced-motion: reduce)"),
|
|
"Campaign must retain an explicit reduced-motion presentation contract",
|
|
);
|
|
assert.ok(
|
|
styles.includes("@media (max-width:"),
|
|
"Campaign must retain responsive narrow-viewport layouts",
|
|
);
|
|
assert.ok(
|
|
styles.includes(":focus-visible"),
|
|
"Campaign-specific interactive controls must retain visible keyboard focus",
|
|
);
|
|
for (const handler of ["onFirst", "onPrevious", "onNext", "onLast"]) {
|
|
const buttonLine = preview
|
|
.split("\n")
|
|
.find((line) => line.includes(`<button`) && line.includes(`navigation.${handler}`));
|
|
assert.ok(
|
|
buttonLine?.includes("aria-label="),
|
|
`${handler} must remain an explicitly labelled button`,
|
|
);
|
|
}
|
|
|
|
for (const { file, value } of sources) {
|
|
if (!value.includes('role="dialog"')) continue;
|
|
assert.fail(
|
|
`${path.relative(sourceRoot, file)} implements a local dialog; use Core's Dialog component`,
|
|
);
|
|
}
|
|
|
|
console.log("Campaign accessibility structural contract passed.");
|