Windows WebDriver- WinAppDriver- Page Objects

WebDriver
28 Shares
WinAppDriver Page Objects

In the last article from the WebDriver Series, I showed you how you could automate Windows desktop apps through WinAppDriver. A framework developed by Microsoft that utilizes the WebDriver Wire Protocol and uses the same client-server architecture as Selenium WebDriver. In this post, I am going to show you how to make your tests more readable and maintainable through the integration of the popular among the Selenium folks- Page Object Design Pattern. Since I couldn't find any official documentation or articles about the usage of the Selenium built-in page objects' mechanism, I am going to show you my improved version of it.

Page Object Design Pattern WinAppDriver

You can read my previous article (Automate Windows Desktop Apps with WebDriver- WinAppDriver) to learn how to create your first desktop application's tests. As you may know, the page objects are designed to represent the UI elements of the web pages in more concise and readable way. There you can find all items' locators so that you can update them in a single location. Also, you can define common actions so that you don't copy paste code. You can check my articles about the subject in the Design Pattern Series.

In the following example, again I am going automate the Windows Desktop Calculator shipped with Windows 10.

Windows 10 Calculator

Also, I am not sure that the suffix Page is correct in the context of desktop applications. So, I named my page objects- Views. Probably, we can use the words component or window as well. These classes are placed under the folder Views. Moreover, once again we are going to use partial classes to separate the different components of the page object- actions, assertions, elements. During the build process, there are combined in a single type, so we need to give the three files different names. 

WinAppDriver Page Objects Structure

StandardView.Elements

public partial class CalculatorStandardView
{
public WindowsElement ZeroButton => _driver.FindElementByName("Zero");
public WindowsElement OneButton => _driver.FindElementByName("One");
public WindowsElement TwoButton => _driver.FindElementByName("Two");
public WindowsElement ThreeButton => _driver.FindElementByName("Three");
public WindowsElement FourButton => _driver.FindElementByName("Four");
public WindowsElement FiveButton => _driver.FindElementByName("Five");
public WindowsElement SixButton => _driver.FindElementByName("Six");
public WindowsElement SevenButton => _driver.FindElementByName("Seven");
public WindowsElement EightButton => _driver.FindElementByName("Eight");
public WindowsElement NineButton => _driver.FindElementByName("Nine");
public WindowsElement PlusButton => _driver.FindElementByName("Plus");
public WindowsElement MinusButton => _driver.FindElementByName("Minus");
public WindowsElement EqualsButton => _driver.FindElementByName("Equals");
public WindowsElement DivideButton => _driver.FindElementByName("Divide by");
public WindowsElement MultiplyByButton => _driver.FindElementByName("Multiply by");
public WindowsElement ResultsInput => _driver.FindElementByAccessibilityId("CalculatorResults");
}

Here we use the new lambda syntax for defining properties in C#. You can see how readable the code is in only 20 lines of code.

StandardView.Actions

In the StandardView.Actions file, we define a single public method that takes two integer numbers and the operation that we want to perform as a character. We have two separate private methods. The first is responsible for clicking the correct digit based on the int parameter. The second one does the same but for the operators (+, -, /, *). We have one more private method that converts the text result to decimal which we later use in the assert method.

public partial class CalculatorStandardView
{
private readonly WindowsDriver<WindowsElement> _driver;
public CalculatorStandardView(WindowsDriver<WindowsElement> driver) => _driver = driver;
public void PerformCalculation(int num1, char operation, int num2)
{
ClickByDigit(num1);
PerformOperations(operation);
ClickByDigit(num2);
EqualsButton.Click();
}
private void ClickByDigit(int digit)
{
switch (digit)
{
case 1:
OneButton.Click();
break;
case 2:
TwoButton.Click();
break;
case 3:
ThreeButton.Click();
break;
case 4:
FourButton.Click();
break;
case 5:
FiveButton.Click();
break;
case 6:
SixButton.Click();
break;
case 7:
SevenButton.Click();
break;
case 8:
EightButton.Click();
break;
case 9:
NineButton.Click();
break;
default:
throw new NotSupportedException($"Not Supported digit = {digit}");
}
}
private void PerformOperations(char operation)
{
switch (operation)
{
case '+':
PlusButton.Click();
break;
case '-':
MinusButton.Click();
break;
case '=':
EqualsButton.Click();
break;
case '*':
MultiplyByButton.Click();
break;
case '/':
DivideButton.Click();
break;
default:
throw new NotSupportedException($"Not Supported operation = {operation}");
}
}
private string GetCalculatorResultText() => ResultsInput.Text.Replace("Display is", string.Empty).Trim();
}

StandardView.Assertions

public partial class CalculatorStandardView
{
public void AssertResult(decimal expectedReslt)
{
string strResult = GetCalculatorResultText();
var actualResult = decimal.Parse(strResult);
Assert.AreEqual(expectedReslt, actualResult);
}
}

