// Send-note confirm dialog appears only when 2+ recipients are picked. // One recipient sends directly; two or more still gate behind the // AlertDialog confirmation. import { test, expect } from "@playwright/test"; import pg from "pg"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { throw new Error("DATABASE_URL must be set to run this UI test"); } const TEST_PASSWORD = "TestPass123!"; const TEST_PASSWORD_HASH = "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu"; const pool = new pg.Pool({ connectionString: DATABASE_URL }); const createdUserIds = []; const createdNoteIds = []; function uniq() { return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } async function createUser(prefix, displayEn) { const username = `${prefix}_${uniq()}`; const { rows } = await pool.query( `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active) VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`, [username, `${username}@example.com`, TEST_PASSWORD_HASH, displayEn], ); const id = rows[0].id; createdUserIds.push(id); await pool.query( `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = 'user'`, [id], ); return { id, username, displayEn }; } async function loginViaUi(page, username) { await page.goto("/login"); await page.locator("#username").fill(username); await page.locator("#password").fill(TEST_PASSWORD); await Promise.all([ page.waitForURL((url) => !url.pathname.endsWith("/login"), { timeout: 15_000, }), page.locator('form button[type="submit"]').click(), ]); } async function composeNote(page, content) { await page.goto("/notes"); await expect(page.getByTestId("notes-page")).toBeVisible(); await page.getByTestId("notes-composer-open").click(); await page.getByTestId("notes-composer-content").fill(content); const createPromise = page.waitForResponse( (r) => new URL(r.url()).pathname === "/api/notes" && r.request().method() === "POST" && r.ok(), { timeout: 15_000 }, ); await page.getByTestId("notes-composer-save").click(); const created = await createPromise; const note = await created.json(); createdNoteIds.push(note.id); return note; } async function openSendDialog(page, noteId) { const card = page.getByTestId(`note-card-${noteId}`); await expect(card).toBeVisible(); await card.hover(); await page.getByTestId(`note-card-send-${noteId}`).click(); await expect(page.getByTestId("send-note-dialog")).toBeVisible(); } async function pickRecipient(page, recipient) { await page.getByTestId("send-note-search").fill(""); await page.getByTestId("send-note-search").fill(recipient.displayEn); await page.getByTestId(`send-recipient-option-${recipient.id}`).click(); } test.afterAll(async () => { if (createdNoteIds.length > 0) { await pool.query( `DELETE FROM note_replies WHERE note_id = ANY($1::int[])`, [createdNoteIds], ); await pool.query( `DELETE FROM note_recipients WHERE note_id = ANY($1::int[])`, [createdNoteIds], ); await pool.query(`DELETE FROM notes WHERE id = ANY($1::int[])`, [ createdNoteIds, ]); } if (createdUserIds.length > 0) { await pool.query(`DELETE FROM notes WHERE user_id = ANY($1::int[])`, [ createdUserIds, ]); await pool.query( `DELETE FROM notifications WHERE user_id = ANY($1::int[])`, [createdUserIds], ); await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [ createdUserIds, ]); await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [ createdUserIds, ]); } await pool.end(); }); test("single-recipient send skips the confirm dialog", async ({ page }) => { const sender = await createUser("notes_send1_sender", "Single Sender"); const recipient = await createUser("notes_send1_recipient", "Solo Recipient"); await loginViaUi(page, sender.username); const note = await composeNote(page, `Solo send ${uniq()}`); await openSendDialog(page, note.id); await pickRecipient(page, recipient); const sendPromise = page.waitForResponse( (r) => new URL(r.url()).pathname === `/api/notes/${note.id}/send` && r.request().method() === "POST" && r.ok(), { timeout: 15_000 }, ); await page.getByTestId("send-note-submit").click(); await sendPromise; await expect(page.getByTestId("send-note-confirm")).toBeHidden(); await expect(page.getByTestId("send-note-dialog")).toBeHidden(); }); test("multi-recipient send still requires the confirm dialog", async ({ page, }) => { const sender = await createUser("notes_send2_sender", "Multi Sender"); const r1 = await createUser("notes_send2_r1", "Recipient One"); const r2 = await createUser("notes_send2_r2", "Recipient Two"); await loginViaUi(page, sender.username); const note = await composeNote(page, `Group send ${uniq()}`); await openSendDialog(page, note.id); await pickRecipient(page, r1); await pickRecipient(page, r2); // Track every /send request so we can assert none fired before // the user explicitly confirmed the multi-recipient send. const preConfirmSendRequests = []; const sendPath = `/api/notes/${note.id}/send`; const requestListener = (req) => { if ( new URL(req.url()).pathname === sendPath && req.method() === "POST" ) { preConfirmSendRequests.push(req.url()); } }; page.on("request", requestListener); await page.getByTestId("send-note-submit").click(); // Two+ recipients → confirm dialog must appear; no /send call yet. const confirm = page.getByTestId("send-note-confirm"); await expect(confirm).toBeVisible(); // The confirm body should mention the recipient count (2 here) so // users see what they're about to send to. await expect(confirm).toContainText("2"); // Give the network a beat to prove no send fired pre-confirmation. await page.waitForTimeout(250); expect(preConfirmSendRequests).toEqual([]); page.off("request", requestListener); const sendPromise = page.waitForResponse( (r) => new URL(r.url()).pathname === `/api/notes/${note.id}/send` && r.request().method() === "POST" && r.ok(), { timeout: 15_000 }, ); await page.getByTestId("send-note-confirm-submit").click(); await sendPromise; await expect(page.getByTestId("send-note-dialog")).toBeHidden(); });