Files
tolaria/src/utils/vaultImages.test.ts
2026-05-10 10:30:31 +02:00

262 lines
9.5 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { resolveImageUrls, portableImageUrls } from './vaultImages'
let tauriMode = false
vi.mock('@tauri-apps/api/core', () => ({
convertFileSrc: vi.fn((path: string) => `asset://localhost/${encodeURIComponent(path)}`),
}))
vi.mock('../mock-tauri', () => ({
isTauri: () => tauriMode,
}))
function assetUrl(path: string): string {
return `asset://localhost/${encodeURIComponent(path)}`
}
function httpAssetUrl(path: string): string {
return `http://asset.localhost/${encodeURIComponent(path)}`
}
describe('resolveImageUrls', () => {
it('is a no-op outside Tauri', () => {
tauriMode = false
const markdown = '![alt](attachments/file.png)'
expect(resolveImageUrls(markdown, '/vault')).toBe(markdown)
})
it('is a no-op when vaultPath is empty', () => {
tauriMode = true
const markdown = '![alt](attachments/file.png)'
expect(resolveImageUrls(markdown, '')).toBe(markdown)
})
it('converts relative attachment paths to asset URLs', () => {
tauriMode = true
const markdown = '![screenshot](attachments/1776369786040-CleanShot_2026-04-16.png)'
expect(resolveImageUrls(markdown, '/vault')).toBe(
`![screenshot](${assetUrl('/vault/attachments/1776369786040-CleanShot_2026-04-16.png')})`,
)
})
it('converts Windows relative attachment paths without mixed separators', () => {
tauriMode = true
const vaultPath = 'C:\\Users\\lnq12\\Documents\\tolaria-test\\Getting Started'
const markdown = '![BlockNote image](attachments/1776508281809-CleanShot.png)'
expect(resolveImageUrls(markdown, vaultPath)).toBe(
`![BlockNote image](${assetUrl('C:\\Users\\lnq12\\Documents\\tolaria-test\\Getting Started\\attachments\\1776508281809-CleanShot.png')})`,
)
})
it('leaves already-correct asset URLs unchanged', () => {
tauriMode = true
const url = assetUrl('/vault/attachments/file.png')
const markdown = `![alt](${url})`
expect(resolveImageUrls(markdown, '/vault')).toBe(markdown)
})
it('rewrites legacy asset URLs from a different vault', () => {
tauriMode = true
const legacyUrl = assetUrl('/Users/luca/Workspace/tolaria-getting-started/attachments/CleanShot.png')
const markdown = `![CleanShot](${legacyUrl})`
expect(resolveImageUrls(markdown, '/Users/john/Documents/Getting Started')).toBe(
`![CleanShot](${assetUrl('/Users/john/Documents/Getting Started/attachments/CleanShot.png')})`,
)
})
it('rewrites Windows legacy asset URLs from a different vault', () => {
tauriMode = true
const legacyUrl = httpAssetUrl('C:\\Users\\old\\Workspace\\tolaria-getting-started\\attachments\\CleanShot.png')
const markdown = `![CleanShot](${legacyUrl})`
expect(resolveImageUrls(markdown, 'C:\\Users\\john\\Documents\\Getting Started')).toBe(
`![CleanShot](${assetUrl('C:\\Users\\john\\Documents\\Getting Started\\attachments\\CleanShot.png')})`,
)
})
it('leaves already-correct http asset URLs unchanged', () => {
tauriMode = true
const url = httpAssetUrl('/vault/attachments/file.png')
const markdown = `![alt](${url})`
expect(resolveImageUrls(markdown, '/vault')).toBe(markdown)
})
it('leaves external URLs unchanged', () => {
tauriMode = true
const httpImage = '![logo](https://example.com/logo.png)'
const dataImage = '![icon](data:image/png;base64,abc123)'
expect(resolveImageUrls(httpImage, '/vault')).toBe(httpImage)
expect(resolveImageUrls(dataImage, '/vault')).toBe(dataImage)
})
it('handles multiple images in one document', () => {
tauriMode = true
const markdown = `![a](${assetUrl('/old/attachments/a.png')})\n\n![b](attachments/b.png)`
const result = resolveImageUrls(markdown, '/vault')
expect(result).toContain(`![a](${assetUrl('/vault/attachments/a.png')})`)
expect(result).toContain(`![b](${assetUrl('/vault/attachments/b.png')})`)
})
it('preserves alt text and title attributes', () => {
tauriMode = true
const markdown = '![my screenshot](attachments/file.png "starter vault")'
expect(resolveImageUrls(markdown, '/vault')).toBe(
`![my screenshot](${assetUrl('/vault/attachments/file.png')} "starter vault")`,
)
})
it('resolves note-relative image paths against the active note directory', () => {
tauriMode = true
const markdown = '![shot](./img/Meeting Snapshot.png)'
expect(resolveImageUrls(markdown, '/vault', '/vault/projects/notes/plan.md')).toBe(
`![shot](${assetUrl('/vault/projects/notes/img/Meeting Snapshot.png')})`,
)
})
it('resolves parent traversal from the active note directory', () => {
tauriMode = true
const markdown = '![diagram](../shared/Architecture.png)'
expect(resolveImageUrls(markdown, '/vault', '/vault/projects/notes/plan.md')).toBe(
`![diagram](${assetUrl('/vault/projects/shared/Architecture.png')})`,
)
})
it('keeps remote and data image URLs unchanged when notePath is present', () => {
tauriMode = true
const httpImage = '![logo](https://example.com/logo.png)'
const dataImage = '![icon](data:image/png;base64,abc123)'
expect(resolveImageUrls(httpImage, '/vault', '/vault/projects/plan.md')).toBe(httpImage)
expect(resolveImageUrls(dataImage, '/vault', '/vault/projects/plan.md')).toBe(dataImage)
})
it('skips unknown asset URLs without an attachments segment', () => {
tauriMode = true
const url = httpAssetUrl('/some/other/path/file.png')
const markdown = `![alt](${url})`
expect(resolveImageUrls(markdown, '/vault')).toBe(markdown)
})
})
describe('portableImageUrls', () => {
it('converts vault attachment asset URLs to relative paths', () => {
const url = assetUrl('/vault/attachments/1776369786040-CleanShot.png')
const markdown = `![screenshot](${url})`
expect(portableImageUrls(markdown, '/vault')).toBe(
'![screenshot](attachments/1776369786040-CleanShot.png)',
)
})
it('converts legacy asset protocol attachment URLs to relative paths', () => {
const url = httpAssetUrl('/vault/attachments/legacy.png')
const markdown = `![screenshot](${url})`
expect(portableImageUrls(markdown, '/vault')).toBe(
'![screenshot](attachments/legacy.png)',
)
})
it('converts Windows extended-length asset URLs to relative paths', () => {
const url = httpAssetUrl('\\\\?\\C:\\Users\\lnq12\\Documents\\tolaria-test\\Getting Started\\attachments\\1777388840027-shot.png')
const markdown = `![screenshot](${url})`
expect(portableImageUrls(markdown, 'C:\\Users\\lnq12\\Documents\\tolaria-test\\Getting Started')).toBe(
'![screenshot](attachments/1777388840027-shot.png)',
)
})
it('is a no-op when vaultPath is empty', () => {
const url = assetUrl('/vault/attachments/file.png')
const markdown = `![alt](${url})`
expect(portableImageUrls(markdown, '')).toBe(markdown)
})
it('unwraps asset URLs from other vaults to absolute filesystem paths', () => {
const url = assetUrl('/other-vault/attachments/file.png')
const markdown = `![alt](${url})`
expect(portableImageUrls(markdown, '/vault')).toBe('![alt](/other-vault/attachments/file.png)')
})
it('leaves relative and external paths unchanged', () => {
const relativeImage = '![alt](attachments/file.png)'
const httpImage = '![logo](https://example.com/logo.png)'
expect(portableImageUrls(relativeImage, '/vault')).toBe(relativeImage)
expect(portableImageUrls(httpImage, '/vault')).toBe(httpImage)
})
it('handles multiple images', () => {
const markdown = `![a](${assetUrl('/vault/attachments/a.png')})\n\n![b](${assetUrl('/vault/attachments/b.png')})`
const result = portableImageUrls(markdown, '/vault')
expect(result).toContain('![a](attachments/a.png)')
expect(result).toContain('![b](attachments/b.png)')
})
it('preserves title attributes when converting to portable paths', () => {
const markdown = `![shot](${assetUrl('/vault/attachments/a.png')} "starter vault")`
expect(portableImageUrls(markdown, '/vault')).toBe('![shot](attachments/a.png "starter vault")')
})
it('serializes vault-local asset URLs relative to the active note directory', () => {
const markdown = `![shot](${assetUrl('/vault/projects/notes/img/Meeting Snapshot.png')})`
expect(portableImageUrls(markdown, '/vault', '/vault/projects/notes/plan.md')).toBe(
'![shot](./img/Meeting Snapshot.png)',
)
})
it('serializes vault-local asset URLs with parent traversal when needed', () => {
const markdown = `![diagram](${assetUrl('/vault/projects/shared/Architecture.png')})`
expect(portableImageUrls(markdown, '/vault', '/vault/projects/notes/plan.md')).toBe(
'![diagram](../shared/Architecture.png)',
)
})
it('unwraps external asset URLs to filesystem paths instead of saving asset scheme URLs', () => {
const markdown = `![external](${assetUrl('/Users/luca/Pictures/photo.png')})`
expect(portableImageUrls(markdown, '/vault')).toBe('![external](/Users/luca/Pictures/photo.png)')
})
})
describe('resolveImageUrls / portableImageUrls round-trip', () => {
it('keeps relative attachment markdown stable', () => {
tauriMode = true
const markdown = '![shot](attachments/file.png)'
expect(portableImageUrls(resolveImageUrls(markdown, '/vault'), '/vault')).toBe(markdown)
})
it('keeps note-relative markdown stable', () => {
tauriMode = true
const markdown = '![shot](./img/Meeting Snapshot.png)'
const notePath = '/vault/projects/notes/plan.md'
expect(
portableImageUrls(resolveImageUrls(markdown, '/vault', notePath), '/vault', notePath),
).toBe(markdown)
})
})