import { expect, test, type Page } from "@playwright/test"; async function setup(page: Page, options: { firstFailure?: boolean; delay?: Promise } = {}) { const writes: Record[] = []; await page.route("**/api/v1/conformance/review-state", async (route) => { writes.push(route.request().postDataJSON()); if (options.delay) await options.delay; if (options.firstFailure && writes.length === 1) { await route.fulfill({ status: 409, json: { detail: "Synthetic review revision conflict" } }); return; } await route.fulfill({ json: { saved: true } }); }); return writes; } async function openAttachmentGroup(page: Page, query = "") { page.on("pageerror", (error) => { throw error; }); await page.goto(`/?campaign-bulk-review${query}`); await page.getByRole("button", { name: "Open grouped review" }).click(); if (await page.getByRole("combobox").isEnabled()) await page.getByRole("combobox").selectOption("same-attachment-condition"); return page.getByRole("dialog"); } test("grouped review accepts only exact selected eligible IDs with shared required reason", async ({ page }) => { const writes = await setup(page); const dialog = await openAttachmentGroup(page); await expect(dialog.getByRole("checkbox")).toHaveCount(3); await expect(dialog.getByRole("button", { name: "Accept 3 selected messages" })).toBeDisabled(); await dialog.getByRole("checkbox", { name: "job-001@example.test" }).focus(); await page.keyboard.press("Space"); await dialog.getByRole("textbox").fill("The optional file is intentionally omitted for these recipients."); await dialog.getByRole("button", { name: "Accept 2 selected messages" }).click(); await expect(dialog).not.toBeVisible(); expect(writes).toEqual([{ buildToken: "build-one", categoryKey: "same-attachment-condition", jobIds: ["job-000", "job-002"], reason: "The optional file is intentionally omitted for these recipients." }]); }); test("failed grouped save retains reason and selection, and retries explicitly", async ({ page }) => { const writes = await setup(page, { firstFailure: true }); const dialog = await openAttachmentGroup(page); await dialog.getByRole("textbox").fill("Accepted after inspecting frozen evidence."); await dialog.getByRole("checkbox", { name: "job-001@example.test" }).focus(); await page.keyboard.press("Space"); await dialog.getByRole("button", { name: "Accept 2 selected messages" }).click(); await expect(dialog.getByRole("alert")).toContainText("Synthetic review revision conflict"); await expect(dialog.getByRole("textbox")).toHaveValue("Accepted after inspecting frozen evidence."); await expect(dialog.getByRole("checkbox", { name: "job-001@example.test" })).not.toBeChecked(); expect(writes).toHaveLength(1); await dialog.getByRole("button", { name: "Accept 2 selected messages" }).click(); await expect(dialog).not.toBeVisible(); expect(writes).toHaveLength(2); expect(writes[1]).toEqual(writes[0]); }); test("grouped save blocks double clicks and dismissal until acknowledged", async ({ page }) => { let acknowledge!: () => void; const writes = await setup(page, { delay: new Promise((resolve) => { acknowledge = resolve; }) }); const dialog = await openAttachmentGroup(page); await dialog.getByRole("textbox").fill("Shared operational exception."); const accept = dialog.getByRole("button", { name: "Accept 3 selected messages" }); await accept.evaluate((button: HTMLButtonElement) => { button.click(); button.click(); }); await expect.poll(() => writes.length).toBe(1); await expect(dialog.getByRole("button", { name: /^(Cancel|Abbrechen)$/ })).toBeDisabled(); await page.keyboard.press("Escape"); await expect(dialog).toBeVisible(); acknowledge(); await expect(dialog).not.toBeVisible(); }); test("grouped review is bounded and clearly counts the remaining matching messages", async ({ page }) => { const writes = await setup(page); const dialog = await openAttachmentGroup(page, "&large"); await expect(dialog.getByRole("checkbox")).toHaveCount(200); await expect(dialog).toContainText("Showing 200 of 205 matching unreviewed messages in this category; 200 selected."); await dialog.getByRole("textbox").fill("Confirmed shared exception."); await dialog.getByRole("button", { name: "Accept 200 selected messages" }).click(); await expect(dialog).not.toBeVisible(); expect(writes[0].jobIds).toHaveLength(200); }); test("a replaced build or missing permission cannot be bulk accepted", async ({ page }) => { const writes = await setup(page); let dialog = await openAttachmentGroup(page); await dialog.getByRole("textbox").fill("Do not save stale build evidence."); await page.getByTestId("replace-build").evaluate((button: HTMLButtonElement) => button.click()); await expect(dialog.getByRole("alert")).toContainText("The build changed"); await expect(dialog.getByRole("button", { name: "Accept 3 selected messages" })).toBeDisabled(); dialog = await openAttachmentGroup(page, "&read-only"); await expect(dialog.getByRole("button", { name: /Accept .* selected messages/ })).toBeDisabled(); expect(writes).toHaveLength(0); }); test("German bulk review labels and category changes preserve explicit scope", async ({ page }) => { const writes = await setup(page); const dialog = await openAttachmentGroup(page, "&language=de"); await expect(dialog).toHaveAccessibleName("Gleichartige Prüfbedingungen bestätigen"); await expect(dialog.getByRole("button", { name: "3 ausgewählte Nachrichten bestätigen" })).toBeDisabled(); await dialog.getByRole("combobox").selectOption("other-condition"); await expect(dialog.getByRole("checkbox")).toHaveCount(1); await expect(dialog.getByRole("textbox", { name: "Gemeinsamer Prüfvermerk" })).toHaveValue(""); await dialog.getByRole("button", { name: "1 ausgewählte Nachrichten bestätigen" }).click(); await expect(dialog).not.toBeVisible(); expect(writes[0]).toMatchObject({ categoryKey: "other-condition", jobIds: ["other-category"], reason: "" }); });