Files
tolaria/src/hooks/useEditorTabSwap.selection.test.ts
2026-05-08 16:13:52 +02:00

117 lines
3.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, renderHook } from '@testing-library/react'
import { useEditorTabSwap } from './useEditorTabSwap'
const initialBlocks = [{
type: 'paragraph',
content: [{ type: 'text', text: 'Old body', styles: {} }],
children: [],
}]
function makeTab(path: string, content: string) {
return {
entry: { path, title: 'Note A', filename: 'note-a.md', type: 'Note', status: 'Active', aliases: [], isA: '' } as never,
content,
}
}
function makeEditor(docRef: { current: unknown[] }) {
const editor = {
get prosemirrorView() { return {} },
onMount: (callback: () => void) => {
callback()
return () => {}
},
replaceBlocks: vi.fn((_oldBlocks, nextBlocks) => { docRef.current = nextBlocks as unknown[] }),
insertBlocks: vi.fn(),
blocksToMarkdownLossy: vi.fn(() => 'Old body'),
tryParseMarkdownToBlocks: vi.fn(() => [{
type: 'paragraph',
content: [{ type: 'text', text: 'Fresh after background reload.', styles: {} }],
children: [],
}]),
_tiptapEditor: {
state: { doc: { content: { size: 8 } } },
commands: {
setContent: vi.fn(),
setTextSelection: vi.fn(),
},
},
}
Object.defineProperty(editor, 'document', { get: () => docRef.current })
return editor
}
function installFrameAndScrollSpies() {
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
callback(0)
return 0
})
vi.spyOn(document, 'querySelector').mockReturnValue({ scrollTop: 0 } as unknown as Element)
}
async function flushEditorTick() {
await act(() => new Promise<void>((resolve) => setTimeout(resolve, 0)))
}
function appendFocusedEditorSelection() {
const container = document.createElement('div')
container.className = 'editor__blocknote-container'
const editable = document.createElement('div')
editable.contentEditable = 'true'
editable.textContent = 'Old current note caret'
container.appendChild(editable)
document.body.appendChild(container)
const range = document.createRange()
range.setStart(editable.firstChild!, 8)
range.collapse(true)
const selection = window.getSelection()!
selection.removeAllRanges()
selection.addRange(range)
editable.focus()
return { container, editable, selection }
}
describe('useEditorTabSwap selection refresh', () => {
afterEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
})
it('clears stale editor DOM selection before refreshing the current note content', async () => {
installFrameAndScrollSpies()
const docRef = { current: initialBlocks as unknown[] }
const editor = makeEditor(docRef)
const tabA = makeTab('a.md', '---\ntitle: Note A\n---\n\n# Note A\n\nOld body.')
const refreshedTabA = makeTab(
'a.md',
'---\ntitle: Note A\n---\n\n# Note A\n\nFresh after background reload.',
)
const rendered = renderHook(
({ tabs }) => useEditorTabSwap({
tabs,
activeTabPath: 'a.md',
editor: editor as never,
}),
{ initialProps: { tabs: [tabA] } },
)
await flushEditorTick()
const { editable, selection } = appendFocusedEditorSelection()
const selectionChanges = vi.fn()
document.addEventListener('selectionchange', selectionChanges)
expect(selection.rangeCount).toBe(1)
rendered.rerender({ tabs: [refreshedTabA] })
await flushEditorTick()
expect(window.getSelection()?.rangeCount).toBe(0)
expect(selectionChanges).toHaveBeenCalled()
expect(document.activeElement).not.toBe(editable)
document.removeEventListener('selectionchange', selectionChanges)
})
})