22

I have to test a web-application which contains a drag and drop area for uploading files from the local file system. My test environment is based on C#.

For the automation testing I have used Selenium, but it is not possible to drag files from the file system. The upload area is a div tag (no input tag). So what's the best way to do it? AutoIt (is it possible to drop in a web browser)? Sikuli?

Ratmir Asanov
  • 6,237
  • 5
  • 26
  • 40
Markoo91
  • 390
  • 1
  • 2
  • 12
  • 1
    Florent B's answer didn't work for me for some reason - AutoIt did the trick https://stackoverflow.com/a/38513989/1141876 – fiat Oct 12 '17 at 04:54

4 Answers4

24

It's possible with Selenium alone, but it's not simple. It requires to inject a new INPUT element in the page to receive the file through SendKeys. Then, the script needs to simulate the drop by sending the dragenter, dragover, drop events to the targeted area.

static void Main(string[] args)
{
    var driver = new ChromeDriver();
    driver.Url = "https://react-dropzone.js.org/";

    IWebElement droparea = driver.FindElementByCssSelector("[data-preview='Basic example'] [style]");
    DropFile(droparea, @"C:\Users\florent\Desktop\capture.png");

    driver.Quit();
}

const string JS_DROP_FILE = "for(var b=arguments[0],k=arguments[1],l=arguments[2],c=b.ownerDocument,m=0;;){var e=b.getBoundingClientRect(),g=e.left+(k||e.width/2),h=e.top+(l||e.height/2),f=c.elementFromPoint(g,h);if(f&&b.contains(f))break;if(1<++m)throw b=Error('Element not interractable'),b.code=15,b;b.scrollIntoView({behavior:'instant',block:'center',inline:'center'})}var a=c.createElement('INPUT');a.setAttribute('type','file');a.setAttribute('style','position:fixed;z-index:2147483647;left:0;top:0;');a.onchange=function(){var b={effectAllowed:'all',dropEffect:'none',types:['Files'],files:this.files,setData:function(){},getData:function(){},clearData:function(){},setDragImage:function(){}};window.DataTransferItemList&&(b.items=Object.setPrototypeOf([Object.setPrototypeOf({kind:'file',type:this.files[0].type,file:this.files[0],getAsFile:function(){return this.file},getAsString:function(b){var a=new FileReader;a.onload=function(a){b(a.target.result)};a.readAsText(this.file)}},DataTransferItem.prototype)],DataTransferItemList.prototype));Object.setPrototypeOf(b,DataTransfer.prototype);['dragenter','dragover','drop'].forEach(function(a){var d=c.createEvent('DragEvent');d.initMouseEvent(a,!0,!0,c.defaultView,0,0,0,g,h,!1,!1,!1,!1,0,null);Object.setPrototypeOf(d,null);d.dataTransfer=b;Object.setPrototypeOf(d,DragEvent.prototype);f.dispatchEvent(d)});a.parentElement.removeChild(a)};c.documentElement.appendChild(a);a.getBoundingClientRect();return a;";

static void DropFile(IWebElement target, string filePath, double offsetX = 0, double offsetY = 0)
{
    if (!File.Exists(filePath))
        throw new FileNotFoundException(filePath);

    IWebDriver driver = ((RemoteWebElement)target).WrappedDriver;
    IJavaScriptExecutor jse = (IJavaScriptExecutor)driver;

    IWebElement input = (IWebElement)jse.ExecuteScript(JS_DROP_FILE, target, offsetX, offsetY);
    input.SendKeys(filePath);
}

Source: https://gist.github.com/florentbr/349b1ab024ca9f3de56e6bf8af2ac69e

