Appium, Android and MAUI

Prerequisites

You’ll need Appium server (I’m using Appium Server UI), I installed into my Windows c:\tools\Appium folder and next (whilst not a requirement but is very useful) grab the latest version of Appium Inspector this will allows us to inspect elements within our running Android application, I unzipped this to c:\tools\AppiumInspector.

Appium Server GUI has a button to run the inspector but, unless I’m missing something, this simply sends you to the GitHub repo for the inspector, so run the inspector yourself separately.

Creating a simple app.

Create yourself a MAUI application (mine’s call MauiUIAutomationTest, so you’re see this listed in various places below, if you create your own name, remember to update in the code as well).

Like my previous Appium test samples the code is simple (I’ll recreate here). The MainPage.xaml within the ContentPage looks like this

<Grid>
   <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition />
      <RowDefinition />
   </Grid.RowDefinitions>

   <Entry Grid.Row="0" AutomationId="input" Margin="10" Text="{Binding Input}" />
   <Button Grid.Row="1" AutomationId="copy" Margin="10" Command="{Binding ProcessCommand}" Text="Copy" />
   <Label Grid.Row="2" AutomationId="output" Margin="10" Text="{Binding Output}"/>
</Grid>

I’m using MVVM Community Toolkit and so MainPageViewModel.cs is simply this

public partial class MainPageViewModel : ObservableObject
{
    [ObservableProperty] private string input;
    [ObservableProperty] private string output;

    [RelayCommand]
    private void Process()
    {
       Output = Input;
    }
}

Creating our test code

I’ve created an NUnit test class library and added the NuGet package Appium.WebDriver (yes I know it’s an app. but that’s the NuGet package we need). If you’re creating your unit test class library from scratch you’ll need NUnit and NUnit3TestAdapter installed as dependencies as well).

Now my first unit test is, as follows

public class Tests
{
    private AndroidDriver<AndroidElement> _driver;

    [SetUp]
    public void Setup()
    {
        var driverOptions = new AppiumOptions();
        driverOptions.AddAdditionalCapability(MobileCapabilityType.PlatformName, "Android");
        driverOptions.AddAdditionalCapability(MobileCapabilityType.AutomationName, "UiAutomator2"); 
        driverOptions.AddAdditionalCapability(MobileCapabilityType.DeviceName, "Pixel_3_XL_API_29");

        _driver = new AndroidDriver<AndroidElement>(new Uri("http://localhost:4723/wd/hub"), driverOptions); 
        _driver.ActivateApp("com.companyname.mauiuiautomationtest");
    }

    [TearDown]
    public void TearDown()
    {
        _driver.Quit();
    }

    [Test]
    public void ChangeInput_ThenCopy_ExpectMatchingOutput()
    {
        var entry = _driver.FindElementById("com.companyname.mauiuiautomationtest:id/input666");
        entry.Clear();
        entry.SendKeys("Hello World");
        var button = _driver.FindElementById("com.companyname.mauiuiautomationtest:id/copy666");
        button.Click();
        var label = _driver.FindElementById("com.companyname.mauiuiautomationtest:id/output666");
        Assert.That(label.Text, Is.EqualTo("Hello World"));
    }
}

Obviously this is not perfect as we’re running the setup and tear down code after every test, but as I only have one test, I don’t care (it’s for the reader to do this correctly). Now we setup the driver options, the platformName is ofcourse Android, the deviceName relates to the device you’re using, I’m using the Android emulator for the Pixel 3 API 29, finally automationName specifies the automation engine to use, to be honest you could remove this and it’ll default the standard Appium engine but UiAutomator2 is listed as Appiums flagship engine, so let’s use that.

I had all sorts of issues trying to get Appium to start my MAUI app (as it will try to install it), however I think this is likely down to the way Visual Studio builds the apk, i.e. I probably need to publish a signed apk or the likes, but for now I’ll let Visual Studio deploy the app and then access it via Appium. Hence we have the following code to active an already installed app.

 
_driver.ActivateApp("com.companyname.mauiuiautomationtest");

Eager to see something working?

If you’re eager to see something working, then

  • Deploy your application to your Android emulator (I just get Visual Studio to deploy it)
  • Run the Appium Server GUI and start a new session
  • Run your unit test

If all went well the test was green and if not, check your application name etc.

Appium Inspector

The Appium server does not come with an inspector now, you need to download it separately (as stated earlier). Ofcourse we can probably get a lot done without an inspector, but it helps and gives confidence that if it see’s our id’s etc. then Appium will. You’ll need to tell the inspector to connect to the Appium server – make sure that Remote Path has the URL /wd/hub and you have valid the following Desired Capabilities

{
  "appium:automationName": "UIAutomator2",
  "appium:deviceName": "Pixel_3_XL_API_29",
  "platformName": "Android"
}

Ensure your have Appium server is running. Before you run the inspector just make sure your app. is running (just makes things simpler) and now start a session from your Appium Inspector. It should show you the screen of the current running application.

As you can see from the code below, we expect a “copy” automation id

<Button Grid.Row="1" AutomationId="copy" Margin="10" Command="{Binding ProcessCommand}" Text="Copy" />

MAUI AutomationId’s do not appear as AutomationId or Accessibility Id for that matter, but instead they become id’s but prefixed with you app name, i.e. that’s why our unit tests has com.companyname.mauiuiautomationtest:id/output. All that really matters is that we have a unique value on an Appium accessible property to do some UI Automation testing. We do have the option of XPath locators, but these should probably be used as a the last resort, if we do not have anything else to locate against.

Creating a drawable control in MAUI

MAUI includes relatively primitive graphics drawing options for the developer. Your class can implement the IDrawable interface and be assigned to a GraphicsView visual element.

So for example, let’s implement a barebones IDrawable

internal class MyDrawable : IDrawable
{
    public void Draw(ICanvas canvas, RectF dirtyRect)
    {
        canvas.DrawRectangle(1, 1, dirtyRect.Width, dirtyRect.Height);
    }
}

Obviously this does very little of interest except draws a rectangle in the view area that the code which hosts the drawable take up.

