Files
buzz/desktop/tests/e2e/message-author-overlap.spec.ts
cynfria cec5c8fd92 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>
2026-09-10 10:00:15 -07:00

160 lines
5.9 KiB
TypeScript

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