microsoft / WinAppDriver

Windows Application Driver
MIT License
3.67k stars 1.4k forks source link

Start ClickOnce Application with AppiumOptions #1161

Open vberesnev opened 4 years ago

vberesnev commented 4 years ago

Hello. I have some problems with starting click-once application by AppiumOptions class. My code is too simple:

AppiumOptions appiumOptions = new AppiumOptions();
appiumOptions.AddAdditionalCapability("ms:waitForAppLaunch", "30");
appiumOptions.AddAdditionalCapability("ms:experimental-webdriver", false);
appiumOptions.AddAdditionalCapability("deviceName", "WindowsPC");
appiumOptions.AddAdditionalCapability("platformName", "Windows");
appiumOptions.AddAdditionalCapability("app", "C:\Path\To\My\ApplicationName.appref-ms");

var winDriver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appiumOptions);

and I catch Exception "OpenQA.Selenium.WebDriverException: 'Failed to locate opened application window with appId: C:\Path\To\My\ApplicationName.appref-ms, and processId: 7356'" on the last line, but my click once app;ication starts without any problems.

I am using ms:waitForAppLaunch with 30 seconds of timeout (it's enought) but it can't help me. Exception

anunay1 commented 4 years ago

Does you application have a splash screen? Check the window name after the application is opened. In the screen shot I see that you starting the application from app data roaming. folder, better option will be to start the application from cortana.

[ClassInitialize] public static void Setup(TestContext context) { // Create a session for Desktop DesiredCapabilities desktopCapabilities = new DesiredCapabilities(); desktopCapabilities.SetCapability("app", "Root"); desktopSession = new WindowsDriver(new Uri(WindowsApplicationDriverUrl), desktopCapabilities); Assert.IsNotNull(desktopSession);

        // Launch Cortana Window using Windows Key + S keyboard shortcut to allow session creation to find it
        desktopSession.Keyboard.SendKeys(OpenQA.Selenium.Keys.Meta + "s" + OpenQA.Selenium.Keys.Meta);
        Thread.Sleep(TimeSpan.FromSeconds(1));
        WindowsElement CortanaWindow = desktopSession.FindElementByName("Cortana");
        string CortanaTopLevelWindowHandle = CortanaTopLevelWindowHandle = (int.Parse(CortanaWindow.GetAttribute("NativeWindowHandle"))).ToString("x");

        // Create session for already running Cortana
        DesiredCapabilities appCapabilities = new DesiredCapabilities();
        appCapabilities.SetCapability("appTopLevelWindow", CortanaTopLevelWindowHandle);
        cortanaSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appCapabilities);
        Assert.IsNotNull(cortanaSession);

        // Set implicit timeout to 5 seconds to make element search to retry every 500 ms for at most ten times
        cortanaSession.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
    }

And to start the application I use the below code to find add and remove programs, you can use your application name:

// Type "add" in Cortana search box searchBox.SendKeys("add"); Thread.Sleep(TimeSpan.FromSeconds(1)); var bingPane = cortanaSession.FindElementByName("Bing"); Assert.IsNotNull(bingPane);

        // Verify that a shortcut to "System settings Add or remove programs" is shown as a search result
        var bingResult = bingPane.FindElementByXPath("//ListItem[starts-with(@Name, \"Add or remove\")]");
        Assert.IsNotNull(bingResult);
        Assert.IsTrue(bingResult.Text.Contains("System settings"));

You can find more info at the below link: https://github.com/microsoft/WinAppDriver/wiki/Frequently-Asked-Questions

mialeska commented 4 years ago

Hi @vberesnev , I had an experience automating ClickOnce application using the WinAppDriver. The main point of the solution is that you need to start your "C:\Path\To\My\ApplicationName.appref-ms" as a process, and then attach to the opened application using the WinAppDriver root session (also known as "desktop session").

