Files
TX/artifacts/tx-os/tests/notes-inbox.spec.mjs
T
riyadhafraa 2b31b5e3aa Remove title field from notes and update tests to reflect changes
Refactors the notes feature by removing the `title` field and updating all related tests and UI components to use `content` instead.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7464969c-726a-4ec2-a3b2-8ff63f91f6ed
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw
Replit-Helium-Checkpoint-Created: true
2026-05-11 07:53:40 +00:00

224 lines
7.6 KiB
JavaScript

// E2E test for the in-app messaging flow built on the Notes page:
// - Sender (user A) creates a note via the composer at /notes
// - Sender clicks "Send" on the card and picks user B as recipient
// - Recipient (user B) signs in, opens /notes → "Inbox" tab
// - Sees the note from A with an "Unread" badge → opens it (which marks read)
// - Replies inline; reply persists in the thread dialog
// - Sender re-opens /notes → "Sent" tab and sees the recipient with the
// "Replied" status pill.
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 };
}
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();
});
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 logout(page) {
await page.context().clearCookies();
}
test("sender → recipient → reply flow updates Inbox + Sent tabs", async ({
page,
}) => {
const sender = await createUser("notes_e2e_sender", "Sender Person");
const recipient = await createUser("notes_e2e_recipient", "Recipient Person");
// ---- Sender composes + sends a note ----
await loginViaUi(page, sender.username);
await page.goto("/notes");
await expect(page.getByTestId("notes-page")).toBeVisible();
const noteContent = `Please review attached. ${uniq()}`;
await page.getByTestId("notes-composer-open").click();
await page.getByTestId("notes-composer-content").fill(noteContent);
const createPromise = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === "/api/notes" &&
r.request().method() === "POST" &&
r.status() >= 200 &&
r.status() < 300,
{ timeout: 15_000 },
);
await page.getByTestId("notes-composer-save").click();
const createResp = await createPromise;
const createdNote = await createResp.json();
createdNoteIds.push(createdNote.id);
// The new card should appear with our content; click its Send button.
const card = page.getByTestId(`note-card-${createdNote.id}`);
await expect(card).toBeVisible();
await card.hover();
await page.getByTestId(`note-card-send-${createdNote.id}`).click();
const sendDialog = page.getByTestId("send-note-dialog");
await expect(sendDialog).toBeVisible();
await page.getByTestId("send-note-search").fill(recipient.displayEn);
await page
.getByTestId(`send-recipient-option-${recipient.id}`)
.click();
const sendPromise = page.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 page.getByTestId("send-note-submit").click();
// Confirm-before-send dialog: explicit confirmation step.
await expect(page.getByTestId("send-note-confirm")).toBeVisible();
await page.getByTestId("send-note-confirm-submit").click();
await sendPromise;
await expect(sendDialog).toBeHidden();
// ---- Switch to recipient session ----
await logout(page);
await loginViaUi(page, recipient.username);
await page.goto("/notes");
await page.getByTestId("notes-overflow-menu-trigger").click();
await page.getByTestId("notes-overflow-received").click();
const inboxCard = page.getByTestId(`received-note-card-${createdNote.id}`);
await expect(inboxCard).toBeVisible();
await expect(inboxCard).toContainText(noteContent);
await expect(inboxCard).toContainText(sender.displayEn);
await expect(
inboxCard.getByTestId("status-badge-unread"),
).toBeVisible();
// Open the thread (this auto-marks read).
const readPromise = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === `/api/notes/${createdNote.id}/read` &&
r.request().method() === "POST" &&
r.status() >= 200 &&
r.status() < 300,
{ timeout: 15_000 },
);
await inboxCard.click();
const threadDialog = page.getByTestId("note-thread-dialog");
await expect(threadDialog).toBeVisible();
await readPromise;
// Reply inline.
const replyText = "Acknowledged, thanks.";
await page.getByTestId("thread-reply-input").fill(replyText);
const replyPromise = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === `/api/notes/${createdNote.id}/reply` &&
r.request().method() === "POST" &&
r.status() >= 200 &&
r.status() < 300,
{ timeout: 15_000 },
);
await page.getByTestId("thread-reply-submit").click();
await replyPromise;
// Reply now appears in the thread.
await expect(threadDialog).toContainText(replyText);
await page.keyboard.press("Escape");
// ---- Back to sender → Sent tab shows replied status ----
await logout(page);
await loginViaUi(page, sender.username);
await page.goto("/notes");
await page.getByTestId("notes-overflow-menu-trigger").click();
await page.getByTestId("notes-overflow-sent").click();
const sentCard = page.getByTestId(`sent-note-card-${createdNote.id}`);
await expect(sentCard).toBeVisible();
await expect(sentCard).toContainText(noteContent);
await expect(sentCard).toContainText(recipient.displayEn);
// Per-recipient status pill flipped to "replied".
const recipientPill = page.getByTestId(
`sent-recipient-status-${createdNote.id}-${recipient.id}`,
);
await expect(recipientPill).toBeVisible();
// Open the thread from the sender side and confirm the reply is visible.
await sentCard.click();
const senderThread = page.getByTestId("note-thread-dialog");
await expect(senderThread).toBeVisible();
await expect(senderThread).toContainText(replyText);
});