Automated web testing is helpful when you want to validate a web application as a whole. It shouldn’t be the base of your testing pyramid as web testing can be difficult to develop and maintain. In this post we will look at how to take advantage of Selenium using the Selenium PowerShell module, along with Pester, to develop a test framework. Selenium is heavily used for testing Universal Dashboard.

Getting Started

The first thing you will need is to install the Selenium PowerShell module. The module is available on the PowerShell Gallery so you can use Install-Module to get it local.

Install-Module Selenium

Next, you’ll want to pick the browser (or browsers) you’d like to test with and ensure they are installed. You can currently use Chrome, Internet Explorer and Firefox with the Selenium module. To create a new browser instance, you will want to use the corresponding cmdlet.

$Driver = Start-SeChrome

This cmdlet will create a new Selenium Web Driver instance of the browser. After running this cmdlet, you should see a Chrome window open. You are now ready to start automating it.

For the remainder of this post, we will use Universal Dashboard to create a website and then test it with Selenium and Pester. If you want to follow along, feel free to install the module via the gallery.

Install-Module UniversalDashboard -AcceptLicense

Navigating to a Page

First, let’s spin up a new web site using Universal Dashboard. This website will just have some header text that says “Hello, World”

$Dashboard=New-UDDashboard-Title "Dashboard"-Content {New-UDHeading-Text "Hello, World"}
Start-UDDashboard -Dashboard $Dashboard -Port 1000 -Force

Next, let’s instruct Chrome to navigate to the website.

Enter-SeUrl -Driver $Driver -Url "http://localhost:1000"

Chrome should now be sitting on the Universal Dashboard home page.

Finding Elements on the Page

When performing web browser automation, one of the main challenges you will face is successfully finding elements within the HTML document. The cmdlet Find-SeElement is the way you will locate elements once you have navigated to the page. You can find elements by tag, ID, class name, CSS selector, name, link text, partial link text and xpath.
You will likely use the developer tools in your browser quite a bit when doing browser automation. If you right click on an element in a web page and then select Inspect Element, you will be taken to the developer tools and they will provide all kinds of info about that element.

Finding by ID

Let’s modify our website to provide an ID to our heading.
$Dashboard = New-UDDashboard-Title "Dashboard"-Content { New-UDHeading-Text "Hello, World"-Id "Heading" }
This is one of the easiest and fastest ways to find an element within an HTML document. Not all elements will have an ID but if you have control over it and want to do testing of the page, this is a good way to go. If you don’t specify an ID for an element in UD, it will create one using a random GUID.
$Element = Find-SeElement -Driver $Driver -Id 'Heading'
You’ll see that you now have a single Selenium element. Elements are rich objects that provide all kinds of properties and methods.
TypeName: OpenQA.Selenium.Remote.RemoteWebElement

Name MemberType Definition
---- ---------- ----------
Clear Method void Clear(), void IWebElement.Clear()
Click Method void Click(), void IWebElement.Click()
Equals Method bool Equals(System.Object obj)
FindElement Method OpenQA.Selenium.IWebElement FindElement(OpenQA.Selenium.By by), OpenQA.Selenium.IWebElement ISearchContext... 
FindElementByClassName Method OpenQA.Selenium.IWebElement FindElementByClassName(string className), OpenQA.Selenium.IWebElement IFindsBy... 
FindElementByCssSelector Method OpenQA.Selenium.IWebElement FindElementByCssSelector(string cssSelector), OpenQA.Selenium.IWebElement IFin...
FindElementById Method OpenQA.Selenium.IWebElement FindElementById(string id), OpenQA.Selenium.IWebElement IFindsById.FindElement... 
FindElementByLinkText Method OpenQA.Selenium.IWebElement FindElementByLinkText(string linkText), OpenQA.Selenium.IWebElement IFindsByLi...
FindElementByName Method OpenQA.Selenium.IWebElement FindElementByName(string name), OpenQA.Selenium.IWebElement IFindsByName.FindE...

Finding by Class Name

CSS class names are another way of locating elements within your document. Find-SeElement can return more than one element. Selecting by class name has a good chance of doing just that. For example, if we modified our page to contain 2 DIV tags and with the same class name, Find-SeElement would return both.

