Drag-and-Drop in Watir

A month ago I was working with a team that had a need to write acceptance tests for a feature that included drag-and-drop. My initial search turned up some code that implemented it well. I have since tried to find that code again with no success so I am sorry for not being able to give credit where it is due. Anyway, I enhanced the code I originally found and wish to present it here.

What is presented below is an Internet Explorer only solution because that is what the client needed. I would be happy to work with somebody to make this enhancement more robust and add Firefox support. Just let me know if you have the time and want to help.

The first piece of code is a new module that provides low level access to mouse events. Here it is:

module  WindowsInput
  SetCursorPos = Win32API.new('user32', 'SetCursorPos', 'II', 'I')
  SendInput = Win32API.new('user32', 'SendInput', 'IPI', 'I')
  INPUT_MOUSE = 0
  MOUSEEVENTF_LEFTDOWN = 0x0002
  MOUSEEVENTF_LEFTUP = 0x0004

  module_function

  def send_input(inputs)
    n = inputs.size
    ptr = inputs.collect {|i| i.to_s}.join
    SendInput.call(n, ptr, inputs[0].size)
  end

  def create_mouse_input(mouse_flag)
    mi = Array.new(7,0)
    mi[0] = INPUT_MOUSE
    mi[4] = mouse_flag
    mi.pack('LLLLLLL')
  end

  def move_mouse(x, y)
    SetCursorPos.call(x,y)
  end

  def left_click
    send_input([leftdown_event, leftup_event])
  end

  def left_down
    send_input([leftdown_event])
  end

  def left_up
    send_input([leftup_event])
  end

  def leftup_event
    create_mouse_input(MOUSEEVENTF_LEFTUP)
  end

  def leftdown_event
    create_mouse_input(MOUSEEVENTF_LEFTDOWN)
  end
end

Next are my enhancements to Element:

module Watir
  class Element
    def drag_drop_on(target, src_offset=0, dst_offset=0)
      assert_target target
      drop_x = target.left_edge_absolute + dst_offset
      drop_y = target.top_edge_absolute + dst_offset
      drag_to(drop_x, drop_y, src_offset)
    end

    def drag_drop_distance(distance_x, distance_y, src_offset=0, dst_offset=0)
      drag_x, drag_y = source_x_y(src_offset)
      drop_x = drag_x + distance_x + dst_offset
      drop_y = drag_y + distance_y + dst_offset
      drag_to(drop_x, drop_y, src_offset)
    end

    def drag_drop_at(drop_x, drop_y, src_offset=0)
      drag_to(drop_x, drop_y, src_offset)
    end

    def drag_drop_below(target, src_offset=0, dst_offset=0)
      assert_target target
      drop_x = target.left_edge_absolute + dst_offset
      drop_y = target.bottom_edge_absolute + 2 + dst_offset
      drag_to(drop_x, drop_y, src_offset)
    end

    def drag_drop_above(target, src_offset=0, dst_offset=0)
      assert_target target
      drop_x = target.left_edge_absolute + dst_offset
      drop_y = target.top_edge_absolute - 2 + dst_offset
      drag_to(drop_x, drop_y, src_offset)
    end

    private

    def drag_to(drop_x, drop_y, src_offset)
      drag_x, drag_y = source_x_y(src_offset)
      WindowsInput.move_mouse(drag_x, drag_y)
      WindowsInput.left_down
      WindowsInput.move_mouse(drop_x, drop_y)
      WindowsInput.left_up
    end

    def source_x_y(src_offset)
      return left_edge_absolute + src_offset, top_edge_absolute + src_offset
    end

    def assert_target(target)
      target.assert_exists
      target.assert_enabled
    end

    def top_edge
      assert_target self
      ole_object.getBoundingClientRect.top.to_i
    end

    def top_edge_absolute
      top_edge + page_container.document.parentWindow.screenTop.to_i
    end

    def left_edge
      assert_target self
      ole_object.getBoundingClientRect.left.to_i
    end

    def left_edge_absolute
      left_edge + page_container.document.parentWindow.screenLeft.to_i
    end

    def bottom_edge
      assert_target self
      ole_object.getBoundingClientRect.bottom.to_i
    end

    def bottom_edge_absolute
      bottom_edge + page_container.document.parentWindow.screenTop.to_i
    end

    def left_click
      x, y = source_x_y(0)
      WindowsInput.move_mouse(x+2,y+2)
      WindowsInput.left_click
    end
  end
end

The methods of interest here are drag_drop_on, drag_drop_distance, drag_drop_at, drag_drop_below, and drag_drop_above. These methods allow you to drag a Watir::Element to some destination designated by the method you call.

Using drag-and-drop

The team I was coaching used ExtJS for their javascript library. I wrote three simple Cucumber Scenarios for the team that demonstrated how to use the drag-and-drop features that ran against the ExtJS online samples. Here is the feature file:

 Scenario: simple drag and drop
    Given I am on the extjs free drag-drop example page
    When I drag item 1 around the page
    Then it should move

  Scenario: drag and drop with a grid
    Given I am on the extjs grid drag-drop example page
    When I drag Joe from the first grid to the second grid
    Then he should be in the second grid

  Scenario: drag something below another object
    Given I am on the extjs grid to tree example
    When I add AT&T to the root of the tree
    And I add Boeing to the root of the tree
    And I drag Leaf1 below AT&T
    And I drag Boeing above Leaf1
    Then Leaf1 should be the last item in the list

And here are the step definitions:

