test(desktop): cover inbox agent header alignment

Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-08-26 17:54:40 +10:00
parent 5b6e3737af
commit 235bae0cc2
2 changed files with 250 additions and 67 deletions

View File

@@ -236,7 +236,7 @@ export function InboxMessageRow({
start of a line on its own. Gap matches the container's, so
spacing reads the same either side of the divider.
*/}
<span className="inline-flex min-w-0 items-baseline gap-x-2">
<span className="inline-flex min-w-0 items-center gap-x-2">
<MessageMetaSeparator />
{timestampNode}
</span>

View File

@@ -1,7 +1,8 @@
import { readFileSync } from "node:fs";
import { expect, test, type Locator } from "@playwright/test";
import { expect, test, type Locator, type Page } from "@playwright/test";
import type { RelayEvent } from "../../src/shared/api/types";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import { expectCornerRadiusPx, expectSmoothCorners } from "../helpers/css";
@@ -49,16 +50,230 @@ async function expectThreadReplyUnobscured(row: Locator) {
.toBe(true);
}
async function measureInlineBaseline(locator: Locator) {
return locator.evaluate((element) => {
const marker = document.createElement("span");
marker.style.cssText =
"display:inline-block;width:0;height:0;margin:0;padding:0;border:0";
element.append(marker);
const baseline = marker.getBoundingClientRect().top;
marker.remove();
return baseline;
const FONT_SIZES = ["smaller", "default", "larger"] as const;
const GEOMETRY_TOLERANCE_PX = 0.25;
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
type MockInboxFeedItem = Pick<
RelayEvent,
"content" | "created_at" | "id" | "kind" | "pubkey" | "tags"
> & {
category: "mention";
channel_id: string;
channel_name: string;
};
type MockInboxWindow = Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
id?: string;
mentionPubkeys?: string[];
pubkey?: string;
}) => RelayEvent;
__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: MockInboxFeedItem) => unknown;
};
async function waitForStableAgentHeader(row: Locator, timestampTestId: string) {
await row.evaluate(async (element, timestampId) => {
await document.fonts.ready;
const targets = [
element.querySelector<HTMLElement>('[data-testid="message-author"]'),
element.querySelector<HTMLElement>('[data-testid="message-agent-owner"]'),
element.querySelector<HTMLElement>(`[data-testid="${timestampId}"]`),
];
if (targets.some((target) => !target)) {
throw new Error("Expected a complete agent message header.");
}
await new Promise<void>((resolve, reject) => {
let previousSignature: string | null = null;
let stableFrameCount = 0;
let sampledFrameCount = 0;
const sample = () => {
const signature = targets
.map((target) => {
const rect = target?.getBoundingClientRect();
return rect
? [rect.x, rect.y, rect.width, rect.height]
.map((value) => value.toFixed(3))
.join(",")
: "missing";
})
.join("|");
stableFrameCount =
signature === previousSignature ? stableFrameCount + 1 : 0;
sampledFrameCount += 1;
previousSignature = signature;
if (stableFrameCount >= 2) {
resolve();
} else if (sampledFrameCount >= 120) {
reject(new Error("Agent message header layout did not stabilize."));
} else {
requestAnimationFrame(sample);
}
};
requestAnimationFrame(sample);
});
}, timestampTestId);
}
async function measureAgentHeaderGeometry(
row: Locator,
timestampTestId: string,
) {
return row.evaluate((element, timestampId) => {
const author = element.querySelector<HTMLElement>(
'[data-testid="message-author"]',
);
const ownerTreatment = element.querySelector<HTMLElement>(
'[data-testid="message-agent-owner"]',
);
const managedBy = Array.from(
ownerTreatment?.querySelectorAll<HTMLElement>('[aria-hidden="true"]') ??
[],
).find((candidate) => candidate.textContent?.trim() === "managed by");
const owner = ownerTreatment?.querySelector<HTMLElement>(".font-semibold");
const icon = ownerTreatment?.querySelector<SVGElement>("svg");
const timestamp = element.querySelector<HTMLElement>(
`[data-testid="${timestampId}"]`,
);
if (
!author ||
!ownerTreatment ||
!managedBy ||
!owner ||
!icon ||
!timestamp
) {
throw new Error("Expected measurable agent message header geometry.");
}
const ownerStyle = getComputedStyle(ownerTreatment);
const timestampStyle = getComputedStyle(timestamp);
const ownerRect = ownerTreatment.getBoundingClientRect();
const iconRect = icon.getBoundingClientRect();
const iconCenterOffset =
iconRect.top +
iconRect.height / 2 -
(ownerRect.top + ownerRect.height / 2);
const baselineElements = [author, managedBy, owner, timestamp];
const markers = baselineElements.map((baselineElement) => {
const marker = document.createElement("span");
marker.style.cssText =
"display:inline-block;width:0;height:0;margin:0;padding:0;border:0";
baselineElement.append(marker);
return marker;
});
try {
const [
authorBaseline,
managedByBaseline,
ownerBaseline,
timestampBaseline,
] = markers.map((marker) => marker.getBoundingClientRect().top);
return {
baselines: {
author: authorBaseline,
managedBy: managedByBaseline,
owner: ownerBaseline,
timestamp: timestampBaseline,
},
iconCenterOffset,
ownerTypography: {
fontSize: ownerStyle.fontSize,
lineHeight: ownerStyle.lineHeight,
},
timestampTypography: {
fontSize: timestampStyle.fontSize,
lineHeight: timestampStyle.lineHeight,
},
};
} finally {
for (const marker of markers) marker.remove();
}
}, timestampTestId);
}
async function expectAlignedAgentHeader(
page: Page,
row: Locator,
timestampTestId: string,
surface: string,
) {
for (const fontSize of FONT_SIZES) {
await page.evaluate((value) => {
document.documentElement.dataset.fontSize = value;
}, fontSize);
await waitForStableAgentHeader(row, timestampTestId);
const geometry = await measureAgentHeaderGeometry(row, timestampTestId);
for (const [label, baseline] of Object.entries(geometry.baselines)) {
expect(
Math.abs(baseline - geometry.baselines.author),
`${surface} ${label} baseline at ${fontSize} font size`,
).toBeLessThanOrEqual(GEOMETRY_TOLERANCE_PX);
}
expect(geometry.ownerTypography).toEqual(geometry.timestampTypography);
expect(
Math.abs(geometry.iconCenterOffset),
`${surface} agent icon center at ${fontSize} font size`,
).toBeLessThanOrEqual(GEOMETRY_TOLERANCE_PX);
}
}
async function seedAgentInboxMessage(page: Page) {
await page.waitForFunction(() => {
const win = window as MockInboxWindow;
return (
typeof win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" &&
typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function"
);
});
return page.evaluate(
({ channelId, currentPubkey, senderPubkey }) => {
const win = window as MockInboxWindow;
const emitMessage = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__;
const pushFeedItem = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__;
if (!emitMessage || !pushFeedItem) {
throw new Error("Mock Inbox helpers are unavailable.");
}
const message = emitMessage({
channelName: "general",
content: "Agent Inbox alignment check.",
id: "a9".repeat(32),
mentionPubkeys: [currentPubkey],
pubkey: senderPubkey,
});
pushFeedItem({
category: "mention",
channel_id: channelId,
channel_name: "general",
content: message.content,
created_at: message.created_at,
id: message.id,
kind: message.kind,
pubkey: message.pubkey,
tags: message.tags,
});
return message.id;
},
{
channelId: GENERAL_CHANNEL_ID,
currentPubkey: TEST_IDENTITIES.tyler.pubkey,
senderPubkey: TEST_IDENTITIES.alice.pubkey,
},
);
}
async function measureThreadSummaryGeometry(summaryRow: Locator) {
@@ -390,7 +605,7 @@ test.beforeEach(async ({ page }, testInfo) => {
await installMockBridge(page, mock);
});
test("agent owner label identifies the agent and owner", async ({ page }) => {
test("agent owner label aligns in the timeline and Inbox", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
@@ -409,63 +624,31 @@ test("agent owner label identifies the agent and owner", async ({ page }) => {
"Agent managed by",
);
const author = aliceMessage.getByTestId("message-author");
const managedBy = ownerTreatment.getByText("managed by", { exact: true });
const owner = ownerTreatment.locator(".font-semibold");
const timestamp = aliceMessage.getByTestId("message-timestamp");
await expectAlignedAgentHeader(
page,
aliceMessage,
"message-timestamp",
"timeline",
);
for (const fontSize of ["smaller", "default", "larger"]) {
await page.evaluate((value) => {
document.documentElement.dataset.fontSize = value;
}, fontSize);
await page.goto("/");
await expect(page.getByTestId("home-inbox-list")).toBeVisible();
const inboxMessageId = await seedAgentInboxMessage(page);
const [
authorBaseline,
managedByBaseline,
ownerBaseline,
timestampBaseline,
] = await Promise.all([
measureInlineBaseline(author),
measureInlineBaseline(managedBy),
measureInlineBaseline(owner),
measureInlineBaseline(timestamp),
]);
const [ownerTypography, timestampTypography] = await Promise.all([
ownerTreatment.evaluate((element) => {
const style = getComputedStyle(element);
return { fontSize: style.fontSize, lineHeight: style.lineHeight };
}),
timestamp.evaluate((element) => {
const style = getComputedStyle(element);
return { fontSize: style.fontSize, lineHeight: style.lineHeight };
}),
]);
const iconCenterOffset = await ownerTreatment.evaluate((element) => {
const icon = element.querySelector("svg");
if (!icon) throw new Error("Expected the agent owner icon.");
const ownerRect = element.getBoundingClientRect();
const iconRect = icon.getBoundingClientRect();
return (
iconRect.top +
iconRect.height / 2 -
(ownerRect.top + ownerRect.height / 2)
);
});
expect(
{ managedByBaseline, ownerBaseline, timestampBaseline },
`message header baselines at ${fontSize} font size`,
).toEqual({
managedByBaseline: authorBaseline,
ownerBaseline: authorBaseline,
timestampBaseline: authorBaseline,
});
expect(ownerTypography).toEqual(timestampTypography);
expect(
Math.abs(iconCenterOffset),
`agent icon center at ${fontSize} font size`,
).toBeLessThan(0.01);
}
await page.getByTestId(`home-inbox-item-${inboxMessageId}`).click();
const inboxMessage = page.getByTestId("home-inbox-selected-message");
await expect(inboxMessage).toContainText("Agent Inbox alignment check.");
const inboxOwnerTreatment = inboxMessage.getByTestId("message-agent-owner");
await expect(
inboxOwnerTreatment.getByText("managed by", { exact: true }),
).toBeVisible();
await expect(inboxOwnerTreatment.locator(".font-semibold")).toHaveText("bob");
await expectAlignedAgentHeader(
page,
inboxMessage,
"inbox-message-timestamp",
"Inbox",
);
});
test("send a message and see it in timeline", async ({ page }) => {