It was quite tricky for me, also I needed to handle application updates, so we ended up with creating a wrapper over the WinAppDriver - this one: https://github.com/aquality-automation/aquality-winappdriver-dotnet. It has in-build utilities to work with the root session, handling the process start/stop, starting WinAppDriver programmatically. You can specify all the capabilities you need in the settings.json file instead of hardcoding them. It has a fancy logging for all build-in methods. It also implements PageObject pattern, have built-in classes for common UI elements, such as Forms, WIndows, TextBoxes, Buttons and so on.

You can add this library as a Nuget package Aquality.WinAppDriver

Please take a look at example test here: https://github.com/aquality-automation/aquality-winappdriver-dotnet/blob/master/Aquality.WinAppDriver/tests/Aquality.WinAppDriver.Tests/Applications/WindowHandleApplicationFactoryTests.cs . What you need will look very similar and quite simple:

using Aquality.WinAppDriver.Applications;
using Aquality.WinAppDriver.Extensions;
using Aquality.WinAppDriver.Forms;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using System;

namespace YourNamespace
{
    public class YourClass
    {
        private const string ApplicationPath = @"C:\Path\To\Your\ApplicationName.appref-ms";
        // we prefer to store such path in settings.json, but you could have it in code if you mind

        static void Main(string[] args)
        {
            AqualityServices.ProcessManager.Start(ApplicationPath);
            AqualityServices.SetWindowHandleApplicationFactory(rootSession => new ApplicationWindow(() => rootSession).GetNativeWindowHandle());
            // continue working with your application using respective Window/Form classes
        }

        private class ApplicationWindow : Window
        {
            public ApplicationWindow(Func<WindowsDriver<WindowsElement>> customSessionSupplier) 
                : base(MobileBy.AccessibilityId("YourAppWindowLocator"), "Your Application", customSessionSupplier)
            {
            }
        }
    }
}
vberesnev commented 4 years ago

Hi @vberesnev , I had an experience automating ClickOnce application using the WinAppDriver. The main point of the solution is that you need to start your "C:\Path\To\My\ApplicationName.appref-ms" as a process, and then attach to the opened application using the WinAppDriver root session (also known as "desktop session").

It was quite tricky for me, also I needed to handle application updates, so we ended up with creating a wrapper over the WinAppDriver - this one: https://github.com/aquality-automation/aquality-winappdriver-dotnet. It has in-build utilities to work with the root session, handling the process start/stop, starting WinAppDriver programmatically. You can specify all the capabilities you need in the settings.json file instead of hardcoding them. It has a fancy logging for all build-in methods. It also implements PageObject pattern, have built-in classes for common UI elements, such as Forms, WIndows, TextBoxes, Buttons and so on.

You can add this library as a Nuget package Aquality.WinAppDriver

Please take a look at example test here: https://github.com/aquality-automation/aquality-winappdriver-dotnet/blob/master/Aquality.WinAppDriver/tests/Aquality.WinAppDriver.Tests/Applications/WindowHandleApplicationFactoryTests.cs . What you need will look very similar and quite simple:

using Aquality.WinAppDriver.Applications;
using Aquality.WinAppDriver.Extensions;
using Aquality.WinAppDriver.Forms;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using System;

namespace YourNamespace
{
    public class YourClass
    {
        private const string ApplicationPath = @"C:\Path\To\Your\ApplicationName.appref-ms";
        // we prefer to store such path in settings.json, but you could have it in code if you mind

        static void Main(string[] args)
        {
            AqualityServices.ProcessManager.Start(ApplicationPath);
            AqualityServices.SetWindowHandleApplicationFactory(rootSession => new ApplicationWindow(() => rootSession).GetNativeWindowHandle());
            // continue working with your application using respective Window/Form classes
        }

        private class ApplicationWindow : Window
        {
            public ApplicationWindow(Func<WindowsDriver<WindowsElement>> customSessionSupplier) 
                : base(MobileBy.AccessibilityId("YourAppWindowLocator"), "Your Application", customSessionSupplier)
            {
            }
        }
    }
}