Given /^I am on the extjs free drag\-drop example page$/ do
  @browser.goto "http://examples.extjs.eu/freedrag.html"
  @browser.wait
end

When /^I drag item 1 around the page$/ do
  item_one = @browser.div(:id => 'item-1')
  item_one.drag_drop_distance(0, 50)
  item_one.drag_drop_distance(50, 0)
  item_one.drag_drop_distance(0, -50)
  item_one.drag_drop_distance(-50, 0)
end

Then /^it should move$/ do
  # sleeping to allow person to observe the results
  sleep 3
end

Given /^I am on the extjs grid drag\-drop example page$/ do
  @browser.goto "http://examples.extjs.eu/ddgrids.html"
  @browser.wait
end

When /^I drag Joe from the first grid to the second grid$/ do
  joe = @browser.div(:class => 'x-grid3-row  x-grid3-row-first')
  target = @browser.div(:id => 'ext-gen124')
  joe.drag_drop_on(target)
end

Then /^he should be in the second grid$/ do
  # sleeping to allow person to observe the results
  sleep 3
end

Given /^I am on the extjs grid to tree example$/ do
  @browser.goto "http://examples.extjs.eu/grid2treedrag.html"
  @browser.wait
end

When /^I add AT&T to the root of the tree$/ do
  att = @browser.div(:text => 'AT&T Inc.')
  root = @browser.link(:class => 'x-tree-node-anchor')
  att.drag_drop_on(root)
end

When /^I add Boeing to the root of the tree$/ do
  boing = @browser.div(:text => 'Boeing Co.')
  root = @browser.link(:class => 'x-tree-node-anchor')
  boing.drag_drop_on(root)
end

When /^I drag Leaf1 below AT&T$/ do
  leaf1 = @browser.span(:text => 'Leaf 1')
  att = @browser.span(:text => 'AT&T Inc.')
  leaf1.drag_drop_below(att)
end

When /^I drag Boeing above Leaf1$/ do
  boing = @browser.span(:text => 'Boeing Co.')
  target = @browser.span(:text => 'Leaf 1')
  boing.drag_drop_above(target)
end

Then /^Leaf1 should be the last item in the list$/ do
  leaf1 = @browser.span(:text => 'Leaf 1')
  att = @browser.span(:text => 'AT&T Inc.')
  boing = @browser.span(:text => 'Boeing Co.')
  att.top_edge.should be < boing.top_edge
  boing.top_edge.should be < leaf1.top_edge

  # sleeping to allow person to observe the results
  sleep 3
end

Let me know what you think about this solution.

15 thoughts on “Drag-and-Drop in Watir

  1. I’m not so familiar with Cucumber, but shouldn’t all “Then” steps perform some assertation about the situation? “sleep 3” is nice for people to observe, but that doesn’t ever tell anything if something is wrong. Even those observing people might not understand when something should or should not happen by just looking. Or even you.

    I’d say that you should just have more “When” and “Then” steps there to actually have them useful.

    If you’d change the usages of Win32API to ruby-ffi [1] (it should be quite easy) then it might end up in Watir’s repo. Of course i’d check out WebDriver’s and/or Watir-WebDriver’s API for drag & drop also.

    [1] https://github.com/ffi/ffi

    • Jarmo, thanks for the comments. You are right that Then steps should perform some verification. The example here was what I shared with the team I was coaching to give them some idea how to use the extension I wrote for them and as such was not really testing anything. I in turn helped them write real features against their application (not here).

      ffi is a very good choice and I should have selected it when I did this work. What you see here was trying to solve a problem experienced by the team I was working with and I didn’t plan to move it beyond that group. Do you think there is the need for drag-and-drop in the community? If so, I could place this in github and make the necessary changes to make it viable.

  2. Pingback: Tweets that mention Drag-and-Drop in Watir | CheezyWorld -- Topsy.com

  3. Cheezy –

    I think the link you are looking for is here: http://wiki.openqa.org/display/WTR/Right+Click+an+Element. I wrote this with some really great feedback from Bill Agee and Paul Rogers. I never posted the drag and drop variant that is suggested in the ExtJS forums but I did work with it at work and never had a chance to repost. Yours looks better :).

    How was your client experience with Watir/ExtJS? Our company is looking at going to ExtJS and we aren’t sure if Watir or Selenium would be the best choice.

    • We had no problem driving ExtJS with Watir once we figured out how to identify the different types of controls. I am sure Selenium would also work.

  4. I’m not sure where you are getting your info, but great topic. I needs to spend some time learning more or understanding more. Thanks for excellent info I was looking for this info for my mission.

  5. Thanks for your code, I really like it.
    I was trying to do this using AutoIt and Watir… but with a lot of problems.

  6. It’s weird but I need to sleep for 2 seconds after I move the mouse in order to load a a flex comboBox and then click and it seems the sleep is done after the click and not in the moment I wrote it:
    move where combo is
    click to open the combo
    move where the option I want to click is
    sleep 2 seconds to wait the options are displayed
    click on desired option

    do you know why is it happening?

    • Sorry, I don’t. I haven’t had the need to perform drag-and-drop for a while so I haven’t spent the time researching it.

  7. Hi, really like this, its very simple i’m learning a lot with this, i’m new in testing also, i’m testing a webpage with selenium Webdriver, i need a little help, i’m trying to write a test that use a silverlight button to upload a file, anybody can help me with that?

    • I personally have no experience with silverlight so I am not much help here. My suggestion would be to ask your questions on the #selenium channel on irc or on a selenium mailing list.

Leave a Reply

Your email address will not be published. Required fields are marked *