So to display this in our XAML we declare the drawable as a resource, like this

<ContentPage.Resources>
   <drawables:MyDrawable x:Key="MyDrawable" />
</ContentPage.Resources>

Finally we assign this drawable to a Graphics views which hosts the drawable like this

<GraphicsView Drawable="{StaticResource MyDrawable}"
   HeightRequest="120"
   WidthRequest="400" />

The ICanvas used within the IDrawable offers a lot of methods to draw lines, rectangles, text etc.

Note: It reminds me of the Canvas in Delphi all those many years back.

Ofcourse, we might prefer to remove the requirement of using resources and the GraphicsView. We’re not really removing them so much as just creating our own GraphicsView, so for example let’s create our control like this

public class MyGraphics : GraphicsView
{
    public MyGraphics()
    {
        Drawable = new MyDrawable();
    }
}

and now we can just use like this

<MyGraphics 
   HeightRequest="120"
   WidthRequest="400" />

WinAppDriver, connecting to existing instance of an application

In a previous post I showed how we can use specflow.actions.json to configure the WinAppDriver with the application name etc. but what if you want to simply connect to an existing instance of an application?

If we’re stick with Specflow then we would probably create a file in the Drivers folder of our test project (or ofcourse add one if it doesn’t exist)

public class WinAppDriver : IDisposable
{
   provate WindowsDriver<WindowsElement> _driver;

   public WindowsDriver<WindowsElement> Current
   {
      get 
      {
         if(!_driver != null) 
         {
            return _driver;
         }

         var appWindowHandle = new IntPtr();
         foreach(var clsProcess in Process.GetProcesses())
         {
            if(clsProcess.ProcessName.Contains("MyApp"))
            {
               appWindowHandle = clsProcess.MainWindowHandle;
               break;
            }
         }

         var appWindowHandleHex = appWindowHandle.ToString("x");

         var options = new AppiumOptions
         {
            PlatFormName = "Windows"
         };

         options.AddAdditionalCapability("deviceName", "WindowsPC");
         options.AddAdditionalCapability("appTopLevelWindows", appWindowHandleHex);

         return _driver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723", options);
      }
   }
}

Testing Windows Package Application

With a packaged application, i.e. from the Window Store we cannot just supply the path of the .exe, instead we need the Package.appxmanifest “Package family name” which can be used as the “app” value

Appium, WinAppDriver and UI testing

I’ve used Teststack.White (and other) UI automation/testing tools in the past for WPF/WinForms testing. It looks like this project has been deprecated along with the Microsoft offering as part of Visual Studio Enterprise, i.e. CodedUI.

The blub on the Microsoft site regarding CodedUI deprecation suggests the alternative for desktop application testing is Appium and WinAppDriver.

Appium is a framework for native, hybrid and mobile web apps. It’s has multi-language support (just like Selenium) and is based on the mobile JSON wite protocol which is an extension to the Selenium JSON wire protocol, it also shares similarities in it’s API to Selenium, but Selenium is a web automation framework, whereas Appium allows us to run UI automation tests against native mobile and desktop.

So where does the WinAppDriver (in the posts title) come into this?

WinAppDriver is a Microsoft driver that Appium calls into via it’s API. WinAppDriver runs as a server on your local machine and provides automation services for UWP, WPF and WinForms.

Note: Whilst there’s aspects of WinAppDriver that are on Github, the actually source for the server is not.

The idea is WinAppDriver (WAD) is run, we use an Appium NuGet library to interact with it via the Appium API.

As stated, this post is all about the Windows desktop, but Appium also works with mobile, so we’ll look at mobile in another post.

Setting things up

  • You’ll need WinAppDriver, look at the releases and download from there (I’m using v1.2.1)
  • Install WinAppDriver, note where it’s installed, it’ll be something like C:\Program Files (x86)\Windows Application Driver\
  • Now for WinAppDriver to work on Win10, you’ll need to go to Developer Settings and set Developer Mode to ON

We’re going to use Specflow to write our test with, although this is immaterial to the actual UI automation testing it seems that it’s a tech. that’s often used alongside UI Automation testing.

Note: If you do not have the Specflow extension installed in Visual Studio then go and add that, it’ll add some nice syntax highlighting as well as templates etc.

Testing our app.

Let’s create a simple WPF application like the one in my previous Selenium post, it’ll contain a TextBox for input, a Button which when clicked will copy the text from input to a Label which is our output – simple…

Here’s some XAML you can slot into a MainWindow.xaml

<Grid>
   <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
      <RowDefinition Height="Auto" />
   </Grid.RowDefinitions>
   <TextBox Grid.Row="0" Margin="10" Text="{Binding Input}" />
   <Button Grid.Row="1" Margin="10" Command="{Binding ProcessCommand}">Copy</Button>
   <Label Grid.Row="2" Margin="10" Content="{Binding Output}"/>
</Grid>

and here’s the view model (which uses the MVVM Community toolkit source generators)

public partial class MainWindowViewModel : ObservableObject
{
    [ObservableProperty] private string input;
    [ObservableProperty] private string output;

    [RelayCommand]
    private void Process()
    {
        Output = Input;
    }
}

Creating the tests

Now create a new class library (we could simply use the Specflow template but let’s do things by hand to see all the nitty gritty)

  • Create a NUnit test project (or whatever test framework you prefer)
  • As we’re going to use Specflow.NUnit, add the nuget package
  • Also add Specflow.Actions.WindowsAppDriver nuget package
  • Add specflow.actions.json to the root of the test project folder, it should look something like this
    {
      "windowsAppDriver": {
        "capabilities": {
          "app": "path and exe of your application"
        },
        "WindowsAppDriverPath": "path on WinAppDriver including WinAppDriver.exe"
      }
    }
    
  • Create a folder names Features and another named Steps within out project
  • Add a new item, choose a Specflow feature and I’ve named mine TestApplication.feature

Let’s get into the code

We’re going to follow the basic steps we took for the Selenium testing, so in our TestApplication.feature we’ll start with the first scenario

Feature: Test Application

@Default
Scenario: Check initial state
* Check the default values are correct