Hello! Thank you for your answer! I am using the way then I get Desktop WAD Session, and then I get my opened application. I see you have good knowledge of WinAppDriver. Can you help me with my second issue? https://github.com/microsoft/WinAppDriver/issues/1182 I need to "refresh" UA tree, but I don't know how I can do it

mialeska commented 4 years ago

@vberesnev From what I know, WAD "refreshes" elements tree by default each time you're searching for the element. Do you experience some "caching" that makes you unable to find elements in the changed UI? If so, then you probably searching relatively from some element, and you need to update the searchContext source. For example,

var parentElement = driverSession.FindElement(By.Name("parentName")); var childElement = parentElement .FindElement(By.Name("childName")); // <...> some actions, as result you have UI changed var newChildElement = parentElement .FindElement(By.Name("newChildName")); //this could fail as you cached parentElement before your changing UI actions //you need to refresh the search context of the parent, by finding it again: parentElement = driverSession.FindElement(By.Name("parentName")); //now you should be able to find your new child: var newChildElement = parentElement .FindElement(By.Name("newChildName"));

If I'm getting this wrong and it's not what happening in your case, please describe in more details: what problems are you facing with?

vberesnev commented 4 years ago

@mialeska Thank you, I will try your method and write feedback to you!

vberesnev commented 4 years ago

@mialeska I tried it:

//App is opened on the first page

var parentElement = driver.FindElement(By.Name(_mainWindowTitle)); //Get application window as parent element

parentElement.ClearCache();   // with this method and without it - no result
parentElement.DisableCache(); // with this method and without it - no result

var menu = parentElement.FindElementByAccessibilityId("mergedMenuControl"); //get menu element

var menuItems = menu.FindElements(By.ClassName("Button")); //get buttons array
menuItems[1].Click(); // click second page, second page open

Thread.Sleep(5000); //sleep for load second page

parentElement = driver.FindElement(By.Name(_mainWindowTitle)); //reload parent
var chart = parentElement.FindElements(By.ClassName("ChartGroupContainerView")).FirstOrDefault(); //second try to reloading parent on the second page (returns NULL)

if (chart == null) 
{
    parentElement = driver.FindElement(By.Name(_mainWindowTitle)); //second try to reloading parent
    chart = parentElement.FindElements(By.ClassName("ChartGroupContainerView")).FirstOrDefault(); //second try to find element
}

chart.Click(); //Exception because of chart is NULL

I tried to reload WinAppDriver process and get new session (I get new session, but I still could not find element on the second page).

I have not any ideas...

mialeska commented 4 years ago

@vberesnev I'm afraid that your problem is that your second page is not attached to your main window. AFAIK the best option here would be to search for the parent windowElement of your second page from the root (desktop) WAD session, and then to find your elements relatively from that windowElement. As I said before, from my perspective the most convenient way to handle this is implemented within Aquality.WinAppDriver package, you should give it a try :)

vberesnev commented 4 years ago

@mialeska I used Inspect.exe for find UI elements name, classe and id. So, then I open my app on the first page, I see elements for first page (and main window is parent). Then I click menu item, second page opened, but in Inspect elements tree does not reload. I press "refresh" button, but it does not help. And only after my click to some element on the second page, Ispect refreshes elements tree.

vberesnev commented 4 years ago

@mialeska It seems I find the resolve of this problem! But it's too ugly, lol :) Using Inspect.exe I noticed that refresh of UI elements starts after mouse over one of element second page. But I haven't access to this element by name or id before refreshing. So my actions: I open second page, move mouse coursor to the element by pixcel coordinate (my app is fulscreen), after that WAD refreshs tree and I have access to the UI elements of the second page. Thank you so much, you gave me way to resolving my issue! :) Have a nice day!

splekhan commented 4 years ago

