has_many :codes

Testing HTML5 drag and drop with Capybara

Published  

In the past I’ve usually used jQuery UI’s Sortable whenever I needed to implement drag and drop functionality to sort items within the same list or move items from a list to another. Testing this with Capybara is fairly easy either using this library to simulate drag events, or by first moving an item to another position with jQuery and then programmatically triggering Sortable’s update event.

However I am getting rid of jQuery as I have started to use Vue.js extensively for an app I’m working on, and am now using Vue.Draggable to implement the same drag and drop functionality. It’s basically a component that uses Sortable.js behind the scenes, so it’s based on the native HTML5 drag and drop API. The component itself is very easy to use, but at first I found it a bit hard to test with Capybara, which I use for system tests in Rails apps.

Unfortunately, I couldn’t find a way to test this with Capybara/Selenium directly; it seems like sortable lists aren’t entirely supported or something like that. After some searching though I found another tiny JavaScript library called drag-mock that can be used to trigger HTML5 drag and drop events, which is exactly what I needed. It hasn’t been updated in a while but it still works.

So how do we use it with Capybara? Firstly, I am assuming that you are using acts_as_list or similar to implement sorting with your ActiveRecord models or anyway server side.

Then you’ll need to add drag-mock to your app somehow. I choose to always require it in the Webpacker bundle directly (assigning window.dragMock) so I can use it from within the browser manually, if needed (it’s a small library, I think something like 5KB gzipped so it doesn’t affect much the final size of the bundle IMO), but you may want to include the library only in the tests where it’s needed, by reading its content and executing it with execute_script within the tests, before triggering drag and drop events.

Below is an example for testing the sorting of items within the same list. In this example I’m using Rails’ own system tests but it would be more or less the same thing with Rspec, if you use that.

  test "changes the position of an item within the same list" do
    item1 = ... # load from fixture or create with factory
    item2 = ... # load from fixture or create with factory

    assert_equal 1, item1.position
    assert_equal 2, item2.position

    visit some_path

    page.execute_script <<-EOS
      var dragSource = document.querySelector('#item_#{item2.id}');
      var dropTarget = document.querySelector('#item_#{item1.id}');

      window.dragMock.dragStart(dragSource).delay(100).dragOver(dropTarget).delay(100).drop(dropTarget);
    EOS

    sleep 0.3

    wait_for_ajax

    assert_equal 2, item1.reload.position
    assert_equal 1, item2.reload.position
  end

First, we ensure the positions of the items are initially what we expect, and visit the page that contains the sortable list(s). Then we execute some JavaScript which uses drag-mock to trigger the drag and drop events in sequence using the first item as the dragSource and the second item as the dropTarget; note the small delay between one event and the following - without these delays for some reason the drag and drop wouldn’t work properly. Then we wait for 300ms to give the JavaScript time to execute due to those delays (I know, I know… using sleep in tests is a bad practice but I can’t think of an alternative in this case); we also wait for the AJAX request that updates the positions server side to complete (the implementation of wait_for_ajax depends on how you are executing AJAX requests), and finally we check that the items’ positions have changed in the database too.

Testing when an item is moved to another list is very similar:

  test "moves an item to another list" do
    item1_list1 = ... # load from fixture or create with factory
    item2_list2 = ... # load from fixture or create with factory
    item1_list2 = ... # load from fixture or create with factory

    assert_equal 2, item2_list1.position
    assert_equal 1, item1_list2.position

    visit some_path

    page.execute_script <<-EOS
      var dragSource = document.querySelector('#item_#{item2_list1.id}');
      var dropTarget = document.querySelector('#item_#{item1_list2.id}');

      window.dragMock.dragStart(dragSource).delay(100).dragOver(dropTarget).delay(100).drop(dropTarget);
    EOS

    sleep 0.3

    wait_for_ajax

    item2_list1.reload
    item1_list2.reload

    list2 = item1_list2.list

    assert_equal 1, item2_list1.position
    assert_equal 2, item1_list2.position
    assert_equal list2, item2_list1.item_group
  end

In this second case I want to test two things: 1) that the item I select is actually moved to the new list, 2) that the position that this item has in the new list after the drag and drop also affects the positions of the other items in that list. For this reason, I am assuming that there are two items in the source list and that I am dragging the second item, which initially has position 2. Similarly, I am assuming that the target list already has a single item with position 1. So, once we drop the second item from the source list, on the first item of the target list, the result we expect is that the position of the dragged item has changed from 2 to 1 and the position of the single element in the target list has changed from 1 to 2, having been “pushed down” by the dragged item. We also ensure that the the dragged item now belongs to the target list.

That’s it. This way we can test sorting the items within a list, as well as moving an item from a list to another. It feels a little like a hack having to execute some JavaScript instead of using Capybara/Selenium somehow, but at least it works. I am curious to hear if someone knows of alternative ways to achieve the same. In the meantime, hope it can save someone some time.

© Vito Botta