Now Specflow.Actions.WindowsAppDriver actually gives us an AppDriver that we can constructor inject into our scenarios, so we don’t have to set anything up if all the defaults are as expected. So in our TestApplicationStepDefinitions.cs generated from the Specflow feature file we have

We’re not loading the application as part of the scenario, this will happen automatically when the AppDriver is created by the Specflow library. So we’ll just check the defaults of the page exist and are correct on startup.

[Binding]
public class TestApplicationStepDefinitions : IDisposable
{
    private readonly AppDriver _windowsDriver;

    public TestApplicationStepDefinitions(AppDriver appDriver)
    {
        _windowsDriver = appDriver;
        _windowsDriver.Current.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(5000);
    }

    public void Dispose()
    {    
        _windowsDriver.Current.Quit();
        _windowsDriver.Dispose();
    }

    [Given(@"Check the default values are correct")]
    public void GivenCheckTheDefaultValuesAreCorrect()
    {
    }
}

If we run the test scenario the test application will display and then on dispose, it quits and closes. So far, so good. Let’s now implement the GivenLoadThePage method.

We need to add name’s or id’s to our WPF controls so that we can locate our elements. The WAD and Windows in general uses Name on things like WinForms, or in WPF we should use AutomationId’s like this

AutomationProperties.AutomationId="input"

So for example we would change our XAML to add AutomationId’s like this

<TextBox Grid.Row="0" AutomationProperties.AutomationId="input" Margin="10" Text="{Binding Input}" />
<Button Grid.Row="1" AutomationProperties.AutomationId="copy" Margin="10" Command="{Binding ProcessCommand}">Copy</Button>
<Label Grid.Row="2" AutomationProperties.AutomationId="output" Margin="10" Content="{Binding Output}"/>

Now we can write the following into the GivenCheckTheDefaultValuesAreCorrect method

_windowsDriver.Current.FindElementByAccessibilityId("input").Text
   .Should().BeEmpty();
_windowsDriver.Current.FindElementByAccessibilityId("copy")
   .Should().NotBeNull();
_windowsDriver.Current.FindElementByAccessibilityId(("output")).Text
   .Should().BeEmpty();

Notice we’re using FindElementByAccessibilityId as there’s no FindElementByAutomationId. The AccessibilityId method of Appium maps to AutomationId within WAD and ofcourse some of the Appium methods are meaningless to WAD, such as FindElementByCssSelector.

  • AccessibilityId maps to AutomationId
  • ClassName maps to ClassName
  • Name maps to Name

Tools

Before we move on with our second test scenario. All this is fairly easy, locating your elements is fairly easy when it’s liberally coated in AutomationId’s OR an app you control and hence can add these id’s. In situations where you cannot edit the source you’ll need to look for one of the other potential keys to locate elements….

If you’ve installed the Windows 10 SDK installed then check Program Files (for example C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64) for the inspect.exe. This utility will show you the properties of different Windows, and you can drill down to find the name, classname and/or automation id for different controls within your application.

Similar to inspect is UIAVerify which is an old tool but pretty good (it does crash occasionally though). Microsoft states this is a legacy tool and recommends Accessibility Insights which I’ve not used much yet, but looks nicer than inspect and with some of the capabilities of UIAVerify (and so far, crashes less).

Another alterantive tool is the WinAppDriver UIRecorder. At the time of writing this seems a pretty basic tool but useful but one useful aspect is that in can grab XPath for your elements.

Onto the second scenario

Okay so we’ve got our tools and now want to write a second scenario, as listed below (just add to your current .feature file)

@Copy
Scenario: Check input is copied to output
* Copy input to output

If you’ve come from reading the previous Selenium post, you’ll not be learning anything new, for anyone that’s just come straight to this post. We’ll now create the GivenCopyInputToOutput method that maps to the Given step in this new feature

[Given(@"Copy input to output")]
public void GivenCopyInputToOutput()
{
   _windowsDriver.Current.FindElementByAccessibilityId("input").SendKeys("Hello World");
   _windowsDriver.Current.FindElementByAccessibilityId("copy").Click();
   _windowsDriver.Current.FindElementByAccessibilityId("output").Text
      .Should().Be("Hello World");
}

If you run this scenario it should send the keys/string “Hello World” to the input field, it’ll then click the copy button and the output field should update to show the same text.
Now, this worked well because we are using the AutomationId. However, our button also has the Name property correctly set so we could use FindElementByName, let’s change thing so we try to find the “copy” button to use XPath using the Name property

_windowsDriver.Current.FindElementByXPath("//Button[@Name = 'Copy']").Click();

Note: When running against an application with a lot of elements, XPath can easily be 10-20s slower, so where possible try to stick to using name and automation id (and class name if that helps).

One thing the XPath option does offer though that’s really useful, is the power of XPath and so we can search for elements which maybe change dynamically in a prescribed way. So the example XPath above we’re looking for a button with a name of “Copy”, but maybe we have a button who’s name changes to “Copy First Name” depending on usage, now with XPath we can write something like

_windowsDriver.Current.FindElementByXPath("//Button[contains(@Name, 'Copy')]").Click();

Hence we locate a button with a Name that contains the string “Copy”. This could ofcourse be problematic if we have many buttons with the string “Copy” in which cases we can use FindElementsByXPath to get a collection of elements, then try to figure out what we want from there.

What next?

My Selenium post, section headed as What next? covers the topic of race conditions and using the Actions API. The code for Appium is pretty much the same as that for Selenium but ofcourse using the Windows driver and syntax/API, we’ll repeat some of that post here for completeness but if you’re read the Selenium post you’ll probably already be aware of the things mentioned here.

One of the biggest issues with UI Automation testing is the problem around race conditions, i.e. our automation test tries to locate an element that’s either not yet been displayed, or worse still, was displayed then hidden.

With regards waiting for a UI element to appear, we can ofcourse add some form of polling with a timeout (the Selenium API includes this), with regards something that was displayed before we were able to locate it, we’d obvious need to look at handling this is some fashion (most likely a combination of timeout and looking for some other element that might tell us that the former has gone – for example a progress indicator may have disappeared, but if the controls are enabled we don’t care that we couldn’t locate the progress indicator).

