Module: Capybara::Apparition::Drag

Included in:
Node
Defined in:
lib/capybara/apparition/node/drag.rb

Constant Summary collapse

DROP_STRING =
<<~JS
  function(strings){
    var dt = new DataTransfer(),
        opts = { cancelable: true, bubbles: true, dataTransfer: dt };
    for (var i=0; i < strings.length; i++){
      if (dt.items) {
        dt.items.add(strings[i]['data'], strings[i]['type']);
      } else {
        dt.setData(strings[i]['type'], strings[i]['data']);
      }
    }
    var dropEvent = new DragEvent('drop', opts);
    this.dispatchEvent(dropEvent);
  }
JS
DROP_FILE =
<<~JS
  function(input){
    var files = input.files,
        dt = new DataTransfer(),
        opts = { cancelable: true, bubbles: true, dataTransfer: dt };
    input.parentElement.removeChild(input);
    if (dt.items){
      for (var i=0; i<files.length; i++){
        dt.items.add(files[i]);
      }
    } else {
      Object.defineProperty(dt, "files", {
        value: files,
        writable: false
      });
    }
    var dropEvent = new DragEvent('drop', opts);
    this.dispatchEvent(dropEvent);
  }
JS
ATTACH_FILE =
<<~JS
  function(){
    var input = document.createElement('INPUT');
    input.type = "file";
    input.id = "_capybara_drop_file";
    input.multiple = true;
    document.body.appendChild(input);
    return input;
  }
JS
MOUSEDOWN_TRACKER =
<<~JS
  window.capybara_mousedown_prevented = null;
  document.addEventListener('mousedown', ev => {
    window.capybara_mousedown_prevented = ev.defaultPrevented;
  }, { once: true, passive: true })
JS
LEGACY_DRAG_CHECK =
<<~JS
  (function(el){
    if ([true, null].includes(window.capybara_mousedown_prevented)){
      return true;
    }
    do {
      if (el.draggable) return false;
    } while (el = el.parentElement );
    return true;
  })(arguments[0])
JS
HTML5_DRAG_DROP_SCRIPT =
<<~JS
  let source = arguments[0];
  const target = arguments[1];
  const step_delay = arguments[2] * 1000;
  const drop_modifiers = arguments[3];
  const key_aliases = {
    'cmd': 'meta',
    'command': 'meta',
    'control': 'ctrl',
  };

  function rectCenter(rect){
    return new DOMPoint(
      (rect.left + rect.right)/2,
      (rect.top + rect.bottom)/2
    );
  }

  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 dragStart() {
    return new Promise( resolve => {
      var dragEvent = new DragEvent('dragstart', opts);
      source.dispatchEvent(dragEvent);
      setTimeout(resolve, step_delay)
    })
  }

  function dragEnter() {
    return new Promise( resolve => {
      target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
      let targetRect = target.getBoundingClientRect(),
      sourceCenter = rectCenter(source.getBoundingClientRect());

      drop_modifiers.map(key => key_aliases[key] || key)
                    .forEach(key => opts[key + 'Key'] = true);

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

  function dragOnto() {
    return new Promise( resolve => {
      var targetCenter = rectCenter(target.getBoundingClientRect());
      dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
      dragOverEvent = new DragEvent('dragover', dragOverOpts);
      target.dispatchEvent(dragOverEvent);
      setTimeout(resolve, step_delay, { drop: dragOverEvent.defaultPrevented, opts: dragOverOpts});
    })
  }

  function dragLeave({ drop, opts: dragOverOpts }) {
    return new Promise( resolve => {
      var dragLeaveOptions = { ...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);
      setTimeout(resolve, step_delay);
    })
  }

  const dt = new DataTransfer();
  const 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);
  }

  dragStart().then(dragEnter).then(dragOnto).then(dragLeave)
JS

Instance Method Summary collapse

Instance Method Details

#drag_by(x, y, delay: 0.1) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/capybara/apparition/node/drag.rb', line 33

def drag_by(x, y, delay: 0.1)
  pos = visible_center
  raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?

  other_pos = { x: pos[:x] + x, y: pos[:y] + y }
  raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test['selector'], pos]) unless mouse_event_test?(**pos)

  @page.mouse.move_to(**pos).down
  sleep delay
  @page.mouse.move_to(**other_pos)
  sleep delay
  @page.mouse.up
end

#drag_to(other, delay: 0.1, html5: nil, drop_modifiers: []) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/capybara/apparition/node/drag.rb', line 5

def drag_to(other, delay: 0.1, html5: nil, drop_modifiers: [])
  drop_modifiers = Array(drop_modifiers)

  driver.execute_script MOUSEDOWN_TRACKER
  scroll_if_needed
  m = @page.mouse
  m.move_to(**visible_center)
  sleep delay
  m.down
  html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
  if html5
    driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, other, delay, drop_modifiers
    m.up(**other.visible_center)
  else
    @page.keyboard.with_keys(drop_modifiers) do
      begin
        other.scroll_if_needed
        sleep delay
        m.move_to(**other.visible_center)
        sleep delay
      ensure
        m.up
        sleep delay
      end
    end
  end
end

#drop(*args) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/capybara/apparition/node/drag.rb', line 47

def drop(*args)
  if args[0].is_a? String
    input = evaluate_on ATTACH_FILE
    tag_name = input['description'].split(/[\.#]/, 2)[0]
    input = Capybara::Apparition::Node.new(driver, @page, input['objectId'], tag_name: tag_name)
    input.set(args)
    evaluate_on DROP_FILE, objectId: input.id
  else
    items = args.each_with_object([]) do |arg, arr|
      arg.each_with_object(arr) do |(type, data), arr_|
        arr_ << { type: type, data: data }
      end
    end
    evaluate_on DROP_STRING, value: items
  end
end