Files
buzz/desktop/tests/e2e/inbox-reactions.spec.ts
morgmart c5166f2164 feat(desktop): simplify the message action rail (#6529)
## Problem

Quick-reaction shortcuts make the message action rail visually noisy.
Reaction access should remain first-class, but through one predictable
Add reaction action rather than several learned emoji shortcuts.

## Change

The rail is now:

**Add reaction · Reply · Copy link · More**

- Remove all quick-reaction shortcut buttons and their divider from the
message action rail.
- Preserve Add reaction as the first action, including its existing
picker, tooltip, accessible name, keyboard behavior, reaction behavior,
and feedback.
- Keep the existing Link2 Copy link action, shared copy handler,
More-menu entry, eligibility guards, and success feedback unchanged.
- Keep reveal, positioning, responsiveness, styling, and remaining menu
paths unchanged.

The now-unused quick-reaction rendering component and imports were
removed from `MessageActionBar`; shared reaction learning remains intact
for other reaction surfaces.

## Tests

Focused smoke coverage verifies:

- zero `React with …` shortcut buttons;
- exact ordered rail: `Open reactions → Reply → Copy link → More
actions`;
- the rail and More-menu paths emit the same canonical thread-aware
`buzz://message` URL;
- existing success feedback;
- pending and huddle rows omit both copy-link surfaces;
- the action bar stays within the open thread panel.

The zero-shortcut contract was mutation-checked by restoring the prior
production action bar: the focused test failed causally with expected 0
versus received 3 quick-reaction buttons.

## Validation

At `4f062e0d60b0f0c16b6862cdea7137c287060947`:

- `pnpm exec biome check src/features/messages/ui/MessageActionBar.tsx
tests/e2e/message-copy-link.spec.ts` — passed.
- `pnpm exec tsc --noEmit` — passed.
- `pnpm test` — 5,432 passed, 0 failed.
- `pnpm build:e2e` — passed; existing dynamic-import and chunk-size
warnings only.
- `pnpm exec playwright test tests/e2e/message-copy-link.spec.ts
--project=smoke` — 2 passed.
- Pre-push `file-size-check`, `desktop-check`, `desktop-typecheck`, and
`desktop-test` — passed.

Vogue’s design review: **SHIP** — reaction remains discoverable and
accessible as the visible first action; the extra click is an
intentional efficiency tradeoff for the simpler hierarchy.

---------

Signed-off-by: Trace (Engineer) <9d485ff0c62915e08a801162c195c7ef096f6f6143925407255cea2656c4b8a4@buzz.block.builderlab.xyz>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Signed-off-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz>
Co-authored-by: Trace (Engineer) <9d485ff0c62915e08a801162c195c7ef096f6f6143925407255cea2656c4b8a4@buzz.block.builderlab.xyz>
Co-authored-by: Rivet <a08d9a8418c7ff03afe19964724c8fd87bf1776ab9e9b9cafb8cc920edd02a6e@buzz.block.builderlab.xyz>
2026-08-24 16:28:51 -07:00

185 lines
6.8 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import type { RelayEvent } from "../../src/shared/api/types";
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const SHOTS = "test-results/inbox-reactions";
type MockWindow = Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
parentEventId?: string | null;
pubkey?: string;
mentionPubkeys?: string[];
/** 64-hex id — required for the message to be a valid reaction target. */
id?: string;
}) => RelayEvent;
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
category: "mention" | "needs_action" | "activity" | "agent_activity";
channel_id: string | null;
channel_name: string;
content: string;
created_at: number;
id: string;
kind: number;
pubkey: string;
tags: string[][];
}) => unknown;
};
// Regression test: a reaction added from the Inbox detail pane must survive
// the post-toggle refetch. Inbox items are typically thread replies, which the
// server-assembled channel window does NOT carry reactions for — the Inbox
// must hydrate them by `#e` reference or the optimistic pill vanishes.
test("inbox reaction on a thread-reply mention persists after refetch", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
await page.waitForFunction(() => {
const win = window as MockWindow;
return (
typeof win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" &&
typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function"
);
});
// Seed a thread: alice's top-level message, then alice's reply mentioning
// tyler (the current user). The reply is the inbox item — the shape Wes hit.
// A second, unrelated mention is seeded so the test can genuinely switch
// selection away and back.
const { replyEvent, otherEvent } = await page.evaluate(
({ channelId, currentPubkey, senderPubkey }) => {
const win = window as MockWindow;
const emitMessage = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
const pushFeedItem = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
if (!emitMessage || !pushFeedItem) {
throw new Error("Mock bridge helpers are not installed.");
}
const root = emitMessage({
channelName: "general",
content: "Thread root about the launch.",
pubkey: senderPubkey,
id: "a1".repeat(32),
});
const reply = emitMessage({
channelName: "general",
content: "Reply mentioning you — please react to this.",
parentEventId: root.id,
pubkey: senderPubkey,
mentionPubkeys: [currentPubkey],
id: "b2".repeat(32),
});
pushFeedItem({
id: reply.id,
kind: reply.kind,
pubkey: reply.pubkey,
content: reply.content,
created_at: reply.created_at,
channel_id: channelId,
channel_name: "general",
tags: reply.tags,
category: "mention",
});
const other = emitMessage({
channelName: "general",
content: "Unrelated mention for selection switching.",
pubkey: senderPubkey,
mentionPubkeys: [currentPubkey],
id: "c3".repeat(32),
});
pushFeedItem({
id: other.id,
kind: other.kind,
pubkey: other.pubkey,
content: other.content,
created_at: other.created_at,
channel_id: channelId,
channel_name: "general",
tags: other.tags,
category: "mention",
});
return { replyEvent: reply, otherEvent: other };
},
{
channelId: GENERAL_CHANNEL_ID,
currentPubkey: TEST_IDENTITIES.tyler.pubkey,
senderPubkey: TEST_IDENTITIES.alice.pubkey,
},
);
// Open the inbox item and react through Add reaction and the picker.
const item = page.getByTestId(`home-inbox-item-${replyEvent.id}`);
await item.click();
const detail = page.getByTestId("home-inbox-detail");
await expect(detail).toContainText("please react to this");
const selectedMessage = page.getByTestId("home-inbox-selected-message");
// Deliver a live reaction with the real add_reaction wire shape: `e` target,
// no `h` channel tag. The Inbox must render it without waiting for another
// message or a context refetch.
await page.evaluate(async (eventId) => {
const invoke = (
window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock Tauri invoke bridge is unavailable.");
await invoke("add_reaction", { eventId, emoji: "❤️" });
}, replyEvent.id);
await expect(selectedMessage.getByLabel("Toggle ❤️ reaction")).toBeVisible();
await selectedMessage.hover();
const actionBar = page.getByTestId(`message-action-bar-${replyEvent.id}`);
const [actionBarBox, selectedMessageBox] = await Promise.all([
actionBar.boundingBox(),
selectedMessage.boundingBox(),
]);
expect(actionBarBox).not.toBeNull();
expect(selectedMessageBox).not.toBeNull();
if (!actionBarBox || !selectedMessageBox) {
throw new Error("Inbox message action bar bounds were unavailable.");
}
expect(actionBarBox.y).toBeGreaterThanOrEqual(selectedMessageBox.y);
await actionBar.getByRole("button", { name: "Open reactions" }).click();
const picker = page.locator("em-emoji-picker");
await expect(picker).toBeVisible();
await picker.locator("input[type='search']").fill("thumbs up");
await picker.getByRole("button", { name: "👍" }).first().click();
// The pill must appear AND persist: the post-toggle refetch replaces the
// optimistic state with fetched reaction events. Give the refetch time to
// land before asserting, then assert the pill is still there.
const reactions = selectedMessage.getByTestId("message-reactions");
await expect(reactions).toContainText("👍");
await page.waitForTimeout(1_500);
await expect(reactions).toContainText("👍");
await page.screenshot({ path: `${SHOTS}/01-pill-after-refetch.png` });
// Re-select the item (drops all optimistic state) — the reaction must
// come back purely from the fetched relay data. Select a genuinely
// different item first so the selection actually changes.
const otherItem = page.getByTestId(`home-inbox-item-${otherEvent.id}`);
await otherItem.click();
await expect(detail).toContainText("Unrelated mention");
await item.click();
await expect(detail).toContainText("please react to this");
await expect(
page
.getByTestId("home-inbox-selected-message")
.getByTestId("message-reactions"),
).toContainText("👍");
await page.screenshot({ path: `${SHOTS}/02-pill-after-reselect.png` });
});