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.
240 lines
8.4 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|