import { test, expect } from "./fixtures"; import { awaitAssistantMessage, expectAgentIdle, expectInlineWorkingIndicator, expectTurnCopyButton, expectScrollFollowsNewContent, } from "./helpers/agent-stream"; import { expectScrollStaysFixed, readScrollMetrics, scrollAgentChatToBottom, scrollChatAwayFromBottom, waitForScrollableChat, } from "./helpers/agent-bottom-anchor"; import { delayCreatedAgentInitialTailResponse } from "./helpers/agent-timeline-gate"; import { selectModel } from "./helpers/app"; import { clickNewChat } from "./helpers/launcher"; import { expectComposerVisible, startRunningMockAgent } from "./helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent"; const SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE = 360; test.describe("Agent stream UI", () => { test("auto-scroll sticks to bottom across token bursts", async ({ page }) => { test.setTimeout(120_000); const agent = await startRunningMockAgent(page, { prefix: "stream-scroll-", model: "one-minute-stream", prompt: "Stream for auto-scroll test.", }); try { await awaitAssistantMessage(page); await expectScrollFollowsNewContent(page); } finally { await agent.cleanup(); } }); test("keeps the viewport fixed after the user scrolls away during a stream", async ({ page }) => { test.setTimeout(120_000); const agent = await seedMockAgentWorkspace({ repoPrefix: "stream-scroll-away-", title: "Scroll-away anchor", model: "five-minute-stream", initialPrompt: "emit 120 agent stream updates for scroll-away setup.", }); try { await agent.client.waitForFinish(agent.agentId, 30_000); await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId, }); await expectComposerVisible(page); await agent.client.sendAgentMessage(agent.agentId, "Stream for scroll-away anchor test."); await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible({ timeout: 30_000, }); await awaitAssistantMessage(page); await waitForScrollableChat(page, { minScrollableDistance: SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE, timeout: 30_000, }); const baseline = await scrollChatAwayFromBottom(page, { deltaY: -900, minDistanceFromBottom: 300, }); await expectScrollStaysFixed(page, baseline, { durationMs: 30_000 }); const finalMetrics = await readScrollMetrics(page); expect(finalMetrics.contentHeight).toBeGreaterThan(baseline.contentHeight); } finally { await agent.cleanup(); } }); test("keeps the viewport fixed when delayed authoritative history arrives after scroll-away", async ({ page, withWorkspace, }) => { test.setTimeout(180_000); const timelineGate = await delayCreatedAgentInitialTailResponse(page); const workspace = await withWorkspace({ prefix: "stream-scroll-away-delayed-history-", }); await workspace.navigateTo(); await clickNewChat(page); await page.getByText("Model defaults are still loading").waitFor({ state: "hidden", timeout: 30_000, }); await expectComposerVisible(page); await selectModel(page, "Five minute stream"); const prompt = "Stream for delayed authoritative history scroll-away test."; const composer = page.getByRole("textbox", { name: "Message agent..." }).first(); await composer.fill(prompt); await page.getByRole("button", { name: "Send message" }).click(); await page.getByText(prompt, { exact: true }).first().waitFor({ state: "visible", timeout: 30_000, }); await timelineGate.waitForCreatedAgent(); await timelineGate.waitForDelayedResponse(); await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible({ timeout: 30_000, }); await awaitAssistantMessage(page); await waitForScrollableChat(page, { minScrollableDistance: SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE, timeout: 45_000, }); const baseline = await scrollChatAwayFromBottom(page, { deltaY: -900, minDistanceFromBottom: 300, }); timelineGate.release(); await timelineGate.waitForForwardedResponse(); await expectScrollStaysFixed(page, baseline); }); test("working-indicator transitions to copy-button when stream ends", async ({ page }) => { test.setTimeout(60_000); const agent = await startRunningMockAgent(page, { prefix: "stream-indicator-", model: "ten-second-stream", prompt: "Stream briefly for indicator transition test.", }); try { await awaitAssistantMessage(page); await expectInlineWorkingIndicator(page); await expectAgentIdle(page, 30_000); await scrollAgentChatToBottom(page); await expectTurnCopyButton(page); } finally { await agent.cleanup(); } }); test("shows elapsed timer on first app-created running turn", async ({ page, withWorkspace }) => { test.setTimeout(90_000); const workspace = await withWorkspace({ prefix: "stream-first-app-turn-timer-" }); await workspace.navigateTo(); await clickNewChat(page); await page.getByText("Model defaults are still loading").waitFor({ state: "hidden", timeout: 30_000, }); const prompt = "Stream briefly for first app-created turn timer test."; const composer = page.getByRole("textbox", { name: "Message agent..." }).first(); await composer.fill(prompt); await page.getByRole("button", { name: "Send message" }).click(); await page.getByText(prompt, { exact: true }).first().waitFor({ state: "visible" }); await awaitAssistantMessage(page); await expectInlineWorkingIndicator(page); await page.getByTestId("turn-working-elapsed").waitFor({ state: "visible", timeout: 5_000 }); }); });