Files
buzz/desktop/tests/e2e/message-copy-link.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

183 lines
5.7 KiB
TypeScript

import { expect, test, type Locator, type Page } from "@playwright/test";
import { KIND_HUDDLE_STARTED } from "../../src/shared/constants/kinds";
import { installMockBridge } from "../helpers/bridge";
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
async function latestClipboardWrite(page: Page) {
return page.evaluate(() =>
(window.__BUZZ_E2E_COMMAND_LOG__ ?? []).findLast(
({ command }) => command === "copy_text_to_clipboard",
),
);
}
async function expectCopyLinkUnavailable(row: Locator, messageId: string) {
await row.hover();
await expect(row.getByTestId(`copy-link-message-${messageId}`)).toHaveCount(
0,
);
const moreActions = row.getByTestId(`more-actions-${messageId}`);
if (await moreActions.count()) {
await moreActions.click({ force: true });
await expect(
row.page().getByTestId(`copy-message-link-${messageId}`),
).toHaveCount(0);
await row.page().keyboard.press("Escape");
}
}
test.beforeEach(async ({ page }) => {
await page.context().grantPermissions(["clipboard-read", "clipboard-write"], {
origin: "http://127.0.0.1:4173",
});
await installMockBridge(page);
});
test("message action rail copies the same canonical thread link as More", async ({
page,
}) => {
await page.setViewportSize({ width: 900, height: 700 });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect
.poll(() =>
page.evaluate(
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
),
)
.toBe(true);
const { replyId, rootId } = await page.evaluate(() => {
const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
const root = emit({
channelName: "general",
content: "Copy-link regression root",
id: "a".repeat(64),
});
const reply = emit({
channelName: "general",
content: "Copy-link regression reply",
id: "b".repeat(64),
parentEventId: root.id,
});
return { replyId: reply.id, rootId: root.id };
});
await page
.locator(
`[data-testid="message-thread-summary"][data-thread-head-id="${rootId}"]`,
)
.click();
const threadPanel = page.getByTestId("message-thread-panel");
const replyRow = threadPanel.locator(`[data-message-id="${replyId}"]`);
await expect(replyRow).toContainText("Copy-link regression reply");
await replyRow.hover();
const actionBar = replyRow.getByTestId(`message-action-bar-${replyId}`);
const quickReactions = actionBar.getByRole("button", {
name: /^React with /,
});
await expect(quickReactions).toHaveCount(0);
const orderedActionNames = await actionBar
.getByRole("button")
.evaluateAll((buttons) =>
buttons.map((button) => button.getAttribute("aria-label")),
);
expect(orderedActionNames).toEqual([
"Open reactions",
"Reply",
"Copy link",
"More actions",
]);
const copyLink = actionBar.getByTestId(`copy-link-message-${replyId}`);
await expect(copyLink).toHaveAccessibleName("Copy link");
await copyLink.hover();
await expect(page.getByRole("tooltip", { name: "Copy link" })).toBeVisible();
const expectedLink = `buzz://message?channel=${GENERAL_CHANNEL_ID}&id=${replyId}&thread=${rootId}`;
await copyLink.click();
await expect
.poll(async () => (await latestClipboardWrite(page))?.payload.text)
.toBe(expectedLink);
await expect(
page.locator("[data-sonner-toast]").filter({
hasText: "Link copied to clipboard",
}),
).toBeVisible();
await actionBar.getByTestId(`more-actions-${replyId}`).click();
await page.getByTestId(`copy-message-link-${replyId}`).click();
await expect
.poll(async () => {
const writes = (await page.evaluate(() =>
(window.__BUZZ_E2E_COMMAND_LOG__ ?? []).filter(
({ command }) => command === "copy_text_to_clipboard",
),
)) as Array<{ payload: unknown }>;
return writes.map(({ payload }) => payload);
})
.toEqual([{ text: expectedLink }, { text: expectedLink }]);
const [barBox, panelBox] = await Promise.all([
actionBar.boundingBox(),
threadPanel.boundingBox(),
]);
expect(barBox).not.toBeNull();
expect(panelBox).not.toBeNull();
if (!barBox || !panelBox) throw new Error("Message action bounds missing.");
expect(barBox.x).toBeGreaterThanOrEqual(panelBox.x);
expect(barBox.x + barBox.width).toBeLessThanOrEqual(
panelBox.x + panelBox.width,
);
});
test("pending and huddle rows omit both copy-link surfaces", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect
.poll(() =>
page.evaluate(
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
),
)
.toBe(true);
const { huddleId, pendingId } = await page.evaluate((huddleKind) => {
const emit = window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
if (!emit) throw new Error("Mock message emitter is unavailable.");
const pending = emit({
channelName: "general",
content: "Pending copy-link regression",
id: "c".repeat(64),
pending: true,
});
const huddle = emit({
channelName: "general",
content: JSON.stringify({
ephemeral_channel_id: "10000000-0000-4000-8000-000000000001",
}),
id: "d".repeat(64),
kind: huddleKind,
});
return { huddleId: huddle.id, pendingId: pending.id };
}, KIND_HUDDLE_STARTED);
await expectCopyLinkUnavailable(
page.locator(`[data-message-id="${pendingId}"]`),
pendingId,
);
await expectCopyLinkUnavailable(
page.locator(`[data-message-id="${huddleId}"]`),
huddleId,
);
});