Okay, so let’s look at what we can do to make things a little better…

Selenium (and Appium) has an implicit wait timer which can be applied be used, but as the Selenium documentations states An implicit wait is rarely the best solution. However it is an option, so let’s check out what it looks like

_windowsDriver.Current.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(5000)

It may (and probably is) better that we use explicit timeouts within our code.

Note: I read somewhere that explicit and implicit in the same code can cause problems, so it sounds like it’s probably best to stick to one or the other for the most part or at least be aware of potential issues.

Let’s look at how we can wait for an element to appear. Let’s assume that our code’s Process method has changed to this rather crude (delayed update) code.

[RelayCommand]
private void Process()
{
   Task.Run(() =>
   {
      Thread.Sleep(3000);
   }).ContinueWith(tsk =>
   {
      Output = Input;
   });
}

In this code we very crudely simulate a 3 second operation taking place before Input is copied to Output. Assume it’s a web service call or whatever you like but it will mean that our UI Automation test code (without an an implicit wait of sufficient timeout) will NOT locate the change to Output immediately and thus the test will fail. As you’ll have realised, we now need an explicit wait on this code. Basically we want to poll the UI every n milliseconds for the Output to change. We will do this for a given timeout so if things have not updated in, say 10 seconds then there’s an issue. Here we go down to Selenium based code

var wait = new WebDriverWait(_windowsDriver.Current, TimeSpan.FromSeconds(10));
wait.Until(e => _windowsDriver.Current.FindElementByAccessibilityId("output").Text == "Hello World")
   .Should().BeTrue();

In the above code, we use the Selenium WebDriverWait (don’t worry about the prefix Web it works for non-Web as well). We tell it the driver to use and the timeout. I’ve put 10 seconds here, but the code will actually poll the UI every (by default) 500ms and when the condition is true it will complete, hence will stop as soon as the change is located or after the timeout period, whichever is first.

One caveat is, instead of using the e variable which will be an IWebDriver I use the _windowsDriver.Current as this includes the higher level methods such as FindElementByAccessibilityId.

Actions API

There’s more API’s than just those listed thus far, but to go too much further would mean this post turns into an API tutorial, a little outside the realms of a simple blog post. Let’s end on one more API feature that we need to know about and that’s the Actions API.

We’ve used the WindowsElement (and therefore AppiumElement) to Click and SendKeys, these are seen as “high-level interactions”. Sometimes we want to go a little more “low-level”, this is where the Actions API comes in.

Actions are low level in the sense that you can generate keydown, keyup actions for situations where you might need to send keys CTRL+SHIFT+A for example. In this case you’ll need a CTRL keydown along with a SHIFT key down then a keypress for A finally in reverse order, SHIFT keyup and CTRL keyup. Obviously had these been sent as keys via SendKeys it would end up as a CTRL down and up followed by a SHIFT down and up and so on (i.e. no keys held down for the duration of the interaction).

Actions also allow you to essential put together a whole bunch of actions in a single command. For example the following creates Actions by moving to the Copy button, then double clicking on it – the Perform method invokes the sequence of actions.

Here’s a simple example of an Actions API being use to double click a button

var button = _windowsDriver.Current.FindElementByAccessibilityId("copy");
var actions = new Actions(_windowsDriver.Current);
actions
   .MoveToElement(button)
   .DoubleClick()
   .Perform();

WPF UserControl DependencyProperty Binding

On the weekend I was messing around creating a simple WPF card game application and wanted to just quickly knock together a UserControl with a dependency property (actually there were several controls and several dependency properties) which allows the hosting Window or UserControl to change the property.

Ofcourse we should probably look at implementing this as a lookless control but I was just trying to do the bare minimum to get this working as a proof of concept.

So what I want is UserControl which displays playing card, so let’s call it CardView and it looks like this (excluding the UserControl element for brevity)

<Grid>
   <Border Background="White" Padding="8" CornerRadius="8">
      <Image Source="{Binding CardImage, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
      <Border.Effect>
         <DropShadowEffect />
      </Border.Effect>
    </Border>
</Grid>

and the code behind looks like this

public partial class CardView : UserControl
{
   public static readonly DependencyProperty CardImageProperty = DependencyProperty.Register(
      nameof(CardImage), typeof(Uri), typeof(CardView), new PropertyMetadata(default(Uri)));

   public Uri CardImage
   {
      get => (Uri)GetValue(CardImageProperty);
      set => SetValue(CardImageProperty, value);
   }

   public CardView()
   {
      InitializeComponent();
   }
}

The key thing here is that we have this code-behind dependency property CardImage, and in the XAML we want to show the image of the card that was set through the dependency property.

If this was a lookless control we’d have set this binding up like this

RelativeSource={RelativeSource TemplatedParent}

but there’s no separation here of style and code, instead everything’s in the UserControl, so we have to use

RelativeSource={RelativeSource AncestorType=UserControl}

This will then bind the UserControl Image to the dependency property in code-behind and allow our host Window or UserControl to simply do the following

<local:CardView Width="140" Height="200" 
   CardImage="../Resources/HQ.png"/>

For this example, the png’s will be stored in a Resources folder with their Build Action set to Resource.

UI Automation Testing with Selenium

We’re going on a short journey into the realms of User Interface Automation Testing. This post is dedicated to Selenium which is a Web based automation framework. Now it’s important to note that Selenium is aimed at desktop web and mobile web automation testing – not native desktop, i.e. Windows, WinForms, WPF, UWP, Maui OR Linux, Mac (SwiftUI or UIKit) or native mobile such as Android and iOS.

An alternative web automation testing tool that I will probably look at in another post is Playwright, but ofcourse there are other framework for this.

So why am I starting with Selenium? Well I’m looking at UI Automation testing Windows and mobile apps. and one of the options for testing these is Appium which has it’s root in Selenium (at the very least in that it uses the an extension to the Selenium JSON wire protocol), so I figured it’d be good to have a look at Selenium first, to get an idea of the similarities etc. Okay, this is getting too long winded, let’s get into the code.

