310 lines
12 KiB
TypeScript
310 lines
12 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 = new RegExp(`const ${name}\\s*=\\s*(?:async\\s+)?\\(`);
|
|
const match = source.match(declaration);
|
|
assert.ok(match, `${name} handler should exist.`);
|
|
const start = match.index ?? -1;
|
|
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 und senden/);
|
|
assert.match(source, /useAction/);
|
|
assert.match(source, /outreachSendAction[\s\S]*sendApprovedEmail/);
|
|
|
|
const auditPublishIndex = source.indexOf("Audit veröffentlichen");
|
|
const auditSaveIndex = source.indexOf("Änderungen speichern");
|
|
const emailApprovalIndex = source.indexOf("E-Mail freigeben und senden");
|
|
|
|
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 disables draft/approval/final controls for queued send", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
|
|
assert.match(source, /outreach\.sendStatus\s*===\s*"queued"/);
|
|
assert.match(source, /const isQueuedSend = outreach\?\.sendStatus === "queued"/);
|
|
assert.match(
|
|
source,
|
|
/disabled=\{[\s\S]*busyAction === `\$?\{record\.id\}:outreach-save`[\s\S]*\|\|\s*isQueuedSend[\s\S]*\}/,
|
|
"Outreach save control should be disabled while outreach is queued.",
|
|
);
|
|
assert.match(
|
|
source,
|
|
/disabled=\{[\s\S]*busyAction === `\$?\{record\.id\}:email-approval`[\s\S]*\|\|\s*isQueuedSend[\s\S]*\}/,
|
|
"Email approval control should be disabled while outreach is queued.",
|
|
);
|
|
assert.match(
|
|
source,
|
|
/disabled=\{[\s\S]*busyAction === `\$?\{pendingEmailConfirmation\.id\}:email-send`[\s\S]*\|\|\s*isQueuedSendForConfirmation[\s\S]*\}/,
|
|
"Final send control should be disabled while confirmed outreach is queued.",
|
|
);
|
|
});
|
|
|
|
test("OutreachReviewWorkspace prevents draft mutation handlers for queued outreach", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
const saveOutreachHandler = extractConstFunction(source, "saveOutreach");
|
|
const approveEmailHandler = extractConstFunction(source, "approveEmail");
|
|
|
|
assert.match(saveOutreachHandler, /outreach\.sendStatus\s*===\s*"queued"/);
|
|
assert.match(saveOutreachHandler, /[aA]ufgrund des laufenden Sendevorgangs/);
|
|
|
|
assert.match(approveEmailHandler, /outreach\.sendStatus\s*===\s*"queued"/);
|
|
assert.match(approveEmailHandler, /[aA]ufgrund des laufenden Sendevorgangs/);
|
|
});
|
|
|
|
test("OutreachReviewWorkspace prevents final send when confirmed outreach is queued", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
const sendHandler = extractConstFunction(source, "sendApprovedEmailFromConfirmation");
|
|
|
|
assert.match(
|
|
sendHandler,
|
|
/const isQueuedSend = rows\.some\(/,
|
|
"Final send handler should check current list records for queued status before sending.",
|
|
);
|
|
assert.match(
|
|
sendHandler,
|
|
/\.sendStatus\s*===\s*"queued"/,
|
|
"Final send handler should guard queued status.",
|
|
);
|
|
assert.match(
|
|
sendHandler,
|
|
/setNotice\(\s*"(?:.*(?:[aA]ufgrund[^"\r\n]*Sendevorgang|.*bereits[^"\r\n]*Vorgang|.*bereits[^"\r\n]*im Gange)[^"]*)"/,
|
|
);
|
|
});
|
|
|
|
test("OutreachReviewWorkspace useAction receives a typed send action ref", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
|
|
assert.match(source, /useAction\(api\.outreachSendAction\.sendApprovedEmail\)/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/as \{\s*outreachSendAction:\s*\{\s*sendApprovedEmail:\s*unknown/,
|
|
);
|
|
});
|
|
|
|
test("OutreachReviewWorkspace includes final confirmation UI fields", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
|
|
assert.match(source, /Dialog/);
|
|
assert.match(source, /Empfänger/);
|
|
assert.match(source, /Betreff/);
|
|
assert.match(source, /Absender/);
|
|
assert.match(source, /Audit-Link/);
|
|
assert.match(source, /sender/);
|
|
assert.doesNotMatch(source, /Konfigurierter SMTP-Absender/);
|
|
});
|
|
|
|
test("approveEmail opens confirmation after save and approval", 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");
|
|
const confirmationIndex = handler.indexOf("setPendingEmailConfirmation");
|
|
|
|
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(confirmationIndex >= 0, "Approval should open a confirmation dialog.");
|
|
assert.ok(draftIndex < saveIndex, "Approval should read draft before save.");
|
|
assert.ok(saveIndex < approveIndex, "Approval should save before approve.");
|
|
assert.ok(approveIndex < confirmationIndex, "Confirmation should open after approval.");
|
|
assert.equal(
|
|
/sendApprovedEmail/.test(handler),
|
|
false,
|
|
"Approval should not call sendApprovedEmail.",
|
|
);
|
|
|
|
assert.match(handler, /emailSubject:\s*draft\.emailSubject/);
|
|
assert.match(handler, /emailBody:\s*draft\.emailBody/);
|
|
assert.match(handler, /followUpDraft:\s*draft\.followUpDraft/);
|
|
assert.match(
|
|
handler,
|
|
/approvalData\.sender/,
|
|
);
|
|
assert.ok(
|
|
!/approvalData\.sender\s*\?\?\s*SMTP_SENDER_PLACEHOLDER/.test(handler),
|
|
"Sender should come from approvalResult without placeholder fallback.",
|
|
);
|
|
});
|
|
|
|
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("final email send handler calls sendApprovedEmail with outreach id", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
const handler = extractConstFunction(source, "sendApprovedEmailFromConfirmation");
|
|
|
|
assert.match(handler, /await sendApprovedEmail\(\{\s*id:\s*confirmation\.id/);
|
|
assert.ok(
|
|
/await sendApprovedEmail\([\s\S]*id:\s*confirmation\.id[\s\S]*\}/.test(handler),
|
|
"Final handler should pass the currently confirmed outreach id.",
|
|
);
|
|
assert.ok(
|
|
handler.indexOf("E-Mail gesendet.") >= 0,
|
|
"Final send should show a success notice.",
|
|
);
|
|
assert.ok(
|
|
/Retry|erneut|nochmal|nicht versendet|nicht gesendet/.test(handler),
|
|
"Final send should surface a retry-oriented failure notice.",
|
|
);
|
|
assert.equal(
|
|
handler.indexOf("setPendingEmailConfirmation(null)") >= 0,
|
|
true,
|
|
"Final handler should clear confirmation on success.",
|
|
);
|
|
});
|
|
|
|
test("canceling confirmation does not send", async () => {
|
|
const source = await readFile(outreachWorkspacePath, "utf8");
|
|
const handler = extractConstFunction(source, "closeEmailConfirmation");
|
|
|
|
assert.ok(handler.includes("setPendingEmailConfirmation(null)"));
|
|
assert.equal(/sendApprovedEmail/.test(handler), false);
|
|
});
|
|
|
|
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/);
|
|
});
|