fix: warm parsed blocks for large notes
This commit is contained in:
143
src/hooks/editorParsedBlockPreload.test.tsx
Normal file
143
src/hooks/editorParsedBlockPreload.test.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { MutableRefObject } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { cacheNoteContent, clearPrefetchCache } from './useTabManagement'
|
||||
import {
|
||||
PARSED_BLOCK_PRELOAD_DELAY_MS,
|
||||
PARSED_BLOCK_PRELOAD_MIN_BYTES,
|
||||
useParsedBlockPreload,
|
||||
} from './editorParsedBlockPreload'
|
||||
|
||||
type RefSet = {
|
||||
activeTabPathRef: MutableRefObject<string | null>
|
||||
editorMountedRef: MutableRefObject<boolean>
|
||||
foregroundWorkAtRef: MutableRefObject<number>
|
||||
rawModeRef: MutableRefObject<boolean>
|
||||
}
|
||||
|
||||
function makeEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {
|
||||
return {
|
||||
path: '/vault/large.md',
|
||||
filename: 'large.md',
|
||||
title: 'Large',
|
||||
isA: 'Note',
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
status: 'Active',
|
||||
archived: false,
|
||||
modifiedAt: 1_700_000_000,
|
||||
createdAt: 1_700_000_000,
|
||||
fileSize: PARSED_BLOCK_PRELOAD_MIN_BYTES + 1,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
sidebarLabel: null,
|
||||
template: null,
|
||||
sort: null,
|
||||
view: null,
|
||||
visible: null,
|
||||
organized: false,
|
||||
favorite: false,
|
||||
favoriteIndex: null,
|
||||
listPropertiesDisplay: [],
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
hasH1: true,
|
||||
fileKind: 'markdown',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function makeRefs(overrides: Partial<{
|
||||
activeTabPath: string | null
|
||||
editorMounted: boolean
|
||||
foregroundWorkAt: number
|
||||
rawMode: boolean
|
||||
}> = {}): RefSet {
|
||||
return {
|
||||
activeTabPathRef: { current: overrides.activeTabPath ?? '/vault/open.md' },
|
||||
editorMountedRef: { current: overrides.editorMounted ?? true },
|
||||
foregroundWorkAtRef: { current: overrides.foregroundWorkAt ?? 0 },
|
||||
rawModeRef: { current: overrides.rawMode ?? false },
|
||||
}
|
||||
}
|
||||
|
||||
function renderParsedPreload(refs: RefSet, prepareParsedBlocks: (event: {
|
||||
entry: VaultEntry | null
|
||||
path: string
|
||||
content: string
|
||||
}) => Promise<void>) {
|
||||
return renderHook(() => useParsedBlockPreload({
|
||||
...refs,
|
||||
prepareParsedBlocks,
|
||||
}))
|
||||
}
|
||||
|
||||
async function emitResolvedContent(entry: VaultEntry, content = '# Large\n\nBody'): Promise<void> {
|
||||
await act(async () => {
|
||||
cacheNoteContent(entry.path, content, entry)
|
||||
await vi.advanceTimersByTimeAsync(PARSED_BLOCK_PRELOAD_DELAY_MS)
|
||||
})
|
||||
}
|
||||
|
||||
describe('useParsedBlockPreload', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-05-14T12:00:00Z'))
|
||||
clearPrefetchCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clearPrefetchCache()
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('prepares parsed blocks for a warmed large markdown note after foreground work is idle', async () => {
|
||||
const refs = makeRefs()
|
||||
const prepareParsedBlocks = vi.fn(async () => {})
|
||||
const entry = makeEntry()
|
||||
|
||||
renderParsedPreload(refs, prepareParsedBlocks)
|
||||
await emitResolvedContent(entry, '# Large\n\nPrepared body.')
|
||||
|
||||
expect(prepareParsedBlocks).toHaveBeenCalledWith({
|
||||
entry,
|
||||
path: entry.path,
|
||||
content: '# Large\n\nPrepared body.',
|
||||
})
|
||||
})
|
||||
|
||||
it('skips entries that should not compete with the foreground editor', async () => {
|
||||
const activeEntry = makeEntry({ path: '/vault/active.md' })
|
||||
const refs = makeRefs({ activeTabPath: activeEntry.path })
|
||||
const prepareParsedBlocks = vi.fn(async () => {})
|
||||
renderParsedPreload(refs, prepareParsedBlocks)
|
||||
|
||||
await emitResolvedContent(activeEntry)
|
||||
await emitResolvedContent(makeEntry({ path: '/vault/small.md', fileSize: PARSED_BLOCK_PRELOAD_MIN_BYTES - 1 }))
|
||||
await emitResolvedContent(makeEntry({ path: '/vault/file.bin', fileKind: 'binary' }))
|
||||
|
||||
expect(prepareParsedBlocks).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('drops a candidate that becomes active before the idle preparse starts', async () => {
|
||||
const entry = makeEntry({ path: '/vault/soon-active.md' })
|
||||
const refs = makeRefs()
|
||||
const prepareParsedBlocks = vi.fn(async () => {})
|
||||
|
||||
renderParsedPreload(refs, prepareParsedBlocks)
|
||||
cacheNoteContent(entry.path, '# Soon Active\n\nBody', entry)
|
||||
refs.activeTabPathRef.current = entry.path
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PARSED_BLOCK_PRELOAD_DELAY_MS)
|
||||
})
|
||||
|
||||
expect(prepareParsedBlocks).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -5,7 +5,7 @@ import { subscribeNoteContentResolved } from './noteContentCache'
|
||||
export const PARSED_BLOCK_PRELOAD_MIN_BYTES = 32 * 1024
|
||||
export const PARSED_BLOCK_PRELOAD_DELAY_MS = 1800
|
||||
export const PARSED_BLOCK_PRELOAD_FOREGROUND_IDLE_MS = 1500
|
||||
export const PARSED_BLOCK_PRELOAD_ENABLED = false
|
||||
export const PARSED_BLOCK_PRELOAD_ENABLED = true
|
||||
|
||||
type PrepareParsedBlocks = (event: NoteContentResolvedEvent) => Promise<void>
|
||||
|
||||
|
||||
@@ -1,5 +1,215 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { slugifyPathStem } from './editorTabContent'
|
||||
import {
|
||||
extractEditorBody,
|
||||
getH1TextFromBlocks,
|
||||
normalizeParsedImageBlocks,
|
||||
replaceTitleInFrontmatter,
|
||||
slugifyPathStem,
|
||||
} from './editorTabContent'
|
||||
|
||||
function makeHeadingBlocks(
|
||||
content: Array<Record<string, unknown>>,
|
||||
level = 1,
|
||||
) {
|
||||
return [{
|
||||
type: 'heading',
|
||||
props: { level },
|
||||
content,
|
||||
}]
|
||||
}
|
||||
|
||||
describe('extractEditorBody', () => {
|
||||
it('strips frontmatter and preserves H1 heading for new note content', () => {
|
||||
const content = '---\ntitle: Untitled note\ntype: Note\nstatus: Active\n---\n\n# Untitled note\n\n'
|
||||
expect(extractEditorBody(content)).toBe('# Untitled note\n\n')
|
||||
})
|
||||
|
||||
it('strips frontmatter and preserves H1 with body content', () => {
|
||||
const content = '---\ntitle: Test\n---\n# Test\n\nBody text here.'
|
||||
expect(extractEditorBody(content)).toBe('# Test\n\nBody text here.')
|
||||
})
|
||||
|
||||
it('preserves H1 and body content after frontmatter', () => {
|
||||
const content = '---\ntitle: My Note\ntype: Note\n---\n\n# My Note\n\nFirst paragraph.\n\nSecond paragraph.'
|
||||
expect(extractEditorBody(content)).toBe('# My Note\n\nFirst paragraph.\n\nSecond paragraph.')
|
||||
})
|
||||
|
||||
it('handles content without frontmatter', () => {
|
||||
const content = '# Just a Heading\n\nSome body text.'
|
||||
expect(extractEditorBody(content)).toBe('# Just a Heading\n\nSome body text.')
|
||||
})
|
||||
|
||||
it('handles content without frontmatter or heading', () => {
|
||||
const content = 'Just plain text.'
|
||||
expect(extractEditorBody(content)).toBe('Just plain text.')
|
||||
})
|
||||
|
||||
it('handles completely empty content', () => {
|
||||
expect(extractEditorBody('')).toBe('')
|
||||
})
|
||||
|
||||
it('handles frontmatter-only content', () => {
|
||||
const content = '---\ntitle: Empty\n---\n'
|
||||
expect(extractEditorBody(content)).toBe('')
|
||||
})
|
||||
|
||||
it('preserves wikilinks in body', () => {
|
||||
const content = '---\ntitle: Test\n---\n\n# Test\n\nSee [[Other Note]] for details.'
|
||||
expect(extractEditorBody(content)).toBe('# Test\n\nSee [[Other Note]] for details.')
|
||||
})
|
||||
|
||||
it('preserves non-leading headings', () => {
|
||||
const content = '---\ntitle: Test\n---\n\nSome intro text.\n\n# A Heading\n\nMore text.'
|
||||
expect(extractEditorBody(content)).toBe('Some intro text.\n\n# A Heading\n\nMore text.')
|
||||
})
|
||||
|
||||
it('preserves H1 for buildNoteContent output', () => {
|
||||
const content = '---\ntitle: My Project\ntype: Project\nstatus: Active\n---\n\n# My Project\n\n'
|
||||
expect(extractEditorBody(content)).toBe('# My Project\n\n')
|
||||
})
|
||||
|
||||
it('preserves an empty H1 for untitled-note content', () => {
|
||||
const content = '---\ntype: Note\nstatus: Active\n---\n\n# \n\n'
|
||||
expect(extractEditorBody(content)).toBe('# \n\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getH1TextFromBlocks', () => {
|
||||
it('returns text from H1 heading block', () => {
|
||||
const blocks = makeHeadingBlocks([{ type: 'text', text: 'My Title', styles: {} }])
|
||||
expect(getH1TextFromBlocks(blocks)).toBe('My Title')
|
||||
})
|
||||
|
||||
it('returns null for empty blocks', () => {
|
||||
expect(getH1TextFromBlocks([])).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for non-heading first block', () => {
|
||||
const blocks = [{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Just text' }],
|
||||
}]
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for H2 heading', () => {
|
||||
const blocks = makeHeadingBlocks([{ type: 'text', text: 'Subtitle' }], 2)
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('concatenates multiple text spans', () => {
|
||||
const blocks = makeHeadingBlocks([
|
||||
{ type: 'text', text: 'Hello ' },
|
||||
{ type: 'text', text: 'World' },
|
||||
])
|
||||
expect(getH1TextFromBlocks(blocks)).toBe('Hello World')
|
||||
})
|
||||
|
||||
it('returns null for empty H1 content', () => {
|
||||
const blocks = makeHeadingBlocks([])
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for whitespace-only H1', () => {
|
||||
const blocks = makeHeadingBlocks([{ type: 'text', text: ' ' }])
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when blocks is null/undefined', () => {
|
||||
expect(getH1TextFromBlocks(null as unknown as unknown[])).toBeNull()
|
||||
expect(getH1TextFromBlocks(undefined as unknown as unknown[])).toBeNull()
|
||||
})
|
||||
|
||||
it('filters non-text inline content', () => {
|
||||
const blocks = makeHeadingBlocks([
|
||||
{ type: 'text', text: 'Title' },
|
||||
{ type: 'wikilink', props: { target: 'linked' } },
|
||||
])
|
||||
expect(getH1TextFromBlocks(blocks)).toBe('Title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('replaceTitleInFrontmatter', () => {
|
||||
it('replaces title value in frontmatter', () => {
|
||||
const fm = '---\ntitle: Old Title\ntype: Note\n---\n\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'New Title')).toBe('---\ntitle: New Title\ntype: Note\n---\n\n')
|
||||
})
|
||||
|
||||
it('handles title with extra spaces after colon', () => {
|
||||
const fm = '---\ntitle: Old Title\n---\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'New Title')).toBe('---\ntitle: New Title\n---\n')
|
||||
})
|
||||
|
||||
it('returns unchanged frontmatter when no title line exists', () => {
|
||||
const fm = '---\ntype: Note\n---\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'New Title')).toBe('---\ntype: Note\n---\n')
|
||||
})
|
||||
|
||||
it('replaces only the title line, not other fields', () => {
|
||||
const fm = '---\ntitle: Old\ntype: Note\nstatus: Active\n---\n\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'Updated')).toBe('---\ntitle: Updated\ntype: Note\nstatus: Active\n---\n\n')
|
||||
})
|
||||
|
||||
it('handles empty string as frontmatter', () => {
|
||||
expect(replaceTitleInFrontmatter('', 'Title')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeParsedImageBlocks', () => {
|
||||
it('clears broken-image fallback widths from local asset image blocks', () => {
|
||||
const blocks = normalizeParsedImageBlocks([{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fshot.png',
|
||||
previewWidth: 16,
|
||||
},
|
||||
}])
|
||||
|
||||
expect(blocks).toEqual([{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fshot.png',
|
||||
previewWidth: undefined,
|
||||
},
|
||||
}])
|
||||
})
|
||||
|
||||
it('preserves explicit image widths and non-local image URLs', () => {
|
||||
const blocks = normalizeParsedImageBlocks([
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fresized.png',
|
||||
previewWidth: 240,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'https://example.test/preview.png',
|
||||
previewWidth: 16,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(blocks).toEqual([
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fresized.png',
|
||||
previewWidth: 240,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'https://example.test/preview.png',
|
||||
previewWidth: 16,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('slugifyPathStem', () => {
|
||||
it('preserves Unicode title stems for untitled rename detection', () => {
|
||||
|
||||
@@ -1,203 +1,8 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { extractEditorBody, getH1TextFromBlocks, replaceTitleInFrontmatter, RICH_EDITOR_CHANGE_DEBOUNCE_MS, useEditorTabSwap } from './useEditorTabSwap'
|
||||
import { normalizeParsedImageBlocks } from './editorTabContent'
|
||||
import { cacheNoteContent, clearPrefetchCache } from './useTabManagement'
|
||||
import { RICH_EDITOR_CHANGE_DEBOUNCE_MS, useEditorTabSwap } from './useEditorTabSwap'
|
||||
import { cacheParsedNoteBlocks, clearParsedNoteBlockCache } from './editorParsedBlockCache'
|
||||
|
||||
describe('extractEditorBody', () => {
|
||||
it('strips frontmatter and preserves H1 heading for new note content', () => {
|
||||
const content = '---\ntitle: Untitled note\ntype: Note\nstatus: Active\n---\n\n# Untitled note\n\n'
|
||||
expect(extractEditorBody(content)).toBe('# Untitled note\n\n')
|
||||
})
|
||||
|
||||
it('strips frontmatter and preserves H1 with body content', () => {
|
||||
const content = '---\ntitle: Test\n---\n# Test\n\nBody text here.'
|
||||
expect(extractEditorBody(content)).toBe('# Test\n\nBody text here.')
|
||||
})
|
||||
|
||||
it('preserves H1 and body content after frontmatter', () => {
|
||||
const content = '---\ntitle: My Note\ntype: Note\n---\n\n# My Note\n\nFirst paragraph.\n\nSecond paragraph.'
|
||||
expect(extractEditorBody(content)).toBe('# My Note\n\nFirst paragraph.\n\nSecond paragraph.')
|
||||
})
|
||||
|
||||
it('handles content without frontmatter', () => {
|
||||
const content = '# Just a Heading\n\nSome body text.'
|
||||
expect(extractEditorBody(content)).toBe('# Just a Heading\n\nSome body text.')
|
||||
})
|
||||
|
||||
it('handles content without frontmatter or heading', () => {
|
||||
const content = 'Just plain text.'
|
||||
expect(extractEditorBody(content)).toBe('Just plain text.')
|
||||
})
|
||||
|
||||
it('handles completely empty content', () => {
|
||||
expect(extractEditorBody('')).toBe('')
|
||||
})
|
||||
|
||||
it('handles frontmatter-only content', () => {
|
||||
const content = '---\ntitle: Empty\n---\n'
|
||||
expect(extractEditorBody(content)).toBe('')
|
||||
})
|
||||
|
||||
it('preserves wikilinks in body', () => {
|
||||
const content = '---\ntitle: Test\n---\n\n# Test\n\nSee [[Other Note]] for details.'
|
||||
expect(extractEditorBody(content)).toBe('# Test\n\nSee [[Other Note]] for details.')
|
||||
})
|
||||
|
||||
it('preserves non-leading headings', () => {
|
||||
const content = '---\ntitle: Test\n---\n\nSome intro text.\n\n# A Heading\n\nMore text.'
|
||||
expect(extractEditorBody(content)).toBe('Some intro text.\n\n# A Heading\n\nMore text.')
|
||||
})
|
||||
|
||||
it('preserves H1 for buildNoteContent output', () => {
|
||||
const content = '---\ntitle: My Project\ntype: Project\nstatus: Active\n---\n\n# My Project\n\n'
|
||||
expect(extractEditorBody(content)).toBe('# My Project\n\n')
|
||||
})
|
||||
|
||||
it('preserves an empty H1 for untitled-note content', () => {
|
||||
const content = '---\ntype: Note\nstatus: Active\n---\n\n# \n\n'
|
||||
expect(extractEditorBody(content)).toBe('# \n\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getH1TextFromBlocks', () => {
|
||||
it('returns text from H1 heading block', () => {
|
||||
const blocks = makeHeadingBlocks([{ type: 'text', text: 'My Title', styles: {} }])
|
||||
expect(getH1TextFromBlocks(blocks)).toBe('My Title')
|
||||
})
|
||||
|
||||
it('returns null for empty blocks', () => {
|
||||
expect(getH1TextFromBlocks([])).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for non-heading first block', () => {
|
||||
const blocks = [{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Just text' }],
|
||||
}]
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for H2 heading', () => {
|
||||
const blocks = makeHeadingBlocks([{ type: 'text', text: 'Subtitle' }], 2)
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('concatenates multiple text spans', () => {
|
||||
const blocks = makeHeadingBlocks([
|
||||
{ type: 'text', text: 'Hello ' },
|
||||
{ type: 'text', text: 'World' },
|
||||
])
|
||||
expect(getH1TextFromBlocks(blocks)).toBe('Hello World')
|
||||
})
|
||||
|
||||
it('returns null for empty H1 content', () => {
|
||||
const blocks = makeHeadingBlocks([])
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for whitespace-only H1', () => {
|
||||
const blocks = makeHeadingBlocks([{ type: 'text', text: ' ' }])
|
||||
expect(getH1TextFromBlocks(blocks)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when blocks is null/undefined', () => {
|
||||
expect(getH1TextFromBlocks(null as unknown as unknown[])).toBeNull()
|
||||
expect(getH1TextFromBlocks(undefined as unknown as unknown[])).toBeNull()
|
||||
})
|
||||
|
||||
it('filters non-text inline content', () => {
|
||||
const blocks = makeHeadingBlocks([
|
||||
{ type: 'text', text: 'Title' },
|
||||
{ type: 'wikilink', props: { target: 'linked' } },
|
||||
])
|
||||
expect(getH1TextFromBlocks(blocks)).toBe('Title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('replaceTitleInFrontmatter', () => {
|
||||
it('replaces title value in frontmatter', () => {
|
||||
const fm = '---\ntitle: Old Title\ntype: Note\n---\n\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'New Title')).toBe('---\ntitle: New Title\ntype: Note\n---\n\n')
|
||||
})
|
||||
|
||||
it('handles title with extra spaces after colon', () => {
|
||||
const fm = '---\ntitle: Old Title\n---\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'New Title')).toBe('---\ntitle: New Title\n---\n')
|
||||
})
|
||||
|
||||
it('returns unchanged frontmatter when no title line exists', () => {
|
||||
const fm = '---\ntype: Note\n---\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'New Title')).toBe('---\ntype: Note\n---\n')
|
||||
})
|
||||
|
||||
it('replaces only the title line, not other fields', () => {
|
||||
const fm = '---\ntitle: Old\ntype: Note\nstatus: Active\n---\n\n'
|
||||
expect(replaceTitleInFrontmatter(fm, 'Updated')).toBe('---\ntitle: Updated\ntype: Note\nstatus: Active\n---\n\n')
|
||||
})
|
||||
|
||||
it('handles empty string as frontmatter', () => {
|
||||
expect(replaceTitleInFrontmatter('', 'Title')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeParsedImageBlocks', () => {
|
||||
it('clears broken-image fallback widths from local asset image blocks', () => {
|
||||
const blocks = normalizeParsedImageBlocks([{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fshot.png',
|
||||
previewWidth: 16,
|
||||
},
|
||||
}])
|
||||
|
||||
expect(blocks).toEqual([{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fshot.png',
|
||||
previewWidth: undefined,
|
||||
},
|
||||
}])
|
||||
})
|
||||
|
||||
it('preserves explicit image widths and non-local image URLs', () => {
|
||||
const blocks = normalizeParsedImageBlocks([
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fresized.png',
|
||||
previewWidth: 240,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'https://example.test/preview.png',
|
||||
previewWidth: 16,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(blocks).toEqual([
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'asset://localhost/%2Fvault%2Fattachments%2Fresized.png',
|
||||
previewWidth: 240,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
props: {
|
||||
url: 'https://example.test/preview.png',
|
||||
previewWidth: 16,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
const blocksA = [{ type: 'paragraph', content: [{ type: 'text', text: 'A' }] }]
|
||||
|
||||
function makeTextParagraphBlock(text: string) {
|
||||
@@ -248,17 +53,6 @@ function makeMockEditor(docRef: { current: unknown[] }) {
|
||||
return editor
|
||||
}
|
||||
|
||||
function makeHeadingBlocks(
|
||||
content: Array<Record<string, unknown>>,
|
||||
level = 1,
|
||||
) {
|
||||
return [{
|
||||
type: 'heading',
|
||||
props: { level },
|
||||
content,
|
||||
}]
|
||||
}
|
||||
|
||||
function makeLongNoteBlocks(wordCount: number) {
|
||||
const words = Array.from({ length: wordCount }, (_, index) => `word${index}`)
|
||||
const paragraphs: unknown[] = []
|
||||
@@ -512,32 +306,6 @@ describe('useEditorTabSwap raw mode sync', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('does not preparse prefetched note blocks in the background', async () => {
|
||||
clearPrefetchCache()
|
||||
const tabA = makeTab('a.md', 'Note A')
|
||||
const tabB = {
|
||||
...makeTab('b.md', 'Likely Next'),
|
||||
content: '---\ntitle: Likely Next\n---\n\n# Likely Next\n\nPrepared body.',
|
||||
}
|
||||
|
||||
const { mockEditor, rerenderWith } = await createSwapHarness({
|
||||
initialProps: { tabs: [tabA], activeTabPath: 'a.md', rawMode: false },
|
||||
})
|
||||
mockEditor.tryParseMarkdownToBlocks.mockClear()
|
||||
|
||||
cacheNoteContent('b.md', tabB.content)
|
||||
await flushEditorTick()
|
||||
|
||||
expect(mockEditor.tryParseMarkdownToBlocks).not.toHaveBeenCalled()
|
||||
|
||||
await rerenderWith({ tabs: [tabB], activeTabPath: 'b.md' })
|
||||
|
||||
expect(mockEditor.tryParseMarkdownToBlocks).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Prepared body.'),
|
||||
)
|
||||
clearPrefetchCache()
|
||||
})
|
||||
|
||||
it('clears stale editor DOM selection before switching notes', async () => {
|
||||
const tabA = makeTab('a.md', 'Note A')
|
||||
const tabB = makeTab('b.md', 'Note B')
|
||||
|
||||
Reference in New Issue
Block a user