New-UDElement-tag div -Attributes @{
   className='myClass'
}
New-UDElement-tag div -Attributes @{ 
   className='myClass'
}

To find these DIVs, use the ClassName argument of Find-SeElement.

$Element = Find-SeElement -Driver $Driver -ClassName 'myClass'
$Element | Measure-Object

Count : 2
Average : 
Sum : 
Maximum : 
Minimum : 
Property :
You cannot specify multiple class names with this parameter.

Finding by CSS Selector

CSS Selectors allow you to select elements based on their tag, class name or attributes. You can use the Css parameter to find elements by a CSS selector. Let’s modify our website to have a text input control and select it via a CSS selector.

New-UDElement-tag input -Attributes @{
   type="text"
}

To find this input control, you the following CSS selector.

Find-SeElement -Driver $Driver -Css 'input[type=text]'

Finding by Tag

You can also find elements based on their tag. This could be useful for something like finding all images in a website. Let’s modify our website to have a couple images.

New-UDImage-Url https://ironmansoftware.com/wp-content/uploads/2019/06/ironermanhead-1.png
New-UDImage-Url https://i0.wp.com/ironmansoftware.com/wp-content/uploads/2019/07/uddemo.png
New-UDImage-Url https://i1.wp.com/ironmansoftware.com/wp-content/uploads/2019/01/logo-1.png
To select all the images, use the TagName parameter.
Find-SeElement -Driver $Driver -TagName 'img'

Finding Child Elements

So far, all the examples I have shown have been finding elements from the root document and searching down through the entire document’s tree. It’s also possible to find children of a particular element. You can do this by use the Element parameter of Find-SeElement rather than the Driver parameter.

Let’s update our page to create a nested list and find the nested list items in the list.

New-UDElement-Tag ul -Id 'list'-Content {
   New-UDElement-Tag li -Content {"item1"}
   New-UDElement-Tag li -Content {"item2"}
   New-UDElement-Tag li -Content {"item3"}
   New-UDElement-Tag li -Content {"item4"}
}
To find all the nested li elements, we can use Find-SeElement twice. First we find the list, then we find the children.
$Element = Find-SeElement -Driver $Driver -Id list
Find-SeElement -TagName li -Element $Element | Measure-Object

Working with Elements

Once we have found the elements, we will want to begin to work with them. The objects themselves have lots of properties and methods. Here are a few common ones that I see myself using frequently.

Retrieving Element Text

It’s very easy to access the text of an element. This will include the text of the element itself and all the children of that element. If we have a div with some text in our website like this.

New-UDElement -Tag div -Id 'text' -Content { "Some Text" }

We can then find that element and display its text like this.

(Find-SeElement -Driver $Driver -Id text).Text

Clicking Elements

In order to interact with the website, it will be required to click at some point and time. To click, you can find the element and then use the Invoke-SeClick cmdlet to issue a click to the element. Let’s update our website to include a button that shows a toast when clicked.

New-UDButton-Text "Click me!" -Id 'button' -OnClick {
   Show-UDToast-Message "I have been clicked!"
}
To click this button, we can use a combination of Find-SeElement and Invoke-SeClick.
Find-SeElement -Driver $Driver -Id button | Invoke-SeClick
Sometimes, elements will be hidden beneath other elements due to the state of the page. When this happens, the default Invoke-SeClick won’t suffice for clicking the element. You can issue a click event through JavaScript by supplying the JavaScriptClick parameter of Invoke-SeClick. Make sure to include the Driver when calling it this way.
Find-SeElement -Driver $Driver -Id button | Invoke-SeClick -Driver $Driver -JavaScriptClick

Sending Key Strokes

You can send key strokes to elements like textboxes by using the Send-SeKeys command. First, let’s create a website with a textbox.
New-UDTextbox-Id 'textbox'
Now we can send key strokes to that text box.
$Element = Find-SeElement -Driver $Driver -Id textbox
Send-SeKeys -Element $Element -Keys "Hello, there!"
You send special key strokes to the textbox, you should use the Selenium Keys enum. For example, if we wanted to send an enter key stroke, you could do it like this.
$Element = Find-SeElement -Driver $Driver -Id textbox
Send-SeKeys -Element $Element -Keys ([OpenQA.Selenium.Keys]::Enter)

