297 lines
8.2 KiB
TypeScript
297 lines
8.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { renderHook } from '@testing-library/react'
|
|
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 appendFileBlock({
|
|
container,
|
|
blockId,
|
|
innerClass,
|
|
}: {
|
|
container: HTMLElement
|
|
blockId: string
|
|
innerClass: string
|
|
}) {
|
|
const block = document.createElement('div')
|
|
block.className = 'bn-block'
|
|
block.dataset.id = blockId
|
|
|
|
const fileBlock = document.createElement('div')
|
|
fileBlock.dataset.fileBlock = 'true'
|
|
|
|
const inner = document.createElement('div')
|
|
inner.className = innerClass
|
|
fileBlock.appendChild(inner)
|
|
block.appendChild(fileBlock)
|
|
container.appendChild(block)
|
|
|
|
return inner
|
|
}
|
|
|
|
function createToolbarHoverScenario(withToolbarButton = false) {
|
|
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'
|
|
block.appendChild(fileBlock)
|
|
container.appendChild(block)
|
|
document.body.appendChild(container)
|
|
|
|
const toolbar = document.createElement('div')
|
|
toolbar.className = 'bn-formatting-toolbar'
|
|
const toolbarButton = withToolbarButton ? document.createElement('button') : null
|
|
if (toolbarButton) toolbar.appendChild(toolbarButton)
|
|
document.body.appendChild(toolbar)
|
|
|
|
setRect(fileBlock, rect(300, 130, 140, 90))
|
|
setRect(toolbar, rect(322, 78, 96, 24))
|
|
|
|
return { container, toolbarButton }
|
|
}
|
|
|
|
function makeEditor(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
getSelection: vi.fn(() => null),
|
|
getTextCursorPosition: vi.fn(() => ({ block: { id: 'paragraph-block', type: 'paragraph' } })),
|
|
setTextCursorPosition: vi.fn(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('blockNoteFormattingToolbarHoverGuard', () => {
|
|
afterEach(() => {
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
it('treats the gap between the selected image block and toolbar as part of the hover bridge', () => {
|
|
expect(
|
|
isWithinFormattingToolbarHoverBridge(
|
|
{ x: 368, y: 104 },
|
|
rect(300, 130, 140, 90),
|
|
rect(322, 78, 96, 24),
|
|
),
|
|
).toBe(true)
|
|
})
|
|
|
|
it('ignores invisible rectangles and missing hover bridge context', () => {
|
|
expect(
|
|
isWithinFormattingToolbarHoverBridge(
|
|
{ x: 10, y: 10 },
|
|
rect(300, 130, 0, 90),
|
|
rect(322, 78, 96, 24),
|
|
),
|
|
).toBe(false)
|
|
|
|
expect(
|
|
shouldSuppressFormattingToolbarHoverUpdate({
|
|
eventTarget: document.body,
|
|
point: { x: 350, y: 90 },
|
|
container: null,
|
|
doc: document,
|
|
selectedFileBlockId: 'image-block',
|
|
}),
|
|
).toBe(false)
|
|
|
|
expect(
|
|
shouldSuppressFormattingToolbarHoverUpdate({
|
|
eventTarget: document.body,
|
|
point: { x: 350, y: 90 },
|
|
container: document.createElement('div'),
|
|
doc: document,
|
|
selectedFileBlockId: null,
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
|
|
it.each([
|
|
{
|
|
name: 'suppresses hover updates when the pointer is already over the toolbar',
|
|
point: { x: 350, y: 90 },
|
|
eventTarget: 'toolbarButton',
|
|
expected: true,
|
|
withToolbarButton: true,
|
|
},
|
|
{
|
|
name: 'suppresses hover updates while the pointer crosses the image-toolbar bridge',
|
|
point: { x: 368, y: 104 },
|
|
eventTarget: 'body',
|
|
expected: true,
|
|
withToolbarButton: false,
|
|
},
|
|
{
|
|
name: 'leaves unrelated pointer movement alone',
|
|
point: { x: 520, y: 220 },
|
|
eventTarget: 'body',
|
|
expected: false,
|
|
withToolbarButton: false,
|
|
},
|
|
])('$name', ({ point, eventTarget, expected, withToolbarButton }) => {
|
|
const { container, toolbarButton } = createToolbarHoverScenario(withToolbarButton)
|
|
const target = eventTarget === 'toolbarButton' ? toolbarButton : document.body
|
|
|
|
expect(
|
|
shouldSuppressFormattingToolbarHoverUpdate({
|
|
eventTarget: target as EventTarget,
|
|
point,
|
|
container,
|
|
doc: document,
|
|
selectedFileBlockId: 'image-block',
|
|
}),
|
|
).toBe(expected)
|
|
})
|
|
|
|
it('uses the filename bridge fallback and restores the last selected file block while the toolbar stays open', () => {
|
|
const container = document.createElement('div')
|
|
document.body.appendChild(container)
|
|
const bridge = appendFileBlock({
|
|
container,
|
|
blockId: 'image-block',
|
|
innerClass: 'bn-file-name-with-icon',
|
|
})
|
|
|
|
const toolbar = document.createElement('div')
|
|
toolbar.className = 'bn-formatting-toolbar'
|
|
document.body.appendChild(toolbar)
|
|
|
|
setRect(bridge, rect(300, 130, 140, 90))
|
|
setRect(toolbar, rect(322, 78, 96, 24))
|
|
|
|
const editor = makeEditor()
|
|
|
|
const { rerender } = renderHook(
|
|
({ selectedFileBlockId, isOpen }) =>
|
|
useBlockNoteFormattingToolbarHoverGuard({
|
|
editor: editor as never,
|
|
container,
|
|
selectedFileBlockId,
|
|
isOpen,
|
|
}),
|
|
{
|
|
initialProps: { selectedFileBlockId: 'image-block', isOpen: true },
|
|
},
|
|
)
|
|
|
|
rerender({ selectedFileBlockId: null, isOpen: true })
|
|
|
|
window.dispatchEvent(new MouseEvent('mousemove', {
|
|
clientX: 368,
|
|
clientY: 104,
|
|
bubbles: true,
|
|
}))
|
|
|
|
expect(editor.setTextCursorPosition).toHaveBeenCalledWith('image-block')
|
|
|
|
vi.mocked(editor.setTextCursorPosition).mockClear()
|
|
rerender({ selectedFileBlockId: null, isOpen: false })
|
|
rerender({ selectedFileBlockId: null, isOpen: true })
|
|
|
|
window.dispatchEvent(new MouseEvent('mousemove', {
|
|
clientX: 368,
|
|
clientY: 104,
|
|
bubbles: true,
|
|
}))
|
|
|
|
expect(editor.setTextCursorPosition).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('uses the add-file-button fallback and swallows restore errors when the block disappears', () => {
|
|
const container = document.createElement('div')
|
|
document.body.appendChild(container)
|
|
const bridge = appendFileBlock({
|
|
container,
|
|
blockId: 'image-block',
|
|
innerClass: 'bn-add-file-button',
|
|
})
|
|
|
|
const toolbar = document.createElement('div')
|
|
toolbar.className = 'bn-formatting-toolbar'
|
|
document.body.appendChild(toolbar)
|
|
|
|
setRect(bridge, rect(300, 130, 140, 90))
|
|
setRect(toolbar, rect(322, 78, 96, 24))
|
|
|
|
const editor = makeEditor({
|
|
setTextCursorPosition: vi.fn(() => {
|
|
throw new Error('gone')
|
|
}),
|
|
})
|
|
|
|
const { rerender } = renderHook(
|
|
({ selectedFileBlockId, isOpen }) =>
|
|
useBlockNoteFormattingToolbarHoverGuard({
|
|
editor: editor as never,
|
|
container,
|
|
selectedFileBlockId,
|
|
isOpen,
|
|
}),
|
|
{
|
|
initialProps: { selectedFileBlockId: 'image-block', isOpen: true },
|
|
},
|
|
)
|
|
|
|
rerender({ selectedFileBlockId: null, isOpen: true })
|
|
|
|
expect(() => {
|
|
window.dispatchEvent(new MouseEvent('mousemove', {
|
|
clientX: 368,
|
|
clientY: 104,
|
|
bubbles: true,
|
|
}))
|
|
}).not.toThrow()
|
|
})
|
|
|
|
it('leaves the cursor alone when the selected file block is already active', () => {
|
|
const container = document.createElement('div')
|
|
document.body.appendChild(container)
|
|
const bridge = appendFileBlock({
|
|
container,
|
|
blockId: 'image-block',
|
|
innerClass: 'bn-file-name-with-icon',
|
|
})
|
|
|
|
const toolbar = document.createElement('div')
|
|
toolbar.className = 'bn-formatting-toolbar'
|
|
document.body.appendChild(toolbar)
|
|
|
|
setRect(bridge, rect(300, 130, 140, 90))
|
|
setRect(toolbar, rect(322, 78, 96, 24))
|
|
|
|
const editor = makeEditor({
|
|
getSelection: vi.fn(() => ({
|
|
blocks: [{ id: 'image-block', type: 'image' }],
|
|
})),
|
|
})
|
|
|
|
renderHook(() =>
|
|
useBlockNoteFormattingToolbarHoverGuard({
|
|
editor: editor as never,
|
|
container,
|
|
selectedFileBlockId: 'image-block',
|
|
isOpen: true,
|
|
}),
|
|
)
|
|
|
|
window.dispatchEvent(new MouseEvent('mousemove', {
|
|
clientX: 368,
|
|
clientY: 104,
|
|
bubbles: true,
|
|
}))
|
|
|
|
expect(editor.setTextCursorPosition).not.toHaveBeenCalled()
|
|
})
|
|
})
|