Files
kandev/apps/web/e2e/tests/chat/toolbar-overflow.spec.ts
T

150 lines
5.7 KiB
TypeScript

import { type Page } from "@playwright/test";
import { test, expect } from "../../fixtures/test-base";
import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import { SessionPage } from "../../pages/session-page";
/**
* Seed a task + session and navigate to the session page.
* Waits for the mock agent to finish (idle input visible).
*/
async function seedAndNavigate(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
): Promise<SessionPage> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Toolbar Overflow Test",
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();
// waitForChatIdle (not a raw idleInput wait) rides out the WS-subscribe race:
// the mock agent can settle RUNNING->WAITING_FOR_INPUT before the client's WS
// subscription registers, so the idle placeholder never renders without a reload.
await session.waitForChatIdle({ timeout: 30_000 });
return session;
}
/** Force the toolbar container to a specific max-width via inline style. */
async function constrainToolbar(page: Page, maxWidth: string | null) {
await page.evaluate((mw) => {
const el = document.querySelector('[data-testid="chat-input-toolbar"]') as HTMLElement;
if (!el) return;
if (mw) {
el.style.maxWidth = mw;
} else {
el.style.removeProperty("max-width");
}
}, maxWidth);
// Allow ResizeObserver to fire
await page.waitForTimeout(200);
}
test.describe("Toolbar overflow menu", () => {
test.describe.configure({ retries: 1 });
test("collapses toolbar items and expands inline when toggled", async ({
testPage,
apiClient,
seedData,
}) => {
await seedAndNavigate(testPage, apiClient, seedData);
const toolbar = testPage.getByTestId("chat-input-toolbar");
await expect(toolbar).toBeVisible({ timeout: 10_000 });
// At default width, collapsible items should be visible inline
const modelItem = testPage.getByTestId("toolbar-item-model");
const mcpItem = testPage.getByTestId("toolbar-item-mcp");
const overflowBtn = testPage.getByTestId("toolbar-overflow-menu");
await expect(modelItem).toBeVisible({ timeout: 5_000 });
await expect(mcpItem).toBeVisible({ timeout: 5_000 });
await expect(overflowBtn).not.toBeVisible();
// Check context badge visibility: add a context item via the popover
const contextBtn = toolbar.locator("button", { has: testPage.locator("svg.tabler-icon-at") });
await contextBtn.click();
const searchInput = testPage.getByPlaceholder("Search files and prompts...");
await expect(searchInput).toBeVisible({ timeout: 5_000 });
// Toggle the Plan context checkbox to get contextCount > 0
const planCheckbox = testPage.getByText("Plan", { exact: true });
await planCheckbox.click();
// Close the popover
await testPage.locator("body").click({ position: { x: 10, y: 10 } });
// Badge should be visible at full width
const contextBadge = contextBtn.locator("span.rounded-full");
await expect(contextBadge).toBeVisible({ timeout: 3_000 });
// Constrain toolbar to a narrow width to force overflow
await constrainToolbar(testPage, "300px");
// Collapsible items should disappear and expand toggle should appear
await expect(overflowBtn).toBeVisible({ timeout: 5_000 });
await expect(modelItem).not.toBeVisible();
// Context badge should be hidden when collapsed to avoid clipping
await expect(contextBadge).not.toBeVisible();
// Submit button should remain visible (always-visible item). Target the
// submit testid specifically — the voice input button is also round, so a
// bare `button.rounded-full` locator now matches both and fails strict mode.
const submitBtn = toolbar.getByTestId("submit-message-button");
await expect(submitBtn).toBeVisible();
// Click expand toggle — items appear inline (scrollable)
await overflowBtn.click();
await expect(modelItem).toBeVisible({ timeout: 5_000 });
await expect(mcpItem).toBeVisible({ timeout: 5_000 });
// Collapse button should still be visible to toggle back
await expect(overflowBtn).toBeVisible();
// Click again to collapse back
await overflowBtn.click();
await expect(modelItem).not.toBeVisible({ timeout: 5_000 });
// Remove constraint — items should reappear inline normally
await constrainToolbar(testPage, null);
await expect(modelItem).toBeVisible({ timeout: 5_000 });
await expect(mcpItem).toBeVisible({ timeout: 5_000 });
await expect(overflowBtn).not.toBeVisible();
});
test("context popover opens when @ button is clicked", async ({
testPage,
apiClient,
seedData,
}) => {
await seedAndNavigate(testPage, apiClient, seedData);
const toolbar = testPage.getByTestId("chat-input-toolbar");
await expect(toolbar).toBeVisible({ timeout: 10_000 });
// Find the @ context button and click it
const contextBtn = toolbar.locator("button", { has: testPage.locator("svg.tabler-icon-at") });
await expect(contextBtn).toBeVisible({ timeout: 5_000 });
await contextBtn.click();
// The context popover should open with "Context" header and search input
const popoverContent = testPage.getByText("Select files and prompts to include");
await expect(popoverContent).toBeVisible({ timeout: 5_000 });
const searchInput = testPage.getByPlaceholder("Search files and prompts...");
await expect(searchInput).toBeVisible();
});
});