2

For example, I have this simple replicate of a webpage (in reality the code is not mine and I am not in control of that):

<!DOCTYPE html>
<html>
    <head>
        <title>Test</title>
    </head>
    <body>
        <button>Click me!</button>

        <script>
            document.querySelector("button").addEventListener("click", () => {
                const txt = document.createElement("input");
                txt.type = "file";

                txt.addEventListener("change", () => {
                    console.log(txt.files[0]);
                });

                txt.click();
            });
        </script>
    </body>
</html>

As you can see, all we see is a button. When the button is clicked, it creates an input and activates it without ever attach it to the HTML document.

Please advise how do I automate in this case (i.e click the button, choose a file). I am using C# .NET if it's relevant but I think a general direction is okay.

Luke Vo
  • 17,859
  • 21
  • 105
  • 181

3 Answers3

3

If your input were added to the DOM this would be a simple question, as I'm sure you're aware. The normal process looks something like this:

<input type="file" id="uploadhere" />
IWebElement element = driver.FindElement(By.Id("uploadhere"));
element.SendKeys("C:\\Some_Folder\\MyFile.txt");

SendKeys handles the magic of the actual upload but it does so not by accessing the file upload menu, but by interacting with the input element directly.

Your problem is that the input element isn't in the DOM and isn't visible. The WebDriver API is designed to work with dynamic changing DOM elements that are visible to the user, mimicing the interactions with the UI. It can click on your button because it's visible, but it doesn't see the input element because it is not in the DOM or visible.

You're going to hit a hard wall here and be unable to solve this with Selenium directly, but there may be a workaround. The Automation API provided by .Net can provide you with a way to monitor the FileDialog itself.

Something like this may provide a path:

System.Windows.Forms.SendKeys.SendWait("pathToFile")

Or even a direct action, but this is fragile:

Actions action = new Actions(driver);
action.SendKeys(pObjElement, Keys.Space).Build().Perform();
Thread.Sleep(TimeSpan.FromSeconds(2));
var dialogHWnd = FindWindow(null, "Select a file to upload..."); // Here goes the title of the dialog window
var setFocus = SetForegroundWindow(dialogHWnd);
if (setFocus)
{
    Thread.Sleep(TimeSpan.FromSeconds(2));
    System.Windows.Forms.SendKeys.SendWait(pFile);
    System.Windows.Forms.SendKeys.SendWait("{DOWN}");
    System.Windows.Forms.SendKeys.SendWait("{TAB}");
    System.Windows.Forms.SendKeys.SendWait("{TAB}");
    System.Windows.Forms.SendKeys.SendWait("{ENTER}");
}
James Tomasino
  • 3,520
  • 1
  • 20
  • 38
  • Ah... that's a nice workaround I can use. So stuck myself to Selenium and didn't think of the huge possibilities I could have with powerful .NET and Win32 APIs. Thanks a lot! – Luke Vo Aug 10 '20 at 17:28
2

Suppress the click beforehand with a script injection. The file input will still be created on click but the modal file dialog won't appear. You'll also have to insert the input in the DOM for it to be retrieved via Selenium:

string JS_PREP_FILE_INPUT = @"
HTMLInputElement.prototype.click = function () {
    if (!this.parentNode) {
        this.style.display = 'none';
        document.documentElement.appendChild(this);
        this.addEventListener('change', () => this.remove());
    }
}
";

driver.ExecuteScript(JS_PREP_FILE_INPUT);

driver.FindElement(By.CssSelector("button"))
    .Click();

driver.FindElement(By.CssSelector("input[type=file]"))
    .SendKeys("C:\\myfile.txt");
Florent B.
  • 41,537
  • 7
  • 86
  • 101
  • Thanks, this is a cool and brilliant idea without using workaround! So glad I saved the bounty until today. Will award the bounty if there is no better solution tomorrow :) – Luke Vo Aug 12 '20 at 18:59
  • @Luke Vo, thanks for asking a challenging question! – Florent B. Aug 13 '20 at 19:05
  • No, thank YOU :) your solution is so simple yet effective. I don't even need Selenium anymore, just a simple Chrome Extension since it can be achieved with Javascript. – Luke Vo Aug 13 '20 at 20:43
0

As per the script you have shared:

<body>
    <button>Click me!</button>

    <script>
        document.querySelector("button").addEventListener("click", () => {
            const txt = document.createElement("input");
            txt.type = "file";

            txt.addEventListener("change", () => {
                console.log(txt.files[0]);
            });

            txt.click();
        });
    </script>
</body>

Will add an WebElement of type <input> with value of the type attribute as file within the DOM Tree as follows:

<input type="file" ...>

However, I don't see any issue in locating the same <input> tag once it is added within the HTML DOM. Ideally, you should be able to locate the element inducing WebDriverWait for the element_to_be_clickable() and you can use either of the following Locator Strategies:

  • Using CssSelector considering the element is the only <input> element in the DOM Tree:

    new WebDriverWait(driver, TimeSpan.FromSeconds(20)).Until(ExpectedConditions.ElementToBeClickable(By.CssSelector("input[type='file']"))).SendKeys("/filename/with/absolute/path");
    
  • Using XPath considering the element is the only <input> element in the DOM Tree:

    new WebDriverWait(driver, TimeSpan.FromSeconds(20)).Until(ExpectedConditions.ElementToBeClickable(By.XPath("//input[@type='file']"))).SendKeys("/filename/with/absolute/path");
    
  • Using XPath considering multiple <input> element exists within the DOM Tree:

    new WebDriverWait(driver, TimeSpan.FromSeconds(20)).Until(ExpectedConditions.ElementToBeClickable(By.XPath("//button[text()='Click me!']//following::input[@type='file']"))).SendKeys("/filename/with/absolute/path");
    

Using SeleniumExtras.WaitHelpers

Incase you need SeleniumExtras.WaitHelpers:

new WebDriverWait(driver, TimeSpan.FromSeconds(10)).Until(SeleniumExtras.WaitHelpers.ExpectedConditions.ElementToBeClickable(By.XPath("//input[@type='file']"))).SendKeys("/filename/with/absolute/path");
undetected Selenium
  • 183,867
  • 41
  • 278
  • 352