mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
Scroll chat with Page Up and Page Down after clicking messages (#7033)
* fix(app): focus chat feed when clicking message content * test(app): wait for new chat output before checking scroll position Co-authored-by: Ivan Pantic <panta@talentkit.io> * test(app): capture the keyboard paging regression * test(app): verify paged chat text stays in place --------- Co-authored-by: Ivan Pantic <panta@talentkit.io> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Ivan Pantic
parent
f56c013928
commit
d9b874f9d0
@@ -1,7 +1,7 @@
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import {
|
||||
composerLocator,
|
||||
blurComposerInput,
|
||||
composerLocator,
|
||||
enterComposerFullscreen,
|
||||
exitComposerFullscreen,
|
||||
expectComposerDraft,
|
||||
@@ -25,6 +25,100 @@ import {
|
||||
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({
|
||||
|
||||
@@ -544,7 +544,7 @@ async function readTimelineViewport(page: Page): Promise<TimelineViewportSnapsho
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForTimelineGeometryToSettle(page: Page): Promise<void> {
|
||||
export async function waitForTimelineGeometryToSettle(page: Page): Promise<void> {
|
||||
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||
await scroll.evaluate(
|
||||
(element) =>
|
||||
|
||||
@@ -1298,6 +1298,8 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
<div style={viewportStyle} data-window-content>
|
||||
<div
|
||||
ref={handleScrollContainerRef}
|
||||
// Let clicks focus the feed for browser keyboard scrolling without adding a Tab stop.
|
||||
tabIndex={-1}
|
||||
data-testid="agent-chat-scroll"
|
||||
data-overlay-scrollbar={scrollEnabled && !isMobileBreakpoint ? "true" : undefined}
|
||||
id={`agent-chat-scroll-${shouldUseVirtualizer ? "web-dom-virtualized" : "web-dom-scroll"}`}
|
||||
|
||||
@@ -127,7 +127,7 @@ function inspectChatDragExclusions(scroll: Element) {
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
const headerY = headerRect.top + headerRect.height / 2;
|
||||
const content = scroll.firstElementChild;
|
||||
const focusScope = scroll.closest("[tabindex]");
|
||||
const focusScope = scroll.closest("[data-window-content]")?.parentElement?.closest("[tabindex]");
|
||||
if (!content || !focusScope) throw new Error("Expected chat content and its focus scope");
|
||||
const contentRect = content.getBoundingClientRect();
|
||||
const chatX = (contentRect.left + contentRect.right) / 2;
|
||||
|
||||
Reference in New Issue
Block a user