import type { EditorView } from '@tiptap/pm/view' import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { getDraggedBlockDir, getDraggedBlockElement } from './getDraggedBlockDir.js' function createMockView(overrides: Partial = {}): EditorView { const editorDom = document.createElement('div') return { dom: editorDom, nodeDOM: vi.fn(() => null), domAtPos: vi.fn(() => ({ node: editorDom, offset: 0 })), ...overrides, } as unknown as EditorView } function mockDirection(map: Map) { const original = window.getComputedStyle vi.spyOn(window, 'getComputedStyle').mockImplementation((el: Element) => { const dir = map.get(el) if (dir) { return { direction: dir } as CSSStyleDeclaration } return original(el) }) } describe('getDraggedBlockDir', () => { beforeEach(() => { vi.restoreAllMocks() }) describe('getDraggedBlockElement', () => { it('should prefer the child at domAtPos when nodeDOM resolves to the editor root', () => { const editorRoot = document.createElement('div') const paragraph = document.createElement('p') editorRoot.appendChild(paragraph) const view = createMockView({ dom: editorRoot, nodeDOM: vi.fn(() => editorRoot), domAtPos: vi.fn(() => ({ node: editorRoot, offset: 0 })), }) expect(getDraggedBlockElement(view, 0)).toBe(paragraph) }) }) afterEach(() => { vi.restoreAllMocks() }) describe('nodeDOM returns an Element', () => { it('should return "rtl" when the block element has direction rtl', () => { const block = document.createElement('p') const dirMap = new Map([[block, 'rtl']]) mockDirection(dirMap) const view = createMockView({ nodeDOM: vi.fn(() => block) }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) it('should return "ltr" when the block element has direction ltr', () => { const block = document.createElement('p') const dirMap = new Map([[block, 'ltr']]) mockDirection(dirMap) const view = createMockView({ nodeDOM: vi.fn(() => block) }) expect(getDraggedBlockDir(view, 5)).toBe('ltr') }) }) describe('nodeDOM returns a non-Element — domAtPos fallback', () => { it('should resolve the child at offset when domAtPos returns a parent container', () => { const parent = document.createElement('div') const ltrChild = document.createElement('p') const rtlChild = document.createElement('p') parent.appendChild(ltrChild) parent.appendChild(rtlChild) const dirMap = new Map([ [parent, 'ltr'], [rtlChild, 'rtl'], ]) mockDirection(dirMap) const view = createMockView({ nodeDOM: vi.fn(() => null), domAtPos: vi.fn(() => ({ node: parent, offset: 1 })), }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) it('should fall back to the parent node when child at offset is a text node', () => { const parent = document.createElement('p') parent.appendChild(document.createTextNode('hello')) const dirMap = new Map([[parent, 'rtl']]) mockDirection(dirMap) const view = createMockView({ nodeDOM: vi.fn(() => null), domAtPos: vi.fn(() => ({ node: parent, offset: 0 })), }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) }) describe('regression: mixed RTL/LTR', () => { it('should read direction from the dragged block, not the parent wrapper', () => { const editorRoot = document.createElement('div') const rtlBlock = document.createElement('p') const dirMap = new Map([ [editorRoot, 'ltr'], [rtlBlock, 'rtl'], ]) mockDirection(dirMap) const view = createMockView({ dom: editorRoot, nodeDOM: vi.fn(() => rtlBlock), }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) it('should read direction from child at domAtPos offset, not from parent', () => { const listWrapper = document.createElement('ul') const rtlItem = document.createElement('li') listWrapper.appendChild(rtlItem) const dirMap = new Map([ [listWrapper, 'ltr'], [rtlItem, 'rtl'], ]) mockDirection(dirMap) const view = createMockView({ nodeDOM: vi.fn(() => null), domAtPos: vi.fn(() => ({ node: listWrapper, offset: 0 })), }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) it('should normalize a Text node from domAtPos to its parentElement instead of falling back to editor root', () => { const editorRoot = document.createElement('div') const rtlParagraph = document.createElement('p') const textNode = document.createTextNode('مرحبا') rtlParagraph.appendChild(textNode) const dirMap = new Map([ [editorRoot, 'ltr'], [rtlParagraph, 'rtl'], ]) mockDirection(dirMap) const view = createMockView({ dom: editorRoot, nodeDOM: vi.fn(() => null), domAtPos: vi.fn(() => ({ node: textNode, offset: 3 })), }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) }) describe('ultimate fallback to editor root', () => { it('should use editor root direction when nodeDOM and domAtPos both return non-elements', () => { const editorRoot = document.createElement('div') const textNode = document.createTextNode('text') const dirMap = new Map([[editorRoot, 'rtl']]) mockDirection(dirMap) const view = createMockView({ dom: editorRoot, nodeDOM: vi.fn(() => textNode), domAtPos: vi.fn(() => ({ node: textNode, offset: 0 })), }) expect(getDraggedBlockDir(view, 0)).toBe('rtl') }) it('should default to "ltr" when no direction is computed anywhere', () => { const editorRoot = document.createElement('div') const textNode = document.createTextNode('text') vi.spyOn(window, 'getComputedStyle').mockReturnValue({ direction: '', } as CSSStyleDeclaration) const view = createMockView({ dom: editorRoot, nodeDOM: vi.fn(() => textNode), domAtPos: vi.fn(() => ({ node: textNode, offset: 0 })), }) expect(getDraggedBlockDir(view, 0)).toBe('ltr') }) }) })