From 1456d461a9c0098c87a2ff2414c4454d5b1fdec7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 15 Oct 2026 19:14:43 +0200 Subject: [PATCH] test(markdown): isolate image arrival from reload positioning Mounted row totals change as history virtualizes, so observe the new response and tool directly. Hold the real PNG response after reload until the reading position settles, then verify its reserved height and viewport instead of attributing initial bottom-follow positioning to image decoding. --- .../browser/agent-message-submission.spec.ts | 14 +- .../browser/agent-tab-image-stability.spec.ts | 165 +++++------------- 2 files changed, 49 insertions(+), 130 deletions(-) diff --git a/packages/app/e2e/browser/agent-message-submission.spec.ts b/packages/app/e2e/browser/agent-message-submission.spec.ts index 3bfda6b4b0..38598f4e65 100644 --- a/packages/app/e2e/browser/agent-message-submission.spec.ts +++ b/packages/app/e2e/browser/agent-message-submission.spec.ts @@ -1154,17 +1154,15 @@ test.describe("Agent message submission", () => { page, page.getByTestId("assistant-message").last(), ); - const assistantMessageCount = await page.getByTestId("assistant-message").count(); - const toolCallCount = await page.getByTestId("tool-call-badge").count(); await composer.press("Enter"); const userMessage = page.getByTestId("user-message").filter({ hasText: prompt }).last(); await expect(userMessage).toBeVisible(); - await expect - .poll(async () => page.getByTestId("assistant-message").count()) - .toBeGreaterThan(assistantMessageCount); - await expect - .poll(async () => page.getByTestId("tool-call-badge").count()) - .toBeGreaterThan(toolCallCount); + await expect( + page.getByTestId("assistant-message").filter({ + hasText: "I'll take a look at the scroll anchor behavior you described.", + }), + ).toBeVisible(); + await awaitToolCall(page, "Read"); await finishTimelineRowStabilityCheck(); }); diff --git a/packages/app/e2e/browser/agent-tab-image-stability.spec.ts b/packages/app/e2e/browser/agent-tab-image-stability.spec.ts index de4edc93b7..7149e1aa25 100644 --- a/packages/app/e2e/browser/agent-tab-image-stability.spec.ts +++ b/packages/app/e2e/browser/agent-tab-image-stability.spec.ts @@ -26,6 +26,10 @@ import { import { expectAgentReadyToInterrupt } from "../support/helpers/agent-stream"; import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client"; import path from "node:path"; +import { + rememberTimelineViewport, + scrollTimelinePromptIntoView, +} from "../support/helpers/timeline-pagination"; const test = base.extend<{ imageWorkspace: SeededWorkspace }>({ imageWorkspace: async ({ page: _page }, provide) => { @@ -152,123 +156,34 @@ test("reloading a timeline reserves image height without a load-time scroll jump fileName: "reload-geometry-image.png", }); const imageAgent = await createSettledMockAgent(workspace, "Reload image geometry"); - await emitSettledAssistantImage(workspace.client, imageAgent, image); - await appendSettledTimelineTurns(workspace.client, imageAgent, 3); + const delivery = await serveDelayedImage(path.join(workspace.repoPath, image.relativePath)); + try { + delivery.release(); + await emitSettledAssistantImage(workspace.client, imageAgent, image, delivery.url); + await appendSettledTimelineTurns(workspace.client, imageAgent, 3); + await openAssistantImageTimeline(page, imageAgent); + await expectAssistantImageRendered(page, image); - await page.addInitScript( - ({ accessibleName }) => { - interface ImageGeometryEvent { - atMs: number; - complete: boolean; - height: number; - naturalHeight: number; - naturalWidth: number; - scrollTop: number; - width: number; - } - const state = { - accessibleName, - events: [] as ImageGeometryEvent[], - mutationObserver: null as MutationObserver | null, - resizeObserver: null as ResizeObserver | null, - target: null as Element | null, - }; - const record = () => { - const target = state.target; - if (!target) return; - const imageElement = - target instanceof HTMLImageElement ? target : target.querySelector("img"); - const scroll = target.closest('[data-testid="agent-chat-scroll"]'); - const rect = target.getBoundingClientRect(); - const event: ImageGeometryEvent = { - atMs: performance.now(), - complete: imageElement?.complete ?? false, - height: rect.height, - naturalHeight: imageElement?.naturalHeight ?? 0, - naturalWidth: imageElement?.naturalWidth ?? 0, - scrollTop: scroll instanceof HTMLElement ? scroll.scrollTop : -1, - width: rect.width, - }; - const previous = state.events.at(-1); - if ( - !previous || - previous.complete !== event.complete || - previous.height !== event.height || - previous.naturalHeight !== event.naturalHeight || - previous.naturalWidth !== event.naturalWidth || - previous.scrollTop !== event.scrollTop || - previous.width !== event.width - ) { - state.events.push(event); - } - }; - const inspect = () => { - const target = Array.from(document.querySelectorAll("img[alt]")).find( - (element) => element.getAttribute("alt") === accessibleName, - ); - if (!target || target === state.target) { - record(); - return; - } - state.target = target; - target.addEventListener("load", record, { once: true }); - state.resizeObserver?.disconnect(); - state.resizeObserver = new ResizeObserver(record); - state.resizeObserver.observe(target); - record(); - }; - const start = () => { - state.mutationObserver = new MutationObserver(inspect); - state.mutationObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ["src", "style"], - childList: true, - subtree: true, - }); - inspect(); - }; - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", start, { once: true }); - } else { - start(); - } - ( - window as unknown as { - __paseoReloadImageGeometry?: typeof state; - } - ).__paseoReloadImageGeometry = state; - }, - { accessibleName: image.alt }, - ); + delivery.holdNextRequest(); + await page.reload(); + await delivery.waitUntilRequested(); + const rendered = page.getByRole("button", { name: image.alt }).first(); + await expect(rendered).toHaveAttribute("src", /^data:image\/gif/); + await scrollTimelinePromptIntoView( + page, + "image-history-turn-2: emit 1 coalesced agent stream updates", + ); + const beforeLoad = await rememberTimelineViewport(page); + const reservedHeight = (await rendered.boundingBox())?.height; + expect(reservedHeight).toBe(320); - await openAssistantImageTimeline(page, imageAgent); - await expectAssistantImageRendered(page, image); - await page.reload(); - await expectAssistantImageRendered(page, image); - - const events = await page.evaluate(() => { - const state = ( - window as unknown as { - __paseoReloadImageGeometry?: { events: Array }; - } - ).__paseoReloadImageGeometry; - if (!state) throw new Error("Reload image geometry observer was not installed"); - return state.events; - }); - const geometryEvents = events as Array<{ - complete: boolean; - height: number; - scrollTop: number; - }>; - const loadedIndex = geometryEvents.findIndex((event) => event.complete && event.height > 0); - expect(loadedIndex, JSON.stringify(events)).toBeGreaterThanOrEqual(0); - const beforeLoad = geometryEvents[Math.max(0, loadedIndex - 1)]!; - const loaded = geometryEvents[loadedIndex]!; - expect( - new Set(geometryEvents.filter((event) => event.height > 0).map((event) => event.height)).size, - ).toBe(1); - expect(loaded.height).toBe(beforeLoad.height); - expect(loaded.scrollTop).toBe(beforeLoad.scrollTop); + delivery.release(); + await expectAssistantImageRendered(page, image); + expect((await rendered.boundingBox())?.height).toBe(reservedHeight); + await expect.poll(() => rememberTimelineViewport(page)).toEqual(beforeLoad); + } finally { + await delivery.close(); + } }); test("a real assistant PNG remains reachable through pagination and remount", async ({ @@ -365,13 +280,18 @@ test("pending assistant images reserve their decoded height", async ({ /** Serve the fixture's real PNG bytes; release controls when the decoder can finish. */ async function serveDelayedImage(file: string) { let release!: () => void; - const ready = new Promise((resolve) => { - release = resolve; - }); + let ready: Promise; let requested!: () => void; - const requestStarted = new Promise((resolve) => { - requested = resolve; - }); + let requestStarted: Promise; + const holdNextRequest = () => { + ready = new Promise((resolve) => { + release = resolve; + }); + requestStarted = new Promise((resolve) => { + requested = resolve; + }); + }; + holdNextRequest(); const server = createServer((_request, response) => { requested(); void ready.then(() => { @@ -384,7 +304,8 @@ async function serveDelayedImage(file: string) { if (!address || typeof address === "string") throw new Error("Image server did not listen"); return { url: `http://127.0.0.1:${address.port}/image.png`, - release, + release: () => release(), + holdNextRequest, waitUntilRequested: () => requestStarted, close: async () => { release();