Intrinsic image dimensions can arrive after the timeline has painted, outside the React segment-change path. Let the browser preserve the visible content anchor during that resize instead of exposing a frame before the deferred bottom correction.
211 lines
7.6 KiB
TypeScript
211 lines
7.6 KiB
TypeScript
import { expect, test as base } from "../support/fixtures";
|
|
import {
|
|
appendSettledTimelineTurns,
|
|
createNearTenMegabyteAssistantPng,
|
|
createSettledMockAgent,
|
|
createSmallAssistantPng,
|
|
emitSettledAssistantImage,
|
|
expectAssistantImageNotMounted,
|
|
expectAssistantImageRendered,
|
|
openAssistantImageTimeline,
|
|
openExistingImageAgentTabs,
|
|
remountAndRecoverAssistantImageFromHistory,
|
|
sendFollowUpAndExpectVisibleResponse,
|
|
switchAwayAndBackWithoutImageInstability,
|
|
userPagesUntilAssistantImageRenders,
|
|
} from "../support/helpers/assistant-images";
|
|
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
|
|
|
const test = base.extend<{ imageWorkspace: SeededWorkspace }>({
|
|
imageWorkspace: async ({ page: _page }, provide) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "agent-tab-image-stability-" });
|
|
try {
|
|
await provide(workspace);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
},
|
|
});
|
|
|
|
test("switching between settled agent tabs keeps a real assistant PNG rendered", async ({
|
|
imageWorkspace: workspace,
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const image = await createSmallAssistantPng(workspace, {
|
|
alt: "Real file image",
|
|
fileName: "assistant-preview.png",
|
|
});
|
|
const imageAgent = await createSettledMockAgent(workspace, "Image timeline");
|
|
const otherAgent = await createSettledMockAgent(workspace, "Other timeline");
|
|
await emitSettledAssistantImage(workspace.client, imageAgent, image);
|
|
|
|
await openExistingImageAgentTabs(page, { imageAgent, otherAgent });
|
|
await expectAssistantImageRendered(page, image);
|
|
await switchAwayAndBackWithoutImageInstability(page, { image, imageAgent, otherAgent });
|
|
});
|
|
|
|
test("reloading a timeline anchors near-tail assistant image growth", async ({
|
|
imageWorkspace: workspace,
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const image = await createSmallAssistantPng(workspace, {
|
|
alt: "Reload geometry image",
|
|
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);
|
|
|
|
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('[role="img"]')).find(
|
|
(element) => element.getAttribute("aria-label") === accessibleName,
|
|
);
|
|
if (!target || target === state.target) {
|
|
record();
|
|
return;
|
|
}
|
|
state.target = target;
|
|
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 },
|
|
);
|
|
|
|
await openAssistantImageTimeline(page, imageAgent);
|
|
await expectAssistantImageRendered(page, image);
|
|
await page.reload();
|
|
await expectAssistantImageRendered(page, image);
|
|
await page.waitForTimeout(500);
|
|
|
|
const events = await page.evaluate(() => {
|
|
const state = (
|
|
window as unknown as {
|
|
__paseoReloadImageGeometry?: { events: Array<unknown> };
|
|
}
|
|
).__paseoReloadImageGeometry;
|
|
if (!state) throw new Error("Reload image geometry observer was not installed");
|
|
return state.events;
|
|
});
|
|
const geometryEvents = events as Array<{ height: number; scrollTop: number }>;
|
|
const initialGeometry = geometryEvents.find((event) => event.height > 0);
|
|
if (!initialGeometry)
|
|
throw new Error(`Image geometry was never measurable: ${JSON.stringify(events)}`);
|
|
const expandedGeometry = geometryEvents.find((event) => event.height > initialGeometry.height);
|
|
if (!expandedGeometry)
|
|
throw new Error(`Image geometry never expanded: ${JSON.stringify(events)}`);
|
|
expect(expandedGeometry.scrollTop, JSON.stringify(events)).toBeGreaterThan(
|
|
initialGeometry.scrollTop,
|
|
);
|
|
});
|
|
|
|
test("a real assistant PNG remains reachable through pagination and remount", async ({
|
|
imageWorkspace: workspace,
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const image = await createSmallAssistantPng(workspace, {
|
|
alt: "Paginated real file image",
|
|
fileName: "paginated-assistant-preview.png",
|
|
});
|
|
const imageAgent = await createSettledMockAgent(workspace, "Paginated image timeline");
|
|
await emitSettledAssistantImage(workspace.client, imageAgent, image);
|
|
await appendSettledTimelineTurns(workspace.client, imageAgent, 40);
|
|
|
|
await openAssistantImageTimeline(page, imageAgent);
|
|
await expectAssistantImageNotMounted(page, image);
|
|
await userPagesUntilAssistantImageRenders(page, image);
|
|
await remountAndRecoverAssistantImageFromHistory(page, image);
|
|
});
|
|
|
|
test("a near-10 MiB real assistant PNG renders and the app remains responsive", async ({
|
|
imageWorkspace: workspace,
|
|
page,
|
|
}) => {
|
|
test.setTimeout(180_000);
|
|
const image = await createNearTenMegabyteAssistantPng(workspace, {
|
|
alt: "Large real file image",
|
|
fileName: "large-assistant-preview.png",
|
|
});
|
|
const imageAgent = await createSettledMockAgent(workspace, "Large image timeline");
|
|
await emitSettledAssistantImage(workspace.client, imageAgent, image);
|
|
|
|
await openAssistantImageTimeline(page, imageAgent);
|
|
await expectAssistantImageRendered(page, image);
|
|
await sendFollowUpAndExpectVisibleResponse(page, {
|
|
prompt: "confirm responsiveness: emit 1 coalesced agent stream updates",
|
|
response: "stress-update-0",
|
|
});
|
|
});
|