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((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) }) })