@vberesnev how exactly did you move mouse cursor? I tried "moveToElement" but it didn't help.

vberesnev commented 4 years ago

@splekhan Hello! moveToElement can't help you because WAD doesn't see needed element before reloading of UI elements tree. So, I'm using this crutch:

var parentElement = driver.FindElement(By.Name(_mainWindowTitle)); //Get application window as parent element

 var menu = parentElement.FindElementByAccessibilityId("MenuControl"); //get menu element

 var menuItems = menu.FindElements(By.ClassName("Button")); //get buttons array
menuItems[1].Click(); // click second page, second page open

Thread.Sleep(2000); // sleep for load second page

driver.Mouse.MouseMove(parentElement.Coordinates, 575, 75); // move mouse to element with coordinates 575,75 for activate mousehover event and reloading UI elements tree 
Thread.Sleep(5000);

I have reloading of UI tree after mouse over to button on second page and tooltip showing.

It is terrible solution to the problem, but it's work for me

splekhan commented 4 years ago

@splekhan Hello! moveToElement can't help you because WAD doesn't see needed element before reloading of UI elements tree. So, I'm using this crutch:

var parentElement = driver.FindElement(By.Name(_mainWindowTitle)); //Get application window as parent element

 var menu = parentElement.FindElementByAccessibilityId("MenuControl"); //get menu element

 var menuItems = menu.FindElements(By.ClassName("Button")); //get buttons array
menuItems[1].Click(); // click second page, second page open

Thread.Sleep(2000); // sleep for load second page

driver.Mouse.MouseMove(parentElement.Coordinates, 575, 75); // move mouse to element with coordinates 575,75 for activate mousehover event and reloading UI elements tree 
Thread.Sleep(5000);

I have reloading of UI tree after mouse over to button on second page and tooltip showing.

It is terrible solution to the problem, but it's work for me

Thank you! I'll try this and let you know how it went. I think that the issue we've faced with is kinda popular. WAD should have the caution on its "box" to warn consumers about the inability to refresh the elements.

My app allows to perform "Refresh" by clicking the button in Inspect.exe and it helps. Maybe it's a good idea to invoke this API method using driver.

splekhan commented 4 years ago

@vberesnev thanks again, but it seems like it's not my solution. I noticed that pane tree doesn't refresh when I hover mouse even manually. Only Inspect.exe "Refresh" button does that.

vberesnev commented 4 years ago

@splekhan As I know WAD should reload tree automatically. So, how I found this solution: I open my app, open Isnpect.exe, open second page (Insect did't reload tree), and I was looking for any case, then Inspect.exe reload the tree automatically, without button "refresh". So, I found, if I cover by mouse some button on second page (without click!!!) and invoke tool tip and then I move mouse to some free space -> Inspect.exe reloads UI tree automatically. So I started to use this case in my app. Please, if you will found more lucky case, tell me about it :)

splekhan commented 4 years ago

@vberesnev the only thing that worked is pretty radical ))) I created this method and it's seem like now I have to call it every time I use pagination :( You still think your solution is ugly? Watch mine!

    protected void reloadElements() {
        URL url = null;
        try {
            url = new URL(DRIVER_HUB_URL);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        WebElement existingWindow = getWebDriver().findElement(By.name("Window title name"));
        RemoteWebDriver existingWindowSession = null;
        int handle = Integer.parseInt(existingWindow.getAttribute("NativeWindowHandle"));
        String currentHandle = Integer.toHexString(handle);
        DesiredCapabilities caps = new DesiredCapabilities();
        caps.setCapability("appTopLevelWindow", currentHandle);
        existingWindowSession = new RemoteWebDriver(url, caps);
        setWebDriver(existingWindowSession);
    }
vberesnev commented 4 years ago

@splekhan My solution depends on app window, so if app will not be open full screen or place of elelment will be changed - my solution will go to trash :)

I tried reopen win app driver and reattach my application to it, but it didn't work for me