Files
TX/artifacts/tx-os/tests/notes-checklist.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
8.1 KiB
JavaScript

// E2E regression for the per-note checklist (to-do list) option.
// 1) Composer can switch to checklist mode, items can be added and saved.
// 2) After reload, the saved checklist persists with its items + done state.
// 3) Sending a checklist note delivers the items to the recipient snapshot.
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 = [];
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 };
}
test.afterAll(async () => {
if (createdUserIds.length > 0) {
await pool.query(
`DELETE FROM note_recipients WHERE sender_user_id = ANY($1::int[]) OR recipient_user_id = ANY($1::int[])`,
[createdUserIds],
);
await pool.query(`DELETE FROM notes 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(),
]);
}
test("composer creates a checklist note that persists across reloads", async ({
page,
}) => {
const user = await createUser("notes_checklist_e2e", "Checklist Tester");
await loginViaUi(page, user.username);
await page.goto("/notes");
await expect(page.getByTestId("notes-page")).toBeVisible();
// Open composer and switch to checklist.
await page.getByTestId("notes-composer").click();
await page.getByTestId("notes-composer-kind-toggle").click();
// Add two items via the "Add item" button.
const composer = page.getByTestId("notes-composer");
await composer.getByTestId("notes-composer-checklist-add").click();
// The newly inserted row gets focus; type into it.
await page.keyboard.type("Buy milk");
await composer.getByTestId("notes-composer-checklist-add").click();
await page.keyboard.type("Walk the dog");
// Save via the explicit Save button (synchronous, no portal-click risk).
const savePromise = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === "/api/notes" &&
r.request().method() === "POST" &&
r.status() >= 200 &&
r.status() < 300,
);
await page.getByTestId("notes-composer-save").click();
const resp = await savePromise;
const note = await resp.json();
expect(note.kind).toBe("checklist");
expect(Array.isArray(note.items)).toBe(true);
expect(note.items.map((i) => i.text)).toEqual(["Buy milk", "Walk the dog"]);
// The note card should render the checklist with both rows visible.
const card = page.getByTestId(`note-card-${note.id}`);
await expect(card).toBeVisible();
await expect(card.getByText("Buy milk")).toBeVisible();
await expect(card.getByText("Walk the dog")).toBeVisible();
// Tick the first item from the card, expect a PATCH that flips done.
const firstItemId = note.items[0].id;
const togglePromise = page.waitForResponse(
(r) =>
new URL(r.url()).pathname === `/api/notes/${note.id}` &&
r.request().method() === "PATCH" &&
r.status() >= 200 &&
r.status() < 300,
);
await card
.getByTestId(`note-card-checklist-${note.id}-check-${firstItemId}`)
.click();
const patched = await togglePromise;
const patchedNote = await patched.json();
expect(patchedNote.kind).toBe("checklist");
const patchedFirst = patchedNote.items.find((i) => i.id === firstItemId);
expect(patchedFirst?.done).toBe(true);
// Reload — the persisted checklist with the toggled state must come back.
await page.reload();
await expect(page.getByTestId(`note-card-${note.id}`)).toBeVisible();
const reloadedFirst = page.getByTestId(
`note-card-checklist-${note.id}-check-${firstItemId}`,
);
await expect(reloadedFirst).toBeChecked();
});
test("PATCH items without kind normalizes against the note's current kind", async ({
page,
}) => {
const user = await createUser("notes_checklist_norm", "Norm Tester");
await loginViaUi(page, user.username);
// Create a plain text note (kind defaults to "text").
const textResp = await page.request.post("/api/notes", {
data: { title: `text-note ${uniq()}`, content: "hello" },
});
expect(textResp.ok()).toBe(true);
const textNote = await textResp.json();
expect(textNote.kind).toBe("text");
// Patch only `items` (no kind). Server must coerce items back to null
// because the note's current kind is "text".
const patchTextResp = await page.request.patch(`/api/notes/${textNote.id}`, {
data: { items: [{ id: "x", text: "should be dropped", done: false }] },
});
expect(patchTextResp.ok()).toBe(true);
const patchedText = await patchTextResp.json();
expect(patchedText.kind).toBe("text");
expect(patchedText.items).toBeNull();
// Now create a checklist note and patch only `items` (no kind). Items
// must persist because the note's current kind is "checklist".
const clResp = await page.request.post("/api/notes", {
data: {
title: `cl-note ${uniq()}`,
kind: "checklist",
items: [{ id: "a", text: "first", done: false }],
},
});
const cl = await clResp.json();
const patchClResp = await page.request.patch(`/api/notes/${cl.id}`, {
data: { items: [{ id: "a", text: "first", done: true }] },
});
expect(patchClResp.ok()).toBe(true);
const patchedCl = await patchClResp.json();
expect(patchedCl.kind).toBe("checklist");
expect(patchedCl.items[0].done).toBe(true);
});
test("checklist notes are delivered to recipients with their items", async ({
page,
}) => {
const sender = await createUser("notes_checklist_send_s", "Sender");
const recipient = await createUser("notes_checklist_send_r", "Recipient");
await loginViaUi(page, sender.username);
// Create the checklist note directly via the API (we already cover the
// composer flow above; this test focuses on the send/snapshot path).
const createResp = await page.request.post("/api/notes", {
data: {
title: `send-checklist ${uniq()}`,
kind: "checklist",
items: [
{ id: "a", text: "Item A", done: false },
{ id: "b", text: "Item B", done: true },
],
},
});
expect(createResp.ok()).toBe(true);
const note = await createResp.json();
const sendResp = await page.request.post(`/api/notes/${note.id}/send`, {
data: { recipientUserIds: [recipient.id] },
});
expect(sendResp.ok()).toBe(true);
// Log out and back in as the recipient, then read the inbox.
await page.request.post("/api/auth/logout").catch(() => {});
await loginViaUi(page, recipient.username);
const receivedResp = await page.request.get("/api/notes/received?archived=false");
expect(receivedResp.ok()).toBe(true);
const received = await receivedResp.json();
const delivered = received.find((r) => r.id === note.id);
expect(delivered, "checklist note should appear in recipient inbox").toBeTruthy();
expect(delivered.kind).toBe("checklist");
expect(delivered.items.map((i) => i.text)).toEqual(["Item A", "Item B"]);
expect(delivered.items[1].done).toBe(true);
});