Note: Spoiler alert, whilst there are, ofcourse similarities between Appium and Selenium, the API differs in some cases, but it’s all similar enough to make it an easy transition, but beware desktop apps are not designed around a DOM like HTML and some things, such as search for elements using XPath (which we’ll see later) has a performance overhead on the desktop that much less obvious on the web.

Anyway let’s continue with Selenium and the web for now…

Sample App

My same app is going to be a React app (for no good reason other than I like to keep refreshing my React knowledge). The app. is going to look awful, but I don’t care as all I want to do is automation it and test it, so if you create a React app using yarn create react-app my-app –template typescript and simple change App.tsx to look like this

import { useState } from 'react';
import './App.css';

function App() {
  const [state, setState] = useState({
    input: "",
    output: ""
  });

  return (
    <div className="App">
      <input type="text" value={state.input} onChange={ev => setState({
        ...state,
        input: ev.target.value
      })}/>
      <button onClick={() => setState({
        ...state,
        output: state.input
      })}>Copy</button>
      <div>{state.output}</div>
    </div>
  );
}

export default App;

Note: this code is a little horrible, but as stated, I’m really not interested in the sample app beyond automating it, so feel free to run your own web app using whatever tech. and code you prefer.

Also in index.html, change the title to Test App we’ll use this to check out page loaded, before trying to interact with it.

So the UI above is a simple textbox which takes some input, a button that, when press will copy the input to the output div. Like I said it’s not pretty and it’s very basic, but it’s a good enough starting point as we’ll need to find elements, check then, send keystrokes to them, click a button and check the output text is as expected – we’ll add more as we go – but again I’m not wasting time making the UI or code look good.

Creating your test app.

Now I’m going to be using Selenium from C# – WHAT !?? I hear you cry. Well Selenium has support for Java, Python, C# and more, so why not (and ofcourse I intend to move to doing desktop and mobile testing and that will be using C#, so we can compare the code etc. as we go.

I’m going to be using Specflow to write my tests, if you prefer, ignore the Specflow parts of this post and just look at any Specflow generated methods as unit tests in your prefered unit testing framework. For this and my next post I will be using Specflow, NUnit and FluentAssertions Library. As I have the Specflow templates installed in Visual Studio, I’ve taken the following steps

  • From Create New Project, create a Specflow project
  • Ensure Add FluentAssertsion Library is checked
  • Create the project
  • Add Nuget Package Selenium.WebDriver.ChromeDriver
  • Add Nuget Package Selenium.WebDriver

Let’s start with a very simple test, so changing the Calculator.feature you get from the project template to the following

Feature: Test Application

@Default
Scenario: Check initial state
* Load the page
* Check the default values are correct

Note: I’m using the short hand syntax for Given, i.e. * to simply define the steps of my test – again, I’m just wanting to write the bare minimum definition and support code so we can concentrate on the Selenium bits, but if the above offends, please switch for Given, And, When Then etc.

