Module: Capybara::Apparition::Drag

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

Constant Summary collapse

DROP_STRING =
"function(strings){\n  var dt = new DataTransfer(),\n      opts = { cancelable: true, bubbles: true, dataTransfer: dt };\n  for (var i=0; i < strings.length; i++){\n    if (dt.items) {\n      dt.items.add(strings[i]['data'], strings[i]['type']);\n    } else {\n      dt.setData(strings[i]['type'], strings[i]['data']);\n    }\n  }\n  var dropEvent = new DragEvent('drop', opts);\n  this.dispatchEvent(dropEvent);\n}\n"
DROP_FILE =
"function(input){\n  var files = input.files,\n      dt = new DataTransfer(),\n      opts = { cancelable: true, bubbles: true, dataTransfer: dt };\n  input.parentElement.removeChild(input);\n  if (dt.items){\n    for (var i=0; i<files.length; i++){\n      dt.items.add(files[i]);\n    }\n  } else {\n    Object.defineProperty(dt, \"files\", {\n      value: files,\n      writable: false\n    });\n  }\n  var dropEvent = new DragEvent('drop', opts);\n  this.dispatchEvent(dropEvent);\n}\n"
ATTACH_FILE =
"function(){\n  var input = document.createElement('INPUT');\n  input.type = \"file\";\n  input.id = \"_capybara_drop_file\";\n  input.multiple = true;\n  document.body.appendChild(input);\n  return input;\n}\n"
MOUSEDOWN_TRACKER =
"window.capybara_mousedown_prevented = null;\ndocument.addEventListener('mousedown', ev => {\n  window.capybara_mousedown_prevented = ev.defaultPrevented;\n}, { once: true, passive: true })\n"
LEGACY_DRAG_CHECK =
"(function(el){\n  if ([true, null].includes(window.capybara_mousedown_prevented)){\n    return true;\n  }\n  do {\n    if (el.draggable) return false;\n  } while (el = el.parentElement );\n  return true;\n})(arguments[0])\n"
HTML5_DRAG_DROP_SCRIPT =
"let source = arguments[0];\nconst target = arguments[1];\nconst step_delay = arguments[2] * 1000;\nconst drop_modifiers = arguments[3];\nconst key_aliases = {\n  'cmd': 'meta',\n  'command': 'meta',\n  'control': 'ctrl',\n};\n\nfunction rectCenter(rect){\n  return new DOMPoint(\n    (rect.left + rect.right)/2,\n    (rect.top + rect.bottom)/2\n  );\n}\n\nfunction pointOnRect(pt, rect) {\n  var rectPt = rectCenter(rect);\n  var slope = (rectPt.y - pt.y) / (rectPt.x - pt.x);\n\n  if (pt.x <= rectPt.x) { // left side\n    var minXy = slope * (rect.left - pt.x) + pt.y;\n    if (rect.top <= minXy && minXy <= rect.bottom)\n      return new DOMPoint(rect.left, minXy);\n  }\n\n  if (pt.x >= rectPt.x) { // right side\n    var maxXy = slope * (rect.right - pt.x) + pt.y;\n    if (rect.top <= maxXy && maxXy <= rect.bottom)\n      return new DOMPoint(rect.right, maxXy);\n  }\n\n  if (pt.y <= rectPt.y) { // top side\n    var minYx = (rectPt.top - pt.y) / slope + pt.x;\n    if (rect.left <= minYx && minYx <= rect.right)\n      return new DOMPoint(minYx, rect.top);\n  }\n\n  if (pt.y >= rectPt.y) { // bottom side\n    var maxYx = (rect.bottom - pt.y) / slope + pt.x;\n    if (rect.left <= maxYx && maxYx <= rect.right)\n      return new DOMPoint(maxYx, rect.bottom);\n  }\n\n  return new DOMPoint(pt.x,pt.y);\n}\n\nfunction dragStart() {\n  return new Promise( resolve => {\n    var dragEvent = new DragEvent('dragstart', opts);\n    source.dispatchEvent(dragEvent);\n    setTimeout(resolve, step_delay)\n  })\n}\n\nfunction dragEnter() {\n  return new Promise( resolve => {\n    target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});\n    let targetRect = target.getBoundingClientRect(),\n    sourceCenter = rectCenter(source.getBoundingClientRect());\n\n    drop_modifiers.map(key => key_aliases[key] || key)\n                  .forEach(key => opts[key + 'Key'] = true);\n\n    // fire 2 dragover events to simulate dragging with a direction\n    let entryPoint = pointOnRect(sourceCenter, targetRect);\n    let dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);\n    let dragOverEvent = new DragEvent('dragover', dragOverOpts);\n    target.dispatchEvent(dragOverEvent);\n    setTimeout(resolve, step_delay)\n  })\n}\n\nfunction dragOnto() {\n  return new Promise( resolve => {\n    var targetCenter = rectCenter(target.getBoundingClientRect());\n    dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);\n    dragOverEvent = new DragEvent('dragover', dragOverOpts);\n    target.dispatchEvent(dragOverEvent);\n    setTimeout(resolve, step_delay, { drop: dragOverEvent.defaultPrevented, opts: dragOverOpts});\n  })\n}\n\nfunction dragLeave({ drop, opts: dragOverOpts }) {\n  return new Promise( resolve => {\n    var dragLeaveOptions = { ...opts, ...dragOverOpts };\n    var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);\n    target.dispatchEvent(dragLeaveEvent);\n    if (drop) {\n      var dropEvent = new DragEvent('drop', dragLeaveOptions);\n      target.dispatchEvent(dropEvent);\n    }\n    var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);\n    source.dispatchEvent(dragEndEvent);\n    setTimeout(resolve, step_delay);\n  })\n}\n\nconst dt = new DataTransfer();\nconst opts = { cancelable: true, bubbles: true, dataTransfer: dt };\n\nwhile (source && !source.draggable) {\n  source = source.parentElement;\n}\n\nif (source.tagName == 'A'){\n  dt.setData('text/uri-list', source.href);\n  dt.setData('text', source.href);\n}\nif (source.tagName == 'IMG'){\n  dt.setData('text/uri-list', source.src);\n  dt.setData('text', source.src);\n}\n\ndragStart().then(dragEnter).then(dragOnto).then(dragLeave)\n"

Instance Method Summary collapse

Instance Method Details

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



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

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
# 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
      other.scroll_if_needed
      sleep delay
      m.move_to(**other.visible_center)
      sleep delay
    ensure
      m.up
      sleep delay
    end
  end
end

#drop(*args) ⇒ Object



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

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