FelipeG
FelipeG

Reputation: 141

Python with Selenium: Drag and Drop from file system to webdriver?

I have to automate a web-application, which contains a drag and drop area for uploading files from the local file system. My test environment is developed using Python. For the automation tests I have used Selenium, but it is not possible to drag files from the file system, once the upload area is a div tag (No input tag - this way I know it would be easy).

I read a lot of different articles, but by the moment none worked for me. It's important to highlight that I'm not interested in using AutoIT, only native python with selenium.

I found this Selenium: Drag and Drop from file system to webdriver? what looks really promising, however I do not know to adapt to Python.

Thank you a lot in advance!

Upvotes: 14

Views: 15620

Answers (3)

I_Literally_Cannot
I_Literally_Cannot

Reputation: 170

Its been a few years since the last response but I will add my own for those trying to use react dropzone with selenium, robot framework, or similar. The accepted answer may still work with pure selenium in your case, but as I am using robot framework with a non-standard library, my answer may not be universal. It is very slightly off topic but as I referenced this for creating my own keyword I hope it helps.

In my case I am using Qweb which required some additional setup such as getting the library instance and the webElement.

from robot.libraries.BuiltIn import BuiltIn

JS_DROP_FILE = """
    var target = arguments[0],
        offsetX = arguments[1],
        offsetY = arguments[2],
        
        // I did not need to fetch the document separately, it was already accessible in my case.
        window = document.defaultView;
    
    var input = document.createElement('INPUT');
    input.type = 'file';
    target.appendChild(input);

    input.onchange = function () {
      var rect = target.getBoundingClientRect(),
          x = rect.left + (offsetX || (rect.width / 2)),
          y = rect.top + (offsetY || (rect.height / 2));

      var dataTransfer = new DataTransfer();
      for (var i = 0; i < this.files.length; i++) {
          dataTransfer.items.add(this.files[i]);
      }

      ['dragenter', 'dragover', 'drop'].forEach(function (name) {
        var evt = new MouseEvent(name, {
          bubbles: true,
          cancelable: true,
          clientX: x,
          clientY: y,
          dataTransfer: dataTransfer
        });
        target.dispatchEvent(evt);
      });

      var fileDropEvent = new CustomEvent("file-drop", {
        bubbles: true,
        detail: { dataTransfer: dataTransfer }
      });
      target.dispatchEvent(fileDropEvent);

      setTimeout(function () { target.removeChild(input); }, 25);
    };
    
    return input; // Return the input element

   // # debugging: console.log(var) prints to chrome console

"""

def drag_and_drop_file(drop_target, path):
    qweb = BuiltIn().get_library_instance('Qweb')
    driver = qweb.return_browser() # debug: expect <selenium.webdriver.chrome.webdriver.WebDriver (session="alphabet-soup")>
    # for selenium library, you will use a something along these lines
    # selib = BuiltIn().get_library_instance("SeleniumLibrary")
    # driver = selib.driver
    # element = selib.get_webelement(drop_target)
    element = qweb.get_webelement(drop_target)
    if isinstance(element, list):
      element = element[0] # debug: expect <selenium.webdriver.remote.webelement.WebElement (session="alphabet-soup", element="longer-alphabet-soup")>
    file_input = driver.execute_script(JS_DROP_FILE, element, 0, 0)
    file_input.send_keys(path)
    # debugging: Builtin().log_to_console(var) prints to terminal

Some notable changes: new DataTransfer() and new MouseEvent() are more modern ways to handle these actions in chromium.

Other considerations: in my case the element is attached to the target vs the body, this makes debugging a little easier. You can still use body.appendChild and body.removeChild if you want to.

Working with react: You will preferably need some access to the react code in question, at least an ability to work with a developer, as the code will need to have an event listener. fileDropEvent is dispatched to the target, and thus the react code will need to have a useEffect() that will check for a target, and add an event listener for "file-drop" to it. This isn't the ideal case as you should not have to modify code much for QA purposes, but react-dropzone is a bit more difficult with automated testing.


  const handlePhotosUpload = useCallback(
    // Your photo upload logic here
  );

  useEffect(() => {
    const target = document.getElementById("upload-photos");
    const handleFileDrop = (event: any) => {
       // Prevent default browser handling
       event.preventDefault();
       if (event.detail?.dataTransfer?.files) {
          handlePhotosUpload(Array.from(event.detail.dataTransfer.files));
       }
    };
 
    if (target) {
      target.addEventListener("file-drop", handleFileDrop);
  }
 
    return () => {
       if (target) {
          target.removeEventListener("file-drop", handleFileDrop);
       }
    };
 }, [handlePhotosUpload]);

