import { expect, test } from "@playwright/test"; import { mockLangGraphAPI } from "./utils/mock-api"; test.describe("Sidebar navigation", () => { test("sidebar contains Chats and Agents nav links", async ({ page }) => { mockLangGraphAPI(page); await page.goto("/workspace/chats/new"); // Sidebar uses data-sidebar="menu-button" with asChild rendering on const sidebar = page.locator("[data-sidebar='sidebar']"); await expect(sidebar.locator("a[href='/workspace/chats']")).toBeVisible({ timeout: 15_000, }); await expect(sidebar.locator("a[href='/workspace/agents']")).toBeVisible(); }); test("Agents link navigates to agents page", async ({ page }) => { mockLangGraphAPI(page); await page.goto("/workspace/chats/new"); const sidebar = page.locator("[data-sidebar='sidebar']"); const agentsLink = sidebar.locator("a[href='/workspace/agents']"); await expect(agentsLink).toBeVisible({ timeout: 15_000 }); await agentsLink.click(); await page.waitForURL("**/workspace/agents"); await expect(page).toHaveURL(/\/workspace\/agents/); }); test("Agents button is disabled with a hover tooltip when agents_api is off", async ({ page, }) => { mockLangGraphAPI(page); await page.route("**/api/features", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ agents_api: { enabled: false } }), }), ); await page.goto("/workspace/chats/new"); const sidebar = page.locator("[data-sidebar='sidebar']"); // Chats remains a real link; Agents is no longer a navigable link. await expect(sidebar.locator("a[href='/workspace/chats']")).toBeVisible({ timeout: 15_000, }); await expect(sidebar.locator("a[href='/workspace/agents']")).toHaveCount(0); // The disabled Agents button is rendered and announces its disabled state. const agentsButton = sidebar.getByRole("button", { name: "Agents" }); await expect(agentsButton).toHaveAttribute("aria-disabled", "true"); // The button itself has pointer-events suppressed; force the hover so the // event reaches the wrapping tooltip-trigger span that surfaces the tooltip. await agentsButton.hover({ force: true }); await expect(page.getByText("Feature not enabled").first()).toBeVisible({ timeout: 5_000, }); // Keyboard/screen-reader users get the reason too: the disabled entry // stays in the tab order (focusable) and is wired to a visually-hidden // description rather than relying on the hover-only tooltip. const describedById = await agentsButton.getAttribute("aria-describedby"); expect(describedById).toBeTruthy(); await expect(page.locator(`#${describedById}`)).toHaveText( "Feature not enabled", ); await agentsButton.focus(); await expect(agentsButton).toBeFocused(); }); test("mobile welcome layout stays within viewport and opens sidebar", async ({ page, }) => { await page.setViewportSize({ width: 390, height: 844 }); mockLangGraphAPI(page); await page.goto("/workspace/chats/new"); const viewportWidth = page.viewportSize()?.width ?? 390; const expectInsideViewport = async ( locator: ReturnType, ) => { await expect(locator).toBeVisible({ timeout: 15_000 }); const box = await locator.boundingBox(); expect(box).not.toBeNull(); expect(box!.x).toBeGreaterThanOrEqual(-1); expect(box!.x + box!.width).toBeLessThanOrEqual(viewportWidth + 1); }; await expectInsideViewport(page.getByText(/Welcome to|欢迎使用/).first()); await expectInsideViewport(page.getByRole("textbox").first()); await expectInsideViewport(page.locator("[data-slot='suggestions-list']")); const mobileSidebarTrigger = page .locator("[data-sidebar='trigger']:visible") .first(); await expect(mobileSidebarTrigger).toBeVisible(); await mobileSidebarTrigger.click(); const mobileSidebar = page.locator( "[data-mobile='true'][data-sidebar='sidebar']", ); await expect(mobileSidebar).toBeVisible(); await expect( mobileSidebar.locator("a[href='/workspace/chats']"), ).toBeVisible(); await expect( mobileSidebar.locator("a[href='/workspace/agents']"), ).toBeVisible(); }); });