diff --git a/artifacts/tx-os/src/pages/notes.tsx b/artifacts/tx-os/src/pages/notes.tsx index c7657c43..37a38899 100644 --- a/artifacts/tx-os/src/pages/notes.tsx +++ b/artifacts/tx-os/src/pages/notes.tsx @@ -674,7 +674,9 @@ function NoteCard({ update.mutate({ id: note.id, isPinned: !note.isPinned }); }} className={`shrink-0 p-1 rounded-md hover:bg-black/5 ${ - note.isPinned ? "text-amber-600" : "text-muted-foreground opacity-0 group-hover:opacity-100" + note.isPinned + ? "text-amber-600" + : "text-muted-foreground opacity-0 group-hover:opacity-100 [@media(hover:none)]:opacity-100" }`} aria-label={t("notes.pin", "Pin")} > @@ -693,7 +695,11 @@ function NoteCard({ )}
e.stopPropagation()} > { + 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(), + ]); +} + +// Emulate a touch device on the default (chromium) browser. We avoid +// devices["iPad …"] because that pins the browser to webkit, which +// isn't installed in this environment. Chromium honours hasTouch + +// no mouse so `(hover: none)` matches. +test.use({ + hasTouch: true, + isMobile: true, + viewport: { width: 820, height: 1180 }, +}); + +test.describe("note card actions on touch devices", () => { + test("Send button is visible without hovering on touch devices", async ({ + page, + }) => { + const user = await createUser("notes_touch_actions", "Touch Tester"); + await loginViaUi(page, user.username); + + await page.goto("/notes"); + await expect(page.getByTestId("notes-page")).toBeVisible(); + + // Sanity: the touch CSS variant is what we depend on. If the test + // browser doesn't actually report `(hover: none)`, the assertion + // below would still hold for the wrong reason (no hover applied), + // so guard it. + const noHover = await page.evaluate( + () => window.matchMedia("(hover: none)").matches, + ); + expect(noHover).toBe(true); + + const title = `touch-test ${uniq()}`; + await page.getByTestId("notes-composer").click(); + await page.getByTestId("notes-composer-title").fill(title); + const savePromise = page.waitForResponse( + (r) => + new URL(r.url()).pathname === "/api/notes" && + r.request().method() === "POST" && + r.status() >= 200 && + r.status() < 300, + ); + // Tap outside the composer to trigger auto-save. + await page.touchscreen.tap(10, 10); + const note = await (await savePromise).json(); + + const card = page.getByTestId(`note-card-${note.id}`); + await expect(card).toBeVisible(); + + // The Send button must be visible without any hover gesture. + const sendBtn = page.getByTestId(`note-card-send-${note.id}`); + await expect(sendBtn).toBeVisible(); + + // And it must actually be clickable (not occluded / opacity-0). + const opacity = await sendBtn.evaluate( + (el) => getComputedStyle(el.parentElement).opacity, + ); + expect(Number(opacity)).toBe(1); + }); +});