teamcapybara / capybara

Acceptance test framework for web applications
http://teamcapybara.github.io/capybara/
MIT License
10.01k stars 1.45k forks source link

drag_to (drag and drop) doesn't seem to work #2573

Closed chalmagean closed 2 years ago

chalmagean commented 2 years ago

I'm currently using master, but I've tried the latest version(s) as well.

remote: https://github.com/teamcapybara/capybara.git
revision: b8705059b142930e97fcbd8f34b9409755570c44

remote: https://github.com/titusfortner/webdrivers.git
revision: 9c9f11f8a750b60732a9cdfdc95c37e8f23e8869

Expected Behavior

I expect the test to be able to drag notes over one another just like I can do it manually in Chrome (see screenshot).

CleanShot 2022-08-12 at 16 19 38

Actual Behavior

Failures:

  1) Notes can link notes
     Failure/Error: note2.drag_to(note1)

     Selenium::WebDriver::Error::StaleElementReferenceError:
       stale element reference: element is not attached to the page document
         (Session info: headless chrome=104.0.5112.79)

     [Screenshot Image]: /Users/cezar/Work/smartnotes/tmp/capybara/failures_r_spec_example_groups_notes_can_link_notes_511.png

     # 0   chromedriver                        0x00000001032aaae0 chromedriver + 3828448
     # 1   chromedriver                        0x000000010323ff1c chromedriver + 3391260
     # 2   chromedriver                        0x0000000102f38fcc chromedriver + 217036
     # 3   chromedriver                        0x0000000102f3ba28 chromedriver + 227880
     # 4   chromedriver                        0x0000000102f3b890 chromedriver + 227472
     # 5   chromedriver                        0x0000000102f3bc78 chromedriver + 228472
     # 6   chromedriver                        0x0000000102f928ec chromedriver + 583916
     # 7   chromedriver                        0x0000000102f91b2c chromedriver + 580396
     # 8   chromedriver                        0x0000000102f5e010 chromedriver + 368656
     # 9   chromedriver                        0x000000010328039c chromedriver + 3654556
     # 10  chromedriver                        0x0000000103283c4c chromedriver + 3669068
     # 11  chromedriver                        0x000000010328814c chromedriver + 3686732
     # 12  chromedriver                        0x0000000103284654 chromedriver + 3671636
     # 13  chromedriver                        0x0000000103262b40 chromedriver + 3533632
     # 14  chromedriver                        0x000000010329c414 chromedriver + 3769364
     # 15  chromedriver                        0x000000010329c578 chromedriver + 3769720
     # 16  chromedriver                        0x00000001032b10f0 chromedriver + 3854576
     # 17  libsystem_pthread.dylib             0x00000001aa08026c _pthread_start + 148
     # 18  libsystem_pthread.dylib             0x00000001aa07b08c thread_start + 8
     # ./spec/system/notes_spec.rb:86:in `block (2 levels) in <top (required)>'

Finished in 4.06 seconds (files took 1.58 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/system/notes_spec.rb:66 # Notes can link notes

failures_r_spec_example_groups_notes_can_link_notes_511

Steps to reproduce

This is my test, not sure how relevant it is.

  it "can link notes" do
    driven_by :selenium, using: :headless_chrome
    visit "/notes"

    # Create insight note
    click_on "insight-tab"
    click_on "insight-create"
    fill_in "insight-text", with: "Insight Body 1"
    click_on "insight-submit"

    # Create a second insight note
    click_on "insight-create"
    fill_in "insight-text", with: "Insight Body 2"
    click_on "insight-submit"

    note1 = find(".note", text: "Insight Body 1")
    note2 = find(".note", text: "Insight Body 2")
    note1_id = note1["data-id"]
    note2_id = note2["data-id"]

    note2.drag_to(note1)

    within(note2) do
      expect(page.find(".note-links")).to have_text("#{note1_id}", exact: true)
    end
    within(note1) do
      expect(page.find(".note-links")).to have_text("#{note2_id}", exact: true)
    end
  end
twalpole commented 2 years ago

What library are you using for your drag and drop behavior implementation? Capybara attempts to automatically detect whether your app is using proprietary drag/drop or standard HTML5 based drag/drop behaviors and uses different methods based on what it detects. You can however force it to use one mode of the other. Try

note2.drag_to(note1, html5: true)

or

note2.drag_to(note1, html5: false)

to see if that makes any difference. It's also possible Capybara just isn't compatible with the method your application is using. ie. If you're repeatedly adding/removing elements to the page then Capybara may not be able to emulate the drag/drop

chalmagean commented 2 years ago

I'm using Stimulus JS and doing the drag and drop manually. Like this.

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["note"];

  dragStart(event) {
    event.target.style.opacity = "0.4";
    this.dragSrcEl = event.target;

    event.dataTransfer.effectAllowed = "move";
    event.dataTransfer.setData("text/html", event.target.innerHTML);
  }

  dragEnter(event) {
    event.target.classList.add("over");

    if (event.preventDefault) {
      event.preventDefault();
    }
    return false;
  }

  dragOver(event) {
    if (event.preventDefault) {
      event.preventDefault();
    }
    return false;
  }

  dragLeave(event) {
    event.target.classList.remove("over");
    this.resetOpacity();
  }

  drop(event) {
    event.stopPropagation();

    event.target.classList.remove("over");
    this.resetOpacity();

    fetch("/note_links", {
      method: "POST",
      credentials: "same-origin",
      headers: {
        Accept: "text/vnd.turbo-stream.html",
        "Content-Type": "application/json",
        "X-CSRF-Token": document.head.querySelector("[name='csrf-token']")
          .content,
      },
      body: JSON.stringify({
        source_id: this.dragSrcEl.dataset.id,
        target_id: event.target.dataset.id,
      }),
    })
      .then((response) => response.text())
      .then(Turbo.renderStreamMessage);
  }

  dragEnd() {
    this.resetOpacity();
  }

  resetOpacity() {
    this.noteTargets.forEach((el) => {
      el.style.opacity = "1";
    });
  }
}
<div class="md:w-1/3 note" draggable="true" data-notes-target="note" data-action="dragstart->notes#dragStart dragend->notes#dragEnd drop->notes#drop dragenter->notes#dragEnter dragleave->notes#dragLeave dragover->notes#dragOver" data-id="<%= insight.id %>">
  <div class="note-body flex flex-col mt-4 h-48 bg-white text-gray-600 body-font text-sm border-2 border-gray-200 rounded-md border-opacity-60 overflow-hidden mx-4 shadow-md">
    <div class="flex-none mx-3 my-0 mt-0 py-1 font-bold note-header border-b">
      <div class="note-id">Id: <%= insight.id %></div>
    </div>
    <div class="grow p-0 py-2 mx-3">
      <p class="leading-relaxed mb-3"><%= insight.text %></p>
    </div>
    <div id="insight-note-<%= insight.id %>" class="flex-none py-1 mx-3 text-right">
      <% if insight.insights.present? %>
        <div class="note-links">
          <%= insight.insights.map(&:id).join(", ") %>
        </div>
      <% end %>
    </div>
  </div>
</div>

I've also tried https://github.com/Shopify/draggable with the same effect.

In both cases changing the html5 option had no effect.

chalmagean commented 2 years ago

This was totally my fault. I had a JS error (thank god I thought about opening the console) because of calling .content on null.

I would help to see any JS errors when running the tests.