Files
kandev/apps/web/e2e/tests/chat/clarification-resize.spec.ts
T

168 lines
6.2 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";
/**
* Helper to create a regular task, navigate to it, and wait for idle.
* Mirrors clarification.spec.ts's `seedClarificationTask`, but uses a non-
* blocking scenario so we reach the idle input and can drive the slash
* commands `/ask-single` and `/ask-multiple` from inside the session.
*/
async function seedTaskAndWaitForIdle(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<SessionPage> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
title,
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
if (!task.session_id) throw new Error("createTaskWithAgent did not return a session_id");
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await session.waitForChatIdle();
return session;
}
test.describe("Mock agent clarification slash commands", () => {
test.describe.configure({ retries: 1 });
// Smoke test that the /ask-single alias routes to the clarification scenario.
// The underlying clarification behaviour (option click, multi-question carousel,
// submit, etc.) is exhaustively covered by clarification.spec.ts — this only
// proves the alias is wired up.
test("/ask-single alias triggers the clarification overlay", async ({
testPage,
apiClient,
seedData,
}) => {
const session = await seedTaskAndWaitForIdle(testPage, apiClient, seedData, "Ask Single Alias");
await session.sendMessage("/ask-single");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
await session.clarificationOption("PostgreSQL").click();
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
});
test("/ask-multiple alias triggers the multi-question carousel", async ({
testPage,
apiClient,
seedData,
}) => {
const session = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"Ask Multiple Alias",
);
await session.sendMessage("/ask-multiple");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
await expect(session.clarificationSteps()).toHaveCount(3);
await expect(session.clarificationOverlay()).toContainText("Which database");
await session.clarificationOption("PostgreSQL").click();
await session.clarificationOption("Go").click();
await session.clarificationOption("Docker").click();
await session.clarificationSubmit().click();
await expect(session.clarificationOverlay()).not.toBeVisible({ timeout: 30_000 });
await expect(session.idleInput()).toBeVisible({ timeout: 30_000 });
});
});
test.describe("Clarification overlay resizable layout", () => {
test("starts content-sized, drag grows the overlay, double-click resets", async ({
testPage,
apiClient,
seedData,
}) => {
// Tall viewport so the content-sized overlay has plenty of room to grow
// without bumping into the 50vh safety cap on the first drag.
await testPage.setViewportSize({ width: 1280, height: 1000 });
const session = await seedTaskAndWaitForIdle(
testPage,
apiClient,
seedData,
"Clarification Resize",
);
await session.sendMessage("/ask-multiple");
await expect(session.clarificationOverlay()).toBeVisible({ timeout: 30_000 });
const container = testPage.getByTestId("clarification-overlay-container");
await expect(container).toBeVisible();
const initial = await container.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
overflowY: computed.overflowY,
height: el.getBoundingClientRect().height,
// Inline style is what disambiguates "auto-sized" from "user-dragged".
inlineHeight: (el as HTMLElement).style.height,
};
});
// overflow-y: scroll reserves a permanent scrollbar gutter so users
// discover scrollable content (and the in-overlay Submit) without having
// to wait for macOS auto-hide scrollbars to fade in on hover.
expect(initial.overflowY).toBe("scroll");
// Default state: no inline height → container sizes to its content.
expect(initial.inlineHeight).toBe("");
// Sanity check: content-sized overlay is at least tall enough for the
// question card but well under the 50vh cap.
expect(initial.height).toBeGreaterThan(200);
expect(initial.height).toBeLessThan(1000 * 0.5);
const handle = container.locator("xpath=..").locator("button[aria-label='Resize']");
await expect(handle).toBeVisible();
// Drag the handle upward by 120px → overlay should grow by ~120px.
const handleBox = await handle.boundingBox();
expect(handleBox).not.toBeNull();
const dragDistance = 120;
const startX = handleBox!.x + handleBox!.width / 2;
const startY = handleBox!.y + handleBox!.height / 2;
await testPage.mouse.move(startX, startY);
await testPage.mouse.down();
await testPage.mouse.move(startX, startY - dragDistance, { steps: 10 });
await testPage.mouse.up();
const afterDrag = await container.evaluate((el) => ({
height: el.getBoundingClientRect().height,
inlineHeight: (el as HTMLElement).style.height,
}));
// Drag flipped the container to an explicit pixel height.
expect(afterDrag.inlineHeight).toMatch(/^\d+(\.\d+)?px$/);
// Allow ±10px tolerance for rounding / mouse event coalescing.
expect(afterDrag.height).toBeGreaterThan(initial.height + dragDistance - 10);
// Double-click the handle → back to auto-sized.
await handle.dblclick();
const afterReset = await container.evaluate((el) => ({
height: el.getBoundingClientRect().height,
inlineHeight: (el as HTMLElement).style.height,
}));
expect(afterReset.inlineHeight).toBe("");
expect(Math.abs(afterReset.height - initial.height)).toBeLessThanOrEqual(2);
});
});