diff --git a/packages/app/e2e/browser/file-editing.spec.ts b/packages/app/e2e/browser/file-editing.spec.ts index 3a86b1aeb4..01cbfd0a73 100644 --- a/packages/app/e2e/browser/file-editing.spec.ts +++ b/packages/app/e2e/browser/file-editing.spec.ts @@ -71,6 +71,14 @@ async function selectFileView(page: Page, view: "Preview" | "Source"): Promise { + const image = page.getByRole("img", { name, exact: true }); + await expect(image).toBeVisible(); + await expect + .poll(() => image.locator("img").evaluate((element: HTMLImageElement) => element.naturalWidth)) + .toBeGreaterThan(0); +} + function watchRequestsTo(page: Page, origin: string): string[] { const requests: string[] = []; page.on("request", (request) => { @@ -700,20 +708,18 @@ test.describe("CodeMirror workspace file editing", () => { "utf8", ); await workspace.navigateTo(); - const tree = page.getByTestId("file-explorer-tree-scroll"); - if (!(await tree.isVisible())) await openFileExplorer(page); + await openFileExplorer(page); await expandFolder(page, "docs"); await openFileFromExplorer(page, "guide.md"); - await expectFileTabOpen(page, "guide.md"); + await expectFileTabOpen(page, "docs/guide.md"); const visibleFilePane = page.getByTestId("workspace-file-pane").filter({ visible: true }); await expect(visibleFilePane.getByText("Guide", { exact: true })).toBeVisible(); - const previewImages = visibleFilePane.getByTestId("markdown-preview-image"); - await expect(previewImages).toHaveCount(2); - for (const image of await previewImages.locator("img").all()) { - await expect(image).toBeVisible(); - await expect.poll(async () => (await image.getAttribute("src")) ?? "").not.toBe(""); - } + await expectMarkdownImageLoaded(page, "sibling"); + await expectMarkdownImageLoaded(page, "hero"); + await visibleFilePane.screenshot({ + path: test.info().outputPath("markdown-relative-images.png"), + }); }); test("previews and refreshes an HTML plan while preserving source access", async ({ diff --git a/packages/app/src/file-pane/markdown-preview/image-source.test.ts b/packages/app/src/file-pane/markdown-preview/image-source.test.ts index a0d1ca4fe7..97a22e0a75 100644 --- a/packages/app/src/file-pane/markdown-preview/image-source.test.ts +++ b/packages/app/src/file-pane/markdown-preview/image-source.test.ts @@ -21,13 +21,13 @@ describe("resolveMarkdownFileDirectory", () => { ).toBe("/Users/test/project/docs/星谷云"); }); - it("returns null for home-relative markdown paths", () => { + it("preserves the host home directory for home-relative markdown paths", () => { expect( resolveMarkdownFileDirectory({ markdownPath: "~/.paseo/plans/note.md", workspaceRoot: "/Users/test/project", }), - ).toBeNull(); + ).toBe("~/.paseo/plans"); }); }); @@ -96,14 +96,34 @@ describe("resolveMarkdownPreviewImageSource", () => { ).toBe("/tmp/paseo-preview.png"); }); - it("falls back to the raw relative path when the markdown directory is unknown", () => { + it("resolves images beside a home-relative document without using the workspace root", () => { expect( resolveMarkdownPreviewImageSource({ source: "screenshots/output.png", markdownPath: "~/.paseo/plans/note.md", workspaceRoot: "/Users/test/project", }), - ).toBe("screenshots/output.png"); + ).toBe("~/.paseo/plans/screenshots/output.png"); + }); + + it("decodes a file URI exactly once", () => { + expect( + resolveMarkdownPreviewImageSource({ + source: "file:///tmp/hero%2520image.png", + markdownPath: "docs/guide.md", + workspaceRoot: "/workspace", + }), + ).toBe("/tmp/hero%20image.png"); + }); + + it("resolves parent segments from a home-relative document", () => { + expect( + resolveMarkdownPreviewImageSource({ + source: "../images/hero.png", + markdownPath: "~/docs/guide.md", + workspaceRoot: "/workspace", + }), + ).toBe("~/images/hero.png"); }); it("resolves Windows relative images against a drive-rooted markdown file", () => { diff --git a/packages/app/src/file-pane/markdown-preview/image-source.ts b/packages/app/src/file-pane/markdown-preview/image-source.ts index 50ec95ddff..c7263ac749 100644 --- a/packages/app/src/file-pane/markdown-preview/image-source.ts +++ b/packages/app/src/file-pane/markdown-preview/image-source.ts @@ -27,6 +27,9 @@ function normalizePathSegments(value: string): string { function normalizeAbsolutePath(value: string): string | null { const normalizedInput = trimTrailingSeparators(value.replace(/\\/g, "/")); + if (isHomeRelativePath(normalizedInput)) { + return `~/${normalizePathSegments(normalizedInput.slice(2))}`; + } if (!isAbsolutePath(normalizedInput)) { return null; } @@ -91,11 +94,11 @@ export function resolveMarkdownFileDirectory(input: { workspaceRoot?: string; }): string | null { const markdownPath = input.markdownPath.trim().replace(/\\/g, "/"); - if (!markdownPath || isHomeRelativePath(markdownPath)) { + if (!markdownPath) { return null; } - if (isAbsolutePath(markdownPath)) { + if (isAbsolutePath(markdownPath) || isHomeRelativePath(markdownPath)) { const absolute = normalizeAbsolutePath(markdownPath); return absolute ? parentDirectory(absolute) : null; } @@ -123,7 +126,9 @@ export function resolveMarkdownPreviewImageSource(input: { return source; } - const path = decodeRelativeSource(localFileSourceToPath(source)); + const path = localFileSourceToPath( + source.startsWith("file://") ? source : decodeRelativeSource(source), + ); if (!path) { return null; } diff --git a/packages/app/src/file-pane/markdown-preview/index.tsx b/packages/app/src/file-pane/markdown-preview/index.tsx index 83746a0cf9..a79705323f 100644 --- a/packages/app/src/file-pane/markdown-preview/index.tsx +++ b/packages/app/src/file-pane/markdown-preview/index.tsx @@ -8,7 +8,6 @@ import { MarkdownRenderer, type MarkdownStyles, } from "@/components/markdown/renderer"; -import { MAX_CONTENT_WIDTH } from "@/constants/layout"; import { parseMarkdownPreviewDocument } from "./document"; import { resolveMarkdownPreviewImageSource } from "./image-source"; import { FileMarkdownPreviewImage } from "./preview-image";