import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { defineMock } = vi.hoisted(() => ({ defineMock: vi.fn((factory: (view: unknown) => unknown) => factory), })) vi.mock('@codemirror/view', () => ({ EditorView: class EditorView {}, ViewPlugin: { define: defineMock, }, })) import { getDocumentZoom, zoomCursorFix } from './zoomCursorFix' function mockComputedZoom(value: string) { const real = window.getComputedStyle.bind(window) return vi.spyOn(window, 'getComputedStyle').mockImplementation((elt, pseudo) => { const style = real(elt, pseudo) if (elt === document.documentElement) { return new Proxy(style, { get(target, prop) { if (prop === 'zoom') return value const next = Reflect.get(target, prop) return typeof next === 'function' ? next.bind(target) : next }, }) } return style }) } function mockInlineZoom(value: string) { return vi.spyOn(document.documentElement.style, 'getPropertyValue').mockImplementation((name: string) => { if (name === 'zoom') return value return '' }) } function createView() { const contentDOM = document.createElement('div') const textNode = document.createTextNode('hello') contentDOM.appendChild(textNode) const origPosAtCoords = vi.fn(() => 11) const origPosAndSideAtCoords = vi.fn(() => ({ pos: 13, assoc: -1 as const })) const prototype = { posAtCoords: origPosAtCoords, posAndSideAtCoords: origPosAndSideAtCoords, } const view = Object.assign(Object.create(prototype), { contentDOM, posAtDOM: vi.fn(() => 17), }) return { view, textNode, origPosAtCoords, origPosAndSideAtCoords, } } describe('zoomCursorFix behavior', () => { beforeEach(() => { document.documentElement.style.removeProperty('zoom') delete (document as Document & { caretRangeFromPoint?: (x: number, y: number) => Range | null }).caretRangeFromPoint }) afterEach(() => { vi.restoreAllMocks() }) it('reads inline percentage zoom values when computed zoom is normal', () => { const computedSpy = mockComputedZoom('normal') const inlineSpy = mockInlineZoom('125%') expect(getDocumentZoom()).toBe(1.25) inlineSpy.mockRestore() computedSpy.mockRestore() }) it('falls back to 1 for invalid inline zoom values', () => { const computedSpy = mockComputedZoom('normal') const inlineSpy = mockInlineZoom('banana') expect(getDocumentZoom()).toBe(1) inlineSpy.mockRestore() computedSpy.mockRestore() }) it('uses caretRangeFromPoint when CSS zoom is active and restores prototype methods on destroy', () => { const computedSpy = mockComputedZoom('normal') const inlineSpy = mockInlineZoom('150%') const { view, textNode } = createView() Object.defineProperty(document, 'caretRangeFromPoint', { configurable: true, value: vi.fn(() => ({ startContainer: textNode, startOffset: 2 })), }) const pluginFactory = zoomCursorFix() as unknown as (view: typeof view) => { destroy: () => void } const plugin = pluginFactory(view) expect(view.posAtCoords({ x: 30, y: 40 }, true)).toBe(17) expect(view.posAndSideAtCoords({ x: 30, y: 40 }, false)).toEqual({ pos: 17, assoc: 1 }) expect(view.posAtDOM).toHaveBeenCalledWith(textNode, 2) expect(Object.hasOwn(view, 'posAtCoords')).toBe(true) plugin.destroy() expect(Object.hasOwn(view, 'posAtCoords')).toBe(false) expect(view.posAtCoords({ x: 5, y: 6 }, false)).toBe(11) inlineSpy.mockRestore() computedSpy.mockRestore() }) it('falls back to adjusted coordinates when the browser API is unavailable or returns an unusable range', () => { const computedSpy = mockComputedZoom('normal') const inlineSpy = mockInlineZoom('200%') const pluginFactory = zoomCursorFix() as unknown as (view: ReturnType['view']) => { destroy: () => void } const first = createView() pluginFactory(first.view) expect(first.view.posAtCoords({ x: 40, y: 60 }, true)).toBe(11) expect(first.origPosAtCoords).toHaveBeenCalledWith({ x: 20, y: 30 }, true) expect(first.view.posAndSideAtCoords({ x: 40, y: 60 }, false)).toEqual({ pos: 13, assoc: -1 }) expect(first.origPosAndSideAtCoords).toHaveBeenCalledWith({ x: 20, y: 30 }, false) const second = createView() Object.defineProperty(document, 'caretRangeFromPoint', { configurable: true, value: vi.fn(() => ({ startContainer: document.createTextNode('outside'), startOffset: 0 })), }) pluginFactory(second.view) expect(second.view.posAtCoords({ x: 50, y: 70 }, false)).toBe(11) expect(second.origPosAtCoords).toHaveBeenCalledWith({ x: 25, y: 35 }, false) const third = createView() third.view.posAtDOM.mockImplementation(() => { throw new Error('boom') }) Object.defineProperty(document, 'caretRangeFromPoint', { configurable: true, value: vi.fn(() => ({ startContainer: third.textNode, startOffset: 1 })), }) pluginFactory(third.view) expect(third.view.posAtCoords({ x: 60, y: 80 }, false)).toBe(11) expect(third.origPosAtCoords).toHaveBeenCalledWith({ x: 30, y: 40 }, false) inlineSpy.mockRestore() computedSpy.mockRestore() }) })