From 63d97a6303437ef92d90a0a900680d97150edbef Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 4 Sep 2025 20:36:42 -0400 Subject: [PATCH] improve standard canvas navigation --- src/lib/litegraph/src/CanvasPointer.ts | 60 +- src/lib/litegraph/src/LGraphCanvas.ts | 82 +- ...asPointer.deviceDetection.realData.test.ts | 896 ++++++++++++++++++ .../CanvasPointer.deviceDetection.test.ts | 114 +-- 4 files changed, 1008 insertions(+), 144 deletions(-) create mode 100644 src/lib/litegraph/test/CanvasPointer.deviceDetection.realData.test.ts diff --git a/src/lib/litegraph/src/CanvasPointer.ts b/src/lib/litegraph/src/CanvasPointer.ts index b4175e953..8120b964b 100644 --- a/src/lib/litegraph/src/CanvasPointer.ts +++ b/src/lib/litegraph/src/CanvasPointer.ts @@ -357,16 +357,47 @@ export class CanvasPointer { * Updates the device mode based on event patterns. */ #updateDeviceMode(event: WheelEvent, now: number): void { - if (this.#isTrackpadPattern(event)) { + console.log('event.deltaX:', event.deltaX) + console.log('event.deltaY:', event.deltaY) + + const wheelDeltaY = (event as any).wheelDeltaY + console.log('wheelDeltaY: ', wheelDeltaY) + + // if deltaX is non-zero, it's definitely a trackpad + if (Math.abs(event.deltaX) !== 0) { this.detectedDevice = 'trackpad' - } else if (this.#isMousePattern(event)) { - this.detectedDevice = 'mouse' - } else if ( + } else if (wheelDeltaY !== undefined) { + const absWheelDeltaY = Math.abs(wheelDeltaY) + + // get this wheelDelta from real world testing + const wheelDeltaYThreshold = navigator.platform.includes('Mac') ? 30 : 75 + + if (absWheelDeltaY > wheelDeltaYThreshold) { + if (this.#isTrackpadPattern(event)) { + this.detectedDevice = 'trackpad' + } else { + this.detectedDevice = 'mouse' + } + } else if (absWheelDeltaY > 0) { + this.detectedDevice = 'trackpad' + } + } else { + // in case wheelDeltaY is undefined (e.g. Firefox), fall back to original pattern detection + if (this.#isTrackpadPattern(event)) { + this.detectedDevice = 'trackpad' + } else if (this.#isMousePattern(event)) { + this.detectedDevice = 'mouse' + } + } + + if ( this.detectedDevice === 'trackpad' && this.#shouldBufferLinuxEvent(event) ) { this.#bufferLinuxEvent(event, now) } + + console.log('Detected device:', this.detectedDevice) } /** @@ -392,6 +423,27 @@ export class CanvasPointer { // Pinch-to-zoom: ctrlKey with small deltaY if (event.ctrlKey && Math.abs(event.deltaY) < 10) return true + // Two-finger panning vertically: zero deltaX AND small deltaY, only check this on non-Mac + if ( + !navigator.platform.includes('Mac') && + event.deltaX === 0 && + Math.abs(event.deltaY) < 70 + ) + return true + + const wheelDeltaY = (event as any).wheelDeltaY + + if ( + wheelDeltaY !== undefined && + !navigator.platform.includes('Mac') && + event.deltaX === 0 + ) { + // As tested in real world, on non-Mac, trackpad wheelDeltaY is usually very close to deltaY while two-finger panning vertically + if (Math.abs(Math.abs(event.deltaY) - Math.abs(wheelDeltaY)) < 2) { + return true + } + } + return false } diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index a32b8c354..73c17bd9f 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -3548,17 +3548,33 @@ export class LGraphCanvas // Detect if this is a trackpad gesture or mouse wheel const isTrackpad = this.pointer.isTrackpadGesture(e) - const isCtrlOrMacMeta = - e.ctrlKey || (e.metaKey && navigator.platform.includes('Mac')) - const isZoomModifier = isCtrlOrMacMeta && !e.altKey && !e.shiftKey - if (isZoomModifier || LiteGraph.canvasNavigationMode === 'legacy') { - // Legacy mode or standard mode with ctrl - use wheel for zoom - if (isTrackpad) { - // Trackpad gesture - use smooth scaling - scale *= 1 + e.deltaY * (1 - this.zoom_speed) * 0.18 - this.ds.changeScale(scale, [e.clientX, e.clientY], false) + // we need to separate trackpad from mouse because trackpad has different logic on standard + // mouse: + // 1. wheel scroll without ctrl = zoom in/out canvas + // 2. wheel scroll with ctrl = zoom in/out canvas + // trackpad: + // 1. two finger scroll = pan canvas + // 2. two finger pitch to zoom (included ctrl) = zoom in/out canvas + if (isTrackpad) { + const factor = 0.18 + + if (LiteGraph.canvasNavigationMode === 'standard') { + if (e.ctrlKey || e.metaKey) { + scale *= 1 + e.deltaY * (1 - this.zoom_speed) * factor + this.ds.changeScale(scale, [e.clientX, e.clientY], false) + } else { + this.ds.offset[0] -= e.deltaX * (1 + factor) * (1 / scale) + this.ds.offset[1] -= e.deltaY * (1 + factor) * (1 / scale) + } } else { + scale *= 1 + e.deltaY * (1 - this.zoom_speed) * factor + this.ds.changeScale(scale, [e.clientX, e.clientY], false) + } + } else { + const isZoomModifier = !e.altKey && !e.shiftKey + + if (isZoomModifier || LiteGraph.canvasNavigationMode === 'legacy') { // Mouse wheel - use stepped scaling if (e.deltaY < 0) { scale *= this.zoom_speed @@ -3566,17 +3582,16 @@ export class LGraphCanvas scale *= 1 / this.zoom_speed } this.ds.changeScale(scale, [e.clientX, e.clientY]) - } - } else { - // Standard mode without ctrl - use wheel / gestures to pan - // Trackpads and mice work on significantly different scales - const factor = isTrackpad ? 0.18 : 0.008_333 - - if (!isTrackpad && e.shiftKey && e.deltaX === 0) { - this.ds.offset[0] -= e.deltaY * (1 + factor) * (1 / scale) } else { - this.ds.offset[0] -= e.deltaX * (1 + factor) * (1 / scale) - this.ds.offset[1] -= e.deltaY * (1 + factor) * (1 / scale) + // Standard mode without ctrl - use wheel to pan + const factor = 0.008_333 + + if (e.shiftKey && e.deltaX === 0) { + this.ds.offset[0] -= e.deltaY * (1 + factor) * (1 / scale) + } else { + this.ds.offset[0] -= e.deltaX * (1 + factor) * (1 / scale) + this.ds.offset[1] -= e.deltaY * (1 + factor) * (1 / scale) + } } } @@ -3609,13 +3624,15 @@ export class LGraphCanvas if (e.type == 'keydown') { // TODO: Switch if (e.key === ' ') { - // space - this.read_only = true - if (this._previously_dragging_canvas === null) { - this._previously_dragging_canvas = this.dragging_canvas + // space - only switch to pan mode if we're in standard mode and not already in pan/read_only mode + if (LiteGraph.canvasNavigationMode === 'standard' && !this.read_only) { + this.read_only = true + if (this._previously_dragging_canvas === null) { + this._previously_dragging_canvas = this.dragging_canvas + } + this.dragging_canvas = this.pointer.isDown + block_default = true } - this.dragging_canvas = this.pointer.isDown - block_default = true } else if (e.key === 'Escape') { // esc if (this.linkConnector.isConnecting) { @@ -3659,11 +3676,16 @@ export class LGraphCanvas } } else if (e.type == 'keyup') { if (e.key === ' ') { - // space - this.read_only = false - this.dragging_canvas = - (this._previously_dragging_canvas ?? false) && this.pointer.isDown - this._previously_dragging_canvas = null + // space - only revert if we had temporarily switched to pan mode + if ( + LiteGraph.canvasNavigationMode === 'standard' && + this._previously_dragging_canvas !== null + ) { + this.read_only = false + this.dragging_canvas = + (this._previously_dragging_canvas ?? false) && this.pointer.isDown + this._previously_dragging_canvas = null + } } for (const node of Object.values(this.selected_nodes)) { diff --git a/src/lib/litegraph/test/CanvasPointer.deviceDetection.realData.test.ts b/src/lib/litegraph/test/CanvasPointer.deviceDetection.realData.test.ts new file mode 100644 index 000000000..bad3858a1 --- /dev/null +++ b/src/lib/litegraph/test/CanvasPointer.deviceDetection.realData.test.ts @@ -0,0 +1,896 @@ +/** + * Real QA Data Tests for CanvasPointer Device Detection + * + * This file contains tests based on actual device data collected from QA testing. + * Each test represents real-world behavior from specific devices and platforms. + * + * Test Structure: + * - Platform: The operating system (Mac, Windows, Linux) + * - Device: The specific input device (mouse, trackpad, precision touchpad) + * - Gesture: The type of interaction (scroll, pinch-to-zoom, two-finger pan) + * - Data: Exact event sequences as captured from real devices + * + * @vitest-environment jsdom + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { CanvasPointer } from '../src/CanvasPointer' + +describe('CanvasPointer Device Detection - Real QA Data Tests', () => { + let element: HTMLDivElement + let pointer: CanvasPointer + let originalPlatform: string + + beforeEach(() => { + element = document.createElement('div') + pointer = new CanvasPointer(element) + vi.spyOn(performance, 'now').mockReturnValue(0) + vi.spyOn(global, 'setTimeout') + vi.spyOn(global, 'clearTimeout') + + originalPlatform = navigator.platform + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.clearAllTimers() + + Object.defineProperty(navigator, 'platform', { + value: originalPlatform, + writable: true, + configurable: true + }) + }) + + function mockPlatform(platform: 'Mac' | 'Windows' | 'Linux') { + const platformMap = { + Mac: 'MacIntel', + Windows: 'Win32', + Linux: 'Linux x86_64' + } + + Object.defineProperty(navigator, 'platform', { + value: platformMap[platform], + writable: true, + configurable: true + }) + } + + describe('Mouse wheel detection from real devices', () => { + it('should detect mouse from QA data: Mac mouse with negative wheelDelta pattern', () => { + // Platform: macOS (Mac) + // Device: Mouse + // Expected: All events should be detected as mouse + mockPlatform('Mac') + + const testSequence = [ + { deltaX: 0, deltaY: 12, wheelDeltaY: -36 }, + { deltaX: 0, deltaY: 13, wheelDeltaY: -39 }, + { deltaX: 0, deltaY: 12, wheelDeltaY: -36 }, + { deltaX: 0, deltaY: 13, wheelDeltaY: -39 } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + // Mock time progression (16ms between events for ~60fps) + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe('mouse') + expect(result).toBe(false) + }) + }) + + it('should detect mouse from QA data: Mac mouse events with varying deltaY values', () => { + // This test captures the pattern where deltaY varies slightly (12, 13, 12, 13) + // but wheelDeltaY maintains the 3x ratio (-36, -39, -36, -39) + // Platform: macOS (Mac) + mockPlatform('Mac') + + const testSequence = [ + { deltaX: 0, deltaY: 12, wheelDeltaY: -36, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 13, wheelDeltaY: -39, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 12, wheelDeltaY: -36, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 13, wheelDeltaY: -39, expectedDevice: 'mouse' } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 20) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + pointer.isTrackpadGesture(event) + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + }) + }) + + it('should detect mouse from QA data: Mac mouse with varying scroll speeds', () => { + // Real data from QA testing showing mouse wheel behavior with different scroll speeds + // Platform: macOS (Mac) + // Device: Mouse (with varying scroll speeds) + // Expected: All events should be detected as mouse + mockPlatform('Mac') + + const testSequence = [ + { deltaX: 0, deltaY: 13, wheelDeltaY: -39, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 13, wheelDeltaY: -39, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 26, wheelDeltaY: -78, expectedDevice: 'mouse' }, // Double speed scroll + { deltaX: 0, deltaY: -13, wheelDeltaY: 39, expectedDevice: 'mouse' }, // Reverse direction + { deltaX: 0, deltaY: 12, wheelDeltaY: -36, expectedDevice: 'mouse' } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(false) // Not trackpad + }) + }) + + it('should detect mouse from QA data: Mac mouse with small deltaY pattern', () => { + // Real data from QA testing showing Mac mouse with smaller deltaY values + // Platform: macOS (Mac) + // Device: Mouse (slower/precise scrolling) + // Expected: All events should be detected as mouse + // Note: deltaY is 4.000244140625 with wheelDeltaY of ±120 (30x ratio) + mockPlatform('Mac') + + const testSequence = [ + { + deltaX: 0, + deltaY: 4.000244140625, + wheelDeltaY: -120, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: -4.000244140625, + wheelDeltaY: 120, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: 4.000244140625, + wheelDeltaY: -120, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: -4.000244140625, + wheelDeltaY: 120, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: 4.000244140625, + wheelDeltaY: -120, + expectedDevice: 'mouse' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(false) // Not trackpad + }) + }) + + it('should detect mouse from QA data: Windows mouse with high precision values', () => { + // Real data from QA testing showing Windows mouse wheel behavior + // Platform: Windows + // Device: Mouse with high-precision scrolling + // Expected: All events should be detected as mouse + // Note: Windows has characteristic fractional deltaY values like 111.111... + mockPlatform('Windows') + + const testSequence = [ + { + deltaX: 0, + deltaY: -111.11111615700719, + wheelDeltaY: 133, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: 111.11111615700719, + wheelDeltaY: -133, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: -111.11111615700719, + wheelDeltaY: 133, + expectedDevice: 'mouse' + }, + { + deltaX: 0, + deltaY: -111.11111615700719, + wheelDeltaY: 133, + expectedDevice: 'mouse' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(false) // Not trackpad + }) + }) + + it('should detect mouse from QA data: Windows mouse with integer deltaY pattern', () => { + // Real data from QA testing showing Windows mouse wheel behavior + // Platform: Windows + // Device: Mouse with standard scrolling + // Expected: All events should be detected as mouse + // Note: Windows mouse with clean integer deltaY of 100 and wheelDeltaY of ±120 + mockPlatform('Windows') + + const testSequence = [ + { deltaX: 0, deltaY: 100, wheelDeltaY: -120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: -100, wheelDeltaY: 120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 100, wheelDeltaY: -120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 100, wheelDeltaY: -120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: -100, wheelDeltaY: 120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 100, wheelDeltaY: -120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: 100, wheelDeltaY: -120, expectedDevice: 'mouse' }, + { deltaX: 0, deltaY: -100, wheelDeltaY: 120, expectedDevice: 'mouse' } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(false) // Not trackpad + }) + }) + }) + + describe('Trackpad detection from real devices', () => { + it('should detect trackpad from QA data: Windows trackpad pinch-to-zoom', () => { + // Platform: Windows + // Device: Precision Touchpad (pinch-to-zoom gesture) + // Expected: All events should be detected as trackpad + // Note: Windows trackpad has small deltaY values but constant wheelDeltaY + mockPlatform('Windows') + + const testSequence = [ + { + deltaX: 0, + deltaY: -3.3135088654249674, + wheelDeltaY: 133, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 8.94965318420894, + wheelDeltaY: -133, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -3.654589743292812, + wheelDeltaY: 133, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -23.625750933408778, + wheelDeltaY: 133, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -3.616658329584863, + wheelDeltaY: 133, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -1.999075238624841, + wheelDeltaY: 133, + ctrlKey: true, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY, + ctrlKey: eventData.ctrlKey + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Windows trackpad two-finger scroll', () => { + // Platform: Windows + // Device: Precision Touchpad (two-finger vertical scroll) + // Expected: All events should be detected as trackpad + // Note: Windows trackpad has small deltaY values with matching small wheelDeltaY + mockPlatform('Windows') + + const testSequence = [ + { + deltaX: 0, + deltaY: -2.222222323140144, + wheelDeltaY: 2, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 1.111111161570072, + wheelDeltaY: -1, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 16.66666742355108, + wheelDeltaY: -16, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 4.444444646280288, + wheelDeltaY: -4, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 4.444444646280288, + wheelDeltaY: -4, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 5.55555580785036, + wheelDeltaY: -5, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Windows trackpad horizontal scroll', () => { + // Real data from QA testing showing Windows trackpad horizontal scroll behavior + // Platform: Windows + // Device: Precision Touchpad (two-finger horizontal scroll) + // Expected: All events should be detected as trackpad + // Note: Windows trackpad horizontal scroll has deltaX only, no deltaY or wheelDeltaY + mockPlatform('Windows') + + const testSequence = [ + { + deltaX: -33.33333484710216, + deltaY: 0, + wheelDeltaY: 0, + expectedDevice: 'trackpad' + }, + { + deltaX: -37.77777949338245, + deltaY: 0, + wheelDeltaY: 0, + expectedDevice: 'trackpad' + }, + { + deltaX: 73.33333666362475, + deltaY: 0, + wheelDeltaY: 0, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Mac trackpad pinch-to-zoom', () => { + // Real data from QA testing showing trackpad pinch-to-zoom behavior + // Platform: macOS (Mac) + // Device: MacBook Trackpad (pinch-to-zoom gesture) + // Expected: All events should be detected as trackpad + // Note: ctrlKey is true for pinch-to-zoom on Mac + mockPlatform('Mac') + + const testSequence = [ + { + deltaX: 0, + deltaY: 1.206591010093689, + wheelDeltaY: -120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -1.3895320892333984, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -0.5978795289993286, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY, + ctrlKey: eventData.ctrlKey + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Mac trackpad pinch-to-zoom (set 2)', () => { + // Real data from QA testing showing another Mac trackpad pinch-to-zoom pattern + // Platform: macOS (Mac) + // Device: MacBook Trackpad (pinch-to-zoom gesture) + // Expected: All events should be detected as trackpad + // Note: ctrlKey is true for pinch-to-zoom on Mac, deltaY values vary more in this set + mockPlatform('Mac') + + const testSequence = [ + { + deltaX: 0, + deltaY: 1.9179956912994385, + wheelDeltaY: -120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -1.9791855812072754, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -0.8947280049324036, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -0.8947280049324036, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 0.80277419090271, + wheelDeltaY: -120, + ctrlKey: true, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY, + ctrlKey: eventData.ctrlKey + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Mac trackpad horizontal scroll', () => { + // Real data from QA testing showing Mac trackpad horizontal scroll behavior + // Platform: macOS (Mac) + // Device: MacBook Trackpad (two-finger horizontal scroll) + // Expected: All events should be detected as trackpad + // Note: Mac trackpad horizontal scroll has integer deltaX values, no deltaY or wheelDeltaY + mockPlatform('Mac') + + const testSequence = [ + { deltaX: 9, deltaY: 0, wheelDeltaY: 0, expectedDevice: 'trackpad' }, + { deltaX: -6, deltaY: 0, wheelDeltaY: 0, expectedDevice: 'trackpad' }, + { deltaX: 2, deltaY: 0, wheelDeltaY: 0, expectedDevice: 'trackpad' }, + { deltaX: -3, deltaY: 0, wheelDeltaY: 0, expectedDevice: 'trackpad' }, + { deltaX: 2, deltaY: 0, wheelDeltaY: 0, expectedDevice: 'trackpad' }, + { deltaX: -2, deltaY: 0, wheelDeltaY: 0, expectedDevice: 'trackpad' } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Mac trackpad vertical scroll', () => { + // Real data from QA testing showing Mac trackpad vertical scroll behavior + // Platform: macOS (Mac) + // Device: MacBook Trackpad (two-finger vertical scroll) + // Expected: All events should be detected as trackpad + // Note: Mac trackpad vertical scroll has very small integer deltaY (±1) with wheelDeltaY (±3) + mockPlatform('Mac') + + const testSequence = [ + { deltaX: 0, deltaY: 1, wheelDeltaY: -3, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: -1, wheelDeltaY: 3, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: 1, wheelDeltaY: -3, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: 1, wheelDeltaY: -3, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: 1, wheelDeltaY: -3, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: -1, wheelDeltaY: 3, expectedDevice: 'trackpad' } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Windows trackpad mixed gestures', () => { + // Real data from QA testing showing Windows trackpad with various gestures + // Platform: Windows + // Device: Precision Touchpad (mixed two-finger scrolling) + // Expected: All events should be detected as trackpad + // Note: This sequence shows both horizontal and vertical scrolling patterns + mockPlatform('Windows') + + const testSequence = [ + { + deltaX: 34.4444453, + deltaY: 31.1111119, + wheelDeltaY: -31, + expectedDevice: 'trackpad' + }, + { + deltaX: -3.3333334, + deltaY: -1.1111114, + wheelDeltaY: 1, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -37.77777877854715, + wheelDeltaY: 37, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -28.88888965, + wheelDeltaY: 28, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -62.22222387054825, + wheelDeltaY: 62, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: 6.666666843, + wheelDeltaY: -6, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + // Allow some time between events to avoid cooldown period + vi.spyOn(performance, 'now').mockReturnValue(index * 100) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Windows trackpad vertical scroll', () => { + // Real data from QA testing showing Windows trackpad two-finger vertical scrolling + // Platform: Windows + // Device: Precision Touchpad (two-finger vertical scroll) + // Expected: All events should be detected as trackpad + // Note: Windows trackpad shows small deltaY values with matching wheelDeltaY (opposite sign) + mockPlatform('Windows') + + const testSequence = [ + { deltaX: 0, deltaY: -6, wheelDeltaY: 6, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: 1, wheelDeltaY: -1, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: -19, wheelDeltaY: 18, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: 38, wheelDeltaY: -37, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: -4, wheelDeltaY: 4, expectedDevice: 'trackpad' }, + { deltaX: 0, deltaY: -21, wheelDeltaY: 21, expectedDevice: 'trackpad' } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) // Is trackpad + }) + }) + + it('should detect trackpad from QA data: Windows trackpad pinch gesture', () => { + // Real data from QA testing showing Windows trackpad pinch-to-zoom behavior + // Platform: Windows + // Device: Precision Touchpad (pinch gesture) + // Expected: All events should be detected as trackpad + // Note: Windows trackpad shows small decimal deltaY values with standard wheelDeltaY (-120/120) + mockPlatform('Windows') + + const testSequence = [ + { + deltaX: 0, + deltaY: 0.7864023208, + wheelDeltaY: -120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -1.8231786727, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -5.795222473, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + }, + { + deltaX: 0, + deltaY: -2.065727996, + wheelDeltaY: 120, + ctrlKey: true, + expectedDevice: 'trackpad' + } + ] + + pointer = new CanvasPointer(element) + + testSequence.forEach((eventData, index) => { + vi.spyOn(performance, 'now').mockReturnValue(index * 16) + + const event = new WheelEvent('wheel', { + deltaX: eventData.deltaX, + deltaY: eventData.deltaY, + ctrlKey: eventData.ctrlKey + }) + + Object.defineProperty(event, 'wheelDeltaY', { + value: eventData.wheelDeltaY, + writable: false + }) + + const result = pointer.isTrackpadGesture(event) + + expect(pointer.detectedDevice).toBe(eventData.expectedDevice) + expect(result).toBe(true) + }) + }) + }) +}) diff --git a/src/lib/litegraph/test/CanvasPointer.deviceDetection.test.ts b/src/lib/litegraph/test/CanvasPointer.deviceDetection.test.ts index 6c4c56c08..267f854ed 100644 --- a/src/lib/litegraph/test/CanvasPointer.deviceDetection.test.ts +++ b/src/lib/litegraph/test/CanvasPointer.deviceDetection.test.ts @@ -89,17 +89,6 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', 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, @@ -135,17 +124,6 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', }) 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, @@ -156,17 +134,6 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', 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') - }) }) }) @@ -193,32 +160,6 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', 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) @@ -244,32 +185,6 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', 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', () => { @@ -360,7 +275,7 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', }) describe('500ms Cooldown Period', () => { - it('should NOT allow switching from mouse to trackpad within 500ms', () => { + it('should allow switching from mouse to trackpad within 500ms', () => { pointer.detectedDevice = 'mouse' // First event at time 0 @@ -381,7 +296,7 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', deltaX: 0 }) pointer.isTrackpadGesture(event2) - expect(pointer.detectedDevice).toBe('mouse') + expect(pointer.detectedDevice).toBe('trackpad') }) it('should allow switching from mouse to trackpad after 500ms', () => { @@ -443,7 +358,7 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', // Send first mouse event at time 0 vi.spyOn(performance, 'now').mockReturnValue(0) pointer.isTrackpadGesture( - new WheelEvent('wheel', { deltaY: 60, deltaX: 0 }) + new WheelEvent('wheel', { deltaY: 75, deltaX: 0 }) ) // Send trackpad events within 500ms window @@ -977,7 +892,7 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', for (let i = 0; i < 10; i++) { vi.spyOn(performance, 'now').mockReturnValue(i * 30) // 30ms between events const event = new WheelEvent('wheel', { - deltaY: 60, + deltaY: 75, deltaX: 0 }) pointer.isTrackpadGesture(event) @@ -985,27 +900,6 @@ describe('CanvasPointer Device Detection - Efficient Timestamp-Based TDD Tests', } }) - 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