import { act, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { isWithinFormattingToolbarHoverBridge, shouldSuppressFormattingToolbarHoverUpdate, useBlockNoteFormattingToolbarHoverGuard, } from './blockNoteFormattingToolbarHoverGuard' function rect(left: number, top: number, width: number, height: number) { return DOMRect.fromRect({ x: left, y: top, width, height }) } function setRect(element: HTMLElement, nextRect: DOMRect) { element.getBoundingClientRect = () => nextRect } function setupHoverDom() { const container = document.createElement('div') const block = document.createElement('div') block.className = 'bn-block' block.dataset.id = 'image-block' const fileBlock = document.createElement('div') fileBlock.dataset.fileBlock = 'true' const wrapper = document.createElement('div') wrapper.className = 'bn-visual-media-wrapper' fileBlock.appendChild(wrapper) block.appendChild(fileBlock) container.appendChild(block) document.body.appendChild(container) const toolbar = document.createElement('div') toolbar.className = 'bn-formatting-toolbar' document.body.appendChild(toolbar) setRect(wrapper, rect(300, 130, 140, 90)) setRect(toolbar, rect(322, 78, 96, 24)) return { container, toolbar } } function createEditor(options: { selectionType?: string | null selectionId?: string cursorType?: string cursorId?: string setTextCursorPosition?: ReturnType }) { const { selectionType = 'paragraph', selectionId = 'other-block', cursorType = 'paragraph', cursorId = 'cursor-block', setTextCursorPosition = vi.fn(), } = options return { getSelection: () => ( selectionType ? { blocks: [{ type: selectionType, id: selectionId }] } : null ), getTextCursorPosition: () => ({ block: { type: cursorType, id: cursorId } }), setTextCursorPosition, } } function dispatchBridgeMousemove(target: EventTarget = document.body) { const event = new MouseEvent('mousemove', { bubbles: true, clientX: 368, clientY: 104, }) Object.defineProperty(event, 'target', { configurable: true, value: target }) const stopPropagation = vi.fn() event.stopPropagation = stopPropagation act(() => { window.dispatchEvent(event) }) return stopPropagation } describe('blockNoteFormattingToolbarHoverGuard extra coverage', () => { beforeEach(() => { document.body.innerHTML = '' }) afterEach(() => { vi.restoreAllMocks() document.body.innerHTML = '' }) it('treats invisible rects and missing bridge inputs as non-suppressing', () => { expect( isWithinFormattingToolbarHoverBridge( { x: 12, y: 12 }, rect(10, 10, 0, 20), rect(10, 10, 20, 20), ), ).toBe(false) expect( shouldSuppressFormattingToolbarHoverUpdate({ eventTarget: document.body, point: { x: 12, y: 12 }, container: null, doc: document, selectedFileBlockId: 'image-block', }), ).toBe(false) }) it('remembers the last selected file block while open and clears it when closed', () => { const addSpy = vi.spyOn(window, 'addEventListener') const removeSpy = vi.spyOn(window, 'removeEventListener') const { container } = setupHoverDom() const setTextCursorPosition = vi.fn() const editor = createEditor({ setTextCursorPosition }) const { rerender, unmount } = renderHook( ({ selectedFileBlockId, isOpen }) => useBlockNoteFormattingToolbarHoverGuard({ editor: editor as never, container, selectedFileBlockId, isOpen, }), { initialProps: { selectedFileBlockId: 'image-block', isOpen: true }, }, ) expect(addSpy).toHaveBeenCalledWith('mousemove', expect.any(Function), true) rerender({ selectedFileBlockId: null, isOpen: true }) const stopPropagation = dispatchBridgeMousemove() expect(setTextCursorPosition).toHaveBeenCalledWith('image-block') expect(stopPropagation).toHaveBeenCalledTimes(1) rerender({ selectedFileBlockId: null, isOpen: false }) rerender({ selectedFileBlockId: null, isOpen: true }) dispatchBridgeMousemove() expect(setTextCursorPosition).toHaveBeenCalledTimes(1) unmount() expect(removeSpy).toHaveBeenCalledWith('mousemove', expect.any(Function), true) }) it('skips listener setup when the environment is invalid and safely ignores already-active or failing restores', () => { const addSpy = vi.spyOn(window, 'addEventListener') const { container } = setupHoverDom() const alreadySelectedEditor = createEditor({ selectionType: null, cursorType: 'image', cursorId: 'image-block', setTextCursorPosition: vi.fn(), }) renderHook(() => useBlockNoteFormattingToolbarHoverGuard({ editor: alreadySelectedEditor as never, container: null, selectedFileBlockId: 'image-block', isOpen: true, })) expect(addSpy).not.toHaveBeenCalled() const { unmount } = renderHook(() => useBlockNoteFormattingToolbarHoverGuard({ editor: alreadySelectedEditor as never, container, selectedFileBlockId: 'image-block', isOpen: true, })) dispatchBridgeMousemove() expect(alreadySelectedEditor.setTextCursorPosition).not.toHaveBeenCalled() unmount() const throwingEditor = createEditor({ selectionType: null, cursorType: 'paragraph', cursorId: 'other-block', setTextCursorPosition: vi.fn(() => { throw new Error('gone') }), }) renderHook(() => useBlockNoteFormattingToolbarHoverGuard({ editor: throwingEditor as never, container, selectedFileBlockId: 'image-block', isOpen: true, })) expect(() => dispatchBridgeMousemove()).not.toThrow() }) })