Files
webdev-pipeline/tests/outreach-review-workspace-ui.test.ts
2026-06-05 16:47:22 +02:00

165 lines
6.0 KiB
TypeScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import test from "node:test";
const outreachPagePath = join(
process.cwd(),
"app",
"dashboard",
"outreach",
"page.tsx",
);
const outreachWorkspacePath = join(
process.cwd(),
"components",
"outreach",
"outreach-review-workspace.tsx",
);
function extractConstFunction(source: string, name: string) {
const declaration = `const ${name} = async`;
const start = source.indexOf(declaration);
assert.ok(start >= 0, `${name} handler should exist.`);
const firstBrace = source.indexOf("{", start);
assert.ok(firstBrace >= 0, `${name} handler should have a body.`);
let depth = 0;
for (let index = firstBrace; index < source.length; index += 1) {
const char = source[index];
if (char === "{") {
depth += 1;
}
if (char === "}") {
depth -= 1;
if (depth === 0) {
return source.slice(start, index + 1);
}
}
}
assert.fail(`${name} handler body should close.`);
}
test("/dashboard/outreach mounts the outreach review workspace", async () => {
const source = await readFile(outreachPagePath, "utf8");
assert.doesNotMatch(source, /DashboardPlaceholderPage/);
assert.match(source, /OutreachReviewWorkspace/);
assert.match(
source,
/@\/components\/outreach\/outreach-review-workspace/,
);
});
test("OutreachReviewWorkspace uses the review workspace API and required controls", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.match(source, /api\.outreach(?:\s+as\s+OutreachApi\))?\.listReviewWorkspace/);
assert.match(source, /limit:\s*100/);
assert.match(source, /api\.outreach(?:\s+as\s+OutreachApi\))?\.saveReviewDraft/);
assert.match(source, /api\.outreach(?:\s+as\s+OutreachApi\))?\.approveEmailDraft/);
assert.match(source, /api\.audits\.savePublicAuditContent/);
assert.match(source, /api\.audits\.publishPublicAudit/);
[
"Lead-Details",
"Kontaktquellen",
"Prioritätsgrund",
"Kontaktstrategie",
"Audit-Zusammenfassung",
"Public-Audit",
"Verwendete Skills",
"Quellen anzeigen",
"Raw anzeigen",
"E-Mail-Betreff",
"E-Mail-Text",
"Telefon-Skript",
"Follow-up-Draft",
].forEach((label) => assert.match(source, new RegExp(label)));
});
test("OutreachReviewWorkspace keeps exactly one recommended email subject and body editor", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.equal((source.match(/aria-label="E-Mail-Betreff"/g) ?? []).length, 1);
assert.equal((source.match(/aria-label="E-Mail-Text"/g) ?? []).length, 1);
assert.equal((source.match(/<Input\b/g) ?? []).length, 1);
assert.doesNotMatch(source, /Version\s*[23]|Alternative|Variante/);
});
test("OutreachReviewWorkspace separates audit publication from email approval", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.match(source, /Audit veröffentlichen/);
assert.match(source, /Änderungen speichern/);
assert.match(source, /E-Mail freigeben/);
assert.doesNotMatch(source, /E-Mail freigeben und senden/);
assert.doesNotMatch(source, /api\.outreach\.(send|sendEmail|sendDraft)/);
const auditPublishIndex = source.indexOf("Audit veröffentlichen");
const auditSaveIndex = source.indexOf("Änderungen speichern");
const emailApprovalIndex = source.indexOf("E-Mail freigeben");
assert.ok(auditPublishIndex >= 0);
assert.ok(auditSaveIndex >= 0);
assert.ok(emailApprovalIndex >= 0);
assert.ok(
Math.abs(auditPublishIndex - auditSaveIndex) <
Math.abs(auditPublishIndex - emailApprovalIndex),
"Audit actions should be grouped closer to each other than to email approval.",
);
});
test("OutreachReviewWorkspace gates phone scripts to call-first or missing-contact leads with phone numbers", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
assert.match(source, /hasCallablePhone/);
assert.match(source, /strategy\s*===\s*"call_first"/);
assert.match(source, /lead\?\.contactStatus\s*===\s*"missing_contact"/);
assert.match(source, /Kein Telefon-Skript erforderlich/);
});
test("approveEmail saves the visible outreach draft before approving it", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const handler = extractConstFunction(source, "approveEmail");
const draftIndex = handler.indexOf("const draft = drafts[record.id] ?? getDraft(record)");
const saveIndex = handler.indexOf("await saveReviewDraft");
const approveIndex = handler.indexOf("await approveEmailDraft");
assert.ok(draftIndex >= 0, "Approval should read the current local draft.");
assert.ok(saveIndex >= 0, "Approval should persist the current draft first.");
assert.ok(approveIndex >= 0, "Approval should still call approveEmailDraft.");
assert.ok(
draftIndex < saveIndex && saveIndex < approveIndex,
"Approval should read draft, save it, then approve.",
);
assert.match(handler, /emailSubject:\s*draft\.emailSubject/);
assert.match(handler, /emailBody:\s*draft\.emailBody/);
assert.match(handler, /followUpDraft:\s*draft\.followUpDraft/);
});
test("publishAudit saves the visible audit draft before publishing it", async () => {
const source = await readFile(outreachWorkspacePath, "utf8");
const handler = extractConstFunction(source, "publishAudit");
const draftIndex = handler.indexOf("const draft = drafts[record.id] ?? getDraft(record)");
const saveIndex = handler.indexOf("await savePublicAuditContent");
const publishIndex = handler.indexOf("await publishPublicAudit");
assert.ok(draftIndex >= 0, "Publishing should read the current local audit draft.");
assert.ok(saveIndex >= 0, "Publishing should save the public audit content first.");
assert.ok(publishIndex >= 0, "Publishing should still call publishPublicAudit.");
assert.ok(
draftIndex < saveIndex && saveIndex < publishIndex,
"Publishing should read draft, save it, then publish.",
);
assert.match(handler, /publicSummary:\s*draft\.auditSummary/);
assert.match(handler, /publicBody:\s*draft\.auditBody/);
});