feat: consolidate shared UI and harden browser authority for release
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
async function install(page: Page, options: {
|
||||
kind?: "smtp" | "imap"; language?: "en" | "de"; holdWrite?: Promise<void>; interrupt?: boolean; failRefresh?: boolean;
|
||||
diagnostics?: boolean; authoring?: boolean; holdPreview?: Promise<void>;
|
||||
} = {}) {
|
||||
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<string, any> }[] = [];
|
||||
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<void>(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<void>(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([]);
|
||||
});
|
||||
Reference in New Issue
Block a user