mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 08:30:08 +00:00
## Summary - Fix slot link drag and snap/attraction not working on mobile browsers in Vue Nodes 2.0 mode - Use `document.elementFromPoint()` to get the actual element under the pointer instead of relying on `event.target` ## Root Cause On touch/mobile devices, pointer events have "implicit pointer capture" - when you touch an element, all subsequent pointer events (`pointermove`, `pointerup`) for that touch are sent to the same element where the touch started, regardless of where the pointer moves. The code was using `event.target` to find slots under the pointer for snap/attraction. On touch devices, this always returned the original slot element (where the drag started), not the element currently under the touch point. This caused: - No snap/attraction when dragging links over other slots - Connections failing when dropping on target slots ## Before https://github.com/user-attachments/assets/55b56d5c-9744-4d6c-abfd-3a2136ab25bc ## After https://github.com/user-attachments/assets/5bdf2a22-0025-4ae1-9358-35f0100b67d4 ## Test plan - [ ] Enable Vue Nodes 2.0 mode in settings - [ ] Test on mobile browser or Chrome DevTools mobile simulation - [ ] Drag a link from one node's output slot to another node's input slot - [ ] Verify the link snaps/attracts to compatible slots during drag - [ ] Verify the connection is made successfully on drop Fixes #7224
117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { resolvePointerTarget } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
|
|
|
|
describe('resolvePointerTarget', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('returns element from elementFromPoint when available', () => {
|
|
const targetElement = document.createElement('div')
|
|
targetElement.className = 'lg-slot'
|
|
|
|
const spy = vi
|
|
.spyOn(document, 'elementFromPoint')
|
|
.mockReturnValue(targetElement)
|
|
|
|
const fallback = document.createElement('span')
|
|
const result = resolvePointerTarget(100, 200, fallback)
|
|
|
|
expect(spy).toHaveBeenCalledWith(100, 200)
|
|
expect(result).toBe(targetElement)
|
|
})
|
|
|
|
it('returns fallback when elementFromPoint returns null', () => {
|
|
const spy = vi.spyOn(document, 'elementFromPoint').mockReturnValue(null)
|
|
|
|
const fallback = document.createElement('span')
|
|
fallback.className = 'fallback-element'
|
|
|
|
const result = resolvePointerTarget(100, 200, fallback)
|
|
|
|
expect(spy).toHaveBeenCalledWith(100, 200)
|
|
expect(result).toBe(fallback)
|
|
})
|
|
|
|
it('returns null fallback when both elementFromPoint and fallback are null', () => {
|
|
vi.spyOn(document, 'elementFromPoint').mockReturnValue(null)
|
|
|
|
const result = resolvePointerTarget(100, 200, null)
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
describe('touch/mobile pointer capture simulation', () => {
|
|
it('resolves correct target when touch moves over different element', () => {
|
|
// Simulate the touch scenario:
|
|
// - User touches slot A (event.target = slotA)
|
|
// - User drags over slot B (elementFromPoint returns slotB)
|
|
// - resolvePointerTarget should return slotB, not slotA
|
|
|
|
const slotA = document.createElement('div')
|
|
slotA.className = 'lg-slot slot-a'
|
|
slotA.setAttribute('data-slot-key', 'node1-0-input')
|
|
|
|
const slotB = document.createElement('div')
|
|
slotB.className = 'lg-slot slot-b'
|
|
slotB.setAttribute('data-slot-key', 'node2-0-input')
|
|
|
|
// When pointer is over slotB, elementFromPoint returns slotB
|
|
vi.spyOn(document, 'elementFromPoint').mockReturnValue(slotB)
|
|
|
|
// But the fallback (event.target on touch) is still slotA
|
|
const result = resolvePointerTarget(150, 250, slotA)
|
|
|
|
// Should return slotB (the actual element under pointer), not slotA
|
|
expect(result).toBe(slotB)
|
|
expect(result).not.toBe(slotA)
|
|
})
|
|
|
|
it('falls back to original target when pointer is outside viewport', () => {
|
|
// When pointer is outside the document (e.g., dragged off screen),
|
|
// elementFromPoint returns null
|
|
|
|
const slotA = document.createElement('div')
|
|
slotA.className = 'lg-slot slot-a'
|
|
|
|
vi.spyOn(document, 'elementFromPoint').mockReturnValue(null)
|
|
|
|
const result = resolvePointerTarget(-100, -100, slotA)
|
|
|
|
// Should fall back to the original target
|
|
expect(result).toBe(slotA)
|
|
})
|
|
})
|
|
|
|
describe('integration with slot detection', () => {
|
|
it('returned element can be used with closest() for slot detection', () => {
|
|
// Create a nested structure like the real DOM
|
|
const nodeContainer = document.createElement('div')
|
|
nodeContainer.setAttribute('data-node-id', 'node123')
|
|
|
|
const slotWrapper = document.createElement('div')
|
|
slotWrapper.className = 'lg-slot'
|
|
|
|
const slotDot = document.createElement('div')
|
|
slotDot.className = 'slot-dot'
|
|
slotDot.setAttribute('data-slot-key', 'node123-0-input')
|
|
|
|
slotWrapper.appendChild(slotDot)
|
|
nodeContainer.appendChild(slotWrapper)
|
|
|
|
// elementFromPoint returns the innermost element (slot dot)
|
|
vi.spyOn(document, 'elementFromPoint').mockReturnValue(slotDot)
|
|
|
|
const result = resolvePointerTarget(100, 100, null)
|
|
|
|
// Verify we can use closest() to find parent slot and node
|
|
expect(result).toBeInstanceOf(HTMLElement)
|
|
const htmlResult = result as HTMLElement
|
|
expect(htmlResult.closest('.lg-slot')).toBe(slotWrapper)
|
|
expect(htmlResult.closest('[data-node-id]')).toBe(nodeContainer)
|
|
expect(htmlResult.getAttribute('data-slot-key')).toBe('node123-0-input')
|
|
})
|
|
})
|
|
})
|