Usage in robot framework:

*** Settings ***
Library    QWeb  # Or SeleniumLibrary or Browser, whichever you use
Library   drag-n-drop.py

*** Keywords ***

Upload Photo
    [Arguments]    ${photo}
    [Documentation]    Executes a custom script to drag and drop a photo into the dropzone.
    Scroll to    //*[@id\="upload-photos"]. 
    # note: Qweb requires escape chars in xpath.
    # In selenium, xpath://div[@id="example"] or id:example may work.
    # I did not test this in selenium.
    Drag And Drop File    //*[@id\="upload-photos"]    ${EXECDIR}/files/images/${photo}
    Sleep    2s    # wait for photo to upload

*** Test Cases ***
Upload A Photo
    Upload Photo    my_picture.jpeg

Upvotes: 0

Here's the python version of the trick with input injection via script.

JS_DROP_FILE = """
    var target = arguments[0],
        offsetX = arguments[1],
        offsetY = arguments[2],
        document = target.ownerDocument || document,
        window = document.defaultView || window;

    var input = document.createElement('INPUT');
    input.type = 'file';
    input.onchange = function () {
      var rect = target.getBoundingClientRect(),
          x = rect.left + (offsetX || (rect.width >> 1)),
          y = rect.top + (offsetY || (rect.height >> 1)),
          dataTransfer = { files: this.files };

      ['dragenter', 'dragover', 'drop'].forEach(function (name) {
        var evt = document.createEvent('MouseEvent');
        evt.initMouseEvent(name, !0, !0, window, 0, 0, 0, x, y, !1, !1, !1, !1, 0, null);
        evt.dataTransfer = dataTransfer;
        target.dispatchEvent(evt);
      });

      setTimeout(function () { document.body.removeChild(input); }, 25);
    };
    document.body.appendChild(input);
    return input;
"""

def drag_and_drop_file(drop_target, path):
    driver = drop_target.parent
    file_input = driver.execute_script(JS_DROP_FILE, drop_target, 0, 0)
    file_input.send_keys(path)

As drop_target pass it some element visible on the page which you can get using any function from the family of driver.get_element_by_....

The approach is to invoke a javascript using selenium's execute_script function to emulate drag and drop events. The code works as following:

  1. selenium invokes javascript code
  2. javascript creates input element and attaches it to DOM
  3. javascript attaches a handler to the input which emulates mouse events that happens when user actually drops a file, namely dragenter, dragover, drop.
  4. selenium updates the input with the path to the file. At this point the handler from step 2 is invoked and it emulates drag and drop events.

Upvotes: 36

Ralph
Ralph

Reputation: 29

I know this may be a late answer but just in case for people who find the answer!

If you are using Chrome please go to this site to download the Chrome driver. (Try to find your chrome version thru this and choose the suitable one)

There's still another thing you will need to do I'll show it right now


First: Download chrome driver and copy the Xpath

Step 1: Go to the site you want and copy fullXPath of your "drag and drop", by right click on the drag and drop area then hit the inspect.

Plz do this twice just in case it inspects the right place

Step 2: You will see the highlight color, again right-click on them

then you will find "copy" -> "copy fullXpath"

Finally, let's code

Wait!!! Just one more suggestion plz : If you see something goes wrong with pasting the "Xpath" or "link to the folder" for example

you might use ' ' instead of " "

from selenium import webdriver
driver = webdriver.Chrome('D:\Folder\chromedriver')
driver.get('https://exmaple.com')
drag_&_drop = driver.find_element_by_xpath('paste-the-full-xpath-here')
drag_&_drop.send_keys('D:\Folder\picture.png')
#python 3.9

Upvotes: 1

Related Questions