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 before: title text overlaps the header control
icons](https://github.com/user-attachments/assets/d5a10f98-114f-4466-8012-468616b82807)

**Inbox title — after** (truncates with ellipsis, controls stay clear):

![Inbox title after: title truncates with an ellipsis before the
controls](https://github.com/user-attachments/assets/80520f7c-1120-4dbc-929d-1ef5eceb874b)

**Author name — before** (long name runs past the header row edge):

![Author name before: name glyphs bleed past the action
bar](https://github.com/user-attachments/assets/be9743ae-ef51-4db0-8c46-a0649e98f0d5)

**Author name — after** (clean cutoff):

![Author name after: name truncates cleanly inside the header
row](https://github.com/user-attachments/assets/24cc0412-ca99-40c4-add6-13380b3ae065)

🤖 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:
cynfria
2026-09-10 10:00:15 -07:00
committed by GitHub
parent 051c3a270b
commit cec5c8fd92
10 changed files with 398 additions and 8 deletions

View 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);
});
});

View 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");
});
});