Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions lib/capybara/cuprite/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module Cuprite
class Browser < Ferrum::Browser
extend Forwardable

DRAG_HTML5_JS = File.read(File.expand_path("javascripts/drag.js", __dir__))

delegate %i[send_keys select set hover trigger before_click switch_to_frame
find_modal accept_confirm dismiss_confirm accept_prompt
dismiss_prompt reset_modals] => :page
Expand Down Expand Up @@ -139,19 +141,39 @@ def source
raise NotImplementedError
end

def drag(node, other, steps, delay = nil, scroll = true)
def drag(node, other, drop_modifiers, options = {})
steps = options.fetch(:steps, 1)
delay = options[:delay]
scroll = options.fetch(:scroll, true)
html5 = options[:html5]

execute("_cuprite.dragMousedownTracker()")

node.scroll_into_view if scroll
x1, y1 = node.find_position

mouse.move(x: x1, y: y1)
mouse.down
sleep delay if delay

other.scroll_into_view if scroll
html5 = !evaluate_on(node: node, expression: "_cuprite.legacyDragCheck(this)") if html5.nil?

if html5
drag_html5(node, other, drop_modifiers, delay)
else
modifiers = keyboard.modifiers(drop_modifiers)

x2, y2 = other.find_position
mouse.move(x: x2, y: y2, steps: steps)
other.scroll_into_view if scroll
x2, y2 = other.find_position
mouse.move(x: x2, y: y2, steps: steps)

mouse.up(modifiers: modifiers)
end
end

def drag_html5(node, other, drop_modifiers, delay)
keys = drop_modifiers.map(&:to_s)
evaluate_async(DRAG_HTML5_JS, timeout, node, other, (delay || 0.05) * 1000, keys)
mouse.up
end

Expand Down
118 changes: 118 additions & 0 deletions lib/capybara/cuprite/javascripts/drag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// HTML5 drag-and-drop emulation.
//
// Ported near-verbatim from Capybara's Selenium driver
// (capybara/selenium/extensions/html5_drag.rb, HTML5_DRAG_DROP_SCRIPT) so
// Cuprite matches Capybara's HTML5 drag behaviour and its shared specs. Kept
// close to the source to ease future syncs; known upstream quirks (rectPt.top
// in pointOnRect, undeclared `key`, callback.call(true)) are preserved
// deliberately. See https://github.com/rubycdp/cuprite/issues/314.

function rectCenter(rect){
return new DOMPoint(
(rect.left + rect.right)/2,
(rect.top + rect.bottom)/2
);
Comment thread
myabc marked this conversation as resolved.
}

function pointOnRect(pt, rect) {
var rectPt = rectCenter(rect);
var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);

if (pt.x <= rectPt.x) { // left side
var minXy = slope * (rect.left - pt.x) + pt.y;
if (rect.top <= minXy && minXy <= rect.bottom)
return new DOMPoint(rect.left, minXy);
}

if (pt.x >= rectPt.x) { // right side
var maxXy = slope * (rect.right - pt.x) + pt.y;
if (rect.top <= maxXy && maxXy <= rect.bottom)
return new DOMPoint(rect.right, maxXy);
}

if (pt.y <= rectPt.y) { // top side
var minYx = (rectPt.top - pt.y) / slope + pt.x;
if (rect.left <= minYx && minYx <= rect.right)
return new DOMPoint(minYx, rect.top);
}

if (pt.y >= rectPt.y) { // bottom side
var maxYx = (rect.bottom - pt.y) / slope + pt.x;
if (rect.left <= maxYx && maxYx <= rect.right)
return new DOMPoint(maxYx, rect.bottom);
}

return new DOMPoint(pt.x,pt.y);
}

function dragEnterTarget() {
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
var targetRect = target.getBoundingClientRect();
var sourceCenter = rectCenter(source.getBoundingClientRect());

for (var i = 0; i < drop_modifier_keys.length; i++) {
key = drop_modifier_keys[i];
if (key == "control"){
key = "ctrl"
}
opts[key + 'Key'] = true;
}

var dragEnterEvent = new DragEvent('dragenter', opts);
target.dispatchEvent(dragEnterEvent);

// fire 2 dragover events to simulate dragging with a direction
var entryPoint = pointOnRect(sourceCenter, targetRect)
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
target.dispatchEvent(dragOverEvent);
window.setTimeout(dragOnTarget, step_delay);
}

