mirror of
https://github.com/block/buzz.git
synced 2026-11-05 17:01:35 +00:00
## 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>
183 lines
5.7 KiB
TypeScript
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,
|
|
);
|
|
});
|