Instead of repeating these three lines of code every time, we move them here.

WinAppDriver Page Objects in Tests

Below you can find the happy path tests for the Windows Calculator. The first example doesn't use page objects, and it is much more verbose. The second example uses page objects which makes it much more readable.

Calculator Tests without Page Objects

[TestFixture]
public class CalculatorTests
{
private WindowsDriver<WindowsElement> _driver;
[SetUp]
public void TestInit()
{
var appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
appCapabilities.SetCapability("deviceName", "WindowsPC");
_driver =
new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appCapabilities);
_driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
[TearDown]
public void TestCleanup()
{
if (_driver != null)
{
_driver.Quit();
_driver = null;
}
}
[Test]
public void Addition()
{
_driver.FindElementByName("Five").Click();
_driver.FindElementByName("Plus").Click();
_driver.FindElementByName("Seven").Click();
_driver.FindElementByName("Equals").Click();
var calculatorResult = GetCalculatorResultText();
Assert.AreEqual("12", calculatorResult);
}
[Test]
public void Division()
{
_driver.FindElementByAccessibilityId("num8Button").Click();
_driver.FindElementByAccessibilityId("num8Button").Click();
_driver.FindElementByAccessibilityId("divideButton").Click();
_driver.FindElementByAccessibilityId("num1Button").Click();
_driver.FindElementByAccessibilityId("num1Button").Click();
_driver.FindElementByAccessibilityId("equalButton").Click();
Assert.AreEqual("8", GetCalculatorResultText());
}
[Test]
public void Multiplication()
{
_driver.FindElementByXPath("//Button[@Name='Nine']").Click();
_driver.FindElementByXPath("//Button[@Name='Multiply by']").Click();
_driver.FindElementByXPath("//Button[@Name='Nine']").Click();
_driver.FindElementByXPath("//Button[@Name='Equals']").Click();
Assert.AreEqual("81", GetCalculatorResultText());
}
[Test]
public void Subtraction()
{
_driver.FindElementByXPath("//Button[@AutomationId="num9Button"]").Click();
_driver.FindElementByXPath("//Button[@AutomationId="minusButton"]").Click();
_driver.FindElementByXPath("//Button[@AutomationId="num1Button"]").Click();
_driver.FindElementByXPath("//Button[@AutomationId="equalButton"]").Click();
Assert.AreEqual("8", GetCalculatorResultText());
}
[Test]
[TestCase("One", "Plus", "Seven", "8")]
[TestCase("Nine", "Minus", "One", "8")]
[TestCase("Eight", "Divide by", "Eight", "1")]
public void Templatized(string input1, string operation, string input2, string expectedResult)
{
_driver.FindElementByName(input1).Click();
_driver.FindElementByName(operation).Click();
_driver.FindElementByName(input2).Click();
_driver.FindElementByName("Equals").Click();
Assert.AreEqual(expectedResult, GetCalculatorResultText());
}
private string GetCalculatorResultText()
{
return _driver.FindElementByAccessibilityId("CalculatorResults").Text.Replace("Display is", string.Empty).Trim();
}
}

Calculator Tests with Page Objects

[TestFixture]
public class CalculatorPageObjectsTests
{
private WindowsDriver<WindowsElement> _driver;
private CalculatorStandardView _calcStandardView;
[SetUp]
public void TestInit()
{
var appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App");
appCapabilities.SetCapability("deviceName", "WindowsPC");
_driver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appCapabilities);
_driver.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(5));
_calcStandardView = new CalculatorStandardView(_driver);
}
[TearDown]
public void TestCleanup()
{
if (_driver != null)
{
_driver.Quit();
_driver = null;
}
}
[Test]
public void Addition()
{
_calcStandardView.PerformCalculation(5, '+', 7);
_calcStandardView.AssertResult(12);
}
[Test]
public void Division()
{
_calcStandardView.PerformCalculation(8, '/', 1);
_calcStandardView.AssertResult(8);
}
[Test]
public void Multiplication()
{
_calcStandardView.PerformCalculation(9, '*', 9);
_calcStandardView.AssertResult(81);
}
[Test]
public void Subtraction()
{
_calcStandardView.PerformCalculation(9, '-', 1);
_calcStandardView.AssertResult(8);
}
[Test]
[TestCase(1, '+', 7, 8)]
[TestCase(9, '-', 7, 2)]
[TestCase(8, '/', 4, 2)]
public void Templatized(int num1, char operation, int num2, decimal result)
{
_calcStandardView.PerformCalculation(num1, operation, num2);
_calcStandardView.AssertResult(result);
}
}

You can see yourself how much more readable the tests become. Moreover, if you have to make any changes, you can do it only in a single place. Since we use partial classes the location of potential fixes is easier compared to if everything is put in a single file.