mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## 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>
160 lines
5.9 KiB
TypeScript
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");
|
|
});
|
|
});
|