diff --git a/desktop/src/lib/handlePreviewLink.test.ts b/desktop/src/lib/handlePreviewLink.test.ts new file mode 100644 index 00000000..a1ed84a6 --- /dev/null +++ b/desktop/src/lib/handlePreviewLink.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from 'vitest' +import { handlePreviewLink, type PreviewLinkDeps } from './handlePreviewLink' + +function makeDeps(overrides?: Partial): PreviewLinkDeps { + return { + sessionId: 's1', + serverBaseUrl: 'http://127.0.0.1:8787', + openBrowser: vi.fn(), + openFilePreview: vi.fn(), + openExternal: vi.fn(), + ...overrides, + } +} + +describe('handlePreviewLink', () => { + it('routes loopback urls to openBrowser with the url', () => { + const deps = makeDeps() + const handled = handlePreviewLink('http://localhost:5173/', deps) + expect(handled).toBe(true) + expect(deps.openBrowser).toHaveBeenCalledWith('s1', 'http://localhost:5173/') + expect(deps.openFilePreview).not.toHaveBeenCalled() + expect(deps.openExternal).not.toHaveBeenCalled() + }) + + it('routes absolute html file paths to openBrowser with the preview-fs url', () => { + const deps = makeDeps() + const handled = handlePreviewLink('/Users/x/index.html', deps) + expect(handled).toBe(true) + // previewFsUrl preserves the leading slash, yielding an intentional `//` + // between and the absolute path so the server resolves it as + // an absolute-within-workspace path. + expect(deps.openBrowser).toHaveBeenCalledWith( + 's1', + 'http://127.0.0.1:8787/preview-fs/s1//Users/x/index.html', + ) + expect(deps.openFilePreview).not.toHaveBeenCalled() + expect(deps.openExternal).not.toHaveBeenCalled() + }) + + it('routes relative previewable docs to openFilePreview with the relative path', () => { + const deps = makeDeps() + const handled = handlePreviewLink('docs/report.md', deps) + expect(handled).toBe(true) + expect(deps.openFilePreview).toHaveBeenCalledWith('s1', 'docs/report.md') + expect(deps.openBrowser).not.toHaveBeenCalled() + expect(deps.openExternal).not.toHaveBeenCalled() + }) + + it('routes remote http(s) to openExternal with the url', () => { + const deps = makeDeps() + const handled = handlePreviewLink('https://example.com/', deps) + expect(handled).toBe(true) + expect(deps.openExternal).toHaveBeenCalledWith('https://example.com/') + expect(deps.openBrowser).not.toHaveBeenCalled() + expect(deps.openFilePreview).not.toHaveBeenCalled() + }) + + it('returns false for ignored links and calls no deps', () => { + const deps = makeDeps() + const handled = handlePreviewLink('#x', deps) + expect(handled).toBe(false) + expect(deps.openBrowser).not.toHaveBeenCalled() + expect(deps.openFilePreview).not.toHaveBeenCalled() + expect(deps.openExternal).not.toHaveBeenCalled() + }) +}) diff --git a/desktop/src/lib/handlePreviewLink.ts b/desktop/src/lib/handlePreviewLink.ts new file mode 100644 index 00000000..57e5d164 --- /dev/null +++ b/desktop/src/lib/handlePreviewLink.ts @@ -0,0 +1,43 @@ +import { classifyPreviewLink } from './previewLinkRouter' + +export type PreviewLinkDeps = { + sessionId: string + serverBaseUrl: string + openBrowser: (sessionId: string, url: string) => void + openFilePreview: (sessionId: string, path: string) => void + openExternal: (url: string) => void +} + +/** + * Build a `/preview-fs//` URL for the local server. + * + * Absolute file paths (leading slash) are preserved as-is, so the resulting URL + * carries a `//` between `` and the path. That double slash is + * intentional: the server slices everything after the `` segment and + * runs `path.resolve(workDir, relPath)`, so an absolute path is resolved as an + * absolute-within-workspace path and sandbox-checked against the work dir root. + */ +function previewFsUrl(base: string, sessionId: string, filePath: string): string { + return `${base.replace(/\/$/, '')}/preview-fs/${encodeURIComponent(sessionId)}/${filePath.replace(/^\/+/, '/')}` +} + +/** Returns true if handled (caller should preventDefault). */ +export function handlePreviewLink(href: string, deps: PreviewLinkDeps): boolean { + const cls = classifyPreviewLink(href) + switch (cls.kind) { + case 'browser-localhost': + deps.openBrowser(deps.sessionId, cls.url!) + return true + case 'browser-file': + deps.openBrowser(deps.sessionId, previewFsUrl(deps.serverBaseUrl, deps.sessionId, cls.path!)) + return true + case 'file-preview': + deps.openFilePreview(deps.sessionId, cls.path!) + return true + case 'remote': + deps.openExternal(cls.url!) + return true + default: + return false + } +}