Once you generate your definition steps we end up with the following (but with a couple of code changes to allow the class to adhere to IDisposable

[Binding]
public class TestApplicationStepDefinitions : IDisposable
{
   public void Dispose()
   {
   }

   [Given(@"Load the page")]
   public void GivenLoadThePage()
   {
   }

   [Given(@"Check the default values are correct")]
   public void GivenCheckTheDefaultValuesAreCorrect()
   {
   }
}

It’s probably obvious that we’re going to need to load the page from our sample app. in the GivenLoadThePage method then we’ll get the page elements and check them in GivenCheckTheDefaultValuesAreCorrect by asserting they are as expected.

  • Let’s run our React sample app using yarn start, so we have that available to us.
  • Let’s add a field to the class, this will contain the Chrome web driver (ofcourse you can add packages for other drivers, such as Edge and use those instead). The code and the constructor and the Dispose method now looks like this
    private readonly ChromeDriver _chromeDriver;
    
    public TestApplicationStepDefinitions()
    {
       _chromeDriver = new ChromeDriver();
    }
    
    public void Dispose()
    {
       _chromeDriver.Dispose();
    }
    
  • Next let’s add the following code to GivenLoadThePage
    _chromeDriver.Navigate().GoToUrl("http://localhost:3000");
    

    Ofcourse this assumes you’re web page is running on localhost:3000, so change this to suit. This code will basically start up an instance of Chrome and navigate to the URL, but as there’s now further tests/interaction code at this point, it’ll then be dispose of and closed.

  • Let’s actually check we really did load Chrome and our page was displayed, so following the Navigate line add the following
    _chromeDriver.Title.Equals("Test App", StringComparison.OrdinalIgnoreCase)
       .Should().BeTrue();
    

    Basically we want to check the page has the expected title before we run further test scenarios.

So the first thing you might find is that, the navigation to the page and the tests are so quick you have no idea what was being “seen” by Selenium and therefore whether things were even working. Ofcourse the test we just wrote gives us some confidence, but if you have a typo in the name of your application (for example) you’ll have no idea what went wrong unless you log or write information to console as well – so I’d recommend that when carrying out your tests you log to console (at least) some information to help tell you things, such as which element couldn’t be found etc. However from brevity we’ll ignore such logging for now.

Let’s now fill in the second step we created – this checks the elements are in a default state, i.e. input field is empty, output field is empty and button exists – this will help give us confidence that further tests are not tainted by any incorrect default or starting state.

Selenium uses the method FindElement to locate elements via name, class, id etc. using the By class to define what we’re using to locate an element, so let’s add some code to the GivenCheckTheDefaultValuesAreCorrect method, it should look like this

_chromeDriver.FindElement(By.Name("input")).Text
   .Should().BeEmpty();
_chromeDriver.FindElement(By.Name("copy"))
   .Should().NotBeNull();
_chromeDriver.FindElement(By.Name("output")).Text
   .Should().BeEmpty();

So as you can see, we’re looking for each element and in the input and output case, checking that the text is empty and in the copy element, the button’s state, we’re just basically checking it exists. Ofcourse we could simple ignore the button until we try to use it – that’s down to the person writing the tests, but in the case I’ll check everything exists as expected.

If we now run this scenario via the Visual Studio Test Runner, the first test for the title should pass and then the second will fail – it’ll fail because we never assigned names to our elements within our React Test app. Now if we add name=”input” to the input element in our Test App. then that test will pass but the button fails. Ofcourse we could add a name to this element, but it’s got the text “Copy”, so why not simply look for a button with the text “Copy”, we can do that using the By XPath version of FindElement, so change the “copy” line to the following

_chromeDriver.FindElement(By.XPath("//button[contains(text(), 'Copy')]"))
   .Should().NotBeNull();

Running the test through the Test Runner now shows both the first two assertions were met, but the output element cannot be found – div’s don’t have a name option, but they do had id, so change the Test App div to include id=”output”. Obviously we’ll also need to change out FindElement By.Name to By.Id, i.e.

_chromeDriver.FindElement(By.Id("output")).Text
   .Should().BeEmpty();

Okay so we actually learned a fair amount there about how to get our app up and running and interact (at least by finding) elements on the screen (or within the DOM). Now let’s interact with the page…

Interacting with our application

Once we find an element we can call methods on it such as SendKeys and Click, so for out next test we want to input some string into the input element, click the button and expect to see the same text in the output element, so I’ve created a new scenario

@Copy
Scenario: Check input is copied to output
* Load the page
* Copy input to output

Notice that when each scenario ends, at this time we are disposing of the chrome driver and so the next scenario then needs to start the driver and navigate to the page again – ofcourse there are ways to change our code to stop this, but for now, I’m not worried, the app is small and fast to load. So let’s create the step definition for “Copy input to output”

[Given(@"Copy input to output")]
public void GivenCopyInputToOutput()
{
   _chromeDriver.FindElement(By.Name("input")).SendKeys("Hello World");
   _chromeDriver.FindElement(By.XPath("//button[contains(text(), 'Copy')]")).Click();
   _chromeDriver.FindElement(By.Id("output")).Text
      .Should().Be("Hello World");
}

In the above we find the input element, send the string “Hello World” to it, then press the Copy button and then assert that the output has the expected string (i.e. “Hello World”). As we know that each scenario gets a clean/default webpage we do not need to check the output element is empty first (i.e. to make sure it’s not somehow already got “Hello World” in it). Ofcourse we could pass different strings into this step via Cucumber’s/Specflow’s Example keyword – I’ll leave that to the reader to play with if they wish.

If all went according to plan both scenario’s will complete successfully.

What next?

In this post, I’m not going to dig into all the features of Selenium as what I’ve listed here are the key starting points, however there things to be aware of and gotchas awaiting.

For example one of the biggest issues are race condition (and this relates to ALL version of UI Automation Testing). The first of these might be down to whether elements are currently on the screen, i.e. maybe they’re only written to the DOM when required. Selenium includes implicit waits using

_chromeDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(5000);

As the Selenium documentations states An implicit wait is rarely the best solution. It certainly doesn’t help with the next issue. What if your element IS in the DOM and visible but the process of copying the input to output takes a lot longer than it takes to find the output element. We will end up with the element in it’s default state and therefore the test will fail as it’s run before the output is updated from the button click.

Lets prove that point, I’m going to change the our Test app by first adding the following function

function wait(callback: () => void) {
   setTimeout(callback, 3000);
}

and now the code for the button looks like this

<button onClick={() => {
   wait(() => {
      setState({
         ...state,
         output: state.input
      });
   });
}}>Copy</button>

So what happens here is the button click runs the code after a 3s delay, so the code all still works if, you as the user watch the screen (although obviously it’s slower than expected), but for our test scenario… well just run it and see.

I ran mine and got Expected _chromeDriver.FindElement(By.Id(“output”)).Text to be “Hello World” with a length of 11, but “” has a length of 0, differs near “” (index 0)., so there’s a problem – if we introduce changes to our application which slow things down enough then out tests that once passed will now fail.

As stated earlier implicit waits are not the best solution even for elements not yet being visible, but for scenarios such as this, an implicit wait is useless because we got the element just fine, what we do need to do is wait until the Text changes.

Sadly there’s no such thing, such as an event, to alert us when elements change – so we had to use explicit waits with conditional tests and timeouts, in other words we’ll basically poll the element for it’s text and set a timeout where we decide it’s never coming, something like this

// potential race condition
//_chromeDriver.FindElement(By.Id("output")).Text
//    .Should().Be("Hello World");

// explicit wait (still potential race condition, but hopefully less likely)
var wait = new WebDriverWait(_chromeDriver, TimeSpan.FromSeconds(10));
wait.Until(e => e.FindElement(By.Id("output")).Text == "Hello World")
   .Should().BeTrue();

So now we’re using WebDriverWait to wait until a condition is met with a timeout of 10s, in other words this will essentially poll the test application (the default polling interval is 500ms) and get the element (if an element doesn’t exist the exception is handle for us). It will keep trying until the timeout then will return (in this case) either a True or will throw a WebDriverTimeoutException exception as the condition was not met within the timeout.

As you can see, for testing whether an element exists – i.e. once that might dynamically get created we would just use

var wait = new WebDriverWait(_chromeDriver, TimeSpan.FromSeconds(10));
wait.Until(e => e.FindElement(By.Id("output")).Text
    .Should().Be("Hello World");

Actions API

There’s more API’s than just those listed thus far, but to go too much further would mean this post turns into an API tutorial, a little outside the realms of a simple blog post. Let’s end on one more API feature that we need to know about and that’s the Actions API.

We’ve used the WebElement to Click and SendKeys, these are seen as “high-level interactions”. Sometimes we want to go a little more “low-level”, this is where the Actions API comes in.

Actions are low level in the sense that you can generate keydown, keyup actions for situations where you might need to send keys CTRL+SHIFT+A for example. In this case you’ll need a CTRL keydown along with a SHIFT key down then a keypress for A finally in reverse order, SHIFT keyup and CTRL keyup. Obviously had these been sent as keys via SendKeys it would end up as a CTRL down and up followed by a SHIFT down and up and so on.

Actions also allow you to essential put together a whole bunch of actions in a single command. For example the following creates Actions by moving to the Copy button, then double clicking on it – the Perform method invokes the sequence of actions

var button = _chromeDriver.FindElement(By.XPath("//button[contains(text(), 'Copy')]"));
var actions = new Actions(_chromeDriver);
actions
   .MoveToElement(button)
   .DoubleClick()
   .Perform();

Saving power on Ubuntu Server

I’ve been meaning to look into this for a while. My Ubuntu server acts as a NAS drive. As such it’s on all the time, but really it needn’t be. So let’s look at what we can do to save some energy by “hibernating” late in the evening and restarting early in the morning…

Let’s start with the /usr/sbin/rtcwake command. This application will tell your Linux OS to go into a sleep/hibernate state and you supply it with a time to wake back up, for example

/usr/sbin/rtcwake -m off 

The -m switch specifies the type of suspend. Options include

  • standby this option is the least energy efficient option and is the default option if you do not set the -m switch, it’s also very fast at resuming a system from sleep.
  • mem this option suspends to RAM, this puts everything into a low powered state (except RAM) and offers significant savings in energy.
  • disk this option suspends disk operations, the contents of memory are written to disk then restored when the computer wakes up.
  • off this option turns the computer off completely
  • no this option allows you to specify the awake time but doesn’t suspect the computer. The idea is that the user would put the machine into hibernation
    and rtcwake would then wake it up again at the specified point in time.

We might wish to set the resume time using either seconds (i.e. how many seconds in the future does the system resume) using the -s options, for example to wake up 10 hours after going to sleep we would specify 36000 seconds, like this

/usr/sbin/rtcwake -m off -s 36000

or to specify a time we use the -t switch which requires the number of seconds since Unix epoch (i.e. Jan 1st 1970, 00:00:00) so we still need to calculate to seconds

/usr/sbin/rtcwake -m off -t $(date +%s -d ‘tomorrow 07:00’)

If your hardware clock is set to local time, you’ll want to use the -l switch, for example

/usr/sbin/rtcwake -m off -l -t $(date +%s -d ‘tomorrow 07:00’)

Below is a script taken from StackOverflow. Just save this script as wakeup.sh or whatever file name and chmod to executable.

#!/bin/sh

# unix time in s that we should wake at
waketime=$(date +%s -d '07:00')

# make sure the time we got is in the future
if [ $(date +%s) -gt $waketime ]; then

        # if not add a day 60s * 60s * 24h
        waketime=$(($waketime + 86400))
fi

# time in S from now till then
dif=$(($waketime - $(date +%s)))

# tell the user
echo
echo "Current Time      :  $(date +%s)"
echo "Wake Time         :  $waketime"
echo "Seconds From Now  :  $dif"
echo
echo "Ctrl-C to Cancel in 5s"
echo
sleep 5

# sleep with rtcwake
/usr/sbin/rtcwake -m off -l -s $dif

echo "Shutdown."#!/bin/sh

But wait, we can run this rtcwake command (or the above script) manually but for my scenario I’d really want want it run automatically. So we add a cronjob as sudo (as rtcwake will need to be run as sudo) i.e.

sudo crontab -e

Now add the following

0 0 * * * /home/putridparrot/wakeup.sh > /home/putridparrot/wakeup.log

As per crontab expectations the arguments are as follows

  • MIN: Minute 0-60
  • HOUR: Hour, 24 hour clock hence 0-23
  • MDAY: Day of the Month, 1-31
  • MON: Month, 1-12 or jan, feb…dec
  • DOW: Day of the week, 0-6 or sun, mon, tue…sat
  • COMMAND: The command to run

So with this knowledge, our script will run at 12:00am every day. I found it useful to create a log of the output of the script, just to ensure I could see it had run. Remember > will create a new file overwriting any existing file or you could use >> to append to an existing file if you prefer (but then you might want a cronjob to delete the file periodically).

You can also check the syslog for CRON jobs via

grep CRON /var/log/syslog

Dependency injection using Shell in MAUI

The MauiAppBuilder (as seen in the MauiProgram class) i.e.

var builder = MauiApp.CreateBuilder();

exposes the property Services which is a collection of ServiceDescriptor objects. This is where we register our “services”. We can use AddSingleton or AddTransient or AddScoped.

So for example

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
  • AddSingleton
    As the name suggests, a single instance of a type is created across the lifetime of the application.
  • AddTransient
    A new instance of a type is created for each request, the equivalent of creating a new instance for every type that has a dependency, hence each gets a unique instance.
  • AddScoped
    A new instance of a type is created for a request and all dependencies within that request gets the same instance. For different requests a new instance is created, hence unique across requests.

Whilst the IoC container with MAUI Shell, isn’t as comprehensive in features as some containers, such as Autofac, it’s good enough for many. It does include constructor dependency injection so we will probably fulfil most use cases.

Okay, let’s assume with have a MainPage and on that we have a button which displays an AboutPage. As per the code above suggests. We can create a view model for each page, so we have a MainPageViewModel and AboutPageViewModel, then within the actual .xaml.cs of those pages, in the constructor we have code such as

public MainPage(MainPageViewModel vm)
{
   InitializeComponent();
   BindingContext = vm;
}


public AboutPage(AboutPageViewModel vm)
{
   InitializeComponent();
   BindingContext = vm;
}

Now all we need to do is register the pages and view models within the MauiProgram with the builder, so we have

builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
builder.Services.AddTransient<AboutPage>();
builder.Services.AddTransient<AboutPageViewModel>();

When Maui starts up and loads MainPage.xaml it will inject the MainPageViewModel and also when we navigate to our AboutPage using something like the code below, again the view model is injected

[RelayCommand]
private async Task About() => await AppShell.Current.GoToAsync(nameof(AboutPage));

Note: The example uses the MVVM Community Toolkit, hence the RelayCommandAttribute.

We may need to resolve dependencies ourselves (in other words we’re unable to use the magic of something like constructor injection). Rather strangely I would have expected MAUI to already have code within for this, but it seems that current the solution is to create (or use an existing) ServiceProvider class. For example, the one shown below is taken from David Ortinau’s sample.

public static class ServiceProvider
{
   public static TService GetService<TService>()
      => Current.GetService<TService>();

   public static IServiceProvider Current
      =>
#if WINDOWS10_0_17763_0_OR_GREATER
	MauiWinUIApplication.Current.Services;
#elif ANDROID
        MauiApplication.Current.Services;
#elif IOS || MACCATALYST
	MauiUIApplicationDelegate.Current.Services;
#else
	null;
#endif
}

Now now we can use the static class ServiceProvider to get the Services, for example let’s remove the dependency injection in the AboutPage constructor and instead use the ServiceProvider like this

BindingContext = ServiceProvider.GetService<AboutPageViewModel>();

Ofcourse this is not so easily testable, but the option is there if required.

Navigating to different pages using MAUI and Shell

By default MAUI includes the Shell (see AppShell.xaml/AppShell.xaml.cs). We can use method on this object to navigate to pages in MAUI, for example if we add a simple AboutPage and want to navigate to it from a our view model we can write something like this

[RelayCommand]
private async Task About() => await AppShell.Current.GoToAsync(nameof(AboutPage));

Obviously the nameof(AboutPage) maps the the name we’ll be registering for the AboutPage.

Now we need to register the page within the shell, so go to AppShell.xaml.cs and add the following

Routing.RegisterRoute(nameof(AboutPage), typeof(AboutPage));

That’s all we need to do.

MVVM with the MVVM Community Toolkit

When you first create a MAUI application, you might be surprised to find there’s no view models setup by default. I guess this just allows us to see a bare bones application without any preference for MVVM libraries etc.

Let’s try out the MVVM community toolkit with the template generated MAUI app. This toolkit which uses code generators to allow us to create really simple look view model code without all the ceremony…

  • Add the nuget package CommunityToolkit.Mvvm to your references
  • I’m going to store my view models within the ViewModels folder, so add a new folder to your project named ViewModels
  • We’re going to replace the code within MainPage.xaml.cs with our view model, so add a file to ViewModels named MainPageViewModel.cs and it should look like this to start us off
    using CommunityToolkit.Mvvm.ComponentModel;
    
    namespace MauiAppTest.ViewModels;
    
    public partial class MainPageViewModel : ObservableObject
    {
    }
    

    Note: It’s important to make this a partial class, we’ll see why later. The ObservableObject simply gives us the INotifyPropertyChanged and INotifyPropertyChanging implementations.

  • Now within MainPage.xaml.cs, delete the OnCounterClicked method and also delete the line int count = 0;
  • To MainPage.xaml.cs, within the constructor add
    BindingContext = new MainPageViewModel();
    
  • Finally, for now, go to MainPage.xaml and delete the Button’s XAML
    Clicked="OnCounterClicked"
    

At this point we’ve got a bare bones view model which does nothing, so first off we’ll need a property for the count changes and a command which is called to change the count.

  • Within the MainPageViewModel.cs add the following which will create an observable property
    [ObservableProperty] 
    int count;
    
  • Within the MainPage.xaml, change the button’s text to bind to our Count property
    Text="{Binding Count, StringFormat='Click me {0}'}"
    

You might be wondering where the Count property came from, because in our MainPageViewModel.cs with simply declare a field named count (all lowercase and not a property). Well this is the magic of the MVVM toolkit. If you expand the MainPageViewModel.cs in solution explorer you’ll see the class and expanding that you’ll see a Count property, OnCountChanged and OnCountChanging methods where did these come from?

Originally, when we created our view model class, the first important thing is that the class must be a partial class. This allows the next bit of magic, the source generator, to add code to our class without (obviously) altering our class.

When we declare a field using the ObservablePropertyAttribute the MVVM source generator will create the property (with the expected case) along with methods that we could implement for OnXXXChanging and OnXXXChanged. For now let’s look to complete the functionality required to bind the Button to a command and change the Count.

  • Like the ObservableProperty we have another MVVM magical attribute, so add the following code to the MainPageViewModel class
    // you'll need using CommunityToolkit.Mvvm.Input;
    
    [RelayCommand]
    void IncrementCount()
    {
      Count++;
    }
    
  • Now within MainPage.xaml, add the following to the Button XAML
    Command="{Binding IncrementCountCommand}"
    

    Note how the method name is used suffixed with Command when the generator creates our code.

If you try this out, you should see that clicking the button in the UI increments the counter and displays this all via MVVM binding.

We can add code to tell the UI via binding whether the command can be executed by using one of the RelayCommandAttribute’s parameters, CanExecute. Using this we can tell RelayCommand the name of the method to execute, which should return a bool to tell the binding whether we CanExecute the command or now, for example lets add the following to the MainPageViewModel

[ObservableProperty]
bool canIncrement;

bool CanExecuteIncrement() => canIncrement;

and change the existing RelayCommand to

[RelayCommand(CanExecute = nameof(CanExecuteIncrement))]

If you run this now you’ll find that the initial binding of IncrementCountCommand will the CanExecuteIncrement method call returning false and the button will ofcourse be disabled, and we can not enable it at the moment – so let’s just add a simple Checkbox to our MAUI app that binds to CanIncrement, so add before the Button

<CheckBox IsChecked="{Binding CanIncrement}" />

Whilst this will change/toggle the CanIncrement property it will not (as is) tell the binding to essentially re-run the CanExecuteIncrement method. For this to work we just add another attribute to the canIncrement field, making it look like this

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(IncrementCountCommand))]
bool canIncrement;

The addition of NotifyCanExecuteChangedFor will then call IncrementCountCommand.NotifyCanExecuteChanged() and update the UI accordingly.

Earlier we mentioned that code for the property generates extra methods such as OnXXXChanged. Let’s implement our own OnCanIncrementChanged method. These methods are partial so if we add the following to the MainPageViewModel

partial void OnCanIncrementChanged(bool value)
{
  Debug.WriteLine("OnCanIncrementChanged called");
}

Now, when the property CanIncrement changes, this method is called with the current property value passed as a parameter.

References

MVVM source generators
ObservableProperty attribute
RelayCommand attribute