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);
+ });
+});