Files
govoplan-campaign/webui/tests/template-preview-draft.test.ts
T
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

116 lines
5.8 KiB
TypeScript

import { campaignJsonForAttachmentPreview } from "../src/features/campaigns/utils/templatePreviewDraft";
import { checkContentLibraryCompatibility } from "../src/features/campaigns/utils/contentLibraryCompatibility";
import {
campaignVersionUpdateForRequest,
clientCampaignEditorState
} from "../src/features/campaigns/utils/editorState";
function assert(condition: unknown, message = "assertion failed"): void {
if (!condition) throw new Error(message);
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
const draft = {
recipients: {
from: { name: "", email: "" },
to: [{ name: "Bob", email: " bob@example.org " }, { name: "", email: "" }]
},
entries: {
inline: [{
id: "recipient-1",
active: true,
name: "",
email: "",
from: [],
to: [{ name: "", email: "" }, { name: "Alice", email: " alice@example.org " }],
cc: [],
bcc: [],
merge_to: false
}],
defaults: {
name: "",
email: "",
to: [{ name: "", email: "" }],
attachments: []
}
}
};
const normalized = campaignJsonForAttachmentPreview(draft);
const entries = asRecord(normalized.entries);
const inline = entries.inline as Record<string, unknown>[];
const entry = inline[0];
const to = entry.to as Record<string, unknown>[];
const defaults = asRecord(entries.defaults);
const recipients = asRecord(normalized.recipients);
const globalTo = recipients.to as Record<string, unknown>[];
assert(recipients.from === undefined, "empty global from object is stripped");
assert(globalTo.length === 1 && globalTo[0].email === "bob@example.org", "global recipient arrays are sanitized");
assert(entry.email === undefined, "empty legacy entry email is stripped");
assert(entry.name === undefined, "empty legacy entry name is stripped");
assert(Array.isArray(entry.from) && (entry.from as unknown[]).length === 0, "empty address arrays remain arrays");
assert(to.length === 1 && to[0].email === "alice@example.org", "empty recipient objects are removed and valid addresses are trimmed");
assert(defaults.email === undefined, "empty defaults email is stripped");
assert(Array.isArray(defaults.to) && (defaults.to as unknown[]).length === 0, "empty defaults recipients are removed");
assert((asRecord(draft.entries).inline as Record<string, unknown>[])[0].email === "", "original draft is not mutated");
const compatibility = checkContentLibraryCompatibility([
{ path: "local::display_name", value_type: "string", label: "Display name", required: true },
{ path: "global.case_reference", value_type: "string", label: "Case reference", required: true },
{ path: "count", value_type: "number", label: "Count", required: true },
{ path: "local.to.2.email", value_type: "string", label: "Second recipient", required: true },
{ path: "optional", value_type: "string", required: false }
], new Map([
["local:display_name", "string"],
["local:to", "string"],
["count", "boolean"]
]));
assert(!compatibility.compatible, "missing or wrong-typed fields are incompatible");
assert(compatibility.missing.length === 1 && compatibility.missing[0].path === "global:case_reference", "namespaced missing fields are normalized");
assert(compatibility.incompatible.length === 1 && compatibility.incompatible[0].actual === "boolean", "type mismatches are reported");
// Ordinary saves must not echo the server-owned evidence returned by reads.
// This is the state that previously broke saves when leaving the Template page.
const loadedEditorState = {
created_from: "minimal_campaign",
field_overrides: { department: false },
opt_ins: { inline_guidance: true },
review_send: {
inspection_complete: true,
reviewed_message_keys: ["gabriele.fode", "fruzsina.molnar-gabor"],
issue_decisions: [],
updated_at: "2026-07-15T11:23:31.569257+00:00",
updated_by_user_id: "064fa105-9e69-4f29-8558-a7665eb219df"
},
approval_gate: { request_id: "approval-1", requested_by_user_id: "approver-1" },
smtp: { password: "legacy-secret" }
};
const clientState = clientCampaignEditorState(loadedEditorState);
assert(JSON.stringify(clientState) === JSON.stringify({
created_from: "minimal_campaign",
field_overrides: { department: false },
opt_ins: { inline_guidance: true }
}), "draft and merge metadata contains only client-owned fields");
const savePayload = {
editor_state: loadedEditorState,
base_revision: 7,
campaign_json: { template: { subject: "Updated template" } },
migrate_legacy_mail_settings: true
};
const requestPayload = campaignVersionUpdateForRequest(savePayload);
assert(JSON.stringify(requestPayload.editor_state) === JSON.stringify(clientState), "save, autosave, and fork requests omit review and approval evidence");
assert(!JSON.stringify(requestPayload).includes("legacy-secret"), "unknown legacy metadata is not resubmitted");
assert(requestPayload.base_revision === 7 && requestPayload.migrate_legacy_mail_settings, "concurrency and explicit migration controls are preserved");
assert(requestPayload.campaign_json === savePayload.campaign_json, "campaign content is not rewritten by metadata serialization");
assert(savePayload.editor_state.review_send.inspection_complete, "read response evidence is not mutated");
assert("approval_gate" in savePayload.editor_state, "read response approval evidence is preserved");
assert(Object.keys(clientCampaignEditorState(undefined)).length === 0, "new drafts need no metadata");
const omittedEditorState = { base_revision: 2, editor_state: undefined };
assert(campaignVersionUpdateForRequest(omittedEditorState) === omittedEditorState, "omitted metadata remains omitted");
const nullEditorState = { editor_state: null };
assert(campaignVersionUpdateForRequest(nullEditorState) === nullEditorState, "null metadata retains its unchanged-state semantics");