mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
test(e2e): keep the completed-block, font-readiness, and live-page guards inside journeys
- agent-stream-ui: the auto-scroll journey anchors on the completed intro block by content and requires its Markdown root to stay the same mounted node while later blocks stream (#1989 guard, reshaped off the tail block). - changes-pane: the code-typography journey holds font loads and requires no diff geometry before they resolve, then applies the font change. - agent-timeline-pagination: the live-output page load runs against a thirty-minute mock stream and gates the release on the running turn, so the status assertion no longer races a ten-second turn.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
expectRunningAgentChrome,
|
||||
expectTurnCopyButton,
|
||||
expectScrollFollowsNewContent,
|
||||
observeCompletedMarkdownBlock,
|
||||
} from "../support/helpers/agent-stream";
|
||||
import {
|
||||
expectScrollStaysFixed,
|
||||
@@ -48,7 +49,9 @@ test.describe("Agent stream UI", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("auto-scroll sticks to bottom across token bursts", async ({ page }) => {
|
||||
test("auto-scroll follows token bursts while streamed Markdown grows in place", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(120_000);
|
||||
const agent = await startRunningMockAgent(page, {
|
||||
prefix: "stream-scroll-",
|
||||
@@ -56,8 +59,18 @@ test.describe("Agent stream UI", () => {
|
||||
prompt: "Stream for auto-scroll test.",
|
||||
});
|
||||
try {
|
||||
await awaitAssistantMessage(page);
|
||||
await awaitAssistantMessage(page, "walking through");
|
||||
await expectScrollFollowsNewContent(page);
|
||||
|
||||
await test.step("a completed block stays mounted while later blocks stream", async () => {
|
||||
await awaitAssistantMessage(page, "Now I have a clearer picture");
|
||||
const intro = page
|
||||
.getByTestId("assistant-message")
|
||||
.filter({ hasText: "walking through" })
|
||||
.first();
|
||||
const block = await observeCompletedMarkdownBlock(page, intro);
|
||||
await block.expectLaterStreamKeepsMounted();
|
||||
});
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
|
||||
@@ -134,7 +134,8 @@ test.describe("Agent timeline pagination", () => {
|
||||
|
||||
test("finishes loading an older page while live output continues", async ({ page }) => {
|
||||
test.setTimeout(120_000);
|
||||
const agent = await seedLongMockAgentTimeline({ turns: 40 });
|
||||
// The live turn streams for thirty minutes, so it is still running at every assertion.
|
||||
const agent = await seedLongMockAgentTimeline({ turns: 40, liveTurns: "thirty-minute-stream" });
|
||||
try {
|
||||
const history = await holdOlderHistoryPages(page, agent);
|
||||
await openAgentTimeline(page, agent);
|
||||
@@ -146,10 +147,11 @@ test.describe("Agent timeline pagination", () => {
|
||||
agent.agentId,
|
||||
(snapshot) => snapshot.status === "running",
|
||||
);
|
||||
const timeline = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||
await expect(timeline.getByText("walking through").first()).toBeAttached();
|
||||
history.releasePage(1);
|
||||
|
||||
await userScrollsTimelineToHistoryStart(page);
|
||||
await expectTimelinePromptVisible(page, agent.oldestPrompt);
|
||||
await expect(timeline.getByText(agent.newestOlderPagePrompt, { exact: true })).toBeAttached();
|
||||
await expect(page.getByTestId("load-older-history-spinner")).toBeHidden({ timeout: 5_000 });
|
||||
const running = await agent.client.fetchAgents({ scope: "active" });
|
||||
expect(running.entries.find((entry) => entry.agent.id === agent.agentId)?.agent.status).toBe(
|
||||
|
||||
@@ -980,11 +980,21 @@ test("canvas diff stays sharp while its workspace pane is resized", async ({ pag
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("changes diff applies code size changes to gutter and code typography", async ({ page }) => {
|
||||
test("changes diff waits for configured fonts, then applies code typography changes", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = await createWorkspaceWithMountedTabDiff();
|
||||
await useCodeFont(page, 12);
|
||||
await useUnwrappedDiffLines(page);
|
||||
await openWorkspaceChanges(page, workspace);
|
||||
await holdBrowserFontLoads(page);
|
||||
await navigateToWorkspaceChanges(page, workspace);
|
||||
|
||||
await test.step("commit no geometry before the configured fonts are ready", async () => {
|
||||
await expect(page.getByTestId("git-diff-canvas")).toBeVisible();
|
||||
await expect(page.getByTestId("diff-file-0-body")).toHaveCount(0);
|
||||
await releaseBrowserFontLoads(page);
|
||||
await expectExpandedMountedTabDiff(page);
|
||||
});
|
||||
const before = await readDiffTypographyGeometry(page);
|
||||
|
||||
await changeCodeTypographyFromSettings(page, {
|
||||
@@ -1374,6 +1384,41 @@ async function setOpenChangesPresentation(
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds every font load in the document until released; later loads pass through. */
|
||||
async function holdBrowserFontLoads(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
const fontSet = document.fonts;
|
||||
const originalLoad = fontSet.load.bind(fontSet);
|
||||
const pending: Array<() => void> = [];
|
||||
let released = false;
|
||||
Object.defineProperty(fontSet, "load", {
|
||||
configurable: true,
|
||||
value(font: string, text?: string) {
|
||||
if (released) return originalLoad(font, text);
|
||||
return new Promise<FontFace[]>((resolve, reject) => {
|
||||
pending.push(() => {
|
||||
originalLoad(font, text).then(resolve, reject);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
Object.assign(window, {
|
||||
__releasePaseoDiffFontLoads() {
|
||||
released = true;
|
||||
for (const release of pending.splice(0)) release();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function releaseBrowserFontLoads(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
(
|
||||
window as typeof window & { __releasePaseoDiffFontLoads: () => void }
|
||||
).__releasePaseoDiffFontLoads();
|
||||
});
|
||||
}
|
||||
|
||||
async function expectDiffCodeFontSize(page: Page, fontSize: number): Promise<void> {
|
||||
const canvas = page.getByTestId("git-diff-canvas");
|
||||
await expect
|
||||
@@ -1609,12 +1654,16 @@ async function createWorkspaceWithStickyTransitionDiff(): Promise<DirtyWorkspace
|
||||
}
|
||||
|
||||
async function openWorkspaceChanges(page: Page, workspace: DirtyWorkspace): Promise<void> {
|
||||
await navigateToWorkspaceChanges(page, workspace);
|
||||
await expectExpandedMountedTabDiff(page);
|
||||
}
|
||||
|
||||
async function navigateToWorkspaceChanges(page: Page, workspace: DirtyWorkspace): Promise<void> {
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.id));
|
||||
await waitForWorkspaceTabsVisible(page);
|
||||
await page.getByTestId("workspace-explorer-toggle").first().click();
|
||||
await openChangesInVisibleExplorer(page);
|
||||
await expectExpandedMountedTabDiff(page);
|
||||
}
|
||||
|
||||
/** The Explorer overlay a phone-sized viewport shows, with its Changes tab selected. */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { expect, type Locator, type Page } from "@playwright/test";
|
||||
import { readScrollMetrics, waitForContentGrowth, expectNearBottom } from "./agent-bottom-anchor";
|
||||
|
||||
export async function awaitAssistantMessage(page: Page, hasText?: string | RegExp): Promise<void> {
|
||||
@@ -7,6 +7,62 @@ export async function awaitAssistantMessage(page: Page, hasText?: string | RegEx
|
||||
await expect(target).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes an assistant block that has finished streaming. While later blocks
|
||||
* stream below it, its Markdown root must stay the same mounted node with no
|
||||
* descendants removed. Re-creating completed blocks on every stream update is
|
||||
* what froze Android chats (#1989).
|
||||
*/
|
||||
export async function observeCompletedMarkdownBlock(
|
||||
page: Page,
|
||||
message: Locator,
|
||||
): Promise<{ expectLaterStreamKeepsMounted(): Promise<void> }> {
|
||||
const block = await message.locator(":scope > *").first().elementHandle();
|
||||
const root = await message
|
||||
.locator(":scope > *")
|
||||
.first()
|
||||
.locator(":scope > *")
|
||||
.first()
|
||||
.elementHandle();
|
||||
if (!block || !root) {
|
||||
throw new Error("Expected the completed assistant block to contain a Markdown root");
|
||||
}
|
||||
await page.evaluate((observed) => {
|
||||
const evidence = { removedNodes: 0 };
|
||||
const observer = new MutationObserver((records) => {
|
||||
for (const record of records) evidence.removedNodes += record.removedNodes.length;
|
||||
});
|
||||
observer.observe(observed, { childList: true, subtree: true });
|
||||
Object.assign(window, {
|
||||
__completedMarkdownEvidence: evidence,
|
||||
__completedMarkdownObserver: observer,
|
||||
});
|
||||
}, block);
|
||||
|
||||
return {
|
||||
async expectLaterStreamKeepsMounted() {
|
||||
const { contentHeight } = await readScrollMetrics(page);
|
||||
await waitForContentGrowth(page, contentHeight + 200);
|
||||
const evidence = await page.evaluate(
|
||||
([observedBlock, observedRoot]) => {
|
||||
const state = window as typeof window & {
|
||||
__completedMarkdownEvidence?: { removedNodes: number };
|
||||
__completedMarkdownObserver?: MutationObserver;
|
||||
};
|
||||
state.__completedMarkdownObserver?.disconnect();
|
||||
return {
|
||||
connected: observedRoot.isConnected,
|
||||
sameRoot: observedBlock.firstElementChild === observedRoot,
|
||||
removedNodes: state.__completedMarkdownEvidence?.removedNodes,
|
||||
};
|
||||
},
|
||||
[block, root] as const,
|
||||
);
|
||||
expect(evidence).toEqual({ connected: true, sameRoot: true, removedNodes: 0 });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function awaitToolCall(page: Page, toolName: string | RegExp): Promise<void> {
|
||||
await expect(
|
||||
page.getByTestId("tool-call-badge").filter({ hasText: toolName }).first(),
|
||||
|
||||
@@ -15,6 +15,8 @@ export { holdDaemonHydration };
|
||||
|
||||
interface LongTimelineAgentOptions {
|
||||
turns: number;
|
||||
/** Live turns stream for ten seconds unless a test needs one that outlasts it. */
|
||||
liveTurns?: "ten-second-stream" | "thirty-minute-stream";
|
||||
}
|
||||
|
||||
export interface LongTimelineAgent extends MockAgentWorkspace {
|
||||
@@ -23,6 +25,8 @@ export interface LongTimelineAgent extends MockAgentWorkspace {
|
||||
firstOlderPagePrompt: string;
|
||||
initialTailAnchorPrompt: string;
|
||||
initialTailOldestPrompt: string;
|
||||
/** The newest prompt on the first older page, adjacent to the initial tail. */
|
||||
newestOlderPagePrompt: string;
|
||||
oldestPrompt: string;
|
||||
newestPrompt: string;
|
||||
}
|
||||
@@ -71,7 +75,7 @@ export async function seedLongMockAgentTimeline(
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: "timeline-pagination-",
|
||||
title: "Timeline pagination regression",
|
||||
model: "ten-second-stream",
|
||||
model: options.liveTurns ?? "ten-second-stream",
|
||||
});
|
||||
|
||||
for (let index = 0; index < options.turns; index += 1) {
|
||||
@@ -85,6 +89,7 @@ export async function seedLongMockAgentTimeline(
|
||||
firstOlderPagePrompt: promptForTurn(Math.max(0, options.turns - 40)),
|
||||
initialTailAnchorPrompt: promptForTurn(Math.max(0, options.turns - 5)),
|
||||
initialTailOldestPrompt: promptForTurn(Math.max(0, options.turns - 20)),
|
||||
newestOlderPagePrompt: promptForTurn(Math.max(0, options.turns - 21)),
|
||||
oldestPrompt: promptForTurn(0),
|
||||
newestPrompt: promptForTurn(options.turns - 1),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user