From 9da896bc86fd12f043927021ec7d5dfd797c1633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Thu, 9 Jul 2026 08:14:25 +0100 Subject: [PATCH] fix: truncate long task titles in topbar (#1630) --- .../task/mobile/session-mobile-top-bar.tsx | 2 +- apps/web/components/task/task-top-bar.tsx | 10 +- apps/web/e2e/helpers/topbar-fixtures.ts | 2 + .../layout/task-topbar-long-title.spec.ts | 85 +++++++++++++++++ .../mobile-task-topbar-long-title.spec.ts | 91 +++++++++++++++++++ 5 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 apps/web/e2e/helpers/topbar-fixtures.ts create mode 100644 apps/web/e2e/tests/layout/task-topbar-long-title.spec.ts create mode 100644 apps/web/e2e/tests/task/mobile-task-topbar-long-title.spec.ts diff --git a/apps/web/components/task/mobile/session-mobile-top-bar.tsx b/apps/web/components/task/mobile/session-mobile-top-bar.tsx index 57987b9bd4..3c0453772c 100644 --- a/apps/web/components/task/mobile/session-mobile-top-bar.tsx +++ b/apps/web/components/task/mobile/session-mobile-top-bar.tsx @@ -113,7 +113,7 @@ function MobileTopBarActions({ onMenuClick, }: MobileTopBarActionsProps) { return ( -
+
{isRemoteExecutor && ( - - - +
+ + + - + {taskTitle ?? "Task details"} diff --git a/apps/web/e2e/helpers/topbar-fixtures.ts b/apps/web/e2e/helpers/topbar-fixtures.ts new file mode 100644 index 0000000000..add12de066 --- /dev/null +++ b/apps/web/e2e/helpers/topbar-fixtures.ts @@ -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"'; diff --git a/apps/web/e2e/tests/layout/task-topbar-long-title.spec.ts b/apps/web/e2e/tests/layout/task-topbar-long-title.spec.ts new file mode 100644 index 0000000000..7b92e6647f --- /dev/null +++ b/apps/web/e2e/tests/layout/task-topbar-long-title.spec.ts @@ -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 { + 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); + }); +}); diff --git a/apps/web/e2e/tests/task/mobile-task-topbar-long-title.spec.ts b/apps/web/e2e/tests/task/mobile-task-topbar-long-title.spec.ts new file mode 100644 index 0000000000..9318e17fe4 --- /dev/null +++ b/apps/web/e2e/tests/task/mobile-task-topbar-long-title.spec.ts @@ -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 { + 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); + }); +});