7a2ae8434d
Refactor documentation files and code comments to remove references to Replit, specific task numbers, and other platform-specific identifiers. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fa18e5d4-a810-4bd5-8cde-2a60d64d9e3f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true
188 lines
6.2 KiB
JavaScript
188 lines
6.2 KiB
JavaScript
// e2e: multi-select + bulk-delete on the Sent notes tab.
|
|
// Seeds three sent notes for a user, opens the Sent view, enters
|
|
// selection mode, picks two cards, confirms the bulk delete, and
|
|
// verifies that the count drops by two while the unselected note
|
|
// remains. Uses the same direct-DB seeding pattern as
|
|
// notes-inbox.spec.mjs so the test is hermetic.
|
|
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 createNote(userId, title, content) {
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO notes (user_id, title, content, color, kind)
|
|
VALUES ($1, $2, $3, 'yellow', 'text') RETURNING id, title, content, color, kind`,
|
|
[userId, title, content],
|
|
);
|
|
const note = rows[0];
|
|
createdNoteIds.push(note.id);
|
|
return note;
|
|
}
|
|
|
|
async function addRecipient(note, senderId, recipientId) {
|
|
await pool.query(
|
|
`INSERT INTO note_recipients
|
|
(note_id, sender_user_id, recipient_user_id, title, content, color, kind, status)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'unread')`,
|
|
[note.id, senderId, recipientId, note.title, note.content, note.color, note.kind],
|
|
);
|
|
}
|
|
|
|
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(),
|
|
]);
|
|
}
|
|
|
|
test("Sent tab: select two notes, bulk-delete, count drops by two", async ({
|
|
page,
|
|
}) => {
|
|
const sender = await createUser("sent_bulk_sender", "Sender Bulk");
|
|
const recipient = await createUser("sent_bulk_recipient", "Recipient Bulk");
|
|
|
|
// Seed three sent notes (each needs at least one recipient row to be
|
|
// returned by GET /notes?box=sent).
|
|
const tag = uniq();
|
|
const noteA = await createNote(sender.id, `Bulk A ${tag}`, "alpha");
|
|
const noteB = await createNote(sender.id, `Bulk B ${tag}`, "beta");
|
|
const noteC = await createNote(sender.id, `Bulk C ${tag}`, "gamma");
|
|
const noteAId = noteA.id;
|
|
const noteBId = noteB.id;
|
|
const noteCId = noteC.id;
|
|
await addRecipient(noteA, sender.id, recipient.id);
|
|
await addRecipient(noteB, sender.id, recipient.id);
|
|
await addRecipient(noteC, sender.id, recipient.id);
|
|
|
|
await loginViaUi(page, sender.username);
|
|
await page.goto("/notes");
|
|
await expect(page.getByTestId("notes-page")).toBeVisible();
|
|
|
|
// Switch to Sent tab.
|
|
await page.getByTestId("notes-overflow-menu-trigger").click();
|
|
await page.getByTestId("notes-overflow-sent").click();
|
|
|
|
const cardA = page.getByTestId(`sent-note-card-${noteAId}`);
|
|
const cardB = page.getByTestId(`sent-note-card-${noteBId}`);
|
|
const cardC = page.getByTestId(`sent-note-card-${noteCId}`);
|
|
await expect(cardA).toBeVisible();
|
|
await expect(cardB).toBeVisible();
|
|
await expect(cardC).toBeVisible();
|
|
|
|
// Enter selection mode and pick A + B (leave C).
|
|
await page.getByTestId("sent-bulk-select-toggle").click();
|
|
await expect(page.getByTestId("sent-bulk-bar")).toBeVisible();
|
|
await cardA.click();
|
|
await cardB.click();
|
|
await expect(page.getByTestId("sent-bulk-count")).toContainText("2");
|
|
|
|
// Open confirm and execute the bulk delete.
|
|
await page.getByTestId("sent-bulk-delete").click();
|
|
await expect(page.getByTestId("sent-bulk-confirm")).toBeVisible();
|
|
|
|
const deletedA = page.waitForResponse(
|
|
(r) =>
|
|
new URL(r.url()).pathname === `/api/notes/${noteAId}` &&
|
|
r.request().method() === "DELETE" &&
|
|
r.status() >= 200 &&
|
|
r.status() < 300,
|
|
{ timeout: 15_000 },
|
|
);
|
|
const deletedB = page.waitForResponse(
|
|
(r) =>
|
|
new URL(r.url()).pathname === `/api/notes/${noteBId}` &&
|
|
r.request().method() === "DELETE" &&
|
|
r.status() >= 200 &&
|
|
r.status() < 300,
|
|
{ timeout: 15_000 },
|
|
);
|
|
await page.getByTestId("sent-bulk-confirm-delete").click();
|
|
await Promise.all([deletedA, deletedB]);
|
|
|
|
// A and B are gone; C remains.
|
|
await expect(cardA).toBeHidden();
|
|
await expect(cardB).toBeHidden();
|
|
await expect(cardC).toBeVisible();
|
|
|
|
// Verify in the DB that A and B were actually deleted.
|
|
const { rows } = await pool.query(
|
|
`SELECT id FROM notes WHERE id = ANY($1::int[])`,
|
|
[[noteAId, noteBId, noteCId]],
|
|
);
|
|
const remainingIds = rows.map((r) => r.id);
|
|
expect(remainingIds).toEqual([noteCId]);
|
|
});
|