fix(app): preserve normalized image URLs in the lightbox

Use the decoder URL spelling when tracking image dimensions so mixed-case hosts still initialize the viewport. Keep the source check against queued loads and wait for refreshed image fitting before asserting geometry.
This commit is contained in:
paseo-bot[bot]
2026-11-08 08:06:57 +00:00
parent d0876e032f
commit f38c3d7059
3 changed files with 55 additions and 6 deletions
@@ -146,6 +146,36 @@ test("opens a timeline image in a zoomable lightbox", async ({
await expectAgentReadyToInterrupt(page);
});
test("opens a remote Markdown image whose URL needs normalization", async ({
imageWorkspace: workspace,
page,
}) => {
const image = await createSmallAssistantPng(workspace, {
alt: "Remote image with a mixed-case host",
fileName: "remote.png",
width: 240,
height: 160,
});
const delivery = await serveDelayedImage(path.join(workspace.repoPath, image.relativePath));
const url = delivery.url.replace("127.0.0.1", "LOCALHOST");
try {
delivery.release();
const agent = await createSettledMockAgent(workspace, "Remote image lightbox");
await emitSettledAssistantImage(workspace.client, agent, image, url);
await openAssistantImageTimeline(page, agent);
const rendered = page.getByRole("button", { name: image.alt }).first();
await expect(rendered).toHaveJSProperty("naturalWidth", image.width);
await rendered.click();
await expect(page.getByTestId("attachment-lightbox-canvas")).toBeVisible();
await page.getByTestId("zoomable-viewport-toolbar").hover();
await page.getByRole("button", { name: "Zoom in", exact: true }).click();
await page.getByRole("button", { name: "Close image", exact: true }).click();
await expect(page.getByTestId("attachment-lightbox-image")).toHaveCount(0);
} finally {
await delivery.close();
}
});
test("reloading a timeline reserves image height without a load-time scroll jump", async ({
imageWorkspace: workspace,
page,
+10 -2
View File
@@ -38,6 +38,15 @@ function editor(page: Page) {
return page.getByTestId("file-source-editor").filter({ visible: true }).locator(".cm-content");
}
async function expectTallImageFrame(frame: Locator): Promise<void> {
await expect
.poll(async () => {
const box = await frame.boundingBox();
return box ? box.height - box.width : 0;
})
.toBeGreaterThan(0);
}
function hasHorizontalOverflow(element: HTMLElement): boolean {
return element.scrollWidth > element.clientWidth;
}
@@ -806,8 +815,7 @@ test.describe("CodeMirror workspace file editing", () => {
await pendingLoad.release();
}
await expect(imageCanvas).toBeVisible();
const tallFrame = await transformedContent.boundingBox();
expect(tallFrame!.height).toBeGreaterThan(tallFrame!.width);
await expectTallImageFrame(transformedContent);
});
test("renders local relative images inside Markdown preview", async ({ page, withWorkspace }) => {
@@ -37,14 +37,16 @@ export function ZoomableImage({
testID = "zoomable-image",
wheelActivation = "always",
}: ZoomableImageProps) {
const source = useMemo(() => ({ uri: normalizeImageUri(uri) }), [uri]);
const [loadedImage, setLoadedImage] = useState<{ uri: string; size: ViewportSize } | null>(null);
const resolvedSize = contentSize ?? (loadedImage?.uri === uri ? loadedImage.size : null);
const source = useMemo(() => ({ uri }), [uri]);
const resolvedSize = contentSize ?? (loadedImage?.uri === source.uri ? loadedImage.size : null);
const handleLoad = useCallback(
({ source: { url, width, height } }: ImageLoadEventData) => {
if (url === uri && width > 0 && height > 0) setLoadedImage({ uri, size: { width, height } });
if (url === source.uri && width > 0 && height > 0) {
setLoadedImage({ uri: source.uri, size: { width, height } });
}
},
[uri],
[source.uri],
);
if (!resolvedSize) {
@@ -88,6 +90,15 @@ export function ZoomableImage({
);
}
// Use the same URL spelling as the decoder while preserving opaque native URIs.
function normalizeImageUri(uri: string): string {
try {
return new URL(uri).href;
} catch {
return uri;
}
}
const styles = StyleSheet.create(() => ({
root: { flex: 1, minHeight: 0 },
image: { width: "100%", height: "100%" },