import { describe, it, vi, expect, beforeAll, afterEach, afterAll } from 'vitest'
import isElementClickable from '../../src/scripts/isElementClickable.js'
* A minimalistic scrollIntoView function that emulates browser behavior
* to be attached to the Element.prototype
* @param options - Scroll options or boolean for legacy behavior
*/
function scrollIntoView(options?: {
block?: 'start' | 'center' | 'end'
inline?: 'start' | 'center' | 'end'
} | boolean): void {
const originalRect = this.getBoundingClientRect()
const originalScrollY = window.scrollY
const originalScrollX = window.scrollX
const align = typeof options === 'boolean' || !options ?
{
block: 'start', inline: 'start'
} :
options
const getHorizontalScroll = () => {
switch (align.inline) {
case 'center':
return originalScrollX + originalRect.left - window.innerWidth/2 + originalRect.width/2
case 'end':
return originalScrollX + originalRect.right - window.innerWidth
default:
return originalScrollX + originalRect.left
}
}
const getVerticalScroll = () => {
switch (align.inline) {
case 'center':
return originalScrollY + originalRect.top - window.innerHeight/2 + originalRect.height/2
case 'end':
return originalScrollY + originalRect.bottom - window.innerHeight
default:
return originalScrollY + originalRect.top
}
}
window.scroll(getHorizontalScroll(), getVerticalScroll())
const deltaY = originalScrollY - window.scrollY
const deltaX = originalScrollX - window.scrollX
this.getBoundingClientRect = () => ({
...originalRect,
top: originalRect.top + deltaY,
left: originalRect.left + deltaX
})
}
describe('isElementClickable script', () => {
beforeAll(() => {
global.window = {
innerWidth: 800,
innerHeight: 600,
scrollX: 0,
scrollY: 0,
scroll(x:any, y: any) {
this.scrollX = Math.max(0, x)
this.scrollY = Math.max(0, y)
}
} as any
})
it('should be clickable if in viewport and elementFromPoint matches', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: () => vi.fn(),
contains: () => false
} as unknown as Element
global.document = { elementFromPoint: () => elemMock } as any
expect(isElementClickable(elemMock)).toBe(true)
})
it('should be clickable if in viewport and elementFromPoint is child of elem', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: () => vi.fn(),
contains: () => true
} as unknown as Element
global.document = { elementFromPoint: () => 'some element' as any } as any
expect(isElementClickable(elemMock)).toBe(true)
})
it('should be clickable if in viewport and elementFromPoint is child of elem [Edge]', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: vi.fn(),
contains: () => { throw new Error('should not be called in old Edge!') }
} as unknown as Element
global.window.StyleMedia = (() => { }) as any
let attempts = 0
global.document = { elementFromPoint: () => {
attempts += 1
return { parentNode: attempts > 3 ? elemMock : {} }
} } as any
expect(isElementClickable(elemMock)).toBe(true)
expect(elemMock.scrollIntoView).toBeCalledWith(false)
})
it('should be clickable if in viewport and elementFromPoint if element is Document Fragment [Edge]', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
nodeType: 11,
getClientRects: () => [{}],
scrollIntoView: vi.fn(),
contains: () => { throw new Error('should not be called in old Edge!') }
} as unknown as Element
global.window.StyleMedia = (() => { }) as any
let attempts = 0
global.document = { elementFromPoint: () => {
attempts += 1
return { parentNode: attempts > 3 ? elemMock : {} }
} } as any
expect(isElementClickable(elemMock)).toBe(true)
expect(elemMock.scrollIntoView).toBeCalledWith(false)
})
it('should be clickable if in viewport and elementFromPoint of the rect matches', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{
height: 55,
width: 2200,
top: 33,
left: 45500
}],
scrollIntoView: () => vi.fn(),
contains: () => false
} as unknown as Element
global.document = {
elementFromPoint: (x: number) => {
return x > 45500 ? elemMock : null
}
} as any
expect(isElementClickable(elemMock)).toBe(true)
})
it('should be clickable if in viewport and elementFromPoint matches [nested shadowRoot]', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: () => vi.fn(),
contains: () => false
} as unknown as Element
global.document = {
elementFromPoint: () => ({
shadowRoot: {
elementFromPoint: () => ({
shadowRoot: { elementFromPoint: () => elemMock }
})
}
})
} as any
expect(isElementClickable(elemMock)).toBe(true)
})
it('should be not clickable if in viewport and elementFromPoint matches but is disabled', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: () => vi.fn(),
contains: () => false,
disabled: true
} as unknown as Element
global.document = { elementFromPoint: () => elemMock } as any
expect(isElementClickable(elemMock)).toBe(false)
})
it("should be not clickable if in viewport but elementFromPoint doesn't match", () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: vi.fn(),
contains: () => false
} as unknown as Element
global.document = { elementFromPoint: () => null } as any
expect(isElementClickable(elemMock)).toBe(false)
expect(elemMock.scrollIntoView).toBeCalledWith({ block: 'center', inline: 'center' })
})
it("should be not clickable if in viewport but elementFromPoint doesn't match [shadowRoot]", () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 455
}),
clientHeight: 55,
clientWidth: 22,
getClientRects: () => [{}],
scrollIntoView: () => vi.fn(),
contains: () => false
} as unknown as Element
global.document = {
elementFromPoint: () => ({
shadowRoot: { elementFromPoint: () => null }
})
} as any
expect(isElementClickable(elemMock)).toBe(false)
})
it('should be clickable if not in viewport', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 33,
left: 999
}),
getClientRects: () => [{}],
scrollIntoView: scrollIntoView,
contains: () => false
} as unknown as Element
global.document = { elementFromPoint: () => elemMock } as any
expect(isElementClickable(elemMock)).toBe(true)
})
it('should scroll to the initial position when element not in viewport', () => {
const elemMock: any = {
getBoundingClientRect: () => ({
height: 55,
width: 22,
top: 600,
left: 999
}),
getClientRects: () => [{}],
scrollIntoView: scrollIntoView,
contains: () => false
} as unknown as Element
global.document = { elementFromPoint: () => elemMock } as any
window.scroll(5000, 5000)
isElementClickable(elemMock)
expect(window.scrollX).toBe(5000)
expect(window.scrollY).toBe(5000)
window.scroll(0, 0)
isElementClickable(elemMock)
expect(window.scrollX).toBe(0)
expect(window.scrollY).toBe(0)
})
afterEach(() => {
delete global.window.StyleMedia
})
afterAll(() => {
delete global.window.innerWidth
delete global.window.innerHeight
})
})