import { expect, test, type Page } from "@playwright/test"; async function install(page: Page, options: { kind?: "smtp" | "imap"; language?: "en" | "de"; holdWrite?: Promise; interrupt?: boolean; failRefresh?: boolean; diagnostics?: boolean; authoring?: boolean; holdPreview?: Promise; } = {}) { const kind = options.kind ?? "smtp"; const errors: string[] = []; page.on("pageerror", error => { errors.push(error.message); console.error("Delivery fixture:", error.message); }); const writes: { path: string; payload: Record }[] = []; const wholeReads: string[] = []; let progressReads = 0; let previews = 0; let acknowledged = false; let active = false; let validated = false; const jobs = Array.from({ length: 4 }, (_value, index) => ({ id: `delivery-job-${index + 1}`, entry_index: index + 1, entry_id: `entry-${index + 1}`, recipient_email: `recipient-${index + 1}@example.test`, subject: `Delivery message ${index + 1}`, build_status: "built", validation_status: "ready", queue_status: "draft", send_status: kind === "imap" ? "smtp_accepted" : "not_queued", imap_status: options.diagnostics ? ["pending", "appending", "failed", "outcome_unknown"][index] : kind === "imap" ? "pending" : "not_requested", resolved_recipients: { to: [{ email: `recipient-${index + 1}@example.test` }] }, issues: [], attachments: [], review_decision: { eligible: false }, attempt_count: kind === "imap" ? 1 : 0 })); const version = () => ({ id: "delivery-version", campaign_id: "delivery-campaign", version_number: 1, edit_revision: 1, strong_etag: '"delivery-version:1"', review_build_token: "delivery-build", locked_at: options.authoring && !validated ? null : "2026-09-07T10:00:00Z", workflow_state: options.authoring && !validated ? "draft" : "built", validation_summary: options.authoring && !validated ? null : { ok: true, warning_count: 0, error_count: 0, issues: [] }, build_summary: options.authoring ? null : { built_count: 4, blocked_count: 0 }, execution_snapshot_hash: "frozen-snapshot", raw_json: { campaign: { name: "Delivery fixture" }, server: { mail_profile_id: "profile-fixture" }, delivery: { rate_limit: { messages_per_minute: 30 }, imap_append_sent: { enabled: kind === "imap", folder: "Sent" } }, entries: { inline: jobs.map(job => ({ id: job.entry_id, email: job.recipient_email, active: true })) } }, editor_state: { review_send: { review_build_token: "delivery-build", inspection_complete: true, reviewed_message_keys: [], issue_decisions: [] } } }); const baseJobs = (url?: URL) => { const states = url?.searchParams.getAll("imap_status") ?? []; const selected = states.length ? jobs.filter(job => states.includes(job.imap_status)) : jobs; return { jobs: selected, page: 1, page_size: 200, total: selected.length, total_unfiltered: 4, pages: 1, counts: {}, filtered_counts: {}, review: { blocking_count: 0, required_count: 0, bulk_acceptable_count: 0, reviewed_required_count: 0, inspection_complete: true } }; }; await page.route(url => url.pathname.startsWith("/api/"), async route => { const request = route.request(); const url = new URL(request.url()); if (request.method() === "GET") { if (/\/(workspace\/delta|summary|jobs|jobs\/delta)$/.test(url.pathname)) wholeReads.push(url.pathname); if (url.pathname.endsWith("/workspace/delta")) return route.fulfill({ json: { campaign: { id: "delivery-campaign", name: "Delivery fixture", current_version_id: "delivery-version", status: "draft" }, versions: [version()], current_version: version(), summary: { cards: { jobs_total: 4, sent: kind === "imap" ? 4 : 0, failed: 0 }, delivery: { background_workers_enabled: false }, status_counts: { send: kind === "imap" ? { smtp_accepted: 4 } : { not_queued: 4 }, imap: kind === "imap" ? options.diagnostics ? { pending: 1, appending: 1, failed: 1, outcome_unknown: 1 } : { pending: 4 } : {} }, attachments: {} }, deleted: [], full: true, has_more: false, watermark: "delivery-workspace" } }); if (url.pathname.endsWith("/delivery-options")) return route.fulfill({ json: { worker_queue_available: false, synchronous_send: { allowed: true, eligible_recipient_job_count: 4, policy: { max_recipient_jobs: 200 } }, approval_gate: { configured: false, available: false } } }); if (url.pathname.endsWith("/delivery-progress")) { progressReads++; if (acknowledged && options.failRefresh) return route.fulfill({ status: 503, json: { detail: "Fixture progress unavailable" } }); return route.fulfill({ json: { campaign_id: "delivery-campaign", version_id: "delivery-version", generated_at: "2026-09-07T10:00:00Z", total_jobs: 4, smtp: { total: 4, processed: acknowledged ? 4 : 1, accepted: acknowledged || kind === "imap" ? 4 : 1, active: active && kind === "smtp" ? 1 : 0, pending: acknowledged ? 0 : active ? 2 : 3, failed: 0, outcome_unknown: 0, excluded: 0, paused: 0, cancelled: 0 }, imap: { total: 4, processed: acknowledged ? 4 : 1, appended: acknowledged ? 4 : 1, active: active && kind === "imap" ? 1 : 0, pending: acknowledged ? 0 : active ? 2 : 3, failed: 0, outcome_unknown: 0, excluded: 0 }, status_counts: { send: {}, queue: {}, imap: {} } } }); } if (url.pathname.endsWith("/jobs/delta")) { if (acknowledged && options.failRefresh) return route.fulfill({ status: 503, json: { detail: "Fixture diagnostics unavailable" } }); return route.fulfill({ json: { ...baseJobs(url), full: true, has_more: false, deleted: [], watermark: "diagnostic-watermark" } }); } if (url.pathname.endsWith("/jobs")) { if (acknowledged && options.failRefresh) return route.fulfill({ status: 503, json: { detail: "Fixture diagnostics unavailable" } }); return route.fulfill({ json: baseJobs(url) }); } return route.fulfill({ json: {} }); } if (url.pathname.endsWith("/attachments/preview")) { previews++; if (previews === 1 && options.holdPreview) await options.holdPreview; const unlinked = options.authoring && previews >= 2 && !validated; const file = { id: "newly-unlinked-file", filename: "letter.pdf", display_path: "letters/letter.pdf", linked_to_campaign: false }; return route.fulfill({ json: { campaign_id: "delivery-campaign", version_id: "delivery-version", shared_file_count: 0, rules: [], linkable_files: unlinked ? [file] : [], unused_shared_files: [] } }); } const payload = request.postDataJSON(); writes.push({ path: url.pathname, payload }); if (url.pathname.endsWith("/validate")) { validated = true; return route.fulfill({ json: { ok: true, issues: [] } }); } if (url.pathname.endsWith("/send-now") || url.pathname.endsWith("/append-sent")) { active = true; if (options.holdWrite) await options.holdWrite; active = false; if (options.interrupt) return route.fulfill({ status: 503, json: { detail: "Fixture request interrupted; verify stored outcomes." } }); acknowledged = true; for (const job of jobs) { job.send_status = "smtp_accepted"; if (kind === "imap") job.imap_status = "appended"; } return route.fulfill({ json: { result: { attempted_count: 4, sent_count: 4, failed_count: 0, outcome_unknown_count: 0, paused_count: 0, pending_count: 4, appended_count: 4, processed_count: 4, results: [] } } }); } return route.abort(); }); await page.goto(`/?campaign-delivery-progress&language=${options.language ?? "en"}`); if (options.authoring) await expect.poll(() => previews).toBe(1); else await expect(page.getByRole("table", { name: "campaign-delivery-campaign-workflow-built-messages", exact: true }).getByText("Delivery message 1", { exact: true })).toBeVisible(); return { writes, wholeReads, errors, get progressReads() { return progressReads; }, get previews() { return previews; } }; } async function start(page: Page, kind: "smtp" | "imap") { if (kind === "smtp") { await page.getByRole("button", { name: /^(Send now|Jetzt senden)$/ }).click(); await page.getByRole("alertdialog").getByRole("button", { name: /^(Send now|Jetzt senden)$/ }).click(); } else await page.getByRole("button", { name: /^(Append pending IMAP now|Ausstehende IMAP-Kopien jetzt anhängen)$/ }).click(); } for (const language of ["en", "de"] as const) for (const kind of ["smtp", "imap"] as const) test(`${language}: actual ${kind} request shows live active progress without refreshing the underlying workflow`, async ({ page }) => { let release!: () => void; const held = new Promise(resolve => { release = resolve; }); const fixture = await install(page, { kind, language, holdWrite: held }); await start(page, kind); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); await expect.poll(() => fixture.writes.length).toBe(1); const wholeReads = fixture.wholeReads.length; await expect.poll(() => fixture.progressReads).toBeGreaterThanOrEqual(2); await expect(dialog.getByText(language === "de" ? "In Bearbeitung" : "In progress", { exact: true })).toBeVisible(); await expect(dialog.getByRole("status").first()).toContainText(/1.*4/); expect(fixture.wholeReads.length).toBe(wholeReads); await page.keyboard.press("Escape"); await expect(dialog).toBeVisible(); expect(fixture.writes).toHaveLength(1); release(); await expect(dialog.getByRole("button", { name: /Close|Schließen/, exact: true }).last()).toBeEnabled(); expect(fixture.errors).toEqual([]); }); for (const kind of ["smtp", "imap"] as const) test(`${kind}: failed display refresh preserves acknowledged success`, async ({ page }) => { const fixture = await install(page, { kind, failRefresh: true }); await start(page, kind); const dialog = page.getByRole("dialog"); await expect(dialog.getByRole("button", { name: /Close|Schließen/, exact: true }).last()).toBeEnabled(); await expect(dialog.getByText(/The request finished|Die Anfrage ist abgeschlossen/i).first()).toBeVisible(); await dialog.getByRole("button", { name: /Close|Schließen/, exact: true }).last().click(); await expect(page.getByText(kind === "smtp" ? /Send finished\. SMTP accepted 4/ : /IMAP append processed 4 job/)).toBeVisible(); expect(fixture.writes).toHaveLength(1); expect(fixture.errors).toEqual([]); }); test("an interrupted send remains uncertain and is never replayed by progress polling", async ({ page }) => { const fixture = await install(page, { interrupt: true }); await start(page, "smtp"); const dialog = page.getByRole("dialog"); await expect(dialog.getByText("Fixture request interrupted; verify stored outcomes.", { exact: false })).toBeVisible(); await expect.poll(() => fixture.progressReads).toBeGreaterThanOrEqual(2); expect(fixture.writes).toHaveLength(1); expect(fixture.errors).toEqual([]); }); test("IMAP diagnostics use shared SMTP and IMAP list filters", async ({ page }) => { const fixture = await install(page, { kind: "imap", diagnostics: true }); await page.getByRole("button", { name: /^(Load IMAP diagnostics|IMAP-Diagnose laden)$/ }).click(); const table = page.getByRole("table", { name: "campaign-delivery-campaign-workflow-imap-diagnostics", exact: true }); await expect(table.getByText("recipient-1@example.test", { exact: true })).toBeVisible(); const filterButtons = table.getByRole("button", { name: /^Filter (SMTP|IMAP)$/ }); await expect(filterButtons).toHaveCount(2); await filterButtons.last().click(); await expect(page.getByRole("checkbox", { name: /Pending|Ausstehend/, exact: true })).toBeVisible(); await expect(page.getByRole("checkbox", { name: /Outcome uncertain|Ergebnis ungewiss/, exact: true })).toBeVisible(); await page.getByRole("button", { name: /^(Deselect all|Alle abwählen)$/ }).click(); await page.getByRole("checkbox", { name: "Pending", exact: true }).check(); await page.keyboard.press("Escape"); await expect(table.getByText("recipient-1@example.test", { exact: true })).toBeVisible(); await expect(table.getByText("recipient-2@example.test", { exact: true })).toHaveCount(0); await table.getByRole("button", { name: "Filter SMTP", exact: true }).click(); await expect(page.getByRole("checkbox", { name: "SMTP accepted", exact: true })).toBeVisible(); expect(fixture.writes).toHaveLength(0); expect(fixture.errors).toEqual([]); }); test("acknowledged IMAP append refreshes pending diagnostics immediately", async ({ page }) => { const fixture = await install(page, { kind: "imap" }); await page.getByRole("button", { name: "Load IMAP diagnostics", exact: true }).click(); const table = page.getByRole("table", { name: "campaign-delivery-campaign-workflow-imap-diagnostics", exact: true }); await expect(table.getByText("recipient-1@example.test", { exact: true })).toBeVisible(); await start(page, "imap"); const dialog = page.getByRole("dialog"); await expect(dialog.getByRole("button", { name: "Close", exact: true }).last()).toBeEnabled(); await dialog.getByRole("button", { name: "Close", exact: true }).last().click(); await expect(table).toHaveCount(0); await expect(page.getByText(/IMAP append processed 4 job/)).toBeVisible(); expect(fixture.writes).toHaveLength(1); expect(fixture.errors).toEqual([]); }); test("lock waits for attachment preview and rechecks fresh unlinked matches before validation", async ({ page }) => { let release!: () => void; const held = new Promise(resolve => { release = resolve; }); const fixture = await install(page, { authoring: true, holdPreview: held }); const lock = page.getByRole("button", { name: /^(Lock and validate|Sperren und validieren)$/ }); await expect(lock).toBeDisabled(); expect(fixture.writes).toHaveLength(0); release(); await expect(lock).toBeEnabled(); await lock.click(); await expect.poll(() => fixture.previews).toBeGreaterThanOrEqual(2); await expect(page.getByRole("alertdialog")).toBeVisible(); expect(fixture.writes).toHaveLength(0); await page.getByRole("alertdialog").getByRole("button", { name: /^(Link and lock|Verknüpfen und sperren)$/ }).click(); await expect.poll(() => fixture.writes.length).toBe(1); expect(fixture.writes[0].path).toMatch(/\/validate$/); expect(fixture.writes[0].payload.link_unshared_matches).toBe(true); expect(fixture.errors).toEqual([]); });