Waiting for Elements to Appear

When working with dynamic websites, it’s often necessary to wait awhile for elements to appear on the page. By default, Selenium won’t wait and you’ll receive $null from Find-SeElement because the element isn’t there yet. There are a couple ways to work around this.
The first is to use the Wait-SeElementExists cmdlet to wait for the existence of an element in the document. Let’s create a website that has a button and when you click it, it adds a new element to the page.
New-UDButton-Text 'Add an element'-Id 'add'-OnClick {
    Add-UDElement-ParentId 'parent'-Content {
        New-UDElement-Tag 'div'-Id 'child'
    }
}
New-UDElement-Tag 'div'-Id 'parent'
In order to test this functionality, we need to click the button and then test for the existence of the child element. This could take a couple milliseconds to happen so we need to put some wait functionality into the script. The below snippet will wait for 2 seconds. If the element does not appear before the timeout, an error will be thrown. If the element is found before the 2 seconds, it will be returned by Wait-SeElementExists.
Find-SeElement -Driver $Driver -Id add | Invoke-SeClick
Wait-SeElementExists -Driver $Driver -Id 'child' -Timeout 2
You can also enable a default wait period for all calls to Find-SeElement. This means that any call to Find-SeElement will wait up to the default timeout before throwing an error. This can be configured on the driver itself.
$Driver=Start-SeChrome
$Driver.Manage().Timeouts().ImplicitWait = [TimeSpan]::FromSeconds(10)

Integrating with Pester

Example Test Run From Universal Dashboard

As mentioned previously, Universal Dashboard uses Selenium extensively to test the framework. Here are some tips and tricks I’ve learned when using Selenium with Pester. Here is an example test from the Universal Dashboard repository.

Describe "New-UDButton" {
   Context "Text" {
     Set-TestDashboard {
        New-UDButton-Text "Click Me"-Id "button"
     }
     It "has text" {
         (Find-SeElement-Driver $Driver-Id "button").Text | should be "Click Me"
     }
   }
}

Avoid Opening and Closing the Browser

Selenium testing can be slow. This becomes even more of a problem when you open and close the browser. Rather than opening the browser per describe or per context, I open the browser at the beginning of the test session (in some circumstances this doesn’t work but most of the time).

Whenever I need to start a new test, I just create a new Universal Dashboard instance and navigate to it using the Enter-SeUrl cmdlet of the Selenium module.

$Server.DashboardService.SetDashboard($Dashboard)
Enter-SeUrl-Url "http://localhost:10001"-Driver $Driver

Favor ID element location

Finding elements by ID is favorable because it usually only yields a single element. You will always get what you are looking for. If possible, try to identify elements as best as you can. CSS selectors can also be very helpful as they can narrow down the possible elements greatly. Finding lots of elements by tag name can be slow.

Set an implicit timeout for element location

I’ve found that setting an implicit timeout makes tests easier to ready and the reliability of my tests goes up because I don’t have to remember to wait for an element to exist when writing the tests. There is often a great discrepancy between how tests run on your workstation versus how they will run in your CI pipeline. Ensuring that you have a longer implicit timeout really helps.

Take Advantage of Wait-Debugger to Pause Tests

This might be a more general purpose testing tip but take advantage of the Wait-Debugger command when writing these web automation tests. Often, setting up a website takes awhile and it can be tedious to try and inspect elements, stop the test, try to update the test and run the test again just to find out you need to inspect an element again. Instead, throw a Wait-Debugger at the end of your website setup in your test. It gives you the perfect opportunity to play with the Selenium objects directly and get your test write in way less time.

Summary

Web browser automation is actually pretty easy once you get the hang of it. Hopefully, the Selenium PowerShell module makes it even easier. If you’d like to contribute back to the Selenium PowerShell module, feel free to fork it on GitHub. If you want to see how I’ve been using it in Universal Dashboard, take a look at the GitHub repository.