162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
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<typeof createView>['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()
|
|
})
|
|
})
|