Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
99 lines
7.0 KiB
JavaScript
99 lines
7.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const api = readFileSync("src/api/campaigns.ts", "utf8");
|
|
const page = readFileSync("src/features/reports/AggregateReportsPage.tsx", "utf8");
|
|
const reportPage = readFileSync("src/features/campaigns/CampaignReportPage.tsx", "utf8");
|
|
const reviewPage = readFileSync("src/features/campaigns/ReviewSendPage.tsx", "utf8");
|
|
const reviewPresentation = readFileSync(
|
|
"src/features/campaigns/review/reviewPresentation.tsx",
|
|
"utf8"
|
|
);
|
|
const operatorPage = readFileSync("src/features/operator/OperatorQueuePage.tsx", "utf8");
|
|
const overviewPage = readFileSync("src/features/campaigns/CampaignOverviewPage.tsx", "utf8");
|
|
const attachmentsPage = readFileSync("src/features/campaigns/AttachmentsDataPage.tsx", "utf8");
|
|
const deliveryMode = readFileSync("src/features/campaigns/utils/deliveryMode.ts", "utf8");
|
|
const translationCatalog = readFileSync("src/i18n/generatedTranslations.ts", "utf8");
|
|
|
|
assert(api.includes('"/api/v1/campaigns/aggregate-reports"'), "the report list uses the safe aggregate collection");
|
|
assert(api.includes("/api/v1/campaigns/aggregate-reports/${encodeURIComponent(campaignId)}"), "the selected report uses the safe aggregate projection");
|
|
|
|
assert(page.includes("listAggregateReportCampaigns"), "the page loads only the safe Campaign selector projection");
|
|
assert(page.includes("getAggregateCampaignReport"), "the page loads only the safe aggregate report projection");
|
|
assert(!page.includes("getCampaignReport"), "the aggregate page cannot load the recipient-aware full report");
|
|
assert(!page.includes("getCampaignJobs"), "the aggregate page cannot cache recipient jobs");
|
|
assert(!page.includes("getCampaignJobDetail"), "the aggregate page has no detail route");
|
|
assert(!page.includes("downloadCampaignJobsCsv"), "the aggregate page has no export path");
|
|
assert(!page.includes("localStorage"), "the aggregate page does not persist report data in local storage");
|
|
assert(!page.includes("sessionStorage"), "the aggregate page does not persist report data in session storage");
|
|
assert(page.includes("TableActionGroup"), "campaign selection uses the central icon-only table action group");
|
|
assert.match(page, /<Card\s+title="i18n:govoplan-campaign\.campaign_reports_available_to_you\.f14fa403"\s+bodyLayout="table">\s*<LoadingFrame[\s\S]*?<DataGrid/, "the report selector uses the shared edge-to-edge table body through its loading frame");
|
|
assert.match(attachmentsPage, /<Card\s+id="campaign-global-attachments"[^>]*bodyLayout="table"[^>]*collapsible>[\s\S]*?\{version && draft && <AttachmentRulesDataGrid/, "global attachments retain their collapsible table body and wait for real draft columns before restoring personal widths");
|
|
assert(page.includes("disabled: campaign.id === selectedFromUrl"), "the selected row action stays visible and disabled");
|
|
assert(page.includes('columnType: "from-list"'), "campaign status uses the stable shared list-filter model");
|
|
const expectedCampaignStatuses = [
|
|
"draft",
|
|
"validated",
|
|
"needs_review",
|
|
"ready_to_queue",
|
|
"queued",
|
|
"sending",
|
|
"sent",
|
|
"partially_completed",
|
|
"outcome_unknown",
|
|
"failed",
|
|
"cancelled",
|
|
"archived"
|
|
];
|
|
for (const status of expectedCampaignStatuses) {
|
|
assert(page.includes(`{ value: "${status}", label:`), `the aggregate list declares the ${status} status explicitly`);
|
|
}
|
|
const outcomeCardsStart = page.indexOf("Aggregate outcome cards are deliberately not filter shortcuts");
|
|
const outcomeCardsEnd = page.indexOf('<Card title="i18n:govoplan-campaign.population_and_exclusions', outcomeCardsStart);
|
|
assert(outcomeCardsStart >= 0 && outcomeCardsEnd > outcomeCardsStart, "the aggregate privacy boundary is documented beside the outcome cards");
|
|
assert(!page.slice(outcomeCardsStart, outcomeCardsEnd).includes("onClick"), "aggregate outcome cards cannot create unsuppressed filtered subgroups");
|
|
assert(!page.slice(outcomeCardsStart, outcomeCardsEnd).includes("drilldown="), "privacy-suppressed aggregates remain informational metrics");
|
|
assert(overviewPage.includes("drilldown={{"), "campaign overview metrics expose explicit destinations");
|
|
assert(overviewPage.includes("openSection(\"recipients\")"), "the recipient count opens its owning collection");
|
|
assert(attachmentsPage.includes('revealAttachmentSection("campaign-global-attachments")'), "attachment counts reveal their underlying editor section");
|
|
assert(attachmentsPage.includes("openRecipients"), "per-recipient attachment counts open recipient details");
|
|
assert(reviewPage.includes("messageState: 'list:[\"needs_review\",\"blocked\"]'"), "review counts apply the unified message-state table filter");
|
|
assert(reviewPage.includes('revealReviewElement("campaign-attachment-preview")'), "attachment-result counts reveal the evidence preview");
|
|
assert(!page.includes("{report.population.denominator_definition}"), "the known denominator contract uses a localized UI explanation");
|
|
assert(!page.includes("{report.privacy.rule}"), "the known privacy contract uses a localized UI explanation");
|
|
assert(page.includes("all_persisted_recipient_delivery_jobs_for_the_se.208e90cc"), "the denominator explanation is bilingual");
|
|
assert(page.includes("positive_counts_below_the_threshold_are_hidden_a.1dc97093"), "the suppression rule is bilingual");
|
|
|
|
const touchedSources = [
|
|
page,
|
|
reportPage,
|
|
reviewPage,
|
|
reviewPresentation,
|
|
operatorPage,
|
|
deliveryMode
|
|
].join("\n");
|
|
const touchedKeys = new Set(touchedSources.match(/i18n:govoplan-campaign\.[a-z0-9_]+\.[a-f0-9]{8}/g) ?? []);
|
|
for (const key of touchedKeys) {
|
|
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const catalogEntries = [...translationCatalog.matchAll(new RegExp(`"${escapedKey}":\\s*("(?:[^"\\\\]|\\\\.)*")`, "g"))];
|
|
assert.equal(catalogEntries.length, 2, `${key} is present exactly once in both language catalogs`);
|
|
const english = JSON.parse(catalogEntries[0][1]);
|
|
const german = JSON.parse(catalogEntries[1][1]);
|
|
assert.notEqual(english.trim(), "", `${key} has a non-empty English translation`);
|
|
assert.notEqual(german.trim(), "", `${key} has a non-empty German translation`);
|
|
}
|
|
|
|
const sliceSections = [...translationCatalog.matchAll(/ "i18n:govoplan-campaign\.campaign_reports_available_to_you\.f14fa403":[\s\S]*? "i18n:govoplan-campaign\.worker_queue_committed_value0_message_s_and_publ\.24400d3c":.*\n/g)];
|
|
assert.equal(sliceSections.length, 2, "the current slice has one contiguous English and German catalog section");
|
|
const newEnglishEntries = [...sliceSections[0][0].matchAll(/"(i18n:govoplan-campaign\.[a-z0-9_]+\.[a-f0-9]{8})":\s*("(?:[^"\\]|\\.)*")/g)];
|
|
for (const [, key, encodedEnglish] of newEnglishEntries) {
|
|
const english = JSON.parse(encodedEnglish);
|
|
const digest = createHash("sha1").update(english).digest("hex").slice(0, 8);
|
|
const slug = english.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+/, "").slice(0, 48);
|
|
assert.equal(key, `i18n:govoplan-campaign.${slug}.${digest}`, `${key} uses the normalized source slug and exact SHA-1 suffix`);
|
|
assert(touchedKeys.has(key), `${key} is referenced by the current Campaign UI slices`);
|
|
}
|
|
|
|
console.log("aggregate report UI structure checks passed");
|