mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-29 09:26:00 +00:00
* feat(gateway): add GET /api/features for frontend feature gating (#3757) * feat(agents): add useAgentsApiEnabled feature-flag hook (#3757) * feat(agents): gate /workspace/agents segment on agents_api flag (#3757) * feat(sidebar): grey out Agents button with tooltip when agents_api disabled (#3757) * fix(sidebar): show disabled Agents tooltip on hover, harden a11y + e2e (#3757) - Wrap the disabled Agents button in a hoverable tooltip trigger so the 'feature not enabled' hint shows in the expanded sidebar, not only when collapsed (the SidebarMenuButton tooltip prop is hidden unless collapsed). - Add tabIndex={-1} and drop the redundant onClick preventDefault. - Add e2e coverage for the disabled state + a default /api/features mock. * style(sidebar): move cursor-not-allowed to the hoverable span (#3757) * test(agents): anchor e2e agents-API request filter with a path regex (#3757) * fix(agents): don't expose backend config in disabled message; tell user to contact admin (#3757) The disabled panel previously said 'Set agents_api.enabled: true in config.yaml', leaking backend configuration to end users. Replace with a generic 'not enabled on this server, contact your administrator' message (matching the existing nameStepApiDisabledError copy). e2e now asserts the contact-admin message and that no config.yaml/agents_api text is rendered. * make format * fix(agents): keep agents_api flag sticky during /api/features outage (#3757) Failing open re-mounted the agents UI and re-triggered the 403 storm when agents_api was genuinely disabled and /api/features was down. Persist the last definitive answer and fall back to it (sticky) before failing open, only failing open when nothing has ever been observed. Read the cached value after mount so the first client render matches the server (no hydration mismatch on the non-loading-gated sidebar). * fix(sidebar): make disabled Agents entry keyboard/SR accessible (#3757) The disabled-state explanation was hover-only: tabIndex={-1} removed the entry from the tab order and the reason lived only in a pointer-triggered tooltip. Keep it in the tab order and wire aria-describedby to a visually-hidden reason so keyboard and screen-reader users learn why it is disabled. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
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 <Link>
|
|
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<typeof page.locator>,
|
|
) => {
|
|
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();
|
|
});
|
|
});
|