// E2E regression for the inline notes composer color picker. // The composer auto-saves on outside mousedown. Radix popovers render in // a portal outside the composer's DOM, so picking a color used to count // as an "outside click" and saved the note with the OLD color before the // swatch's onClick had a chance to update state. This test guards that // fix: pick a non-default color, click outside, then assert the saved // note has that color. 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 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 persists chosen color when saving via outside click", async ({ page, }) => { const user = await createUser("notes_color_e2e", "Color Tester"); await loginViaUi(page, user.username); await page.goto("/notes"); await expect(page.getByTestId("notes-page")).toBeVisible(); // Open the composer and type some content. const content = `color-test ${uniq()}`; await page.getByTestId("notes-composer").click(); await page.getByTestId("notes-composer-content").fill(content); // Open the color popover and pick a non-default swatch. await page .getByTestId("notes-composer") .locator('button[aria-label="Color"]') .click(); // NOTE_COLORS[1] is the first non-default swatch ("red"); use it as the // target. The swatch button has aria-label set to its color id. const swatch = page.locator('[role="dialog"] button[aria-label="red"]') .or(page.locator('button[aria-label="red"]')); await swatch.first().click(); // Click far outside the composer to trigger auto-save. const savePromise = page.waitForResponse( (r) => new URL(r.url()).pathname === "/api/notes" && r.request().method() === "POST" && r.status() >= 200 && r.status() < 300, ); await page.mouse.click(10, 10); const resp = await savePromise; const note = await resp.json(); expect(note.content).toBe(content); expect(note.color).toBe("red"); });