mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Refine message actions across conversations
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -135,6 +135,7 @@ export function InboxMessageRow({
|
||||
onReply={canReply ? () => onSelectReplyTarget(message) : undefined}
|
||||
reactionErrorMessage={reactionErrorMessage}
|
||||
reactions={reactions}
|
||||
presentation="menu"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -159,7 +159,7 @@ function MoreActionsMenu({
|
||||
{quickReactionTray ? (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center justify-center gap-0.5 px-1 py-0.5"
|
||||
className="flex w-full items-center justify-between px-1 py-0.5"
|
||||
data-testid={`message-quick-reactions-${message.id}`}
|
||||
>
|
||||
{quickReactionTray}
|
||||
@@ -462,7 +462,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
const [isReactionPickerOpen, setIsReactionPickerOpen] = React.useState(false);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
|
||||
const customEmoji = useCustomEmoji();
|
||||
const quickReactionEmojis = useQuickReactionEmojis(4, customEmoji);
|
||||
const quickReactionEmojis = useQuickReactionEmojis(5, customEmoji);
|
||||
const quickReactionItems = React.useMemo(
|
||||
() =>
|
||||
quickReactionEmojis
|
||||
@@ -507,6 +507,10 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
onReactionBadgeBurstRequest?.(emoji);
|
||||
}
|
||||
|
||||
if (closeMenu) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
|
||||
void onReactionSelect(emoji)
|
||||
.then(() => {
|
||||
recordQuickReactionEmoji(emoji);
|
||||
@@ -516,9 +520,6 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
if (closePicker) {
|
||||
setIsReactionPickerOpen(false);
|
||||
}
|
||||
if (closeMenu) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
[onReactionBadgeBurstRequest, onReactionSelect, wouldAddReaction],
|
||||
@@ -613,7 +614,7 @@ export const MessageActionBar = React.memo(function MessageActionBar({
|
||||
"flex items-center gap-0.5",
|
||||
presentation === "tray"
|
||||
? "p-1"
|
||||
: "gap-0 bg-background [&_button]:h-6 [&_button]:w-6",
|
||||
: "gap-0 bg-transparent [&_button]:h-6 [&_button]:w-6",
|
||||
)}
|
||||
>
|
||||
{presentation === "menu" ? (
|
||||
|
||||
@@ -89,7 +89,6 @@ export const MessageRow = React.memo(
|
||||
huddleMemberPubkeys,
|
||||
huddleMemberPubkeysPending = false,
|
||||
hideAgentAccessBadge = false,
|
||||
actionBarPlacement = "floating",
|
||||
collapseDescendantsLabel,
|
||||
isFollowingThread,
|
||||
isContinuation = false,
|
||||
@@ -532,10 +531,8 @@ export const MessageRow = React.memo(
|
||||
|
||||
const actionBarNode = (
|
||||
<MessageRowActions
|
||||
actionBarPlacement={actionBarPlacement}
|
||||
anchorToBubble={anchorActionBarToBubble}
|
||||
channelId={channelId}
|
||||
isContinuation={isContinuation}
|
||||
isFollowingThread={isFollowingThread}
|
||||
isUnread={isUnread}
|
||||
message={message}
|
||||
|
||||
@@ -11,7 +11,6 @@ export type ThreadDepthGuideAction = {
|
||||
};
|
||||
|
||||
export type MessageRowProps = {
|
||||
actionBarPlacement?: "floating" | "inside";
|
||||
bodyFooter?: ReactNode;
|
||||
channelId?: string | null;
|
||||
collapseDepthGuideActions?: ReadonlyArray<ThreadDepthGuideAction>;
|
||||
|
||||
@@ -6,10 +6,8 @@ import { cn } from "@/shared/lib/cn";
|
||||
import { MessageActionBar } from "./MessageActionBar";
|
||||
|
||||
type MessageRowActionsProps = {
|
||||
actionBarPlacement: "floating" | "inside";
|
||||
anchorToBubble: boolean;
|
||||
channelId?: string | null;
|
||||
isContinuation: boolean;
|
||||
isFollowingThread?: boolean;
|
||||
isUnread?: boolean;
|
||||
message: TimelineMessage;
|
||||
@@ -30,10 +28,8 @@ type MessageRowActionsProps = {
|
||||
};
|
||||
|
||||
export function MessageRowActions({
|
||||
actionBarPlacement,
|
||||
anchorToBubble,
|
||||
channelId,
|
||||
isContinuation,
|
||||
isFollowingThread,
|
||||
isUnread,
|
||||
message,
|
||||
@@ -52,14 +48,6 @@ export function MessageRowActions({
|
||||
reactions,
|
||||
showMessageBubbles,
|
||||
}: MessageRowActionsProps) {
|
||||
const floatingPosition = isContinuation
|
||||
? showMessageBubbles
|
||||
? "sm:-top-1 sm:-translate-y-1/2"
|
||||
: "sm:-top-3 sm:-translate-y-1/2"
|
||||
: showMessageBubbles
|
||||
? "sm:top-2 sm:-translate-y-1/2"
|
||||
: "sm:top-0 sm:-translate-y-1/2";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -68,11 +56,7 @@ export function MessageRowActions({
|
||||
? anchorToBubble
|
||||
? "-right-2 -top-2"
|
||||
: "right-1 top-1"
|
||||
: "right-2 top-1 sm:pointer-events-none",
|
||||
!showMessageBubbles &&
|
||||
(actionBarPlacement === "floating"
|
||||
? floatingPosition
|
||||
: "sm:top-1 sm:translate-y-0"),
|
||||
: "right-1 top-1",
|
||||
)}
|
||||
>
|
||||
<MessageActionBar
|
||||
@@ -91,7 +75,7 @@ export function MessageRowActions({
|
||||
onReply={onReply}
|
||||
onSendToChannel={onSendToChannel}
|
||||
onUnfollowThread={onUnfollowThread}
|
||||
presentation={showMessageBubbles ? "menu" : "tray"}
|
||||
presentation="menu"
|
||||
reactionErrorMessage={reactionErrorMessage}
|
||||
reactions={reactions}
|
||||
/>
|
||||
|
||||
@@ -583,7 +583,6 @@ export function MessageThreadPanel({
|
||||
>
|
||||
<div className="rounded-2xl">
|
||||
<MessageRow
|
||||
actionBarPlacement="inside"
|
||||
channelId={channelId}
|
||||
currentPubkey={currentPubkey}
|
||||
huddleMemberPubkeys={huddleMemberPubkeys}
|
||||
|
||||
@@ -23,6 +23,16 @@ test("quick reactions backfill defaults after stale custom emoji", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("quick reactions fill the expanded fifth slot from recent reactions", () => {
|
||||
assert.deepEqual(resolveQuickReactionEmojis([entry("🔥")], 5, []), [
|
||||
"🔥",
|
||||
"👍",
|
||||
"❤️",
|
||||
"😂",
|
||||
"🎉",
|
||||
]);
|
||||
});
|
||||
|
||||
test("quick reactions skip stale custom emoji before applying the limit", () => {
|
||||
assert.deepEqual(
|
||||
resolveQuickReactionEmojis(
|
||||
|
||||
@@ -730,14 +730,14 @@ test("app font size and conversation density apply independently", async ({
|
||||
);
|
||||
await expect(fontSizeDescription).toHaveText("Adjust text throughout Buzz.");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 17.142857,
|
||||
authorLineHeight: 16,
|
||||
bodyGap: 0.125,
|
||||
fontSize: "calc(17.142857px * .875)",
|
||||
lineHeight: "calc(17.142857px * 1.25)",
|
||||
fontSize: "calc(16px * .875)",
|
||||
lineHeight: "calc(16px * 1.25)",
|
||||
paragraphGap: 0.5,
|
||||
rowPadding: 0.375,
|
||||
timestampFontSize: "calc(17.142857px * .75)",
|
||||
timestampLineHeight: 17.142857,
|
||||
timestampFontSize: "calc(16px * .75)",
|
||||
timestampLineHeight: 16,
|
||||
});
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -761,19 +761,15 @@ test("app font size and conversation density apply independently", async ({
|
||||
return [style.fontSize, style.lineHeight];
|
||||
}),
|
||||
)
|
||||
.toEqual(["15px", "21.4286px"]);
|
||||
.toEqual(["14px", "20px"]);
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "15px",
|
||||
lineHeight: "21.4286px",
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
await expect
|
||||
.poll(readPreviewTimestampScale)
|
||||
.toEqual(["12.8571px", "17.1429px"]);
|
||||
await expect
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["25.7143px", "15px", "19.2857px"]);
|
||||
await expect.poll(readPreviewTimestampScale).toEqual(["12px", "16px"]);
|
||||
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
|
||||
await expect(densityIndicator).toHaveCSS("transition-duration", "0.2s");
|
||||
await expect(densityIndicator).toHaveCSS("transition-property", /transform/);
|
||||
await expect(fontSizeIndicator).toHaveCSS("transition-duration", "0.2s");
|
||||
@@ -798,7 +794,7 @@ test("app font size and conversation density apply independently", async ({
|
||||
};
|
||||
})
|
||||
.toEqual({
|
||||
chipMatchesControl: true,
|
||||
chipMatchesControl: false,
|
||||
previewBackground: "rgba(0, 0, 0, 0)",
|
||||
});
|
||||
const previewSurfaceBox = await previewSurface.boundingBox();
|
||||
@@ -835,8 +831,8 @@ test("app font size and conversation density apply independently", async ({
|
||||
expect(firstPreviewMessageBox.y - previewSurfaceBox.y).toBeLessThanOrEqual(
|
||||
17,
|
||||
);
|
||||
await expect(previewChip).toHaveCSS("padding-top", "4px");
|
||||
await expect(previewChip).toHaveCSS("padding-bottom", "4px");
|
||||
await expect(previewChip).toHaveCSS("padding-top", "0px");
|
||||
await expect(previewChip).toHaveCSS("padding-bottom", "0px");
|
||||
|
||||
await densityIndicator.evaluate((element) => {
|
||||
element.addEventListener(
|
||||
@@ -859,6 +855,37 @@ test("app font size and conversation density apply independently", async ({
|
||||
),
|
||||
)
|
||||
.toBe("compact");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 16,
|
||||
bodyGap: 0,
|
||||
fontSize: "calc(16px * .875)",
|
||||
lineHeight: "calc(16px * 1.25)",
|
||||
paragraphGap: 0.375,
|
||||
rowPadding: 0.25,
|
||||
timestampFontSize: "calc(16px * .75)",
|
||||
timestampLineHeight: 16,
|
||||
});
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
await expect.poll(readPreviewTimestampScale).toEqual(["12px", "16px"]);
|
||||
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
|
||||
|
||||
await larger.click();
|
||||
await expect(root).toHaveAttribute("data-conversation-density", "compact");
|
||||
await expect(root).toHaveAttribute("data-font-size", "larger");
|
||||
await expect(larger).toHaveAttribute("aria-pressed", "true");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
(key) => window.localStorage.getItem(key),
|
||||
FONT_SIZE_STORAGE_KEY,
|
||||
),
|
||||
)
|
||||
.toBe("larger");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 17.142857,
|
||||
bodyGap: 0,
|
||||
@@ -869,6 +896,45 @@ test("app font size and conversation density apply independently", async ({
|
||||
timestampFontSize: "calc(17.142857px * .75)",
|
||||
timestampLineHeight: 17.142857,
|
||||
});
|
||||
await expect
|
||||
.poll(() =>
|
||||
previewMessage.evaluate((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
return [style.fontSize, style.lineHeight];
|
||||
}),
|
||||
)
|
||||
.toEqual(["15px", "21.4286px"]);
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "15px",
|
||||
lineHeight: "21.4286px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
await expect
|
||||
.poll(readPreviewTimestampScale)
|
||||
.toEqual(["12.8571px", "17.1429px"]);
|
||||
await expect
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["25.7143px", "15px", "19.2857px"]);
|
||||
await waitForAnimations(page);
|
||||
await page.getByTestId("appearance-preferences-card").screenshot({
|
||||
path: `${SHOTS}/15-conversation-compact-larger.png`,
|
||||
});
|
||||
|
||||
await spacious.click();
|
||||
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
|
||||
await expect(root).toHaveAttribute("data-font-size", "larger");
|
||||
await expect(spacious).toHaveAttribute("aria-pressed", "true");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 17.142857,
|
||||
bodyGap: 0.25,
|
||||
fontSize: "calc(17.142857px * .875)",
|
||||
lineHeight: "calc(17.142857px * 1.25)",
|
||||
paragraphGap: 0.625,
|
||||
rowPadding: 0.5,
|
||||
timestampFontSize: "calc(17.142857px * .75)",
|
||||
timestampLineHeight: 17.142857,
|
||||
});
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "15px",
|
||||
lineHeight: "21.4286px",
|
||||
@@ -882,93 +948,19 @@ test("app font size and conversation density apply independently", async ({
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["25.7143px", "15px", "19.2857px"]);
|
||||
|
||||
await larger.click();
|
||||
await expect(root).toHaveAttribute("data-conversation-density", "compact");
|
||||
await expect(root).toHaveAttribute("data-font-size", "larger");
|
||||
await expect(larger).toHaveAttribute("aria-pressed", "true");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
(key) => window.localStorage.getItem(key),
|
||||
FONT_SIZE_STORAGE_KEY,
|
||||
),
|
||||
)
|
||||
.toBe("larger");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 18.285714,
|
||||
bodyGap: 0,
|
||||
fontSize: "calc(18.285714px * .875)",
|
||||
lineHeight: "calc(18.285714px * 1.25)",
|
||||
paragraphGap: 0.375,
|
||||
rowPadding: 0.25,
|
||||
timestampFontSize: "calc(18.285714px * .75)",
|
||||
timestampLineHeight: 18.285714,
|
||||
});
|
||||
await expect
|
||||
.poll(() =>
|
||||
previewMessage.evaluate((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
return [style.fontSize, style.lineHeight];
|
||||
}),
|
||||
)
|
||||
.toEqual(["16px", "22.8571px"]);
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "16px",
|
||||
lineHeight: "22.8571px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
await expect
|
||||
.poll(readPreviewTimestampScale)
|
||||
.toEqual(["13.7143px", "18.2857px"]);
|
||||
await expect
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["27.4286px", "16px", "20.5714px"]);
|
||||
await waitForAnimations(page);
|
||||
await page.getByTestId("appearance-preferences-card").screenshot({
|
||||
path: `${SHOTS}/15-conversation-compact-larger.png`,
|
||||
});
|
||||
|
||||
await spacious.click();
|
||||
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
|
||||
await expect(root).toHaveAttribute("data-font-size", "larger");
|
||||
await expect(spacious).toHaveAttribute("aria-pressed", "true");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 18.285714,
|
||||
bodyGap: 0.25,
|
||||
fontSize: "calc(18.285714px * .875)",
|
||||
lineHeight: "calc(18.285714px * 1.25)",
|
||||
paragraphGap: 0.625,
|
||||
rowPadding: 0.5,
|
||||
timestampFontSize: "calc(18.285714px * .75)",
|
||||
timestampLineHeight: 18.285714,
|
||||
});
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "16px",
|
||||
lineHeight: "22.8571px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
await expect
|
||||
.poll(readPreviewTimestampScale)
|
||||
.toEqual(["13.7143px", "18.2857px"]);
|
||||
await expect
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["27.4286px", "16px", "20.5714px"]);
|
||||
|
||||
await smaller.click();
|
||||
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
|
||||
await expect(root).toHaveAttribute("data-font-size", "smaller");
|
||||
await expect(smaller).toHaveAttribute("aria-pressed", "true");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 16,
|
||||
authorLineHeight: 14.857143,
|
||||
bodyGap: 0.25,
|
||||
fontSize: "calc(16px * .875)",
|
||||
lineHeight: "calc(16px * 1.25)",
|
||||
fontSize: "calc(14.857143px * .875)",
|
||||
lineHeight: "calc(14.857143px * 1.25)",
|
||||
paragraphGap: 0.625,
|
||||
rowPadding: 0.5,
|
||||
timestampFontSize: "calc(16px * .75)",
|
||||
timestampLineHeight: 16,
|
||||
timestampFontSize: "calc(14.857143px * .75)",
|
||||
timestampLineHeight: 14.857143,
|
||||
});
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -977,15 +969,19 @@ test("app font size and conversation density apply independently", async ({
|
||||
return [style.fontSize, style.lineHeight];
|
||||
}),
|
||||
)
|
||||
.toEqual(["14px", "20px"]);
|
||||
.toEqual(["13px", "18.5714px"]);
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
fontSize: "13px",
|
||||
lineHeight: "18.5714px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
await expect.poll(readPreviewTimestampScale).toEqual(["12px", "16px"]);
|
||||
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
|
||||
await expect
|
||||
.poll(readPreviewTimestampScale)
|
||||
.toEqual(["11.1429px", "14.8571px"]);
|
||||
await expect
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["22.2857px", "13px", "16.7143px"]);
|
||||
await waitForAnimations(page);
|
||||
await page.getByTestId("appearance-preferences-card").screenshot({
|
||||
path: `${SHOTS}/16-conversation-spacious-smaller.png`,
|
||||
@@ -1028,18 +1024,18 @@ test("app font size and conversation density apply independently", async ({
|
||||
)
|
||||
.toBe("comfortable");
|
||||
await expect.poll(readScale).toEqual({
|
||||
authorLineHeight: 17.142857,
|
||||
authorLineHeight: 16,
|
||||
bodyGap: 0.25,
|
||||
fontSize: "calc(17.142857px * .875)",
|
||||
lineHeight: "calc(17.142857px * 1.25)",
|
||||
fontSize: "calc(16px * .875)",
|
||||
lineHeight: "calc(16px * 1.25)",
|
||||
paragraphGap: 0.625,
|
||||
rowPadding: 0.5,
|
||||
timestampFontSize: "calc(17.142857px * .75)",
|
||||
timestampLineHeight: 17.142857,
|
||||
timestampFontSize: "calc(16px * .75)",
|
||||
timestampLineHeight: 16,
|
||||
});
|
||||
await expect.poll(readSettingsScale).toEqual({
|
||||
fontSize: "15px",
|
||||
lineHeight: "21.4286px",
|
||||
fontSize: "14px",
|
||||
lineHeight: "20px",
|
||||
minHeight: "64px",
|
||||
paddingBlock: "12px",
|
||||
});
|
||||
@@ -1093,10 +1089,10 @@ test("app font size and conversation density apply independently", async ({
|
||||
return [style.fontSize, style.lineHeight];
|
||||
}),
|
||||
)
|
||||
.toEqual(["16px", "22.8571px"]);
|
||||
.toEqual(["15px", "21.4286px"]);
|
||||
await expect
|
||||
.poll(readSettingsChromeScale)
|
||||
.toEqual(["27.4286px", "16px", "20.5714px"]);
|
||||
.toEqual(["25.7143px", "15px", "19.2857px"]);
|
||||
await page.mouse.up();
|
||||
await expect(larger).toHaveAttribute("aria-pressed", "true");
|
||||
await expect
|
||||
@@ -1175,8 +1171,8 @@ test("message layout switches the Appearance preview between Open and Bubbles",
|
||||
"background-color",
|
||||
"rgba(0, 0, 0, 0)",
|
||||
);
|
||||
await expect(previewSurface).toHaveCSS("border-radius", "17.1429px");
|
||||
await expect(previewSurface).toHaveCSS("padding-left", "15px");
|
||||
await expect(previewSurface).toHaveCSS("border-radius", "16px");
|
||||
await expect(previewSurface).toHaveCSS("padding-left", "14px");
|
||||
|
||||
await waitForAnimations(page);
|
||||
const indicator = page.getByTestId("message-style-control-indicator");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
import { selectMessageQuickReaction } from "../helpers/messageActions";
|
||||
|
||||
const SELF_PUBKEY = "deadbeef".repeat(8);
|
||||
const CHANNEL_GENERAL = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
||||
@@ -770,13 +771,7 @@ test.describe("channel activity hover preview", () => {
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "Reacting here means I care" });
|
||||
await expect(rootRow).toBeVisible();
|
||||
await rootRow.hover();
|
||||
const actionBar = page.getByTestId(`message-action-bar-${root.id}`);
|
||||
await expect(actionBar).toBeVisible();
|
||||
await actionBar
|
||||
.getByRole("button", { name: /^React with / })
|
||||
.first()
|
||||
.click();
|
||||
await selectMessageQuickReaction(rootRow, /^React with /);
|
||||
await expect(
|
||||
rootRow.getByRole("button", { name: /^Toggle .* reaction$/ }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { selectMessageAction } from "../helpers/messageActions";
|
||||
|
||||
type MockMessageWindow = Window & {
|
||||
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
||||
@@ -77,9 +78,10 @@ test.describe("channel shared header backdrop", () => {
|
||||
},
|
||||
);
|
||||
|
||||
const replyButton = page.locator('[data-testid^="reply-message-"]').first();
|
||||
await expect(replyButton).toBeVisible();
|
||||
await replyButton.click({ force: true });
|
||||
const rootMessage = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "Root message for shared header backdrop coverage." });
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
|
||||
const sharedBackdrop = page.getByTestId("channel-shared-header-backdrop");
|
||||
|
||||
@@ -3,6 +3,10 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import {
|
||||
openMessageReactionPicker,
|
||||
selectMessageQuickReaction,
|
||||
} from "../helpers/messageActions";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
// Custom-emoji end-to-end guard.
|
||||
@@ -285,28 +289,17 @@ async function quickReactionStorageContains(
|
||||
}, emoji);
|
||||
}
|
||||
|
||||
test("message quick reaction tray stays neutral after selecting a tray emoji", async ({
|
||||
test("message quick reaction menu closes after selecting an emoji", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openGeneral(page);
|
||||
|
||||
const row = reactionTargetRow(page);
|
||||
await expect(row).toBeVisible();
|
||||
await row.hover();
|
||||
|
||||
const quickReactionButton = row.getByRole("button", {
|
||||
name: "React with :+1:",
|
||||
});
|
||||
await expect(quickReactionButton).toBeVisible();
|
||||
await quickReactionButton.click();
|
||||
await selectMessageQuickReaction(row, "React with :+1:");
|
||||
|
||||
await expect(row.getByLabel("Toggle 👍 reaction")).toBeVisible();
|
||||
await row.hover();
|
||||
await expect(quickReactionButton).not.toHaveAttribute("aria-pressed", "true");
|
||||
await expect(quickReactionButton).not.toHaveClass(SELECTED_ACTION_CLASS);
|
||||
await expect(messageReactionTrigger(row)).not.toHaveClass(
|
||||
SELECTED_ACTION_CLASS,
|
||||
);
|
||||
await expect(page.getByRole("menu")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("reacting with a custom emoji renders via the loopback media proxy", async ({
|
||||
@@ -318,8 +311,7 @@ test("reacting with a custom emoji renders via the loopback media proxy", async
|
||||
// open the reaction picker.
|
||||
const row = reactionTargetRow(page);
|
||||
await expect(row).toBeVisible();
|
||||
await row.hover();
|
||||
await row.getByLabel("Open reactions").click();
|
||||
await openMessageReactionPicker(row);
|
||||
|
||||
// emoji-mart renders inside a Shadow DOM web component. Search by shortcode
|
||||
// to surface the custom emoji, then click it.
|
||||
@@ -539,8 +531,7 @@ test("a system message accepts a custom-emoji reaction", async ({ page }) => {
|
||||
|
||||
const row = page.getByTestId("system-message-row").first();
|
||||
await expect(row).toBeVisible();
|
||||
await row.hover();
|
||||
await row.getByLabel("Open reactions").click();
|
||||
await openMessageReactionPicker(row);
|
||||
|
||||
const picker = page.locator("em-emoji-picker");
|
||||
await picker.locator("input[type='search']").fill(REACTION_SHORTCODE);
|
||||
@@ -563,8 +554,7 @@ test("emoji picker search input has spellcheck, autocorrect, and autocapitalize
|
||||
// Open the reaction picker on the seeded reactable message.
|
||||
const row = reactionTargetRow(page);
|
||||
await expect(row).toBeVisible();
|
||||
await row.hover();
|
||||
await row.getByLabel("Open reactions").click();
|
||||
await openMessageReactionPicker(row);
|
||||
|
||||
// Wait for the picker to be visible, then read the shadow-root input attributes.
|
||||
const picker = page.locator("em-emoji-picker");
|
||||
@@ -599,8 +589,7 @@ test("emoji picker search input is focused immediately on open (no manual click
|
||||
// focus via our shadow traversal before the user interacts.
|
||||
const row = reactionTargetRow(page);
|
||||
await expect(row).toBeVisible();
|
||||
await row.hover();
|
||||
await row.getByLabel("Open reactions").click();
|
||||
await openMessageReactionPicker(row);
|
||||
|
||||
const picker = page.locator("em-emoji-picker");
|
||||
await expect(picker.locator("input[type='search']")).toBeVisible();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { selectMessageAction } from "../helpers/messageActions";
|
||||
|
||||
// Fixed pubkey for the owned managed agent seeded in these tests.
|
||||
// Must not collide with any existing e2eBridge constant.
|
||||
@@ -189,8 +190,7 @@ test("owner can delete their owned agent's message from the thread panel", async
|
||||
await expect(agentRow).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Open the thread panel by hovering the message and clicking Reply.
|
||||
await agentRow.hover();
|
||||
await agentRow.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(agentRow, "Reply");
|
||||
|
||||
// Wait for the thread panel and confirm the thread head contains the agent message.
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
import { selectMessageQuickReaction } from "../helpers/messageActions";
|
||||
import type { RelayEvent } from "../../src/shared/api/types";
|
||||
|
||||
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
|
||||
@@ -172,9 +173,7 @@ test("inbox reaction on a thread-reply mention persists after refetch", async ({
|
||||
),
|
||||
).toBeLessThanOrEqual(1);
|
||||
|
||||
await selectedMessage
|
||||
.getByRole("button", { name: "React with :+1:" })
|
||||
.click();
|
||||
await selectMessageQuickReaction(selectedMessage, "React with :+1:");
|
||||
|
||||
// The pill must appear AND persist: the post-toggle refetch replaces the
|
||||
// optimistic state with fetched reaction events. Give the refetch time to
|
||||
|
||||
@@ -120,6 +120,66 @@ test("profile hover uses the channel hover surface", async ({ page }) => {
|
||||
.screenshot({ path: `${SHOTS}/profile-hover.png` });
|
||||
});
|
||||
|
||||
test("open messages use the compact ellipsis actions menu", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem(
|
||||
"buzz.quick-reaction-emojis.v1:e2e-default-community",
|
||||
JSON.stringify([{ count: 2, emoji: "🔥", lastUsedAt: Date.now() }]),
|
||||
);
|
||||
});
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const row = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "React to me with a custom emoji" })
|
||||
.last();
|
||||
await expect(row).toBeVisible();
|
||||
await row.hover();
|
||||
|
||||
const actionBar = row.locator('[data-testid^="message-action-bar-"]');
|
||||
await expect(actionBar).toHaveAttribute("data-presentation", "menu");
|
||||
await expect(
|
||||
actionBar.getByRole("button", { name: "More actions" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
actionBar.getByRole("button", { name: "Open reactions" }),
|
||||
).toHaveCount(0);
|
||||
await expect(actionBar.getByRole("button", { name: "Reply" })).toHaveCount(0);
|
||||
|
||||
const [rowBox, actionBarBox] = await Promise.all([
|
||||
row.boundingBox(),
|
||||
actionBar.boundingBox(),
|
||||
]);
|
||||
if (!rowBox || !actionBarBox) {
|
||||
throw new Error("Expected open message action geometry.");
|
||||
}
|
||||
expect(actionBarBox.y).toBeGreaterThanOrEqual(rowBox.y);
|
||||
expect(actionBarBox.x + actionBarBox.width).toBeLessThanOrEqual(
|
||||
rowBox.x + rowBox.width + 1,
|
||||
);
|
||||
|
||||
await actionBar.getByRole("button", { name: "More actions" }).click();
|
||||
await expect(page.getByRole("menu")).toBeVisible();
|
||||
const quickReactions = page.locator(
|
||||
'[data-testid^="message-quick-reactions-"]',
|
||||
);
|
||||
await expect(quickReactions).toBeVisible();
|
||||
await expect(
|
||||
quickReactions.getByRole("button", { name: /^React with / }),
|
||||
).toHaveCount(5);
|
||||
await expect(
|
||||
page.getByRole("menuitem", { name: "Reply", exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({ path: `${SHOTS}/open-message-actions.png` });
|
||||
});
|
||||
|
||||
test("left-aligned continuation bubbles join on the left edge", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect, test, type Locator } from "@playwright/test";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
import { expectCornerRadiusPx, expectSmoothCorners } from "../helpers/css";
|
||||
import { selectMessageAction } from "../helpers/messageActions";
|
||||
import { openSettings } from "../helpers/settings";
|
||||
|
||||
async function waitForReadyComposerSnapshots(
|
||||
@@ -2575,7 +2576,7 @@ test("thread panel width uses session storage and reset handle", async ({
|
||||
);
|
||||
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
|
||||
await expect
|
||||
@@ -2636,7 +2637,7 @@ test("thread panel width uses session storage and reset handle", async ({
|
||||
await expect(threadPanel).toBeHidden();
|
||||
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
|
||||
await expect
|
||||
@@ -2663,8 +2664,7 @@ test("narrow thread view collapses channel header actions into a menu", async ({
|
||||
const rootMessage = page.locator('[data-message-id="mock-general-alice"]');
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
|
||||
await rootMessage.hover();
|
||||
await page.getByTestId("reply-message-mock-general-alice").click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(threadPanel.getByTestId("message-thread-back")).toHaveCount(0);
|
||||
|
||||
@@ -2707,8 +2707,7 @@ test("single-panel thread view hides channel actions", async ({ page }) => {
|
||||
const rootMessage = page.locator('[data-message-id="mock-general-alice"]');
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
|
||||
await rootMessage.hover();
|
||||
await page.getByTestId("reply-message-mock-general-alice").click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(threadPanel.getByTestId("message-thread-back")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-actions-menu-trigger")).toHaveCount(0);
|
||||
@@ -2765,7 +2764,7 @@ test("thread composer is focused after clicking the reply icon", async ({
|
||||
.getByTestId("message-row")
|
||||
.last();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
@@ -2793,7 +2792,7 @@ test("thread refetch preserves a live reply and reaction received in flight", as
|
||||
if (!rootId) throw new Error("Expected a thread root id.");
|
||||
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
|
||||
@@ -2852,7 +2851,7 @@ test("thread reply appears after relay closes and restores its live subscription
|
||||
.getByTestId("message-row")
|
||||
.last();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
const reply = `Thread reply after CLOSED ${Date.now()}`;
|
||||
@@ -2882,7 +2881,7 @@ test("thread composer keeps focus after sending a thread reply", async ({
|
||||
.getByTestId("message-row")
|
||||
.last();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
@@ -2992,7 +2991,7 @@ test("ArrowUp edits your last thread reply right after sending it", async ({
|
||||
.getByTestId("message-row")
|
||||
.last();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
@@ -3036,7 +3035,7 @@ test("action bar stays within the timeline when the thread panel is open", async
|
||||
|
||||
const rootMessage = timeline.getByTestId("message-row").first();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
|
||||
const wideRow = timeline.getByTestId("message-row").last();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { selectMessageAction } from "../helpers/messageActions";
|
||||
import { openSettings } from "../helpers/settings";
|
||||
|
||||
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
|
||||
@@ -159,8 +160,7 @@ test("back and forward restore open thread panels", async ({ page }) => {
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.first();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
@@ -189,8 +189,7 @@ test("back undoes closing a thread panel", async ({ page }) => {
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.first();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
@@ -212,8 +211,7 @@ test("open thread panels survive reload", async ({ page }) => {
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.first();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
@@ -274,8 +272,7 @@ test("settings is a route: section survives reload, closing returns to the previ
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.first();
|
||||
await rootMessage.hover();
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await selectMessageAction(rootMessage, "Reply");
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
await expect(threadPanel).toBeVisible();
|
||||
const channelUrl = page.url();
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
import { selectMessageQuickReaction } from "../helpers/messageActions";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
|
||||
// Reaction ordering end-to-end guard.
|
||||
@@ -97,10 +98,7 @@ async function addQuickReaction(
|
||||
emoji: string,
|
||||
label: string,
|
||||
) {
|
||||
await row.hover();
|
||||
const btn = row.getByRole("button", { name: `React with ${label}` });
|
||||
await expect(btn).toBeVisible();
|
||||
await btn.click();
|
||||
await selectMessageQuickReaction(row, `React with ${label}`);
|
||||
// Wait for the optimistic pill to appear before continuing.
|
||||
await expect(
|
||||
row
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { expect, type Locator } from "@playwright/test";
|
||||
|
||||
export async function openMessageActions(row: Locator) {
|
||||
await row.hover();
|
||||
const trigger = row.getByRole("button", { name: "More actions" });
|
||||
await expect(trigger).toBeVisible();
|
||||
await expect(trigger).toHaveAttribute("data-state", "closed");
|
||||
await trigger.click();
|
||||
|
||||
const menu = row.page().getByRole("menu");
|
||||
await expect(menu).toBeVisible();
|
||||
return menu;
|
||||
}
|
||||
|
||||
export async function selectMessageAction(row: Locator, name: string) {
|
||||
const menu = await openMessageActions(row);
|
||||
await menu.getByRole("menuitem", { name, exact: true }).click();
|
||||
}
|
||||
|
||||
export async function openMessageReactionPicker(row: Locator) {
|
||||
const menu = await openMessageActions(row);
|
||||
await menu.getByRole("button", { name: "Open reactions" }).click();
|
||||
}
|
||||
|
||||
export async function selectMessageQuickReaction(
|
||||
row: Locator,
|
||||
name: string | RegExp,
|
||||
) {
|
||||
const menu = await openMessageActions(row);
|
||||
const reaction = menu.getByRole("button", { name }).first();
|
||||
await expect(reaction).toBeVisible();
|
||||
await reaction.click();
|
||||
}
|
||||
Reference in New Issue
Block a user