Drag and drop external files with Selenium

Selenium does not allow dropping external files on an element. However it may be mandatory for your test if you want to cover every functionality. We will see how to make it work with Firefox 28+ and Chrome. I haven’t tested that on other browsers. We will see three work around:

  • Simulate a drop event without any file
  • Simulate a drop of a file from the file system (easy way but only for text file)
  • Simulate a drop of any file from the file system

Simulate a drop event without any file

This is a mostly a JavaScript workaround, the files and the drop event are created in JavaScript, that’s why the browser matters.

First a simple version where you don’t want file from your file system, this is enough in most situation:

public void dropFiles(List<String> names, WebElement target) throws IOException {
        
    final JavascriptExecutor exec = (JavascriptExecutor) BrowserDriver.getCurrentDriver();
    String inputId = "seleniumDragAndDropInput";
        
    // Create the FileList
    exec.executeScript(inputId + "_files = [];");
    for (String name: names) {
        exec.executeScript(inputId + "_files.push(new File([new Blob(['This is my content'], {type: 'text/plain'})], '" + name + "'));");
    }
        
    String targetId = target.getAttribute("id");
        
    // Add an id if the target doesn't have one
    if (targetId == null || targetId.isEmpty()) {
        targetId = "seleniumDragAndDropInput_target";
        exec.executeScript("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);";
        
    exec.executeScript(initEventJS);

    if (targetId == "seleniumDragAndDropInput_target") {
        exec.executeScript("document.getElementById('seleniumDragAndDropInput_target').id = null");
    }
        
}

The File creation works only with Firefox 28+, with earlier version it says “operation is unsecure”.

Simulate a drop of a file from the file system (easy way but only for text file)

And now an other example where we are using file from the machine running the Selenium tests. However be careful because it is not really smart and file’s content needs to be without special characters or it will break the JavaScript. You should escape the content if you need more advanced content. I didn’t because I usually using the first solution and code my content. This is enough in most situation where a bit of plain text or xml are enough for my tests.

public void dropFiles(List<File> files, WebElement target) throws IOException {
        
    final JavascriptExecutor exec = (JavascriptExecutor) BrowserDriver.getCurrentDriver();
    String inputId = "seleniumDragAndDropInput";
        
    // Create the FileList
    exec.executeScript(inputId + "_files = [];");
    for (File file : files) {
        exec.executeScript(inputId + "_files.push(new File([new Blob(['" + readFile(file) + "'], {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";
        exec.executeScript("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);";
        
    exec.executeScript(initEventJS);

    if (targetId == "seleniumDragAndDropInput_target") {
        exec.executeScript("document.getElementById('seleniumDragAndDropInput_target').id = null");
    }
        
}

private String readFile( File file ) throws IOException {
    BufferedReader reader = new BufferedReader( new FileReader (file));
    String         line = null;
    StringBuilder  stringBuilder = new StringBuilder();
    String         ls = System.getProperty("line.separator");

    while( ( line = reader.readLine() ) != null ) {
        stringBuilder.append( line );
        stringBuilder.append( ls );
    }
    reader.close();
    return stringBuilder.toString();
}

Simulate a drop of any file from the file system

Here is a solution to actually drop any file from the file system. We have to use an input (type=file) and since we can not set its value via JavaScript (for security reason, or we could upload any file from our visitor 🙂 ), we need to set the value with Selenium with sendKeys. But in order for this to work, the input needs to be visible. That’s why I set the form with a position fixed. We need to delete it once done.

final JavascriptExecutor exec = (JavascriptExecutor) BrowserDriver.getCurrentDriver();

WebElement target = browseContainer.contentListGrid;

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

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

exec.executeScript(
        "var f=dojo.create('form', {id: 'seleniumDragAndDropInput_form', style: {position: 'fixed', left: 0, top: 0, width:'100px', height: '100px'}}, dojo.body());"
        + "dojo.create('input', { type: 'file', id: 'seleniumDragAndDropInput' }, f);");

WebElement input = BrowserDriver.getCurrentDriver().findElement(By.id("seleniumDragAndDropInput"));
input.sendKeys(path);

exec.executeScript(
        "var files=dojo.byId(\"seleniumDragAndDropInput\").files;"
        + "var eve=document.createEvent(\"HTMLEvents\");"
        + "eve.initEvent(\"drop\", true, true);"
        + "eve.dataTransfer = {files:files};"
        + "eve.type = \"drop\";document.getElementById('" + targetId + "').dispatchEvent(eve);");

exec.executeScript("dojo.destroy('seleniumDragAndDropInput_form');");

if (targetId == "seleniumDragAndDropInput_target") {
    exec.executeScript("document.getElementById('seleniumDragAndDropInput_target').id = null");
}

You may need to change a bit if you are not using dojo and replace the byId, create and destroy but I an sure you have that with your favorite javascript framework.

Simulate a drop of multiple files from the file system

With Chrome, you could use almost the same code that the previous one. The only thing to do is to set the input as ‘multiple’ and split paths with a \n.

public static void dropFile(String[] paths, WebElement target) {
    
    StringBuffer b = new StringBuffer();
    for (int i = 0; i < paths.length; i++) {
        File f = new File(paths[i]);
        if (!f.isFile()) {
            Assert.fail(paths[i] + " is not a valid file path.");
        }
        b.append(f.getAbsolutePath());
        if (i < paths.length - 1) {
            b.append("\n");
        }
    }
    
    final JavascriptExecutor exec = (JavascriptExecutor) BrowserDriver.getCurrentDriver();
    
    String targetId = target.getAttribute("id");
    
    // Add an id if the target doesn't have one
    if (targetId == null || targetId.isEmpty()) {
        targetId = "seleniumDragAndDropInput_target";
        exec.executeScript("sId=function(e, i){e.id = i;};sId(arguments[0], arguments[1]);", target, targetId);
    }
    
        exec.executeScript(
                "var f=dojo.create('form', {id: 'seleniumDragAndDropInput_form', style: {position: 'fixed', left: 0, top: 0, width:'100px', height: '100px'}}, dojo.body());"
                + "dojo.create('input', { type: 'file', id: 'seleniumDragAndDropInput', multiple: 'multiple' }, f);");
    
    
        WebElement input = BrowserDriver.getCurrentDriver().findElement(By.id("seleniumDragAndDropInput"));
        input.sendKeys(b.toString());
    
        exec.executeScript(
                "var files=dojo.byId(\"seleniumDragAndDropInput\").files;"
                + "var eve=document.createEvent(\"HTMLEvents\");"
                + "eve.initEvent(\"drop\", true, true);"
                + "eve.dataTransfer = {files:files};"
                + "eve.type = \"drop\";document.getElementById('" + targetId + "').dispatchEvent(eve);");
    
    exec.executeScript("dojo.destroy('seleniumDragAndDropInput_form');");
    
    if (targetId == "seleniumDragAndDropInput_target") {
        exec.executeScript("document.getElementById('seleniumDragAndDropInput_target').id = null");
    }
    
    
}

However, it does not work with Firefox because it does not accept multiple files in one input separated by \n. That’s why we need to use several inputs and then gather files from all inputs in a FileList to be used in the event. Here is the code.

public static void dropFile(String[] paths, WebElement target) {
    
    for (int i = 0; i < paths.length; i++) {
        File f = new File(paths[i]);
        if (!f.isFile()) {
            Assert.fail(paths[i] + " is not a valid file path.");
        }
        paths[i] = f.getAbsolutePath();
    }
    
    
    final JavascriptExecutor exec = (JavascriptExecutor) BrowserDriver.getCurrentDriver();
    
    String targetId = target.getAttribute("id");
    
    // Add an id if the target doesn't have one
    if (targetId == null || targetId.isEmpty()) {
        targetId = "seleniumDragAndDropInput_target";
        exec.executeScript("sId=function(e, i){e.id = i;};sId(arguments[0], arguments[1]);", target, targetId);
    }
    
    // Using one input with multiple file separated by \n does not work with Firefox
    // We need to use several input
    StringBuffer createInputs = new StringBuffer();
    // Create form
    createInputs.append("var f=dojo.create('form', {id: 'seleniumDragAndDropInput_form', style: {position: 'fixed', left: 0, top: 0, width:'100px', height: '100px'}}, dojo.body());");
    // Create inputs
    for (int i = 0; i < paths.length; i++) {
        createInputs.append("dojo.create('input', { type: 'file', id: 'seleniumDragAndDropInput" + i + "'}, f);");
    }
    
    exec.executeScript(createInputs.toString());
    
    // Then set file for each input
    
    for (int i = 0; i < paths.length; i++) {
        WebElement input = BrowserDriver.getCurrentDriver().findElement(By.id("seleniumDragAndDropInput" + i));
        input.sendKeys(paths[i]);
    }
    
    // Write code to gather all inputs files in one array;
    StringBuffer gatherInputs = new StringBuffer();
    gatherInputs.append("var seleniumDragAndDropFiles = [];");
    // Need to add the item method to stick to FileList API
    gatherInputs.append("seleniumDragAndDropFiles.item = function (i) { return seleniumDragAndDropFiles[i]};");
    for (int i = 0; i < paths.length; i++) {
        gatherInputs.append("seleniumDragAndDropFiles.push(dojo.byId(\"seleniumDragAndDropInput" + i + "\").files[0]);");
    }
    
    // Init event with our file
    exec.executeScript(
            gatherInputs.toString()
            + "var eve=document.createEvent(\"HTMLEvents\");"
            + "eve.initEvent(\"drop\", true, true);"
            + "eve.dataTransfer = {files:seleniumDragAndDropFiles};"
            + "eve.type = \"drop\";document.getElementById('" + targetId + "').dispatchEvent(eve);");
    
    exec.executeScript("dojo.destroy('seleniumDragAndDropInput_form');");
    
    if (targetId == "seleniumDragAndDropInput_target") {
        exec.executeScript("document.getElementById('seleniumDragAndDropInput_target').id = null");
    }
    
    
}

 

4 thoughts on “Drag and drop external files with Selenium

  1. Mehdi

    Hi,
    Thanks for the post. I have a test case that need to upload a binary file instead of text/XML file do you know any solution for that?

    Thanks,
    Mehdi

    Reply
    1. Guillaume Post author

      I’m not sure it will be possible because of security concerns. Most solutions will fail because browsers have restrictions on reading file users hasn’t chosen. For instance, you can not inject an input of type file into the DOm and set its value, and then use it in the drop event. That would be a major security breach, since every website could upload any file on our File System.

      I would guess that the only way it could work would be by using Selenium telling the Browser to drop a file, that way the browser could authorize the action, but I don’t thing this is possible yet.

      If I come up with a working solution I’ll let you know.

      Reply
    2. Guillaume Post author

      I updated the post to give you a solution. As I explained, modifying the value of the input can not be done in Javascript for security reasons, so we have to do it with Selenium, which means the input needs to be visible.

      Reply

Leave a Reply