function dragOnTarget() {
var targetCenter = rectCenter(target.getBoundingClientRect());
var dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
target.dispatchEvent(dragOverEvent);
window.setTimeout(dragLeave, step_delay, dragOverEvent.defaultPrevented, dragOverOpts);
}

function dragLeave(drop, dragOverOpts) {
var dragLeaveOptions = Object.assign({}, opts, dragOverOpts);
var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
target.dispatchEvent(dragLeaveEvent);
if (drop) {
var dropEvent = new DragEvent('drop', dragLeaveOptions);
target.dispatchEvent(dropEvent);
}
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
source.dispatchEvent(dragEndEvent);
callback.call(true);
}

var source = arguments[0],
target = arguments[1],
step_delay = arguments[2],
drop_modifier_keys = arguments[3],
callback = arguments[4];

var dt = new DataTransfer();
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };

while (source && !source.draggable) {
source = source.parentElement;
}

if (source.tagName == 'A'){
dt.setData('text/uri-list', source.href);
dt.setData('text', source.href);
}
if (source.tagName == 'IMG'){
dt.setData('text/uri-list', source.src);
dt.setData('text', source.src);
}

var dragEvent = new DragEvent('dragstart', opts);
source.dispatchEvent(dragEvent);

window.setTimeout(dragEnterTarget, step_delay);
19 changes: 19 additions & 0 deletions lib/capybara/cuprite/javascripts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,25 @@ class Cuprite {
return node.contains(selectedNode);
}

dragMousedownTracker() {
window._cupriteMousedownPrevented = null;
document.addEventListener('mousedown', ev => {
window._cupriteMousedownPrevented = ev.defaultPrevented;
}, { once: true, passive: true });
}

legacyDragCheck(node) {
if ([true, null].indexOf(window._cupriteMousedownPrevented) >= 0) {
return true;
}

Comment thread
myabc marked this conversation as resolved.
let el = node;
do {
if (el.draggable) return false;
} while (el = el.parentElement);
return true;
}

// This command is purely for testing error handling
browserError() {
throw new Error("zomg");
Expand Down
5 changes: 4 additions & 1 deletion lib/capybara/cuprite/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Node < Capybara::Driver::Node
delegate %i[description] => :node
delegate %i[browser] => :driver

DRAG_MODIFIER_ALIASES = { control: :ctrl, command: :meta, cmd: :meta }.freeze

def initialize(driver, node)
super(driver, self)
@node = node
Expand Down Expand Up @@ -177,8 +179,9 @@ def hover
def drag_to(other, **options)
options[:steps] ||= 1
options[:scroll] = true unless options.key?(:scroll)
modifiers = Array(options[:drop_modifiers]).map { |m| DRAG_MODIFIER_ALIASES.fetch(m.to_sym, m.to_sym) }

command(:drag, other.node, options[:steps], options[:delay], options[:scroll])
command(:drag, other.node, modifiers, options.slice(:steps, :delay, :scroll, :html5))
end

def drag_by(x, y, **options)
Expand Down
15 changes: 0 additions & 15 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,6 @@ module TestSessions
RSpec.configure do |config|
config.define_derived_metadata do |metadata|
regexes = <<~REGEXP.split("\n").map { |s| Regexp.quote(s.strip) }.join("|")
#drag_to should simulate a single held down modifier key
#drag_to should simulate multiple held down modifier keys
#drag_to should support key aliases
#drag_to HTML5 should HTML5 drag and drop an object
#drag_to HTML5 should HTML5 drag and drop an object child
#drag_to HTML5 should set clientX/Y in dragover events
#drag_to HTML5 should preserve clientX/Y from last dragover event
#drag_to HTML5 should HTML5 drag and drop when scrolling needed
#drag_to HTML5 should drag HTML5 default draggable elements
#drag_to HTML5 should work with SortableJS
#drag_to HTML5 should drag HTML5 default draggable element child
#drag_to HTML5 should simulate a single held down modifier key
#drag_to HTML5 should simulate multiple held down modifier keys
#drag_to HTML5 should support key aliases
#drag_to HTML5 should trigger a dragenter event, before the first dragover event
node Element#drop can drop a file
node Element#drop can drop multiple files
node Element#drop can drop strings
Expand Down
Loading