agent-orchestrator/frontend/e2e/titlebar-brand.spec.ts

83 lines
3.6 KiB
TypeScript

import { expect, test, type Locator, type Page } from "@playwright/test";
// Regression guard for #366 (macOS): the sidebar's "Agent Orchestrator" brand
// must never sit under the fixed TitlebarNav cluster, and the wordmark must stay
// readable. The original bug was board routes (`/` and `/projects/:id`) having no
// topbar, so the sidebar stayed at top-0 and the brand landed in the cluster's
// 56px lane. It is now fixed structurally — the shell renders the topbar on every
// route, so the sidebar always hangs below the titlebar band — and these tests
// lock that invariant in: if a topbar-less route is ever reintroduced, they fail.
//
// macOS-only: TitlebarNav (and the bug) gate on navigator.userAgent looking like
// a Mac, read once at module load. Force a Mac UA so this is deterministic
// regardless of the host/CI OS.
test.use({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
});
const brand = (page: Page) => page.getByText("Agent Orchestrator", { exact: true });
// Two boxes overlap iff they intersect on both axes.
function overlaps(a: { x: number; y: number; width: number; height: number }, b: typeof a) {
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
}
// The brand <span> has `truncate` (overflow:hidden), so it stays "visible" even
// when clipped to nothing. Compare scroll vs client width to prove the wordmark
// is actually fully rendered, not just present-but-clipped.
async function isTruncated(span: Locator) {
return span.evaluate((el) => el.scrollWidth > el.clientWidth + 1);
}
async function expectBrandClearsCluster(page: Page) {
const cluster = page.locator(".titlebar-nav");
await expect(cluster).toBeVisible();
const span = brand(page);
await expect(span).toBeVisible();
const clusterBox = await cluster.boundingBox();
const brandBox = await span.boundingBox();
expect(clusterBox).not.toBeNull();
expect(brandBox).not.toBeNull();
expect(overlaps(brandBox!, clusterBox!)).toBe(false);
expect(await isTruncated(span)).toBe(false);
}
test("home board route: brand clears the macOS titlebar cluster and stays readable", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Projects")).toBeVisible();
await expectBrandClearsCluster(page);
});
test("project board route: brand clears the macOS titlebar cluster and stays readable", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Projects")).toBeVisible();
// In-app nav to /projects/:id (a hard load boots the router at the board).
await page.getByRole("button", { name: "Open api-gateway dashboard" }).click();
// The active project row marks itself aria-current=page once navigation lands.
await expect(page.locator('[aria-current="page"]')).toBeVisible();
await expectBrandClearsCluster(page);
});
test("brand stays put and readable when navigating board → session", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Projects")).toBeVisible();
const boardBrandBox = await brand(page).boundingBox();
expect(boardBrandBox).not.toBeNull();
await page.getByRole("button", { name: "Open Split terminal mux responsibilities" }).click();
await expect(page.locator(".dashboard-app-header")).toBeVisible();
const sessionBrandBox = await brand(page).boundingBox();
expect(sessionBrandBox).not.toBeNull();
// Persistent shell element: no vertical/horizontal jump across the transition.
expect(Math.abs(sessionBrandBox!.x - boardBrandBox!.x)).toBeLessThanOrEqual(1);
expect(Math.abs(sessionBrandBox!.y - boardBrandBox!.y)).toBeLessThanOrEqual(1);
await expectBrandClearsCluster(page);
});