2b31b5e3aa
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
224 lines
8.1 KiB
JavaScript
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);
|
|
});
|