Files
kandev/apps/web/e2e/tests/task/mobile-plan-formatting-toolbar.spec.ts
T
Clément Fiere 2a29104412 fix: harden prompt history and saved prompt aliases (#3369)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
Co-authored-by: Henry <nova28@users.noreply.github.com>
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: João Salavisa <joao.salavisa@gmail.com>
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-09-06 11:34:48 +01:00

211 lines
8.4 KiB
TypeScript

// Filename starts with "mobile-" so this runs under the mobile-chrome project.
import { type Page } from "@playwright/test";
import { test, expect } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
import type { SeedData } from "../../fixtures/test-base";
import { planScript } from "../../helpers/seed-session-messages";
import { SessionPage } from "../../pages/session-page";
const PLAN_TEXT = "Select this mobile plan text";
const FINAL_LINE = "Reachable final mobile plan line";
const PLAN_CONTENT = [
"## Mobile formatting",
PLAN_TEXT,
...Array.from({ length: 40 }, (_, index) => `Mobile detail ${index + 1}`),
FINAL_LINE,
].join("\n\n");
async function seedMobilePlan(
testPage: Page,
apiClient: ApiClient,
seedData: SeedData,
): Promise<SessionPage> {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Mobile Plan formatting toolbar",
seedData.agentProfileId,
{
description: planScript(PLAN_CONTENT),
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();
await expect.poll(() => apiClient.getTaskPlan(task.id), { timeout: 30_000 }).not.toBeNull();
await session.waitForChatIdle({ timeout: 45_000 });
await session.togglePlanMode();
await testPage.getByRole("navigation").getByRole("button", { name: "Plan", exact: true }).tap();
await expect(session.planPanel).toBeVisible({ timeout: 10_000 });
await expect(session.planPanel).toContainText(PLAN_TEXT, { timeout: 15_000 });
return session;
}
async function simulateKeyboardOpen(testPage: Page, height: number): Promise<void> {
await testPage.evaluate((px) => {
const vv = window.visualViewport;
if (!vv) return;
Object.defineProperty(vv, "height", { configurable: true, value: window.innerHeight - px });
vv.dispatchEvent(new Event("resize"));
}, height);
}
async function simulateViewportScroll(testPage: Page, offsetTop: number): Promise<void> {
await testPage.evaluate((y) => {
const vv = window.visualViewport;
if (!vv) return;
Object.defineProperty(vv, "offsetTop", { configurable: true, value: y });
vv.dispatchEvent(new Event("scroll"));
}, offsetTop);
}
test.describe("mobile: Plan formatting toolbar", () => {
test("docks above the keyboard and preserves the selection for Bold", async ({
testPage,
apiClient,
seedData,
prCapture,
}) => {
test.setTimeout(150_000);
const session = await seedMobilePlan(testPage, apiClient, seedData);
const editor = session.planPanel.locator(".ProseMirror:visible");
await expect(editor).toHaveCount(1);
await editor.focus();
const toolbar = testPage.getByTestId("plan-mobile-formatting-toolbar");
await expect(toolbar).toHaveCount(0);
await testPage.keyboard.press("Control+A");
await expect(toolbar).toBeVisible({ timeout: 10_000 });
if (prCapture.capturing) {
await prCapture.screenshot("mobile-plan-formatting-toolbar", {
caption: "Mobile Plan formatting controls docked above the task navigation.",
});
}
const bold = testPage.getByTestId("plan-formatting-action-bold");
await expect(bold).toBeVisible();
const actionButtons = toolbar.locator('button[data-testid^="plan-formatting-action-"]');
await expect(actionButtons).toHaveCount(8);
const actionSizes = await actionButtons.evaluateAll((elements) =>
elements.map((element) => {
const { width, height } = element.getBoundingClientRect();
return { width, height };
}),
);
for (const { width, height } of actionSizes) {
expect(width).toBeGreaterThanOrEqual(44);
expect(height).toBeGreaterThanOrEqual(44);
}
const visualActionSizes = await toolbar
.locator('button[data-testid^="plan-formatting-action-"] > span')
.evaluateAll((elements) =>
elements.map((element) => {
const { width, height } = element.getBoundingClientRect();
return { width, height };
}),
);
await expect(
toolbar.locator('button[data-testid^="plan-formatting-action-"] > span'),
).toHaveCount(8);
for (const { width, height } of visualActionSizes) {
expect(width).toBeLessThanOrEqual(32);
expect(height).toBeLessThanOrEqual(32);
}
await expect
.poll(() => toolbar.evaluate((element) => Math.round(element.getBoundingClientRect().height)))
.toBe(48);
await expect(testPage.getByTestId("plan-formatting-action-comment")).toBeEnabled();
await expect
.poll(() => editor.evaluate((element) => getComputedStyle(element, "::after").height))
.toBe("48px");
const horizontalOverflow = await toolbar.locator(":scope > div").evaluate((element) => ({
scrollWidth: element.scrollWidth,
clientWidth: element.clientWidth,
scrollLeft: element.scrollLeft,
}));
expect(horizontalOverflow.scrollWidth).toBe(horizontalOverflow.clientWidth);
expect(horizontalOverflow.scrollLeft).toBe(0);
const narrowOverflow = await toolbar.locator(":scope > div").evaluate((element) => {
const scroller = element as HTMLElement;
const originalWidth = scroller.style.width;
scroller.style.width = "100px";
const scrollWidth = scroller.scrollWidth;
const clientWidth = scroller.clientWidth;
scroller.scrollLeft = 16;
const canScroll = scroller.scrollLeft > 0;
scroller.style.width = originalWidth;
scroller.scrollLeft = 0;
return { scrollWidth, clientWidth, canScroll };
});
expect(narrowOverflow.scrollWidth).toBeGreaterThan(narrowOverflow.clientWidth);
expect(narrowOverflow.canScroll).toBe(true);
const documentOverflow = await testPage.evaluate(() => ({
documentWidth: document.documentElement.scrollWidth,
documentClientWidth: document.documentElement.clientWidth,
bodyWidth: document.body.scrollWidth,
bodyClientWidth: document.body.clientWidth,
}));
expect(documentOverflow.documentWidth).toBeLessThanOrEqual(
documentOverflow.documentClientWidth,
);
expect(documentOverflow.bodyWidth).toBeLessThanOrEqual(documentOverflow.bodyClientWidth);
const keyboardHeight = 300;
await simulateKeyboardOpen(testPage, keyboardHeight);
const expectedKeyboardTop = await testPage.evaluate(
(height) => `${window.innerHeight - height - 48}px`,
keyboardHeight,
);
await expect
.poll(() => toolbar.evaluate((element) => (element as HTMLElement).style.top))
.toBe(expectedKeyboardTop);
await expect
.poll(() => toolbar.evaluate((element) => (element as HTMLElement).style.bottom))
.toBe("auto");
await expect
.poll(() => editor.evaluate((element) => getComputedStyle(element, "::after").height))
.toBe("296px");
const finalLine = editor.getByText(FINAL_LINE, { exact: true });
const editorScrollContainer = testPage.getByTestId("plan-editor-scroll-container");
const scrollMetrics = await editorScrollContainer.evaluate((element) => {
const scrollContainer = element as HTMLElement;
scrollContainer.scrollTop = scrollContainer.scrollHeight;
return {
scrollTop: scrollContainer.scrollTop,
scrollHeight: scrollContainer.scrollHeight,
clientHeight: scrollContainer.clientHeight,
};
});
expect(scrollMetrics.scrollHeight).toBeGreaterThan(scrollMetrics.clientHeight);
expect(scrollMetrics.scrollTop).toBeGreaterThan(0);
const finalLineRect = await finalLine.evaluate((element) => {
const { bottom, height } = element.getBoundingClientRect();
return { bottom, height };
});
const toolbarTop = await toolbar.evaluate((element) => element.getBoundingClientRect().top);
expect(finalLineRect.height).toBeGreaterThan(0);
expect(finalLineRect.bottom).toBeLessThanOrEqual(toolbarTop + 1);
await simulateViewportScroll(testPage, 48);
const expectedScrolledTop = await testPage.evaluate(
(height) => `${window.innerHeight - height + 48 - 48}px`,
keyboardHeight,
);
await expect
.poll(() => toolbar.evaluate((element) => (element as HTMLElement).style.top))
.toBe(expectedScrolledTop);
await bold.tap();
await expect(editor.locator("strong", { hasText: PLAN_TEXT })).toContainText(PLAN_TEXT);
expect(await editor.evaluate((element) => document.activeElement === element)).toBe(true);
});
});