import { expect, test, type Page } from "../support/fixtures"; import { blurComposerInput, roundTripComposerTabBeforePaint, composerLocator, enterComposerFullscreen, exitComposerFullscreen, expectComposerDraft, expectComposerFocused, expectComposerFullscreenControls, expectComposerNotFocused, expectComposerVisible, expectWorkspaceHeaderVisible, resizeComposerViewport, submitMessage, typeIntoFocusedComposer, } from "../support/helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; import { createMockIdleAgent, openWorkspaceWithAgents } from "../support/helpers/archive-tab"; import { seedWorkspace } from "../support/helpers/seed-client"; import { closeMobileAgentSidebar, closeMobileExplorer, expectMobileAgentSidebarHidden, expectMobileAgentSidebarVisible, expectMobileExplorerHidden, expectMobileExplorerVisible, swipeOpenMobileAgentSidebar, swipeOpenMobileExplorer, } from "../support/helpers/sidebar"; import { readScrollMetrics } from "../support/helpers/agent-bottom-anchor"; import { expectTimelinePromptPositionPreserved, rememberTimelinePromptPosition, seedLongMockAgentTimeline, waitForTimelineGeometryToSettle, } from "../support/helpers/timeline-pagination"; test("clicking chat text or background focuses the feed for keyboard paging", async ({ page, }, testInfo) => { const agent = await seedLongMockAgentTimeline({ turns: 8 }); try { await openAgentRoute(page, agent); const composer = composerLocator(page); const feed = page.getByTestId("agent-chat-scroll").filter({ visible: true }); await composer.fill("Keep this draft"); await feed.getByText(agent.newestPrompt, { exact: true }).click(); await expect(feed).toBeFocused(); const beforePaging = await readScrollMetrics(page); expect(beforePaging.offsetY).toBeGreaterThan(beforePaging.viewportHeight); await page.keyboard.press("PageUp"); await expect .poll(async () => (await readScrollMetrics(page)).offsetY) .toBeLessThan(beforePaging.offsetY - beforePaging.viewportHeight / 2); await page.screenshot({ path: testInfo.outputPath("chat-keyboard-paging.png") }); await page.keyboard.press("PageDown"); await expect .poll(async () => (await readScrollMetrics(page)).distanceFromBottom) .toBeLessThanOrEqual(2); await composer.click(); await expectComposerFocused(page); await feed.click({ position: { x: 2, y: 40 } }); await expect(feed).toBeFocused(); await page.keyboard.press("PageUp"); await expect .poll(async () => (await readScrollMetrics(page)).distanceFromBottom) .toBeGreaterThan(beforePaging.viewportHeight / 2); await waitForTimelineGeometryToSettle(page); const readingPrompt = agent.prompts[4]; await expect(feed.getByText(readingPrompt, { exact: true })).toBeInViewport(); const readingPosition = await rememberTimelinePromptPosition(page, readingPrompt); await agent.client.sendAgentMessage(agent.agentId, "emit 2 coalesced agent stream updates"); await agent.client.waitForFinish(agent.agentId, 15_000); // The daemon finishing does not mean its response has reached the browser. await expect(feed.getByText(/stress-update-1/)).toHaveCount(1); await waitForTimelineGeometryToSettle(page); await expectTimelinePromptPositionPreserved(page, readingPosition); await expectComposerDraft(page, "Keep this draft"); } finally { await agent.cleanup(); } }); test("focusing the feed preserves assistant text selection and code copy controls", async ({ page, context, }) => { const sentence = "Select this assistant sentence."; const agent = await seedMockAgentWorkspace({ repoPrefix: "chat-focus-selection-", title: "Chat focus selection", initialPrompt: "Render text and code.", featureValues: { mockAssistantResponse: `${sentence}\n\n\`\`\`bash\necho example\n\`\`\``, }, }); try { await context.grantPermissions(["clipboard-read", "clipboard-write"]); await agent.client.waitForFinish(agent.agentId, 15_000); await openAgentRoute(page, agent); await composerLocator(page).fill("Keep this draft"); const feed = page.getByTestId("agent-chat-scroll").filter({ visible: true }); await feed.getByText(sentence, { exact: true }).click({ clickCount: 3 }); await expect(feed).toBeFocused(); expect(await page.evaluate(() => window.getSelection()?.toString().trim())).toBe(sentence); await page.keyboard.press("ControlOrMeta+c"); expect((await page.evaluate(() => navigator.clipboard.readText())).trim()).toBe(sentence); const code = feed.locator('[data-paseo-markdown-language="bash"]'); await code.hover(); await code.locator("[data-paseo-markdown-ignore]").click(); await expect(feed).not.toBeFocused(); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("echo example"); await expectComposerDraft(page, "Keep this draft"); } finally { await agent.cleanup(); } }); test("submitting a message leaves the composer ready for the next message", async ({ page }) => { const agent = await seedMockAgentWorkspace({ repoPrefix: "composer-focus-", title: "Composer focus", }); try { await openAgentRoute(page, agent); await expectComposerVisible(page); await submitMessage(page, "First message"); await expectComposerFocused(page); await typeIntoFocusedComposer(page, "Second message"); await expectComposerDraft(page, "Second message"); } finally { await agent.cleanup(); } }); test("opening an agent focuses the composer when a mouse drives the UI", async ({ page }) => { const agent = await seedMockAgentWorkspace({ repoPrefix: "composer-focus-open-", title: "Composer focus on open", }); try { await openAgentRoute(page, agent); await expectComposerFocused(page); } finally { await agent.cleanup(); } }); async function waitForAnimationFrames(page: Page, count: number): Promise { await page.evaluate(async (frames) => { for (let frame = 0; frame < frames; frame += 1) { await new Promise((resolve) => requestAnimationFrame(resolve)); } }, count); } test.describe("touch screen wide enough for the desktop layout", () => { // A tablet or an unfolded foldable gets the desktop layout, but focusing the // composer there raises the on-screen keyboard over the conversation. test.use({ viewport: { width: 1024, height: 768 }, isMobile: true, hasTouch: true }); test("remounting an inline composer does not restore focus on touch", async ({ page }) => { const agent = await seedMockAgentWorkspace({ repoPrefix: "composer-touch-remount-", title: "Touch remount", }); try { await openAgentRoute(page, agent); await composerLocator(page).fill("Keep this draft through resize"); await expectComposerFocused(page); await resizeComposerViewport(page, "portrait"); await expectComposerDraft(page, "Keep this draft through resize"); await expectComposerNotFocused(page); await composerLocator(page).tap(); await expectComposerFocused(page); } finally { await agent.cleanup(); } }); test("returning to an inactive inline composer does not restore focus on touch", async ({ page, }) => { const workspace = await seedWorkspace({ repoPrefix: "composer-touch-retained-" }); try { const first = await createMockIdleAgent(workspace.client, { cwd: workspace.repoPath, workspaceId: workspace.workspaceId, title: "First touch chat", }); const second = await createMockIdleAgent(workspace.client, { cwd: workspace.repoPath, workspaceId: workspace.workspaceId, title: "Second touch chat", }); await openWorkspaceWithAgents(page, [first, second]); // Repeat after an explicit edit so retained focus from an earlier visit is // also dismissed when the same mounted editor becomes inactive again. for (const draft of ["Keep this touch draft", "Edit this touch draft again"]) { await composerLocator(page).fill(draft); await expectComposerFocused(page); // An external keyboard changes tabs without a pointer click blurring the // editor first. The device still has a touch-primary focus policy. await roundTripComposerTabBeforePaint(page, "Second touch chat"); await expectComposerDraft(page, draft); await expectComposerNotFocused(page); await composerLocator(page).tap(); await expectComposerFocused(page); await typeIntoFocusedComposer(page, " after return"); await expectComposerDraft(page, `${draft} after return`); } } finally { await workspace.cleanup(); } }); test("opening an agent leaves the composer unfocused", async ({ page }) => { const agent = await seedMockAgentWorkspace({ repoPrefix: "composer-focus-touch-", title: "Composer focus on touch", }); try { await openAgentRoute(page, agent); await expectComposerVisible(page); expect( await page.evaluate(() => window.matchMedia("(hover: hover) and (pointer: fine)").matches), ).toBe(false); // Autofocus makes its first attempt on the next animation frame. await waitForAnimationFrames(page, 4); await expectComposerNotFocused(page); } finally { await agent.cleanup(); } }); }); const COMPACT_PANELS = [ { name: "agent list", open: swipeOpenMobileAgentSidebar, expectOpen: expectMobileAgentSidebarVisible, close: closeMobileAgentSidebar, expectClosed: expectMobileAgentSidebarHidden, }, { name: "Explorer", open: swipeOpenMobileExplorer, expectOpen: expectMobileExplorerVisible, close: closeMobileExplorer, expectClosed: expectMobileExplorerHidden, }, ] as const; for (const panel of COMPACT_PANELS) { test(`closing the compact ${panel.name} does not restore composer focus`, async ({ page }) => { const agent = await seedMockAgentWorkspace({ repoPrefix: "composer-focus-panel-", title: `Composer focus under ${panel.name}`, }); const draft = "Line one\nLine two\nLine three"; try { await resizeComposerViewport(page, "portrait"); await openAgentRoute(page, agent); await expectComposerVisible(page); await test.step("an untouched editor stays untouched", async () => { await expectComposerNotFocused(page); await panel.open(page); await panel.expectOpen(page); await panel.close(page); await panel.expectClosed(page); await expectComposerNotFocused(page); }); await test.step("an inline editor blurred under the panel stays blurred", async () => { await composerLocator(page).fill(draft); await composerLocator(page).click(); await expectComposerFocused(page); await panel.open(page); await panel.expectOpen(page); await blurComposerInput(page); await panel.close(page); await panel.expectClosed(page); await expectComposerNotFocused(page); await expectComposerDraft(page, draft); }); await test.step("a fullscreen editor blurred under the panel stays blurred", async () => { await composerLocator(page).click(); await enterComposerFullscreen(page); await expectComposerFullscreenControls(page); await expectComposerFocused(page); await panel.open(page); await panel.expectOpen(page); await blurComposerInput(page); await panel.close(page); await panel.expectClosed(page); await expectComposerNotFocused(page); await expectComposerFullscreenControls(page); await expectComposerDraft(page, draft); await exitComposerFullscreen(page); await expectWorkspaceHeaderVisible(page); }); } finally { await agent.cleanup(); } }); }