test: increase strategic coverage
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
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) {
|
||||
@@ -12,7 +14,69 @@ 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(
|
||||
@@ -23,92 +87,210 @@ describe('blockNoteFormattingToolbarHoverGuard', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('suppresses hover updates when the pointer is already over the toolbar', () => {
|
||||
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 = document.createElement('button')
|
||||
toolbar.appendChild(toolbarButton)
|
||||
document.body.appendChild(toolbar)
|
||||
|
||||
setRect(fileBlock, rect(300, 130, 140, 90))
|
||||
setRect(toolbar, rect(322, 78, 96, 24))
|
||||
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: toolbarButton,
|
||||
eventTarget: document.body,
|
||||
point: { x: 350, y: 90 },
|
||||
container,
|
||||
doc: document,
|
||||
selectedFileBlockId: 'image-block',
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('suppresses hover updates while the pointer crosses the image-toolbar bridge', () => {
|
||||
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'
|
||||
document.body.appendChild(toolbar)
|
||||
|
||||
setRect(fileBlock, rect(300, 130, 140, 90))
|
||||
setRect(toolbar, rect(322, 78, 96, 24))
|
||||
|
||||
expect(
|
||||
shouldSuppressFormattingToolbarHoverUpdate({
|
||||
eventTarget: document.body,
|
||||
point: { x: 368, y: 104 },
|
||||
container,
|
||||
doc: document,
|
||||
selectedFileBlockId: 'image-block',
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves unrelated pointer movement alone', () => {
|
||||
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'
|
||||
document.body.appendChild(toolbar)
|
||||
|
||||
setRect(fileBlock, rect(300, 130, 140, 90))
|
||||
setRect(toolbar, rect(322, 78, 96, 24))
|
||||
|
||||
expect(
|
||||
shouldSuppressFormattingToolbarHoverUpdate({
|
||||
eventTarget: document.body,
|
||||
point: { x: 520, y: 220 },
|
||||
container,
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user