From 396718be592126a0ee5fc978fd1ae82e7ced31ea Mon Sep 17 00:00:00 2001 From: Josh at WLTechBlog Date: Tue, 30 Sep 2025 14:53:35 -0500 Subject: [PATCH] bump --- daemon/daemon.go | 462 +++++++++++++++++++++++++++++++++++- daemon/drag_drop_helpers.js | 255 ++++++++++++++++++++ drag-drop-test.html | 309 ++++++++++++++++++++++++ 3 files changed, 1019 insertions(+), 7 deletions(-) create mode 100644 daemon/drag_drop_helpers.js create mode 100644 drag-drop-test.html diff --git a/daemon/daemon.go b/daemon/daemon.go index 9a0d400..de2cae9 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -6480,6 +6480,16 @@ func (d *Daemon) dragAndDropByOffset(tabID, sourceSelector string, offsetX, offs // performDragAndDrop performs the actual drag and drop operation between two elements func (d *Daemon) performDragAndDrop(page *rod.Page, sourceSelector, targetSelector string) error { + // First, try the enhanced HTML5 drag and drop approach + err := d.performHTML5DragAndDrop(page, sourceSelector, targetSelector) + if err == nil { + d.debugLog("HTML5 drag and drop completed successfully") + return nil + } + + d.debugLog("HTML5 drag and drop failed (%v), falling back to mouse events", err) + + // Fallback to the original mouse-based approach // Find source element sourceElement, err := page.Element(sourceSelector) if err != nil { @@ -6524,8 +6534,390 @@ func (d *Daemon) performDragAndDrop(page *rod.Page, sourceSelector, targetSelect return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, targetX, targetY) } +// injectDragDropHelpers injects the JavaScript drag and drop helper functions into the page +func (d *Daemon) injectDragDropHelpers(page *rod.Page) error { + // Read the JavaScript helper file + jsHelpers := ` +// HTML5 Drag and Drop Helper Functions for Cremote +// These functions are injected into web pages to provide reliable drag and drop functionality + +(function() { + 'use strict'; + + // Create a namespace to avoid conflicts + window.cremoteDragDrop = window.cremoteDragDrop || {}; + + /** + * Simulates HTML5 drag and drop between two elements + * @param {string} sourceSelector - CSS selector for source element + * @param {string} targetSelector - CSS selector for target element + * @returns {Promise} - Success status + */ + window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) { + const sourceElement = document.querySelector(sourceSelector); + const targetElement = document.querySelector(targetSelector); + + if (!sourceElement) { + throw new Error('Source element not found: ' + sourceSelector); + } + if (!targetElement) { + throw new Error('Target element not found: ' + targetSelector); + } + + // Make source draggable if not already + if (!sourceElement.draggable) { + sourceElement.draggable = true; + } + + // Create and dispatch dragstart event + const dragStartEvent = new DragEvent('dragstart', { + bubbles: true, + cancelable: true, + dataTransfer: new DataTransfer() + }); + + // Set drag data + dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); + dragStartEvent.dataTransfer.effectAllowed = 'all'; + + const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); + if (!dragStartResult) { + console.log('Dragstart was cancelled'); + return false; + } + + // Small delay to simulate realistic drag timing + await new Promise(resolve => setTimeout(resolve, 50)); + + // Create and dispatch dragover event on target + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dragOverResult = targetElement.dispatchEvent(dragOverEvent); + + // Create and dispatch drop event on target + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dropResult = targetElement.dispatchEvent(dropEvent); + + // Create and dispatch dragend event on source + const dragEndEvent = new DragEvent('dragend', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + sourceElement.dispatchEvent(dragEndEvent); + + return dropResult; + }; + + console.log('Cremote drag and drop helpers loaded successfully'); +})(); +` + + // Inject the JavaScript helpers + _, err := page.Eval(jsHelpers) + if err != nil { + return fmt.Errorf("failed to inject drag and drop helpers: %v", err) + } + + return nil +} + +// performHTML5DragAndDrop performs drag and drop using HTML5 drag events +func (d *Daemon) performHTML5DragAndDrop(page *rod.Page, sourceSelector, targetSelector string) error { + // Inject the helper functions + err := d.injectDragDropHelpers(page) + if err != nil { + return fmt.Errorf("failed to inject helpers: %v", err) + } + + // Execute the HTML5 drag and drop + jsCode := fmt.Sprintf(` + (async function() { + try { + const result = await window.cremoteDragDrop.dragElementToElement('%s', '%s'); + return { success: result, error: null }; + } catch (error) { + return { success: false, error: error.message }; + } + })() + `, sourceSelector, targetSelector) + + result, err := page.Eval(jsCode) + if err != nil { + return fmt.Errorf("failed to execute HTML5 drag and drop: %v", err) + } + + // Parse the result + resultMap := result.Value.Map() + if resultMap == nil { + return fmt.Errorf("invalid result from HTML5 drag and drop") + } + + success, exists := resultMap["success"] + if !exists || !success.Bool() { + errorMsg := "unknown error" + if errorVal, exists := resultMap["error"]; exists && errorVal.Str() != "" { + errorMsg = errorVal.Str() + } + return fmt.Errorf("HTML5 drag and drop failed: %s", errorMsg) + } + + return nil +} + +// injectEnhancedDragDropHelpers injects the complete JavaScript drag and drop helper functions +func (d *Daemon) injectEnhancedDragDropHelpers(page *rod.Page) error { + // Read the complete JavaScript helper file content + jsHelpers := ` +// Enhanced HTML5 Drag and Drop Helper Functions for Cremote +(function() { + 'use strict'; + + // Create a namespace to avoid conflicts + window.cremoteDragDrop = window.cremoteDragDrop || {}; + + /** + * Simulates HTML5 drag and drop between two elements + */ + window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) { + const sourceElement = document.querySelector(sourceSelector); + const targetElement = document.querySelector(targetSelector); + + if (!sourceElement) { + throw new Error('Source element not found: ' + sourceSelector); + } + if (!targetElement) { + throw new Error('Target element not found: ' + targetSelector); + } + + // Make source draggable if not already + if (!sourceElement.draggable) { + sourceElement.draggable = true; + } + + // Create and dispatch dragstart event + const dragStartEvent = new DragEvent('dragstart', { + bubbles: true, + cancelable: true, + dataTransfer: new DataTransfer() + }); + + dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); + dragStartEvent.dataTransfer.effectAllowed = 'all'; + + const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); + if (!dragStartResult) { + return false; + } + + await new Promise(resolve => setTimeout(resolve, 50)); + + // Create and dispatch dragover event on target + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + targetElement.dispatchEvent(dragOverEvent); + + // Create and dispatch drop event on target + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dropResult = targetElement.dispatchEvent(dropEvent); + + // Create and dispatch dragend event on source + const dragEndEvent = new DragEvent('dragend', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + sourceElement.dispatchEvent(dragEndEvent); + + return dropResult; + }; + + /** + * Checks if an element can receive drop events + */ + window.cremoteDragDrop.hasDropEventListener = function(element) { + if (element.ondrop) return true; + if (element.getAttribute('ondrop')) return true; + if (element.ondragover || element.getAttribute('ondragover')) return true; + + const dropIndicators = ['drop-zone', 'dropzone', 'droppable', 'drop-target']; + const className = element.className.toLowerCase(); + return dropIndicators.some(indicator => className.includes(indicator)); + }; + + /** + * Finds the best drop target at given coordinates + */ + window.cremoteDragDrop.findDropTargetAtCoordinates = function(x, y) { + const elements = document.elementsFromPoint(x, y); + + for (const element of elements) { + if (this.hasDropEventListener(element)) { + return element; + } + } + + return elements[0] || null; + }; + + /** + * Enhanced drag to coordinates that finds the best drop target + */ + window.cremoteDragDrop.smartDragToCoordinates = async function(sourceSelector, x, y) { + const sourceElement = document.querySelector(sourceSelector); + + if (!sourceElement) { + throw new Error('Source element not found: ' + sourceSelector); + } + + const targetElement = this.findDropTargetAtCoordinates(x, y); + if (!targetElement) { + throw new Error('No suitable drop target found at coordinates (' + x + ', ' + y + ')'); + } + + const canReceiveDrops = this.hasDropEventListener(targetElement); + + if (canReceiveDrops) { + const success = await this.dragElementToElement(sourceSelector, + targetElement.id ? '#' + targetElement.id : targetElement.tagName.toLowerCase()); + + return { + success: success, + method: 'element-to-element', + targetElement: { + tagName: targetElement.tagName, + id: targetElement.id, + className: targetElement.className, + hasDropListener: true + } + }; + } else { + // Fall back to coordinate-based drag with manual event dispatch + const result = await this.dragElementToCoordinates(sourceSelector, x, y); + result.method = 'coordinate-based'; + return result; + } + }; + + /** + * Simulates HTML5 drag and drop from element to coordinates + */ + window.cremoteDragDrop.dragElementToCoordinates = async function(sourceSelector, x, y) { + const sourceElement = document.querySelector(sourceSelector); + + if (!sourceElement) { + throw new Error('Source element not found: ' + sourceSelector); + } + + const targetElement = document.elementFromPoint(x, y); + if (!targetElement) { + throw new Error('No element found at coordinates (' + x + ', ' + y + ')'); + } + + if (!sourceElement.draggable) { + sourceElement.draggable = true; + } + + const dragStartEvent = new DragEvent('dragstart', { + bubbles: true, + cancelable: true, + dataTransfer: new DataTransfer() + }); + + dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); + dragStartEvent.dataTransfer.effectAllowed = 'all'; + + const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); + if (!dragStartResult) { + return { success: false, reason: 'Dragstart was cancelled', targetElement: null }; + } + + await new Promise(resolve => setTimeout(resolve, 50)); + + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + dataTransfer: dragStartEvent.dataTransfer + }); + + targetElement.dispatchEvent(dragOverEvent); + + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dropResult = targetElement.dispatchEvent(dropEvent); + + const dragEndEvent = new DragEvent('dragend', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + sourceElement.dispatchEvent(dragEndEvent); + + return { + success: dropResult, + targetElement: { + tagName: targetElement.tagName, + id: targetElement.id, + className: targetElement.className, + hasDropListener: this.hasDropEventListener(targetElement) + } + }; + }; + + console.log('Enhanced Cremote drag and drop helpers loaded successfully'); +})(); +` + + // Inject the JavaScript helpers + _, err := page.Eval(jsHelpers) + if err != nil { + return fmt.Errorf("failed to inject enhanced drag and drop helpers: %v", err) + } + + return nil +} + // performDragAndDropToCoordinates performs drag and drop from element to specific coordinates func (d *Daemon) performDragAndDropToCoordinates(page *rod.Page, sourceSelector string, targetX, targetY int) error { + // First, try the enhanced HTML5 approach with smart target detection + err := d.performHTML5DragToCoordinates(page, sourceSelector, targetX, targetY) + if err == nil { + d.debugLog("HTML5 coordinate drag completed successfully") + return nil + } + + d.debugLog("HTML5 coordinate drag failed (%v), falling back to mouse events", err) + + // Fallback to the original mouse-based approach // Find source element sourceElement, err := page.Element(sourceSelector) if err != nil { @@ -6549,21 +6941,67 @@ func (d *Daemon) performDragAndDropToCoordinates(page *rod.Page, sourceSelector return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, float64(targetX), float64(targetY)) } +// performHTML5DragToCoordinates performs HTML5 drag to coordinates with smart target detection +func (d *Daemon) performHTML5DragToCoordinates(page *rod.Page, sourceSelector string, targetX, targetY int) error { + // First, inject the enhanced helper functions that include coordinate support + err := d.injectEnhancedDragDropHelpers(page) + if err != nil { + return fmt.Errorf("failed to inject enhanced helpers: %v", err) + } + + // Execute the smart coordinate drag + jsCode := fmt.Sprintf(` + (async function() { + try { + const result = await window.cremoteDragDrop.smartDragToCoordinates('%s', %d, %d); + return { success: result.success, method: result.method, error: null, targetInfo: result.targetElement }; + } catch (error) { + return { success: false, error: error.message, method: 'failed', targetInfo: null }; + } + })() + `, sourceSelector, targetX, targetY) + + result, err := page.Eval(jsCode) + if err != nil { + return fmt.Errorf("failed to execute HTML5 coordinate drag: %v", err) + } + + // Parse the result + resultMap := result.Value.Map() + if resultMap == nil { + return fmt.Errorf("invalid result from HTML5 coordinate drag") + } + + success, exists := resultMap["success"] + if !exists || !success.Bool() { + errorMsg := "unknown error" + if errorVal, exists := resultMap["error"]; exists && errorVal.Str() != "" { + errorMsg = errorVal.Str() + } + return fmt.Errorf("HTML5 coordinate drag failed: %s", errorMsg) + } + + // Log the method used for debugging + if method, exists := resultMap["method"]; exists && method.Str() != "" { + d.debugLog("HTML5 coordinate drag used method: %s", method.Str()) + } + + return nil +} + // performDragAndDropByOffset performs drag and drop from element by relative offset func (d *Daemon) performDragAndDropByOffset(page *rod.Page, sourceSelector string, offsetX, offsetY int) error { - // Find source element + // First, calculate the target coordinates sourceElement, err := page.Element(sourceSelector) if err != nil { return fmt.Errorf("failed to find source element %s: %v", sourceSelector, err) } - // Get source element position and size sourceBox, err := sourceElement.Shape() if err != nil { return fmt.Errorf("failed to get source element shape: %v", err) } - // Calculate source center point from the first quad if len(sourceBox.Quads) == 0 { return fmt.Errorf("source element has no quads") } @@ -6571,11 +7009,21 @@ func (d *Daemon) performDragAndDropByOffset(page *rod.Page, sourceSelector strin sourceX := (sourceQuad[0] + sourceQuad[2] + sourceQuad[4] + sourceQuad[6]) / 4 sourceY := (sourceQuad[1] + sourceQuad[3] + sourceQuad[5] + sourceQuad[7]) / 4 - // Calculate target point by adding offset - targetX := sourceX + float64(offsetX) - targetY := sourceY + float64(offsetY) + // Calculate target coordinates + targetX := int(sourceX + float64(offsetX)) + targetY := int(sourceY + float64(offsetY)) - return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, targetX, targetY) + // Try the enhanced HTML5 approach first (reuse coordinate logic) + err = d.performHTML5DragToCoordinates(page, sourceSelector, targetX, targetY) + if err == nil { + d.debugLog("HTML5 offset drag completed successfully") + return nil + } + + d.debugLog("HTML5 offset drag failed (%v), falling back to mouse events", err) + + // Fallback to the original mouse-based approach + return d.performDragAndDropBetweenPoints(page, sourceX, sourceY, float64(targetX), float64(targetY)) } // performDragAndDropBetweenPoints performs the actual drag and drop using Chrome DevTools Protocol mouse events diff --git a/daemon/drag_drop_helpers.js b/daemon/drag_drop_helpers.js new file mode 100644 index 0000000..0b8fe95 --- /dev/null +++ b/daemon/drag_drop_helpers.js @@ -0,0 +1,255 @@ +// HTML5 Drag and Drop Helper Functions for Cremote +// These functions are injected into web pages to provide reliable drag and drop functionality + +(function() { + 'use strict'; + + // Create a namespace to avoid conflicts + window.cremoteDragDrop = window.cremoteDragDrop || {}; + + /** + * Simulates HTML5 drag and drop between two elements + * @param {string} sourceSelector - CSS selector for source element + * @param {string} targetSelector - CSS selector for target element + * @returns {Promise} - Success status + */ + window.cremoteDragDrop.dragElementToElement = async function(sourceSelector, targetSelector) { + const sourceElement = document.querySelector(sourceSelector); + const targetElement = document.querySelector(targetSelector); + + if (!sourceElement) { + throw new Error(`Source element not found: ${sourceSelector}`); + } + if (!targetElement) { + throw new Error(`Target element not found: ${targetSelector}`); + } + + // Make source draggable if not already + if (!sourceElement.draggable) { + sourceElement.draggable = true; + } + + // Create and dispatch dragstart event + const dragStartEvent = new DragEvent('dragstart', { + bubbles: true, + cancelable: true, + dataTransfer: new DataTransfer() + }); + + // Set drag data + dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); + dragStartEvent.dataTransfer.effectAllowed = 'all'; + + const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); + if (!dragStartResult) { + console.log('Dragstart was cancelled'); + return false; + } + + // Small delay to simulate realistic drag timing + await new Promise(resolve => setTimeout(resolve, 50)); + + // Create and dispatch dragover event on target + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dragOverResult = targetElement.dispatchEvent(dragOverEvent); + + // Create and dispatch drop event on target + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dropResult = targetElement.dispatchEvent(dropEvent); + + // Create and dispatch dragend event on source + const dragEndEvent = new DragEvent('dragend', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + sourceElement.dispatchEvent(dragEndEvent); + + return dropResult; + }; + + /** + * Simulates HTML5 drag and drop from element to coordinates + * @param {string} sourceSelector - CSS selector for source element + * @param {number} x - Target X coordinate + * @param {number} y - Target Y coordinate + * @returns {Promise} - Result with success status and target element info + */ + window.cremoteDragDrop.dragElementToCoordinates = async function(sourceSelector, x, y) { + const sourceElement = document.querySelector(sourceSelector); + + if (!sourceElement) { + throw new Error(`Source element not found: ${sourceSelector}`); + } + + // Find element at target coordinates + const targetElement = document.elementFromPoint(x, y); + if (!targetElement) { + throw new Error(`No element found at coordinates (${x}, ${y})`); + } + + // Make source draggable if not already + if (!sourceElement.draggable) { + sourceElement.draggable = true; + } + + // Create and dispatch dragstart event + const dragStartEvent = new DragEvent('dragstart', { + bubbles: true, + cancelable: true, + dataTransfer: new DataTransfer() + }); + + dragStartEvent.dataTransfer.setData('text/plain', sourceElement.id || sourceSelector); + dragStartEvent.dataTransfer.effectAllowed = 'all'; + + const dragStartResult = sourceElement.dispatchEvent(dragStartEvent); + if (!dragStartResult) { + return { success: false, reason: 'Dragstart was cancelled', targetElement: null }; + } + + await new Promise(resolve => setTimeout(resolve, 50)); + + // Create dragover event with coordinates + const dragOverEvent = new DragEvent('dragover', { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + dataTransfer: dragStartEvent.dataTransfer + }); + + targetElement.dispatchEvent(dragOverEvent); + + // Create drop event with coordinates + const dropEvent = new DragEvent('drop', { + bubbles: true, + cancelable: true, + clientX: x, + clientY: y, + dataTransfer: dragStartEvent.dataTransfer + }); + + const dropResult = targetElement.dispatchEvent(dropEvent); + + // Dispatch dragend + const dragEndEvent = new DragEvent('dragend', { + bubbles: true, + cancelable: true, + dataTransfer: dragStartEvent.dataTransfer + }); + + sourceElement.dispatchEvent(dragEndEvent); + + return { + success: dropResult, + targetElement: { + tagName: targetElement.tagName, + id: targetElement.id, + className: targetElement.className, + hasDropListener: this.hasDropEventListener(targetElement) + } + }; + }; + + /** + * Checks if an element can receive drop events + * @param {Element} element - Element to check + * @returns {boolean} - Whether element can receive drops + */ + window.cremoteDragDrop.hasDropEventListener = function(element) { + // Check for ondrop attribute + if (element.ondrop) return true; + + // Check for drop event listeners (this is limited but covers common cases) + if (element.getAttribute('ondrop')) return true; + + // Check if element has dragover listeners (usually indicates drop capability) + if (element.ondragover || element.getAttribute('ondragover')) return true; + + // Check for common drop zone classes/attributes + const dropIndicators = ['drop-zone', 'dropzone', 'droppable', 'drop-target']; + const className = element.className.toLowerCase(); + const hasDropClass = dropIndicators.some(indicator => className.includes(indicator)); + + return hasDropClass; + }; + + /** + * Finds the best drop target at given coordinates + * @param {number} x - X coordinate + * @param {number} y - Y coordinate + * @returns {Element|null} - Best drop target element or null + */ + window.cremoteDragDrop.findDropTargetAtCoordinates = function(x, y) { + const elements = document.elementsFromPoint(x, y); + + // Look for elements that can receive drops, starting from topmost + for (const element of elements) { + if (this.hasDropEventListener(element)) { + return element; + } + } + + // If no explicit drop target found, return the topmost element + return elements[0] || null; + }; + + /** + * Enhanced drag to coordinates that finds the best drop target + * @param {string} sourceSelector - CSS selector for source element + * @param {number} x - Target X coordinate + * @param {number} y - Target Y coordinate + * @returns {Promise} - Enhanced result with target analysis + */ + window.cremoteDragDrop.smartDragToCoordinates = async function(sourceSelector, x, y) { + const sourceElement = document.querySelector(sourceSelector); + + if (!sourceElement) { + throw new Error(`Source element not found: ${sourceSelector}`); + } + + // Find the best drop target at coordinates + const targetElement = this.findDropTargetAtCoordinates(x, y); + if (!targetElement) { + throw new Error(`No suitable drop target found at coordinates (${x}, ${y})`); + } + + const canReceiveDrops = this.hasDropEventListener(targetElement); + + // If we found a proper drop target, use element-to-element drag + if (canReceiveDrops) { + const success = await this.dragElementToElement(sourceSelector, + targetElement.id ? `#${targetElement.id}` : targetElement.tagName.toLowerCase()); + + return { + success: success, + method: 'element-to-element', + targetElement: { + tagName: targetElement.tagName, + id: targetElement.id, + className: targetElement.className, + hasDropListener: true + } + }; + } else { + // Fall back to coordinate-based drag + const result = await this.dragElementToCoordinates(sourceSelector, x, y); + result.method = 'coordinate-based'; + return result; + } + }; + + console.log('Cremote drag and drop helpers loaded successfully'); +})(); diff --git a/drag-drop-test.html b/drag-drop-test.html new file mode 100644 index 0000000..aaaa5e1 --- /dev/null +++ b/drag-drop-test.html @@ -0,0 +1,309 @@ + + + + + + Drag and Drop Test Page + + + +

Drag and Drop Test Page

+

This page tests various drag and drop scenarios for debugging automation tools.

+ +
+

Test 1: Element to Element Drag

+

Drag the green box to the drop zone

+
+
Drag Me
+
Drop Here
+
+
+ +
+

Test 2: Multiple Draggable Items

+

Drag any item to any drop zone

+
+
Item A
+
Item B
+
Item C
+
Zone 1
+
Zone 2
+
+
+ +
+

Test 3: Coordinate-based Drop

+

Drag the item and drop it at specific coordinates in the blue area

+
+
Drag to XY
+
+
+ Drop anywhere in this blue area +
+
+
+
+ +
+

Test 4: Offset-based Drag

+

Drag the item by a relative offset

+
+
Offset Drag
+
+ Expected drop area (150px right, 50px down) +
+
+
+ + + + +
+
Event Log:
+
+ + + +