mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
fix(desktop): let inbox title and message author names truncate under narrow panes (#7550)
## Summary Fixes two instances of the same dead-truncate pattern in the desktop app, where a flex item's implicit `min-width: auto` prevented `truncate` from engaging, so long text painted over adjacent controls instead of ellipsizing: - **Inbox detail title** (`InboxDetailPane.tsx`): the clickable context-title button sized to its text instead of shrinking with the pane, overlapping the header controls (open-in-channel, members, huddle, more menu). Fixed by adding `max-w-full`. - **Message author names** (`MessageHeader.tsx` / `UserProfilePopover.tsx`): the `UserProfilePopover` inline-flex trigger wrapper refused to shrink below the name's nowrap width, running long author names under the hover action bar and off the pane edge. Fixed by adding a `triggerClassName` prop to `UserProfilePopover` and passing `min-w-0 max-w-full` at the author call site. Two other suspected instances (project file breadcrumb, drafts pane title) were stress-tested and already truncate correctly — no change. ### Related issue N/A — none found. ### Testing - New Playwright regression tests for both fixes (`inbox-title-overlap.spec.ts`, `message-author-overlap.spec.ts`, registered in the smoke project), each proven to discriminate: they fail with the fix reverted (real measured overlap) and assert the ellipsis actually engages with non-zero title width, so they can't pass vacuously. - Typecheck, lint, and full desktop unit suite green (pre-push hooks); full desktop e2e smoke suite run earlier: 1402 passed, 3 pre-existing unrelated failures (each fails identically with the fix reverted). **Inbox title — before** (long title paints under the header controls):  **Inbox title — after** (truncates with ellipsis, controls stay clear):  **Author name — before** (long name runs past the header row edge):  **Author name — after** (clean cutoff):  🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: cynfria <yescynthia@gmail.com> Signed-off-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tree Trunks <6ba22921d9dc2ad0aa6ecdf63787ddd24726e266d866da31af69f2e4e146ace5@buzz.block.builderlab.xyz>
This commit is contained in:
146
desktop/tests/e2e/inbox-title-overlap.spec.ts
Normal file
146
desktop/tests/e2e/inbox-title-overlap.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Regression test: a long inbox detail title must truncate instead of
|
||||
* running underneath the header controls (open-in-channel, members,
|
||||
* huddle, more menu) when the detail pane is narrow.
|
||||
*
|
||||
* Run: pnpm build:e2e && pnpm exec playwright test --project=smoke \
|
||||
* tests/e2e/inbox-title-overlap.spec.ts
|
||||
* Output: test-results/inbox-title-overlap/
|
||||
*/
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import type { RelayEvent } from "../../src/shared/api/types";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
|
||||
const SHOTS = "test-results/inbox-title-overlap";
|
||||
|
||||
const ENGINEERING_CHANNEL_ID = "1c7e1c02-87bb-5e88-b2da-5a7a9432d0c9";
|
||||
|
||||
type MockWindow = Window & {
|
||||
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
||||
channelName: string;
|
||||
content: string;
|
||||
parentEventId?: string | null;
|
||||
pubkey?: string;
|
||||
mentionPubkeys?: string[];
|
||||
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;
|
||||
};
|
||||
|
||||
test.describe("inbox detail title overflow", () => {
|
||||
// Narrow window matching the report; the list pane is widened to its
|
||||
// resizable maximum so the detail pane is squeezed to its 300px minimum
|
||||
// and "Message in #engineering" cannot fit next to the header controls.
|
||||
test.use({ viewport: { width: 1046, height: 838 } });
|
||||
|
||||
test("long title truncates instead of overlapping header controls", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
window.sessionStorage.setItem(
|
||||
"buzz.desktop.home-inbox-list-width",
|
||||
"520",
|
||||
);
|
||||
});
|
||||
await installMockBridge(page, { mode: "mock" });
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByTestId("home-inbox-list")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
||||
const mentionId = "ab".repeat(32);
|
||||
await page.evaluate(
|
||||
({ channelId, currentPubkey, id, senderPubkey }) => {
|
||||
const win = window as MockWindow;
|
||||
const emit = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
|
||||
const push = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
|
||||
if (!emit || !push) {
|
||||
throw new Error("Mock bridge helpers are not installed.");
|
||||
}
|
||||
const event = emit({
|
||||
channelName: "engineering",
|
||||
content:
|
||||
"Hey, can you look at the release checklist before tomorrow?",
|
||||
id,
|
||||
mentionPubkeys: [currentPubkey],
|
||||
pubkey: senderPubkey,
|
||||
});
|
||||
push({
|
||||
category: "mention",
|
||||
channel_id: channelId,
|
||||
channel_name: "engineering",
|
||||
content: event.content,
|
||||
created_at: event.created_at,
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey,
|
||||
tags: event.tags,
|
||||
});
|
||||
},
|
||||
{
|
||||
channelId: ENGINEERING_CHANNEL_ID,
|
||||
currentPubkey: TEST_IDENTITIES.tyler.pubkey,
|
||||
id: mentionId,
|
||||
senderPubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
},
|
||||
);
|
||||
|
||||
const row = page.getByTestId(`home-inbox-item-${mentionId}`);
|
||||
await expect(row).toBeVisible();
|
||||
await row.click();
|
||||
|
||||
const detail = page.getByTestId("home-inbox-detail");
|
||||
await expect(detail).toBeVisible();
|
||||
const title = detail.getByTestId("home-inbox-context-title");
|
||||
await expect(title).toHaveText("Message in #engineering");
|
||||
await waitForAnimations(page);
|
||||
|
||||
const titleBox = await title.boundingBox();
|
||||
const controlsBox = await detail
|
||||
.getByTestId("home-inbox-open-context")
|
||||
.boundingBox();
|
||||
if (!titleBox || !controlsBox) {
|
||||
throw new Error("Header title or controls not rendered.");
|
||||
}
|
||||
|
||||
// Guard against a vacuous pass: the title must still render with real
|
||||
// width, and the squeeze must actually engage — the inner span's
|
||||
// ellipsis is active only when its content overflows. If layout
|
||||
// constants or fonts ever change so the full title fits, this fails
|
||||
// loudly instead of silently no longer testing truncation.
|
||||
expect(titleBox.width).toBeGreaterThan(0);
|
||||
const isTruncated = await title.evaluate((element) => {
|
||||
const span = element.querySelector("span");
|
||||
return span ? span.scrollWidth > span.clientWidth : false;
|
||||
});
|
||||
expect(isTruncated).toBe(true);
|
||||
|
||||
await page.screenshot({
|
||||
clip: { height: 96, width: 1046, x: 0, y: 0 },
|
||||
path: `${SHOTS}/01-header.png`,
|
||||
});
|
||||
|
||||
// The title's right edge must stop short of the first header control.
|
||||
expect(titleBox.x + titleBox.width).toBeLessThanOrEqual(controlsBox.x + 1);
|
||||
});
|
||||
});
|
||||
159
desktop/tests/e2e/message-author-overlap.spec.ts
Normal file
159
desktop/tests/e2e/message-author-overlap.spec.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Regression test: a long message-author display name must truncate instead
|
||||
* of running past the message header into the timeline edge and the hover
|
||||
* action bar. The author button sits inside the UserProfilePopover trigger
|
||||
* wrapper — a flex item whose `min-width: auto` refused to shrink below the
|
||||
* nowrap width of the name, so the button's `truncate` never engaged.
|
||||
*
|
||||
* Run: pnpm build:e2e && pnpm exec playwright test --project=smoke \
|
||||
* tests/e2e/message-author-overlap.spec.ts
|
||||
* Output: test-results/message-author-overlap/
|
||||
*/
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||||
|
||||
const SHOTS = "test-results/message-author-overlap";
|
||||
|
||||
// Long single-word name: no break opportunities, so the nowrap min-content
|
||||
// width far exceeds the header row at a 900px viewport.
|
||||
const LONG_AUTHOR_NAME =
|
||||
"Alexandrina-Wolfeschlegelsteinhausenbergerdorff-Keihanaikukauakahihuliheekahaunaele-The-Magnificent-Third";
|
||||
|
||||
const MESSAGE_CONTENT = "Author-name truncation regression probe";
|
||||
|
||||
test.describe("message author name overflow", () => {
|
||||
test.use({ viewport: { width: 900, height: 700 } });
|
||||
|
||||
test("long author name truncates instead of overflowing the header row", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
mode: "mock",
|
||||
searchProfiles: [
|
||||
{
|
||||
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
displayName: LONG_AUTHOR_NAME,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await page.waitForFunction(
|
||||
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
|
||||
);
|
||||
await page.evaluate(
|
||||
({ pubkey, content }) => {
|
||||
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
||||
channelName: "general",
|
||||
content,
|
||||
pubkey,
|
||||
});
|
||||
},
|
||||
{ content: MESSAGE_CONTENT, pubkey: TEST_IDENTITIES.alice.pubkey },
|
||||
);
|
||||
|
||||
const row = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: MESSAGE_CONTENT });
|
||||
await expect(row).toBeVisible();
|
||||
const author = row.getByTestId("message-author");
|
||||
await expect(author).toHaveText(LONG_AUTHOR_NAME);
|
||||
await waitForAnimations(page);
|
||||
|
||||
// The ellipsis carrier is the popover-trigger <button> around the author
|
||||
// span — `truncate` on the inline span itself cannot clip.
|
||||
const button = author.locator("xpath=ancestor::button[1]");
|
||||
await expect(button).toBeVisible();
|
||||
|
||||
const buttonBox = await button.boundingBox();
|
||||
const headerBox = await row.getByTestId("message-header").boundingBox();
|
||||
if (!buttonBox || !headerBox) {
|
||||
throw new Error("Author button or message header not rendered.");
|
||||
}
|
||||
|
||||
// Guard against a vacuous pass: the author must still render with real
|
||||
// width, and the squeeze must actually engage — the button's ellipsis is
|
||||
// active only when its content overflows. If layout constants or fonts
|
||||
// ever change so the full name fits, this fails loudly instead of
|
||||
// silently no longer testing truncation.
|
||||
expect(buttonBox.width).toBeGreaterThan(0);
|
||||
const isTruncated = await button.evaluate(
|
||||
(element) => element.scrollWidth > element.clientWidth,
|
||||
);
|
||||
expect(isTruncated).toBe(true);
|
||||
|
||||
await page.screenshot({
|
||||
clip: {
|
||||
height: 72,
|
||||
width: 900,
|
||||
x: 0,
|
||||
y: Math.max(0, headerBox.y - 12),
|
||||
},
|
||||
path: `${SHOTS}/01-author-header.png`,
|
||||
});
|
||||
|
||||
// The name's right edge must stay inside the header row instead of
|
||||
// running under the pane edge / hover action bar.
|
||||
expect(buttonBox.x + buttonBox.width).toBeLessThanOrEqual(
|
||||
headerBox.x + headerBox.width + 1,
|
||||
);
|
||||
|
||||
// The action rail (reactions/reply controls) is absolutely positioned OVER
|
||||
// the row and appears on hover and on focus-within. In both states the
|
||||
// author's painted box must end before the rail's left edge — a name that
|
||||
// merely stays inside the header can still be painted over by the rail.
|
||||
const actionBar = row.locator('[data-testid^="message-action-bar-"]');
|
||||
|
||||
const expectAuthorClearOfActionBar = async (state: string) => {
|
||||
// Playwright's toBeVisible() passes at opacity 0; the rail's shown/hidden
|
||||
// states are driven by opacity, so assert the computed style directly.
|
||||
await expect(actionBar).toHaveCSS("opacity", "1");
|
||||
const authorBox = await button.boundingBox();
|
||||
const actionBarBox = await actionBar.boundingBox();
|
||||
if (!authorBox || !actionBarBox) {
|
||||
throw new Error(`Author or action bar not rendered on ${state}.`);
|
||||
}
|
||||
expect(actionBarBox.width).toBeGreaterThan(0);
|
||||
// The squeeze must still be a real ellipsis, not a vacuous fit.
|
||||
const stillTruncated = await button.evaluate(
|
||||
(element) => element.scrollWidth > element.clientWidth,
|
||||
);
|
||||
expect(stillTruncated).toBe(true);
|
||||
expect(authorBox.x + authorBox.width).toBeLessThanOrEqual(actionBarBox.x);
|
||||
};
|
||||
|
||||
// Pointer hover.
|
||||
await row.hover();
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({
|
||||
clip: {
|
||||
height: 72,
|
||||
width: 900,
|
||||
x: 0,
|
||||
y: Math.max(0, headerBox.y - 12),
|
||||
},
|
||||
path: `${SHOTS}/02-author-header-hovered.png`,
|
||||
});
|
||||
await expectAuthorClearOfActionBar("hover");
|
||||
|
||||
// Reset the pointer, then re-reveal the rail via keyboard focus-within.
|
||||
await page.mouse.move(0, 0);
|
||||
await expect(actionBar).toHaveCSS("opacity", "0");
|
||||
await button.focus();
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({
|
||||
clip: {
|
||||
height: 72,
|
||||
width: 900,
|
||||
x: 0,
|
||||
y: Math.max(0, headerBox.y - 12),
|
||||
},
|
||||
path: `${SHOTS}/03-author-header-focused.png`,
|
||||
});
|
||||
await expectAuthorClearOfActionBar("focus-within");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user