Files
ComfyUI_frontend/tests-ui/tests/litegraph/utils/CanvasPointer.deviceDetection.test.ts
Alexander Brown b994608506 Tests: Vitest configuration cleanup (#5888)
## Summary

Simplify default scripts. Filtering is still available to users, we can
revisit tagging or grouping later.
This fixes the issue where we had tests that were in the codebase but
never run because they weren't under `/src/components`

Also deletes the duplicate litegraph tests and their associated vitest
config file.

## Changes

- **What**: Test cleanup

## Review Focus

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5888-Tests-Vitest-configuration-cleanup-2806d73d36508197b800f68f0b028279)
by [Unito](https://www.unito.io)
2025-10-02 21:01:42 -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)
})
})
})
})
})