import type { EditorView } from '@tiptap/pm/view' import { describe, expect, it } from 'vite-plus/test' import { edgeBlockRect, findClosestTopLevelBlock } from './findNextElementFromCursor.js' /** * Builds a DOMRect-like object for tests, since jsdom does not run layout and * returns an all-zero rect for every element by default. */ function rect({ width, height, top = 0, left = 0, }: { width: number height: number top?: number left?: number }): DOMRect { return { width, height, top, left, right: left + width, bottom: top + height, x: left, y: top, toJSON: () => ({}), } as DOMRect } /** Overrides an element's getBoundingClientRect with a fixed rect. */ function withRect(el: HTMLElement, r: DOMRect): HTMLElement { el.getBoundingClientRect = () => r return el } describe('edgeBlockRect', () => { it('returns the first child that has a real layout box, skipping zero-size widgets', () => { // Mirrors the Pages layout: the first child is a zero-height page-chrome // widget, followed by real content blocks. const container = document.createElement('div') const widget = withRect(document.createElement('div'), rect({ width: 640, height: 0 })) const firstBlock = withRect( document.createElement('p'), rect({ width: 640, height: 24, top: 100 }), ) const lastBlock = withRect( document.createElement('p'), rect({ width: 640, height: 24, top: 200 }), ) container.append(widget, firstBlock, lastBlock) const result = edgeBlockRect(container, 'first') expect(result).not.toBeNull() expect(result?.top).toBe(100) expect(result?.height).toBe(24) }) it('returns the last child that has a real layout box, skipping trailing zero-size widgets', () => { const container = document.createElement('div') const firstBlock = withRect( document.createElement('p'), rect({ width: 640, height: 24, top: 100 }), ) const lastBlock = withRect( document.createElement('p'), rect({ width: 640, height: 24, top: 200 }), ) const trailingWidget = withRect( document.createElement('div'), rect({ width: 640, height: 0, top: 300 }), ) container.append(firstBlock, lastBlock, trailingWidget) const result = edgeBlockRect(container, 'last') expect(result?.top).toBe(200) }) it('returns null when no child has a valid box', () => { const container = document.createElement('div') container.append(withRect(document.createElement('div'), rect({ width: 0, height: 0 }))) expect(edgeBlockRect(container, 'first')).toBeNull() }) }) describe('findClosestTopLevelBlock', () => { /** * Tags a DOM element with a fake ProseMirror view description. Document nodes * expose a `node`; widget decorations do not. */ function tagViewDesc(el: HTMLElement, node: unknown): HTMLElement { ;(el as unknown as { pmViewDesc: { node?: unknown } }).pmViewDesc = { node } return el } function makeView(dom: HTMLElement): EditorView { return { dom } as unknown as EditorView } it('returns the top-level content block for an element inside it', () => { const editorDom = document.createElement('div') const block = tagViewDesc(document.createElement('p'), { type: { name: 'paragraph' } }) const text = document.createElement('span') block.appendChild(text) editorDom.appendChild(block) expect(findClosestTopLevelBlock(text, makeView(editorDom))).toBe(block) }) it('skips widget decorations that have no associated document node', () => { // The Pages page-chrome overlay is a widget decoration: a direct child of // the editor with a view description but no document node. Resolving a hover // to it would align the drag handle to the page header instead of the block. const editorDom = document.createElement('div') const widget = tagViewDesc(document.createElement('div'), undefined) const inner = document.createElement('div') widget.appendChild(inner) editorDom.appendChild(widget) expect(findClosestTopLevelBlock(inner, makeView(editorDom))).toBeUndefined() }) it('returns undefined when the element is not inside the editor', () => { const editorDom = document.createElement('div') const orphan = document.createElement('p') expect(findClosestTopLevelBlock(orphan, makeView(editorDom))).toBeUndefined() }) })