Files
govoplan-campaign/webui/tests/accessibility-contract.test.mjs
zemion c51fc180fb
Module Package Release / publish-packages (push) Successful in 12s
Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
2026-09-08 01:32:26 +02:00

128 lines
5.0 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",
);
const archivePolicyNotice = fs.readFileSync(
path.join(sourceRoot, "features/campaigns/components/CampaignArchiveEncryptionPolicyNotice.tsx"),
"utf8",
);
for (const page of ["AttachmentsDataPage.tsx", "GlobalSettingsPage.tsx"]) {
const value = fs.readFileSync(path.join(sourceRoot, "features/campaigns", page), "utf8");
assert.match(value, /<CampaignArchiveEncryptionPolicyNotice/, `${page} must make archive policy configuration discoverable`);
}
assert.match(archivePolicyNotice, /\/admin\?section=system-campaign-archive-encryption/, "Campaign must link to the actual system Legacy ZipCrypto setting");
assert.match(archivePolicyNotice, /\/admin\?section=tenant-campaign-archive-encryption/, "Campaign must expose the tenant ceiling as well as the system switch");
assert.match(archivePolicyNotice, /policyInstalled && hasScope\(auth, "admin:policies:read"\)/, "Policy admin actions stay conditional on module and permission availability");
assert.match(archivePolicyNotice, /campaigns:archive:use_legacy_zipcrypto/, "The dedicated weak-encryption permission stays explicit");
assert.match(archivePolicyNotice, /setPolicy\(UNAVAILABLE_ARCHIVE_POLICY\)/, "Reloading policy must fail closed while its current state is unknown");
assert.match(archivePolicyNotice, /setRefresh\(\(value\) => value \+ 1\)/, "Operators must be able to reload policy after an administrator changes it");
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.");