Task #433: incoming-note popup — render full note + 3-button action row

- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

Added a new e2e ("recipient popup renders checklist items for a
checklist note") that composes a checklist note via the UI, sends it,
and asserts `incoming-note-popup-checklist` is visible with both
items — locks in the noteKind/items socket plumbing.

All affected e2e tests pass (notes-popup-on-receive note + reply +
checklist, notes-popup-touch-tap both, notes-inbox); queue unit
tests pass.
This commit is contained in:
riyadhafraa
2026-05-07 09:29:31 +00:00
parent ff3c567b3c
commit 213895e9e8
@@ -377,3 +377,105 @@ test("sender sees a floating reply card when the recipient replies", async ({
await recipientCtx.close();
}
});
// Task #433: a checklist note must render the inner sticky-note "as a
// real note" — i.e. the popup body shows a read-only checklist preview
// (not the plain text body), proving `noteKind` + `items` are forwarded
// from the backend through the socket payload into the popup.
test("recipient popup renders checklist items for a checklist note", async ({
browser,
}) => {
test.setTimeout(90_000);
const sender = await createUser("popup_cl_sender", "Popup CL Sender");
const recipient = await createUser("popup_cl_recipient", "Popup CL Recipient");
const senderCtx = await browser.newContext();
const recipientCtx = await browser.newContext();
const senderPage = await senderCtx.newPage();
const recipientPage = await recipientCtx.newPage();
try {
await loginViaUi(recipientPage, recipient.username);
await recipientPage.goto("/");
await recipientPage.waitForTimeout(3500);
await loginViaUi(senderPage, sender.username);
await senderPage.goto("/notes");
await expect(senderPage.getByTestId("notes-page")).toBeVisible();
const noteTitle = `Checklist Note ${uniq()}`;
await senderPage.getByTestId("notes-composer-open").click();
await senderPage.getByTestId("notes-composer-title").fill(noteTitle);
// Switch composer to checklist mode and add two items.
await senderPage.getByTestId("notes-composer-kind-toggle").click();
const list = senderPage.getByTestId("notes-composer-checklist-list");
await expect(list).toBeVisible();
// List starts empty — click "Add item" to insert the first row,
// fill it, press Enter to spawn a second row, fill that one too.
await senderPage.getByRole("button", { name: "Add item" }).click();
// The ChecklistEditor uses shadcn `Input` (no explicit `type=text`),
// so match any non-checkbox input inside the list container.
const itemInputs = list.locator("input:not([type=checkbox])");
await expect(itemInputs.first()).toBeVisible();
await itemInputs.first().fill("Buy milk");
await itemInputs.first().press("Enter");
await expect(itemInputs.nth(1)).toBeVisible();
await itemInputs.nth(1).fill("Walk dog");
await itemInputs.nth(1).blur();
const createPromise = senderPage.waitForResponse(
(r) =>
new URL(r.url()).pathname === "/api/notes" &&
r.request().method() === "POST" &&
r.status() >= 200 &&
r.status() < 300,
{ timeout: 15_000 },
);
await senderPage.getByTestId("notes-composer-save").click();
const createResp = await createPromise;
const createdNote = await createResp.json();
createdNoteIds.push(createdNote.id);
const card = senderPage.getByTestId(`note-card-${createdNote.id}`);
await expect(card).toBeVisible();
await card.hover();
await senderPage.getByTestId(`note-card-send-${createdNote.id}`).click();
await expect(senderPage.getByTestId("send-note-dialog")).toBeVisible();
await senderPage
.getByTestId("send-note-search")
.fill(recipient.displayEn);
await senderPage
.getByTestId(`send-recipient-option-${recipient.id}`)
.click();
const sendPromise = senderPage.waitForResponse(
(r) =>
new URL(r.url()).pathname === `/api/notes/${createdNote.id}/send` &&
r.request().method() === "POST" &&
r.status() >= 200 &&
r.status() < 300,
{ timeout: 15_000 },
);
await senderPage.getByTestId("send-note-submit").click();
await expect(senderPage.getByTestId("send-note-confirm")).toBeVisible();
await senderPage.getByTestId("send-note-confirm-submit").click();
await sendPromise;
const popup = recipientPage.getByTestId("incoming-note-popup");
await expect(popup).toBeVisible({ timeout: 5_000 });
await expect(popup).toContainText(noteTitle);
// Checklist branch: dedicated container is rendered, with each item
// as a row, and the plain text content branch is NOT used.
const checklist = recipientPage.getByTestId(
"incoming-note-popup-checklist",
);
await expect(checklist).toBeVisible();
await expect(checklist).toContainText("Buy milk");
await expect(checklist).toContainText("Walk dog");
await recipientPage.getByTestId("incoming-note-popup-close").click();
await expect(popup).toBeHidden();
} finally {
await senderCtx.close();
await recipientCtx.close();
}
});