Merge branch 'main' of github.com:getpaseo/paseo
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { test } from "../support/fixtures";
|
||||
import {
|
||||
readScrollMetrics,
|
||||
scrollChatAwayFromBottom,
|
||||
} from "../support/helpers/agent-bottom-anchor";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { seedMockAgentWorkspace, type MockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
import {
|
||||
continueToNextQuestion,
|
||||
fillQuestionAnswer,
|
||||
submitQuestionAnswers,
|
||||
waitForQuestionPrompt,
|
||||
} from "../support/helpers/questions";
|
||||
import { openMobileAgentSidebar } from "../support/helpers/sidebar";
|
||||
import {
|
||||
expectTimelinePromptVisible,
|
||||
openAgentTimeline,
|
||||
seedLongMockAgentTimeline,
|
||||
} from "../support/helpers/timeline-pagination";
|
||||
import { switchWorkspaceViaSidebar } from "../support/helpers/workspace-ui";
|
||||
|
||||
async function expectChatAtBottom(page: Page): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => (await readScrollMetrics(page)).distanceFromBottom, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThanOrEqual(72);
|
||||
}
|
||||
|
||||
async function openCompactWorkspace(
|
||||
page: Page,
|
||||
workspace: Pick<MockAgentWorkspace, "workspaceId">,
|
||||
): Promise<void> {
|
||||
await openMobileAgentSidebar(page);
|
||||
await switchWorkspaceViaSidebar({
|
||||
page,
|
||||
serverId: getServerId(),
|
||||
workspaceId: workspace.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
function armDelayedScrollForHiddenChat(page: Page): Promise<void> {
|
||||
return page
|
||||
.locator('[data-testid="agent-chat-scroll"]:visible')
|
||||
.first()
|
||||
.evaluate((root) => {
|
||||
const scroll = root as HTMLElement;
|
||||
return new Promise<void>((resolve) => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (scroll.getClientRects().length > 0) return;
|
||||
observer.disconnect();
|
||||
scroll.dispatchEvent(new Event("scroll"));
|
||||
resolve();
|
||||
});
|
||||
observer.observe(scroll);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("pointer jitter cannot turn geometry changes into scroll intent", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
const longChat = await seedLongMockAgentTimeline({ turns: 30 });
|
||||
const backgroundPrompt = "Continue after a pointer is released outside the chat";
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openAgentTimeline(page, longChat);
|
||||
await expectTimelinePromptVisible(page, longChat.newestPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
|
||||
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||
const bounds = await scroll.boundingBox();
|
||||
expect(bounds, "Expected visible chat scroll bounds").not.toBeNull();
|
||||
await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + bounds!.height / 2 - 3);
|
||||
await page.setViewportSize({ width: 390, height: 1124 });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.mouse.move(410, bounds!.y + bounds!.height / 2 - 3);
|
||||
await page.mouse.up();
|
||||
|
||||
await longChat.client.sendAgentMessage(longChat.agentId, backgroundPrompt);
|
||||
await longChat.client.waitForFinish(longChat.agentId, 20_000);
|
||||
await expectTimelinePromptVisible(page, backgroundPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
} finally {
|
||||
await longChat.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("a retained chat ignores delayed scroll delivery while hidden", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
const longChat = await seedLongMockAgentTimeline({ turns: 30 });
|
||||
const otherWorkspace = await seedMockAgentWorkspace({
|
||||
repoPrefix: "scroll-return-hidden-other-",
|
||||
title: "Other workspace",
|
||||
initialPrompt: "Open the other workspace",
|
||||
});
|
||||
const backgroundPrompt = "emit 500 coalesced agent stream updates";
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openAgentTimeline(page, longChat);
|
||||
await expectTimelinePromptVisible(page, longChat.newestPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
|
||||
const delayedScrollDelivered = armDelayedScrollForHiddenChat(page);
|
||||
await openCompactWorkspace(page, otherWorkspace);
|
||||
await delayedScrollDelivered;
|
||||
await longChat.client.sendAgentMessage(longChat.agentId, backgroundPrompt);
|
||||
await longChat.client.waitForFinish(longChat.agentId, 20_000);
|
||||
|
||||
await openCompactWorkspace(page, longChat);
|
||||
await expectTimelinePromptVisible(page, backgroundPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
} finally {
|
||||
await Promise.allSettled([longChat.cleanup(), otherWorkspace.cleanup()]);
|
||||
}
|
||||
});
|
||||
|
||||
test("a retained streaming chat returns to the bottom and continues following", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(240_000);
|
||||
const longChat = await seedLongMockAgentTimeline({ turns: 30 });
|
||||
const otherWorkspace = await seedMockAgentWorkspace({
|
||||
repoPrefix: "scroll-return-streaming-other-",
|
||||
title: "Other workspace",
|
||||
initialPrompt: "Open the other workspace",
|
||||
});
|
||||
const streamingPrompt = "Continue streaming while this workspace is hidden";
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openAgentTimeline(page, longChat);
|
||||
await expectTimelinePromptVisible(page, longChat.newestPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
|
||||
await longChat.client.sendAgentMessage(longChat.agentId, streamingPrompt);
|
||||
await longChat.client.waitForAgentUpsert(
|
||||
longChat.agentId,
|
||||
(snapshot) => snapshot.status === "running",
|
||||
15_000,
|
||||
);
|
||||
await openCompactWorkspace(page, otherWorkspace);
|
||||
await openCompactWorkspace(page, longChat);
|
||||
|
||||
await expectTimelinePromptVisible(page, streamingPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
await longChat.client.waitForFinish(longChat.agentId, 20_000);
|
||||
await expectChatAtBottom(page);
|
||||
} finally {
|
||||
await Promise.allSettled([longChat.cleanup(), otherWorkspace.cleanup()]);
|
||||
}
|
||||
});
|
||||
|
||||
test("a retained chat preserves an intentional reading position", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
const longChat = await seedLongMockAgentTimeline({ turns: 30 });
|
||||
const otherWorkspace = await seedMockAgentWorkspace({
|
||||
repoPrefix: "scroll-return-reading-position-other-",
|
||||
title: "Other workspace",
|
||||
initialPrompt: "Open the other workspace",
|
||||
});
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openAgentTimeline(page, longChat);
|
||||
await expectTimelinePromptVisible(page, longChat.newestPrompt);
|
||||
const readingPosition = await scrollChatAwayFromBottom(page, {
|
||||
deltaY: -900,
|
||||
minDistanceFromBottom: 300,
|
||||
});
|
||||
|
||||
await openCompactWorkspace(page, otherWorkspace);
|
||||
await openCompactWorkspace(page, longChat);
|
||||
|
||||
await expect
|
||||
.poll(async () => Math.abs((await readScrollMetrics(page)).offsetY - readingPosition.offsetY))
|
||||
.toBeLessThanOrEqual(24);
|
||||
} finally {
|
||||
await Promise.allSettled([longChat.cleanup(), otherWorkspace.cleanup()]);
|
||||
}
|
||||
});
|
||||
|
||||
test("editing keys in an embedded question keep output following", async ({ page }) => {
|
||||
test.setTimeout(240_000);
|
||||
const longChat = await seedLongMockAgentTimeline({ turns: 30 });
|
||||
const questionPrompt = "Emit synthetic questions: two free-write questions.";
|
||||
const firstQuestion = "What is the GitHub private repo URL to push to?";
|
||||
const secondQuestion = "What should the first commit message be?";
|
||||
|
||||
try {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openAgentTimeline(page, longChat);
|
||||
await expectTimelinePromptVisible(page, longChat.newestPrompt);
|
||||
await expectChatAtBottom(page);
|
||||
|
||||
await longChat.client.sendAgentMessage(longChat.agentId, questionPrompt);
|
||||
await waitForQuestionPrompt(page, 30_000);
|
||||
await fillQuestionAnswer(page, {
|
||||
question: firstQuestion,
|
||||
answer: "git@github.com:user/private-repo.git",
|
||||
});
|
||||
const firstInput = page
|
||||
.getByTestId("question-form-card")
|
||||
.first()
|
||||
.getByRole("textbox", { name: firstQuestion });
|
||||
const inputBounds = await firstInput.boundingBox();
|
||||
expect(inputBounds, "Expected embedded question input bounds").not.toBeNull();
|
||||
await page.mouse.move(
|
||||
inputBounds!.x + inputBounds!.width / 2,
|
||||
inputBounds!.y + inputBounds!.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(
|
||||
inputBounds!.x + inputBounds!.width / 2 + 20,
|
||||
inputBounds!.y + inputBounds!.height / 2 - 3,
|
||||
);
|
||||
await page.mouse.up();
|
||||
await firstInput.press("Home");
|
||||
await firstInput.press("ArrowUp");
|
||||
await continueToNextQuestion(page);
|
||||
await fillQuestionAnswer(page, {
|
||||
question: secondQuestion,
|
||||
answer: "Initialize private repo",
|
||||
});
|
||||
await submitQuestionAnswers(page);
|
||||
await longChat.client.waitForFinish(longChat.agentId, 20_000);
|
||||
|
||||
await expectChatAtBottom(page);
|
||||
} finally {
|
||||
await longChat.cleanup();
|
||||
}
|
||||
});
|
||||
@@ -97,7 +97,7 @@ async function expectAgentConsistentlyIdle(page: Page, title: string): Promise<v
|
||||
}
|
||||
|
||||
test.describe("Viewed agent timelines", () => {
|
||||
test("a turn that finishes while unsubscribed reopens with consistently idle chrome", async ({
|
||||
test("a turn that finishes while hidden reopens with consistently idle chrome", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(150_000);
|
||||
@@ -105,9 +105,9 @@ test.describe("Viewed agent timelines", () => {
|
||||
const scenario = await seedViewedTimelineScenario({ firstAgentModel: "one-minute-stream" });
|
||||
try {
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await startVisibleTurn(page, scenario, "Finish after this chat becomes unsubscribed.");
|
||||
await startVisibleTurn(page, scenario, "Finish after this chat becomes hidden.");
|
||||
await selectAgent(page, "Second viewed chat");
|
||||
await subscriptions.waitForSubscribedAgents([scenario.secondAgentId], { timeout: 45_000 });
|
||||
await subscriptions.waitForSubscribedAgents([scenario.firstAgentId, scenario.secondAgentId]);
|
||||
const finish = await scenario.client.waitForFinish(scenario.firstAgentId, 90_000);
|
||||
expect(finish.status).toBe("idle");
|
||||
|
||||
@@ -118,33 +118,7 @@ test.describe("Viewed agent timelines", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("an unsubscribed hidden chat catches up when shown", async ({ page }) => {
|
||||
test.setTimeout(90_000);
|
||||
const subscriptions = observeTimelineSubscriptions(page);
|
||||
const scenario = await seedViewedTimelineScenario();
|
||||
try {
|
||||
await openAgent(page, scenario, scenario.firstAgentId);
|
||||
await selectAgent(page, "Second viewed chat");
|
||||
await subscriptions.waitForSubscribedAgents([scenario.secondAgentId], { timeout: 45_000 });
|
||||
await commitMessage(
|
||||
scenario,
|
||||
scenario.firstAgentId,
|
||||
"Committed after the first chat unsubscribed.",
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Committed after the first chat unsubscribed.", { exact: true }),
|
||||
).toHaveCount(0);
|
||||
await selectAgent(page, "First viewed chat");
|
||||
await expect(
|
||||
page.getByText("Committed after the first chat unsubscribed.", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("(end of synthetic stream)", { exact: true })).toBeVisible();
|
||||
} finally {
|
||||
await scenario.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("a hidden retained chat stays current during unsubscribe grace", async ({ page }) => {
|
||||
test("a hidden hot chat stays current", async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
const subscriptions = observeTimelineSubscriptions(page);
|
||||
const scenario = await seedViewedTimelineScenario();
|
||||
|
||||
Reference in New Issue
Block a user