Files
ComfyUI_frontend/tests-ui/tests/litegraph/utils/CanvasPointer.deviceDetection.test.ts
Arjan Singh 5869b04e57 Merge main (as of 10-06-2025) into rh-test (#5965)
## Summary

Merges latest changes from `main` as of 10-06-2025.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5965-Merge-main-as-of-10-06-2025-into-rh-test-2856d73d3650812cb95fd8917278a770)
by [Unito](https://www.unito.io)

---------

Signed-off-by: Marcel Petrick <mail@marcelpetrick.it>
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Benjamin Lu <benceruleanlu@proton.me>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: snomiao <snomiao@gmail.com>
Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
Co-authored-by: Jake Schroeder <jake.schroeder@isophex.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: Marcel Petrick <mail@marcelpetrick.it>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
Co-authored-by: JakeSchroeder <jake@axiom.co>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: ComfyUI Wiki <contact@comfyui-wiki.com>
2025-10-08 19:06:40 -07:00

1221 lines
38 KiB
TypeScript

/**
* Test-Driven Design (TDD) tests for device detection functionality.
*
* These tests describe the expected behavior for device detection between
* mouse and trackpad inputs using an efficient timestamp-based approach.
*
* Design Philosophy:
* - Uses timestamps (performance.now()) instead of creating timers for every event
* - Creates at most ONE timer (for Linux buffer timeout), not one per wheel event
* - Handles potentially thousands of wheel events per second efficiently
*
* Expected new properties on CanvasPointer:
* - detectedDevice: 'mouse' | 'trackpad'
* - lastWheelEventTime: number // timestamp, not the event itself
* - bufferedLinuxEvent: WheelEvent | undefined
* - bufferedLinuxEventTime: number
* - linuxBufferTimeoutId: number | undefined // single timer handle
*
* Expected new methods on CanvasPointer:
* - detectDevice(event: WheelEvent): void
* - clearLinuxBuffer(): void
*
* Performance: This design can handle 10,000+ events without creating any timers
* (except one for Linux detection), ensuring smooth scrolling performance.
*
* @vitest-environment jsdom
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', () => {
let element: HTMLDivElement
let pointer: CanvasPointer
beforeEach(() => {
element = document.createElement('div')
pointer = new CanvasPointer(element)
// Mock performance.now() for timestamp-based testing
vi.spyOn(performance, 'now').mockReturnValue(0)
vi.spyOn(global, 'setTimeout')
vi.spyOn(global, 'clearTimeout')
})
afterEach(() => {
vi.restoreAllMocks()
vi.clearAllTimers()
})
describe('Initial State', () => {
it('should start in mouse detected mode immediately after loading', () => {
expect(pointer.detectedDevice).toBe('mouse')
})
it('should have no last wheel event time immediately after loading', () => {
expect(pointer.lastWheelEventTime).toBe(0)
expect(pointer.hasReceivedWheelEvent).toBe(false)
})
it('should have no buffered Linux event immediately after loading', () => {
expect(pointer.bufferedLinuxEvent).toBeUndefined()
expect(pointer.bufferedLinuxEventTime).toBe(0)
expect(pointer.linuxBufferTimeoutId).toBeUndefined()
})
})
describe('First Event Detection', () => {
describe('switching to trackpad on first event', () => {
it('should switch to trackpad if first event is pinch-to-zoom with deltaY < 10', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 9.5,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
expect(pointer.lastWheelEventTime).toBe(0) // Records current time
})
it('should switch to trackpad if first event is pinch-to-zoom with deltaY = 9.999', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 9.999,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT switch to trackpad if first event is pinch-to-zoom with deltaY = 10', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should switch to trackpad if first event is two-finger panning with integer values', () => {
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 5,
deltaX: -3
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should switch to trackpad if first event is two-finger panning with ctrlKey true', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 7,
deltaX: 4
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should switch to trackpad if first event is negative pinch-to-zoom with deltaY > -10', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: -9.5,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
})
describe('remaining in mouse mode on first event', () => {
it('should remain in mouse mode if first event is pinch-to-zoom with deltaY >= 10', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 10.1,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should remain in mouse mode if first event is mouse wheel with deltaY = 120', () => {
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 120,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should remain in mouse mode if first event has only deltaY (no deltaX)', () => {
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 30,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
})
})
describe('Mode Switching from Mouse to Trackpad', () => {
beforeEach(() => {
// Ensure we start in mouse mode
pointer.detectedDevice = 'mouse'
// Simulate a previous event to establish timing
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
})
it('should switch to trackpad on two-finger panning with non-zero deltaX and deltaY', () => {
// Simulate 500ms has passed since last event
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 15,
deltaX: 8
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT switch to trackpad on two-finger panning with zero deltaX', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 15,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should NOT switch to trackpad on two-finger panning with zero deltaY', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 0,
deltaX: 15
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should switch to trackpad on pinch-to-zoom with deltaY < 10', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 9.99,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should switch to trackpad on pinch-to-zoom with deltaY = -5.5', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: -5.5,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT switch to trackpad on pinch-to-zoom with deltaY = 10', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should NOT switch to trackpad on pinch-to-zoom with deltaY = -10', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: -10,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
})
describe('Mode Switching from Trackpad to Mouse', () => {
beforeEach(() => {
// Set to trackpad mode
pointer.detectedDevice = 'trackpad'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
})
it('should switch to mouse on clear mouse wheel event with deltaY > 80', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 80.1,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should switch to mouse on clear mouse wheel event with deltaY = 120', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 120,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should switch to mouse on clear mouse wheel event with negative deltaY < -80', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: -90,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should NOT switch to mouse with deltaY = 80', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 80,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT switch to mouse with deltaY = -80', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: -80,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT switch to mouse with deltaY = 79.999', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 79.999,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
})
describe('500ms Cooldown Period', () => {
it('should NOT allow switching from mouse to trackpad within 500ms', () => {
pointer.detectedDevice = 'mouse'
// First event at time 0
vi.spyOn(performance, 'now').mockReturnValue(0)
const event1 = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 60,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
expect(pointer.lastWheelEventTime).toBe(0)
// Try to switch after 499ms - should fail
vi.spyOn(performance, 'now').mockReturnValue(499)
const event2 = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 5,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should allow switching from mouse to trackpad after 500ms', () => {
pointer.detectedDevice = 'mouse'
// First event at time 0
vi.spyOn(performance, 'now').mockReturnValue(0)
const event1 = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 60,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Try to switch after 500ms - should succeed
vi.spyOn(performance, 'now').mockReturnValue(500)
const event2 = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 5,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT allow switching from trackpad to mouse within 500ms', () => {
pointer.detectedDevice = 'trackpad'
// First event at time 0
vi.spyOn(performance, 'now').mockReturnValue(0)
const event1 = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 5,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Try to switch after 400ms - should fail
vi.spyOn(performance, 'now').mockReturnValue(400)
const event2 = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 120,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should maintain cooldown even with multiple events', () => {
pointer.detectedDevice = 'mouse'
// Series of events that would normally trigger trackpad
const trackpadEvents = [
{ deltaY: 5, deltaX: 3 },
{ deltaY: -7, deltaX: 2 },
{ deltaY: 8, deltaX: -4 }
]
// Send first mouse event at time 0
vi.spyOn(performance, 'now').mockReturnValue(0)
pointer.isTrackpadGesture(
new WheelEvent('wheel', { deltaY: 60, deltaX: 0 })
)
// Send trackpad events within 500ms window
trackpadEvents.forEach((eventData, index) => {
vi.spyOn(performance, 'now').mockReturnValue((index + 1) * 100) // 100ms, 200ms, 300ms
const event = new WheelEvent('wheel', eventData)
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse') // Should remain mouse
})
// After 500ms from last event (300ms + 500ms = 800ms), should be able to switch
vi.spyOn(performance, 'now').mockReturnValue(800)
const switchEvent = new WheelEvent('wheel', { deltaY: 5, deltaX: 3 })
pointer.isTrackpadGesture(switchEvent)
expect(pointer.detectedDevice).toBe('trackpad')
})
})
describe('Linux Wheel Event Buffering', () => {
beforeEach(() => {
pointer.detectedDevice = 'trackpad'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
vi.clearAllMocks()
})
it('should buffer possible Linux wheel event and create single timeout', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
vi.spyOn(performance, 'now').mockReturnValue(500) // Allow mode switching
const event = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.bufferedLinuxEvent).toBe(event)
expect(pointer.bufferedLinuxEventTime).toBe(500)
expect(pointer.detectedDevice).toBe('trackpad') // No immediate switch
// Should create exactly ONE timeout for buffer clearing
expect(setTimeoutSpy).toHaveBeenCalledTimes(1)
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 10)
})
it('should reuse timer when buffering new Linux event', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
// First Linux event
vi.spyOn(performance, 'now').mockReturnValue(500)
const event1 = new WheelEvent('wheel', { deltaY: 15, deltaX: 0 })
pointer.isTrackpadGesture(event1)
const firstTimeoutId = pointer.linuxBufferTimeoutId
// Second Linux event before timeout
vi.spyOn(performance, 'now').mockReturnValue(505)
const event2 = new WheelEvent('wheel', { deltaY: 10, deltaX: 0 })
pointer.isTrackpadGesture(event2)
// Should clear the first timeout and create a new one
expect(clearTimeoutSpy).toHaveBeenCalledWith(firstTimeoutId)
expect(setTimeoutSpy).toHaveBeenCalledTimes(2)
expect(pointer.bufferedLinuxEvent).toBe(event2)
})
it('should buffer negative Linux wheel values', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
deltaY: -10,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.bufferedLinuxEvent).toBe(event)
expect(pointer.detectedDevice).toBe('trackpad')
expect(setTimeoutSpy).toHaveBeenCalledTimes(1)
})
it('should NOT buffer event with deltaY < 10', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
deltaY: 9,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.bufferedLinuxEvent).toBeUndefined()
expect(pointer.detectedDevice).toBe('trackpad')
expect(setTimeoutSpy).not.toHaveBeenCalled() // No timer created
})
it('should NOT buffer event with non-zero deltaX', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
vi.spyOn(performance, 'now').mockReturnValue(500)
const event = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 1
})
pointer.isTrackpadGesture(event)
expect(pointer.bufferedLinuxEvent).toBeUndefined()
expect(pointer.detectedDevice).toBe('trackpad')
expect(setTimeoutSpy).not.toHaveBeenCalled() // No timer created
})
it('should switch to mouse if follow-up event has same deltaY within 10ms', () => {
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
// First event - buffered at time 500
vi.spyOn(performance, 'now').mockReturnValue(500)
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
expect(pointer.bufferedLinuxEvent).toBe(event1)
const timeoutId = pointer.linuxBufferTimeoutId
// Follow-up within 10ms with same deltaY
vi.spyOn(performance, 'now').mockReturnValue(509)
const event2 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
expect(pointer.bufferedLinuxEvent).toBeUndefined()
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId) // Timer cleared
})
it('should switch to mouse if follow-up event is divisible by original deltaY within 10ms', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First event - buffered
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up within 10ms with deltaY divisible by 10
vi.spyOn(performance, 'now').mockReturnValue(505)
const event2 = new WheelEvent('wheel', {
deltaY: 30,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
expect(pointer.bufferedLinuxEvent).toBeUndefined()
})
it('should switch to mouse if follow-up deltaY is divisible by original (base 15)', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First event with base 15
const event1 = new WheelEvent('wheel', {
deltaY: 15,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up with multiple of 15
vi.spyOn(performance, 'now').mockReturnValue(508)
const event2 = new WheelEvent('wheel', {
deltaY: 45,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should switch to mouse if original deltaY is divisible by follow-up', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First event with larger value
const event1 = new WheelEvent('wheel', {
deltaY: 30,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up with divisor
vi.spyOn(performance, 'now').mockReturnValue(507)
const event2 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should NOT switch to mouse if follow-up is not divisible', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First event
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up with non-divisible value
vi.spyOn(performance, 'now').mockReturnValue(505)
const event2 = new WheelEvent('wheel', {
deltaY: 13,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should NOT switch to mouse if follow-up comes after 10ms', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First event
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up after 10ms
vi.spyOn(performance, 'now').mockReturnValue(511)
const event2 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should call clearLinuxBuffer method after 10ms timeout', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
vi.useFakeTimers() // Use fake timers just for this test
const event = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.bufferedLinuxEvent).toBe(event)
// Simulate timeout firing
vi.runOnlyPendingTimers()
expect(pointer.bufferedLinuxEvent).toBeUndefined()
expect(pointer.linuxBufferTimeoutId).toBeUndefined()
vi.useRealTimers() // Restore for other tests
})
it('should handle negative Linux wheel values', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First negative event
const event1 = new WheelEvent('wheel', {
deltaY: -15,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up with same negative value
vi.spyOn(performance, 'now').mockReturnValue(505)
const event2 = new WheelEvent('wheel', {
deltaY: -15,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should handle mixed sign Linux wheel values if divisible', () => {
vi.spyOn(performance, 'now').mockReturnValue(500)
// First positive event
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
// Follow-up with negative multiple
vi.spyOn(performance, 'now').mockReturnValue(505)
const event2 = new WheelEvent('wheel', {
deltaY: -30,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should allow buffering during 500ms cooldown as exception', () => {
pointer.detectedDevice = 'trackpad'
// Send initial event at time 0
vi.spyOn(performance, 'now').mockReturnValue(0)
const event1 = new WheelEvent('wheel', {
deltaY: 5,
deltaX: 2
})
pointer.isTrackpadGesture(event1)
// Within cooldown at 100ms, but Linux buffering should still work
vi.spyOn(performance, 'now').mockReturnValue(100)
const event2 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.bufferedLinuxEvent).toBe(event2)
// Follow-up for Linux detection at 105ms
vi.spyOn(performance, 'now').mockReturnValue(105)
const event3 = new WheelEvent('wheel', {
deltaY: 20,
deltaX: 0
})
pointer.isTrackpadGesture(event3)
// Should switch despite being within original 500ms window
expect(pointer.detectedDevice).toBe('mouse')
})
})
describe('Performance and Efficiency', () => {
it('should not create timers for regular wheel events', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
pointer.detectedDevice = 'mouse'
// Simulate rapid scrolling without Linux-like patterns
for (let i = 0; i < 100; i++) {
vi.spyOn(performance, 'now').mockReturnValue(i * 16) // 60fps scrolling
const event = new WheelEvent('wheel', {
deltaY: 120, // Clear mouse wheel value
deltaX: 0
})
pointer.isTrackpadGesture(event)
}
// Should create NO timers for regular mouse wheel events
expect(setTimeoutSpy).not.toHaveBeenCalled()
})
it('should create at most one timer for Linux detection', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
pointer.detectedDevice = 'trackpad'
// Send a Linux-like event that requires buffering
vi.spyOn(performance, 'now').mockReturnValue(500)
const event1 = new WheelEvent('wheel', { deltaY: 10, deltaX: 0 })
pointer.isTrackpadGesture(event1)
// Should create exactly one timer
expect(setTimeoutSpy).toHaveBeenCalledTimes(1)
// Send more regular events
for (let i = 1; i <= 10; i++) {
vi.spyOn(performance, 'now').mockReturnValue(500 + i * 100)
const event = new WheelEvent('wheel', { deltaY: 5, deltaX: 3 })
pointer.isTrackpadGesture(event)
}
// Still only one timer (the Linux buffer timeout)
expect(setTimeoutSpy).toHaveBeenCalledTimes(1)
})
it('should handle thousands of events efficiently', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
let maxTimersCreated = 0
// Simulate extended scrolling session with mixed inputs
for (let i = 0; i < 10000; i++) {
vi.spyOn(performance, 'now').mockReturnValue(i)
// Mix of different event types
const eventType = i % 3
let event: WheelEvent
if (eventType === 0) {
// Mouse wheel
event = new WheelEvent('wheel', {
deltaY: 120,
deltaX: 0
})
} else if (eventType === 1) {
// Trackpad two-finger
event = new WheelEvent('wheel', {
deltaY: Math.floor(Math.random() * 20),
deltaX: Math.floor(Math.random() * 20)
})
} else {
// Pinch to zoom
event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: Math.random() * 10,
deltaX: 0
})
}
pointer.isTrackpadGesture(event)
// Track maximum timers created
maxTimersCreated = Math.max(
maxTimersCreated,
setTimeoutSpy.mock.calls.length
)
}
// Should create at most a few timers for Linux detection, not thousands
expect(maxTimersCreated).toBeLessThan(10)
})
it('should use minimal memory with timestamp approach', () => {
// This test verifies the implementation uses timestamps, not stored events
const initialProps = Object.keys(pointer).length
// Process many events
for (let i = 0; i < 1000; i++) {
vi.spyOn(performance, 'now').mockReturnValue(i * 10)
const event = new WheelEvent('wheel', {
deltaY: 60 + Math.random() * 100,
deltaX: Math.random() * 50
})
pointer.isTrackpadGesture(event)
}
// Should only have a few properties for tracking state
const finalProps = Object.keys(pointer).length
expect(finalProps - initialProps).toBeLessThanOrEqual(5) // Only added minimal tracking properties
// Verify we store timestamps, not events (except Linux buffer)
expect(typeof pointer.lastWheelEventTime).toBe('number')
expect(typeof pointer.bufferedLinuxEventTime).toBe('number')
})
it('should handle rapid mode switching efficiently', () => {
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
// Rapidly switch between mouse and trackpad modes
for (let i = 0; i < 100; i++) {
const baseTime = i * 600 // Every 600ms to allow switching
// Mouse event
vi.spyOn(performance, 'now').mockReturnValue(baseTime)
pointer.isTrackpadGesture(
new WheelEvent('wheel', { deltaY: 120, deltaX: 0 })
)
// Trackpad event
vi.spyOn(performance, 'now').mockReturnValue(baseTime + 500)
pointer.isTrackpadGesture(
new WheelEvent('wheel', { deltaY: 5, deltaX: 3 })
)
}
// Should create minimal or no timers despite 200 events
expect(setTimeoutSpy.mock.calls.length).toBeLessThan(5)
})
})
describe('Edge Cases and Complex Scenarios', () => {
it('should handle float values correctly for mouse detection', () => {
pointer.detectedDevice = 'trackpad'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
vi.spyOn(performance, 'now').mockReturnValue(500)
// Float value <= 80 should NOT switch from trackpad
const event1 = new WheelEvent('wheel', {
deltaY: 60.5,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
expect(pointer.detectedDevice).toBe('trackpad')
// Float value > 80 should switch to mouse
vi.spyOn(performance, 'now').mockReturnValue(1000) // 500ms later
const event2 = new WheelEvent('wheel', {
deltaY: 80.1,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should handle integer values correctly for trackpad detection', () => {
pointer.detectedDevice = 'mouse'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
vi.spyOn(performance, 'now').mockReturnValue(500)
// Integer values in two-finger panning
const event = new WheelEvent('wheel', {
deltaY: 5,
deltaX: 3
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should correctly identify pinch-to-zoom with ctrlKey', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 250.5,
deltaX: 0
})
// This is pinch-to-zoom but deltaY > 10, so stays as mouse on first event
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should handle rapid event sequences', () => {
pointer.detectedDevice = 'mouse'
pointer.lastWheelEventTime = 0
// Simulate rapid scrolling
for (let i = 0; i < 10; i++) {
vi.spyOn(performance, 'now').mockReturnValue(i * 30) // 30ms between events
const event = new WheelEvent('wheel', {
deltaY: 60,
deltaX: 0
})
pointer.isTrackpadGesture(event)
expect(pointer.detectedDevice).toBe('mouse')
}
})
it('should handle boundary values for pinch-to-zoom detection', () => {
// Test deltaY = 10 (boundary)
const event1 = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
expect(pointer.detectedDevice).toBe('mouse')
// Reset and test deltaY = 9.999999
pointer = new CanvasPointer(element)
const event2 = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 9.999999,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('trackpad')
})
it('should handle boundary values for mouse wheel detection', () => {
pointer.detectedDevice = 'trackpad'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
vi.spyOn(performance, 'now').mockReturnValue(500)
// Test deltaY = 80 (boundary)
const event1 = new WheelEvent('wheel', {
deltaY: 80,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
expect(pointer.detectedDevice).toBe('trackpad')
// Test deltaY = 80.000001
vi.spyOn(performance, 'now').mockReturnValue(1000) // 500ms later
const event2 = new WheelEvent('wheel', {
deltaY: 80.000001,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should handle Linux wheel detection with various multiples', () => {
pointer.detectedDevice = 'trackpad'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
vi.spyOn(performance, 'now').mockReturnValue(500)
// Test with base 10 and multiple 50
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 0
})
pointer.isTrackpadGesture(event1)
vi.spyOn(performance, 'now').mockReturnValue(505) // 5ms later
const event2 = new WheelEvent('wheel', {
deltaY: 50,
deltaX: 0
})
pointer.isTrackpadGesture(event2)
expect(pointer.detectedDevice).toBe('mouse')
})
it('should not confuse trackpad integers with Linux wheel', () => {
pointer.detectedDevice = 'trackpad'
pointer.lastWheelEventTime = 0
pointer.hasReceivedWheelEvent = true
vi.spyOn(performance, 'now').mockReturnValue(500)
// Trackpad two-finger panning with integers
const event1 = new WheelEvent('wheel', {
deltaY: 10,
deltaX: 5 // Non-zero deltaX
})
pointer.isTrackpadGesture(event1)
// Should not buffer this as Linux event
expect(pointer.bufferedLinuxEvent).toBeUndefined()
expect(pointer.detectedDevice).toBe('trackpad')
})
})
describe('Input Type Validation', () => {
describe('Two-finger panning validation', () => {
it('should accept integer deltaY values', () => {
const values = [0, 1, -1, 100, -100, 999, -999]
values.forEach((deltaY) => {
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY,
deltaX: 5
})
expect(Number.isInteger(event.deltaY)).toBe(true)
})
})
it('should accept integer deltaX values', () => {
const values = [0, 1, -1, 100, -100, 999, -999]
values.forEach((deltaX) => {
const event = new WheelEvent('wheel', {
ctrlKey: false,
deltaY: 5,
deltaX
})
expect(Number.isInteger(event.deltaX)).toBe(true)
})
})
it('should handle ctrlKey true or false', () => {
;[true, false].forEach((ctrlKey) => {
const event = new WheelEvent('wheel', {
ctrlKey,
deltaY: 5,
deltaX: 3
})
expect(typeof event.ctrlKey).toBe('boolean')
})
})
})
describe('Pinch-to-zoom validation', () => {
it('should always have ctrlKey true', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 5.5,
deltaX: 0
})
expect(event.ctrlKey).toBe(true)
})
it('should accept float deltaY values in range -1000 to 1000', () => {
const values = [-1000, -999.99, -0.1, 0, 0.1, 999.99, 1000]
values.forEach((deltaY) => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY,
deltaX: 0
})
expect(event.deltaY).toBeGreaterThanOrEqual(-1000)
expect(event.deltaY).toBeLessThanOrEqual(1000)
})
})
it('should always have deltaX = 0', () => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: 5.5,
deltaX: 0
})
expect(event.deltaX).toBe(0)
})
})
describe('Mouse input validation', () => {
it('should accept float deltaX values in range -1000 to 1000', () => {
const values = [-1000, -500.5, 0, 500.5, 1000]
values.forEach((deltaX) => {
const event = new WheelEvent('wheel', {
deltaY: 120,
deltaX
})
expect(event.deltaX).toBeGreaterThanOrEqual(-1000)
expect(event.deltaX).toBeLessThanOrEqual(1000)
})
})
it('should have deltaY >= 60 for Windows/Mac mouse', () => {
const values = [60, 60.1, 80, 120, 240]
values.forEach((deltaY) => {
const event = new WheelEvent('wheel', {
deltaY,
deltaX: 0
})
expect(event.deltaY).toBeGreaterThanOrEqual(60)
})
})
it('should have integer deltaY as multiples of 10 or 15 for Linux', () => {
// Base 10 multiples
const base10Values = [10, 20, 30, 40, 50, -10, -20, -30]
base10Values.forEach((deltaY) => {
expect(Number.isInteger(deltaY)).toBe(true)
// Use Math.abs to avoid JavaScript's -0 vs 0 issue with modulo on negative numbers
expect(Math.abs(deltaY) % 10).toBe(0)
})
// Base 15 multiples
const base15Values = [15, 30, 45, 60, -15, -30, -45]
base15Values.forEach((deltaY) => {
expect(Number.isInteger(deltaY)).toBe(true)
// Use Math.abs to avoid JavaScript's -0 vs 0 issue with modulo on negative numbers
expect(Math.abs(deltaY) % 15).toBe(0)
})
})
})
describe('Float vs Integer understanding', () => {
it('should recognize that integers are valid float values', () => {
const integerValues = [0, 1, -1, 10, -10, 100]
integerValues.forEach((value) => {
expect(Number.isInteger(value)).toBe(true)
expect(typeof value === 'number').toBe(true) // Valid as float
})
})
it('should recognize that decimals are NOT valid integer values', () => {
const decimalValues = [0.1, -0.1, 10.5, -10.5, 99.99]
decimalValues.forEach((value) => {
expect(Number.isInteger(value)).toBe(false)
expect(typeof value === 'number').toBe(true) // Still valid as float
})
})
it('should correctly validate pinch-to-zoom deltaY as float', () => {
// These are all valid float values for pinch-to-zoom
const validValues = [0, 1, -1, 0.5, -0.5, 999, -999, 500.123]
validValues.forEach((value) => {
const event = new WheelEvent('wheel', {
ctrlKey: true,
deltaY: value,
deltaX: 0
})
expect(typeof event.deltaY === 'number').toBe(true)
expect(event.deltaY >= -1000 && event.deltaY <= 1000).toBe(true)
})
})
})
})
})