Florent B.
  • 41,537
  • 7
  • 86
  • 101
  • Could you please explain what happens in JavaScript code? Is it right, that you add input, set in this input path to file, then trigger/emulate 'dragenter', 'dragover', 'drop' events? What should happen in the end? I've tried this code and it hasn't worked for me. I see that input was injected on the page and value of input contains file path, but nothing happens with drop area. – Alex Kulinkovich Apr 11 '18 at 19:40
  • 1
    @Alex.K, it's hard to say why from a simple "doesn't work" since the page can handle the drop in many ways. Note that it should no longer work with Firefox since the input need to be intractable. You should post a question with your issue. – Florent B. Apr 11 '18 at 19:52
  • 1
    @Alex.K, try this version: https://gist.github.com/florentbr/349b1ab024ca9f3de56e6bf8af2ac69e – Florent B. Apr 11 '18 at 19:55
  • @Florent B. I will try tomorrow and provide feedback. Regarding details, I use Google Chrome. – Alex Kulinkovich Apr 11 '18 at 20:11
  • @FlorentB. I've tried the code from "drop-files.js" and get the next error: System.InvalidOperationException: 'unknown error: target.getBoundingClientRect is not a function – Alex Kulinkovich Apr 12 '18 at 16:13
  • @FlorentB. I've removed braces for 'arget.getBoundingClientRect()' and got the next error message in the next run: System.InvalidOperationException: 'unknown error: Cannot read property 'left' of undefined. it complains on this line: "cx = rect.left + (offsetX || (rect.width >> 1)),". Any ideas? – Alex Kulinkovich Apr 12 '18 at 16:19
  • 1
    @Alex.K., can't say without a reproducible example. I'll see later to update the code with latest API for dataTransfer. It could be that you are not selecting the correct element with the expected offset. – Florent B. Apr 12 '18 at 16:32
  • 1
    @FlorentB. I've tried your new code for "react-dropzone.js.org" and I don't see, that file is listed in "Dropped files". I did it manually and then I can see my file is listed in "Dropped files". Now I don't have any JavaScript errors, but it didn't work(drag and drop by Selenium). Any ideas? – Alex Kulinkovich Apr 13 '18 at 12:08
  • @FlorentB. I have used your ***GitHub Project*** but it seems to show me **tracebacks** I am using Mac I wish you could help me. – Joe Aug 30 '19 at 15:57
6

The previous answer is correct and works perfectly with the Chrome driver, however might have problems with Mozilla Gecko driver, which throws org.openqa.selenium.ElementNotVisibleException

In order to avoid that, remove input.style.display = 'none';

You can use input.style.opacity = 0; if you need to make it disappear.

3

You can do this with JSExecutor:

public void dropFile(File filePath, WebElement target, int offsetX, int offsetY) {
        if (!filePath.exists())
            throw new WebDriverException("File not found: " + filePath.toString());

        JavascriptExecutor jse = (JavascriptExecutor) driver;

        String 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.style.display = 'none';" +
                        "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;";

        WebElement input = (WebElement) jse.executeScript(JS_DROP_FILE, target, offsetX, offsetY);
        input.sendKeys(filePath.getAbsoluteFile().toString());
        wait.until(ExpectedConditions.stalenessOf(input));
    }
Norayr Sargsyan
  • 1,737
  • 1
  • 12
  • 26
0

If you're using Selenide:

    public static void dragAndDropFileUpload(File file, SelenideElement target) throws IOException {

    String inputId = "seleniumDragAndDropInput";

    // Create the FileList
    executeJavaScript(inputId + "_files = [];");
        executeJavaScript(inputId + "_files.push(new File([new Blob(['" + file.getAbsolutePath() + "'], {type: '" + Files.probeContentType(file.toPath()) + "'})], '" + file.getName() + "'));");


    String targetId = target.getAttribute("id");

    // Add an id if the target doesn't have one
    if (targetId == null || targetId.isEmpty()) {
        targetId = "seleniumDragAndDropInput_target";
        executeJavaScript("sId=function(e, i){e.id = i;};sId(arguments[0], arguments[1]);", target, targetId);
    }

    // Add the item function the the FileList
    // Create the drop event and dispatch it on the target
    String initEventJS = inputId + "_files.item = function (i) {return this[i];};"
            + "var eve=document.createEvent(\"HTMLEvents\");"
            + "eve.initEvent(\"drop\", true, true);"
            + "eve.dataTransfer = {files:seleniumDragAndDropInput_files};"
            + "eve.preventDefault = function () {};"
            + "eve.type = \"drop\";"
            + "document.getElementById('" + targetId + "').dispatchEvent(eve);";

    executeJavaScript(initEventJS);

    if (targetId == "seleniumDragAndDropInput_target") {
        executeJavaScript("document.getElementById('seleniumDragAndDropInput_target').id = null");
    }
}
Michael Dally
  • 195
  • 3
  • 12
  • I should add that this is for Java, but the bulk of it is just wrapping JS. Just replace 'executeJs' with your own implementation of JS executor. – Michael Dally Jul 09 '20 at 10:47