fix: truncate long task titles in topbar (#1630)
This commit is contained in:
@@ -113,7 +113,7 @@ function MobileTopBarActions({
|
||||
onMenuClick,
|
||||
}: MobileTopBarActionsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1" data-testid="mobile-topbar-actions">
|
||||
<MobileRepoPill taskId={taskId ?? null} workspaceId={workspaceId ?? null} />
|
||||
{isRemoteExecutor && (
|
||||
<RemoteCloudTooltip
|
||||
|
||||
@@ -158,13 +158,13 @@ function TopBarLeft({
|
||||
}: TopBarLeftProps) {
|
||||
const showExecutorSettings = shouldShowExecutorEnvironmentControls(remoteExecutorType);
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 min-w-0 overflow-hidden">
|
||||
<Breadcrumb className="min-w-0">
|
||||
<BreadcrumbList className="flex-nowrap text-sm min-w-0">
|
||||
<BreadcrumbItem className="min-w-0">
|
||||
<div className="flex min-w-0 max-w-[min(44rem,45vw)] items-center gap-2.5 overflow-hidden">
|
||||
<Breadcrumb className="min-w-0 max-w-full">
|
||||
<BreadcrumbList className="min-w-0 max-w-full flex-nowrap text-sm">
|
||||
<BreadcrumbItem className="min-w-0 max-w-full">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<BreadcrumbPage className="font-medium truncate">
|
||||
<BreadcrumbPage className="block max-w-full truncate font-medium">
|
||||
{taskTitle ?? "Task details"}
|
||||
</BreadcrumbPage>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const LONG_TASK_TITLE =
|
||||
'Issue #1625: [Bug]: Config-session MCP advertises list_executors but backend returns "Unknown action: mcp.list_executors"';
|
||||
@@ -0,0 +1,85 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { LONG_TASK_TITLE } from "../../helpers/topbar-fixtures";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
type DesktopTopbarMetrics = {
|
||||
documentClientWidth: number;
|
||||
documentScrollWidth: number;
|
||||
stepperLeft: number;
|
||||
titleClientWidth: number;
|
||||
titleRight: number;
|
||||
titleScrollWidth: number;
|
||||
titleWidth: number;
|
||||
toolsRight: number;
|
||||
topbarRight: number;
|
||||
topbarWidth: number;
|
||||
};
|
||||
|
||||
async function readDesktopTopbarMetrics(page: Page): Promise<DesktopTopbarMetrics | null> {
|
||||
return page.evaluate(() => {
|
||||
const topbar = document.querySelector('[data-testid="task-topbar"]') as HTMLElement | null;
|
||||
const title = topbar?.querySelector('[aria-current="page"]') as HTMLElement | null;
|
||||
const stepper = topbar?.querySelector('[data-testid="workflow-stepper"]') as HTMLElement | null;
|
||||
const tools = topbar?.querySelector('[aria-label="Task tools"]') as HTMLElement | null;
|
||||
if (!topbar || !title || !stepper || !tools) return null;
|
||||
|
||||
const topbarRect = topbar.getBoundingClientRect();
|
||||
const titleRect = title.getBoundingClientRect();
|
||||
const stepperRect = stepper.getBoundingClientRect();
|
||||
const toolsRect = tools.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
documentClientWidth: document.documentElement.clientWidth,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
stepperLeft: stepperRect.left,
|
||||
titleClientWidth: title.clientWidth,
|
||||
titleRight: titleRect.right,
|
||||
titleScrollWidth: title.scrollWidth,
|
||||
titleWidth: titleRect.width,
|
||||
toolsRight: toolsRect.right,
|
||||
topbarRight: topbarRect.right,
|
||||
topbarWidth: topbarRect.width,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("Task topbar long title layout", () => {
|
||||
test("truncates long task titles without overlapping workflow or tool controls", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
LONG_TASK_TITLE,
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
const title = testPage.locator('[data-testid="task-topbar"] [aria-current="page"]');
|
||||
await expect(title).toHaveText(LONG_TASK_TITLE, { timeout: 10_000 });
|
||||
await expect(testPage.getByTestId("layout-preset-trigger")).toBeVisible();
|
||||
|
||||
const metrics = await readDesktopTopbarMetrics(testPage);
|
||||
expect(metrics).not.toBeNull();
|
||||
if (!metrics) return;
|
||||
|
||||
expect(metrics.documentScrollWidth).toBeLessThanOrEqual(metrics.documentClientWidth + 1);
|
||||
expect(metrics.titleScrollWidth).toBeGreaterThan(metrics.titleClientWidth + 8);
|
||||
expect(metrics.titleWidth).toBeLessThanOrEqual(metrics.topbarWidth * 0.6);
|
||||
expect(metrics.titleRight).toBeLessThanOrEqual(metrics.stepperLeft + 1);
|
||||
expect(metrics.toolsRight).toBeLessThanOrEqual(metrics.topbarRight + 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { LONG_TASK_TITLE } from "../../helpers/topbar-fixtures";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
type MobileTopbarMetrics = {
|
||||
actionsLeft: number;
|
||||
actionsRight: number;
|
||||
documentClientWidth: number;
|
||||
documentScrollWidth: number;
|
||||
headerLeft: number;
|
||||
headerRight: number;
|
||||
titleClientWidth: number;
|
||||
titleRight: number;
|
||||
titleScrollWidth: number;
|
||||
};
|
||||
|
||||
async function readMobileTopbarMetrics(
|
||||
page: Page,
|
||||
taskTitle: string,
|
||||
): Promise<MobileTopbarMetrics | null> {
|
||||
return page.evaluate((titleText) => {
|
||||
const header = Array.from(document.querySelectorAll("header")).find((node) =>
|
||||
node.textContent?.includes(titleText),
|
||||
) as HTMLElement | undefined;
|
||||
const title = Array.from(header?.querySelectorAll("span") ?? []).find(
|
||||
(node) => node.textContent?.trim() === titleText,
|
||||
) as HTMLElement | undefined;
|
||||
const actions = header?.querySelector('[data-testid="mobile-topbar-actions"]') as
|
||||
| HTMLElement
|
||||
| null
|
||||
| undefined;
|
||||
if (!header || !title || !actions) return null;
|
||||
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
const titleRect = title.getBoundingClientRect();
|
||||
const actionsRect = actions.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
actionsLeft: actionsRect.left,
|
||||
actionsRight: actionsRect.right,
|
||||
documentClientWidth: document.documentElement.clientWidth,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
headerLeft: headerRect.left,
|
||||
headerRight: headerRect.right,
|
||||
titleClientWidth: title.clientWidth,
|
||||
titleRight: titleRect.right,
|
||||
titleScrollWidth: title.scrollWidth,
|
||||
};
|
||||
}, taskTitle);
|
||||
}
|
||||
|
||||
test.describe("Mobile task topbar long title layout", () => {
|
||||
test("truncates long task titles without pushing mobile actions off-screen", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
LONG_TASK_TITLE,
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
const header = testPage.locator("header").filter({ hasText: LONG_TASK_TITLE }).first();
|
||||
await expect(header).toBeVisible({ timeout: 10_000 });
|
||||
await expect(testPage.getByTestId("mobile-session-menu")).toBeVisible();
|
||||
|
||||
const metrics = await readMobileTopbarMetrics(testPage, LONG_TASK_TITLE);
|
||||
expect(metrics).not.toBeNull();
|
||||
if (!metrics) return;
|
||||
|
||||
expect(metrics.documentScrollWidth).toBeLessThanOrEqual(metrics.documentClientWidth + 1);
|
||||
expect(metrics.titleScrollWidth).toBeGreaterThan(metrics.titleClientWidth + 8);
|
||||
expect(metrics.titleRight).toBeLessThanOrEqual(metrics.actionsLeft + 1);
|
||||
expect(metrics.headerLeft).toBeGreaterThanOrEqual(0);
|
||||
expect(metrics.actionsRight).toBeLessThanOrEqual(metrics.headerRight + 1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user