Files
paseo/packages/app/e2e/browser/chat-outline.spec.ts
T
Mohamed BoudraandGitHub 2298e9efdf Fix composer toolbar collapse flicker on tab switch (#2937)
Retained tabs stay mounted under display:none while inactive, which
reports a zero-width onLayout. ControlledAgentControls fed that
straight into its density resolver, so a hidden tab's toolbar
collapsed to its tightest density and painted one frame collapsed
before re-expanding when the tab became visible again.

Extract the zero-width guard that useContainerWidthBelow already had
into a shared readMeasuredWidth helper, and use it in the composer
toolbar's own layout handler so a display:none measurement can never
drive its density.

Also bump the chat outline rail's MIN_PANEL_WIDTH breakpoint (818 ->
918) so the rail disappears earlier under a narrower panel, and widen
the chat-outline e2e spec's viewport so its panel keeps healthy margin
over the new threshold.
2026-08-06 18:37:38 +08:00

240 lines
8.4 KiB
TypeScript

import { expect, test } from "../support/fixtures";
import { trackPromptJumpRequests } from "../support/helpers/agent-timeline-gate";
import {
clickChatOutlineRowEdge,
disableChatOutlineFromAppearance,
expectActiveChatOutlinePrompt,
expectChatOutlinePreview,
expectChatOutlinePrompts,
expectChatOutlinePromptToRemainBare,
expectLiveTurnPromptAboveFoldAndActive,
expectNoChatOutlinePreviewWhileCrossingToSidebar,
expectNoChatOutline,
expectNoChatOutlinePreview,
expectOneActiveChatOutlinePrompt,
focusChatOutlinePrompt,
hoverChatOutlinePrompt,
chatOutlineRail,
movePointerOffChatOutline,
pointAtChatOutlineRowEdge,
pressEnterOnFocusedPrompt,
splitCurrentPanelRight,
} from "../support/helpers/chat-outline";
import {
expectTimelineAtMaximumScrollWithPromptVisible,
expectTimelinePromptNotMounted,
expectTimelinePromptLandedBelowTop,
expectTimelinePromptVisible,
holdOlderHistoryPages,
openAgentTimeline,
scrollThroughOlderHistoryPages,
scrollTimelineToNewestLoadedEdge,
scrollTimelineToOldestLoadedEdge,
seedLongMockAgentTimeline,
type LongTimelineAgent,
} from "../support/helpers/timeline-pagination";
// Wide enough that the timeline panel clears the rail's MIN_PANEL_WIDTH with room
// to spare. At 1280 the panel measures 960, which sits too close to the threshold
// for chrome-width changes elsewhere to stay out of these tests.
const WIDE_VIEWPORT = { width: 1440, height: 900 };
const LOADED_TURNS = 16;
test.describe("desktop chat outline", () => {
test("indexes unloaded prompts and jumps with one bounded merged page", async ({ page }) => {
test.setTimeout(120_000);
const agent = await seedLongMockAgentTimeline({ turns: 80 });
try {
const jumps = await trackPromptJumpRequests(page, agent.agentId);
await page.setViewportSize(WIDE_VIEWPORT);
await openAgentTimeline(page, agent);
const rail = chatOutlineRail(page);
await expectChatOutlinePrompts(page, 80);
await expectTimelinePromptNotMounted(page, agent.oldestPrompt);
await rail.getByRole("tab").nth(39).click();
await expectTimelinePromptLandedBelowTop(page, agent.prompts[39]);
await expect.poll(() => jumps.requests()).toHaveLength(1);
expect(jumps.requests()[0]).toMatchObject({ limit: 40, mergeWindow: true });
await page.getByTestId("scroll-to-bottom-button").click();
await expectTimelinePromptVisible(page, agent.newestPrompt);
const requestsBeforeLoadedJump = jumps.requests().length;
await rail.getByRole("tab").last().click();
await expectTimelinePromptVisible(page, agent.newestPrompt);
expect(jumps.requests()).toHaveLength(requestsBeforeLoadedJump);
await rail.getByRole("tab").first().click();
await expectTimelinePromptVisible(page, agent.oldestPrompt);
await agent.client.sendAgentMessage(agent.agentId, "live prompt while viewing old history");
await agent.client.waitForFinish(agent.agentId, 15_000);
await expectTimelinePromptVisible(page, agent.oldestPrompt);
} finally {
await agent.cleanup();
}
});
test.describe("with the whole conversation loaded", () => {
let agent: LongTimelineAgent;
test.beforeAll(async () => {
test.setTimeout(120_000);
agent = await seedLongMockAgentTimeline({ turns: LOADED_TURNS });
});
test.afterAll(async () => {
await agent.cleanup();
});
test.beforeEach(async ({ page }) => {
await page.setViewportSize(WIDE_VIEWPORT);
await openAgentTimeline(page, agent);
await expectChatOutlinePrompts(page, LOADED_TURNS);
});
test("previews the prompt under the pointer, anywhere across its row", async ({ page }) => {
await hoverChatOutlinePrompt(page, 2);
await expectChatOutlinePreview(page, agent.prompts[1]);
await pointAtChatOutlineRowEdge(page, 7);
await expectChatOutlinePreview(page, agent.prompts[6]);
await movePointerOffChatOutline(page);
await expectNoChatOutlinePreview(page);
});
test("does not preview while crossing the rail toward the sidebar", async ({ page }) => {
await expectNoChatOutlinePreviewWhileCrossingToSidebar(page);
});
test("closes the preview when the pointer leaves after a jump", async ({ page }) => {
await clickChatOutlineRowEdge(page, 4);
await movePointerOffChatOutline(page);
await expectNoChatOutlinePreview(page);
});
test("previews and jumps to the focused prompt from the keyboard", async ({ page }) => {
await focusChatOutlinePrompt(page, 1);
await expectChatOutlinePreview(page, agent.prompts[0]);
await pressEnterOnFocusedPrompt(page);
await expectTimelinePromptLandedBelowTop(page, agent.oldestPrompt);
await expectActiveChatOutlinePrompt(page, 1);
});
test("moves the active mark as the reader jumps and scrolls", async ({ page }) => {
await expectOneActiveChatOutlinePrompt(page);
await clickChatOutlineRowEdge(page, 4);
await expectTimelinePromptVisible(page, agent.prompts[3]);
await expectActiveChatOutlinePrompt(page, 4);
await scrollTimelineToOldestLoadedEdge(page);
await expectActiveChatOutlinePrompt(page, 1);
});
test("keeps a clicked prompt free of persistent selection chrome", async ({ page }) => {
await clickChatOutlineRowEdge(page, 4);
await expectChatOutlinePromptToRemainBare(page, 4);
});
test("clamps the newest prompt at maximum scroll while keeping it visible", async ({
page,
}) => {
await clickChatOutlineRowEdge(page, LOADED_TURNS);
await expectTimelineAtMaximumScrollWithPromptVisible(page, agent.newestPrompt);
});
test("keeps the current prompt active throughout a long live assistant turn", async ({
page,
}) => {
const prompt = "chat outline live turn reading position";
await agent.client.sendAgentMessage(agent.agentId, prompt);
try {
await expectLiveTurnPromptAboveFoldAndActive(page, prompt, LOADED_TURNS + 1);
} finally {
await agent.client.waitForFinish(agent.agentId, 15_000);
}
});
});
test.describe("with virtualized loaded history", () => {
let agent: LongTimelineAgent;
test.beforeAll(async () => {
test.setTimeout(120_000);
agent = await seedLongMockAgentTimeline({ turns: 80 });
});
test.afterAll(async () => {
await agent.cleanup();
});
test.beforeEach(async ({ page }) => {
await page.setViewportSize(WIDE_VIEWPORT);
});
test("pins a loaded prompt inside the virtualized block without fetching", async ({ page }) => {
const history = await holdOlderHistoryPages(page, agent);
await openAgentTimeline(page, agent);
await expectChatOutlinePrompts(page, 80);
await scrollThroughOlderHistoryPages(page, 2, history);
await scrollTimelineToNewestLoadedEdge(page);
const requestsBeforeJump = history.requestCount();
await clickChatOutlineRowEdge(page, 30);
await expectTimelinePromptLandedBelowTop(page, agent.prompts[29]);
expect(history.requestCount()).toBe(requestsBeforeJump);
});
test("pins a newer loaded prompt when jumping downward from old history", async ({ page }) => {
const history = await holdOlderHistoryPages(page, agent);
await openAgentTimeline(page, agent);
await expectChatOutlinePrompts(page, 80);
await scrollThroughOlderHistoryPages(page, 2, history);
await scrollTimelineToNewestLoadedEdge(page);
await clickChatOutlineRowEdge(page, 30);
await expectTimelinePromptLandedBelowTop(page, agent.prompts[29]);
await clickChatOutlineRowEdge(page, 60);
await expectTimelinePromptLandedBelowTop(page, agent.prompts[59]);
});
});
test("hides the rail when a split makes its panel narrow", async ({ page }) => {
const agent = await seedLongMockAgentTimeline({ turns: 2 });
try {
await page.setViewportSize(WIDE_VIEWPORT);
await openAgentTimeline(page, agent);
await splitCurrentPanelRight(page);
await expectNoChatOutline(page);
} finally {
await agent.cleanup();
}
});
test("can be disabled from Appearance settings", async ({ page }) => {
const agent = await seedLongMockAgentTimeline({ turns: 2 });
try {
await page.setViewportSize(WIDE_VIEWPORT);
await openAgentTimeline(page, agent);
await disableChatOutlineFromAppearance(page);
await expectNoChatOutline(page);
} finally {
await agent.cleanup();
}
});
});