28 Jun 2009

Lately, I had to do lots of repetitive tasks at work in order to fix defects in an application. As I grew tired of filling numerous forms with the mandatory data needed for them to validate, I thought that I could maybe use PowerShell to automate the form-filling process.

I very quickly stumbled upon an MSDN article titled Web UI Automation with Windows PowerShell. It was definitely the way to go, but I was not really satisfied with everything I found over there.

Getting a Brower object

This part is similar to the one used in the MSDN article.

$global:ie = New-Object -com "InternetExplorer.Application"
$global:ie.Navigate("about:blank")
$global:ie.visible = $true

Please note that I will use $global:ie and $global:doc all the time, as global variables. I could have used $script: scope variables, but global are better for this task. Here, we are building a bunch of reusable function; declaring these variables as $global: ensures that the variables will be available in scripts that include this utility script. See here for more information on PowerShell variable scopes.

So this, sets $global:ie to a COM Internet Explorer object, navigates to "about:blank" and makes the window visible. Now we need some helper functions to navigate, fill forms and click elements easily.

Helper Functions to Navigate, Click, etc...

First of all, we need a method that yields the script while the current page is loading. So, what it should do is simple: wait for the current page to be loaded then set the $global:doc variable to the loaded document.

Function WaitForPage([int] $delayTime = 100)
{
  $loaded = $false
  
  while ($loaded -eq $false) {
    [System.Threading.Thread]::Sleep($delayTime) 
    
    #If the browser is not busy, the page is loaded
    if (-not $global:ie.Busy)
    {
      $loaded = $true
    }
  }
  
  $global:doc = $global:ie.Document
}

I used an optional parameter here for the delay between checks of the browser's status. It's pretty straight forward, while the browser ($global:ie) is busy, wait. Once it's not busy anymore, assign the document to the $global:doc variable. Let's now define a function to navigate to a given url.

Function NavigateTo([string] $url, [int] $delayTime = 100)
{
  Write-Verbose "Navigating to $url";
  
  $global:ie.Navigate($url)
  
  WaitForPage $delayTime
}

Note that I used Write-Verbose commands around in functions to output some useful information in my script, making it easier to spot mistakes while running it.

Now, as this was made to fill web forms, let's define a function that will fill an input text field with a given value.

Function SetElementValueByName($name, $value, [int] $position = 0) {
  if ($global:doc -eq $null) {
    Write-Error "Document is null";
    break
  }
  $elements = @($global:doc.getElementsByName($name))
  if ($elements.Count -ne 0) {
    $elements[$position].Value = $value
  }
  else {
    Write-Warning "Couldn't find any element with name ""$name""";
  }
}

This is heavily used in my scripts. An HTML form always has lots of input elements that have unique names that need to be filled. So, if you need to fill the username input text with Philippe value, just call this function:

SetElementValueByName "username" "Philippe"

Note that there is also a option parameter that is used as the element's position in the array returned by $global:doc.getElementByName. By default, the used position is 0 because most of the forms will only have one element with a given name. However, it can be that in (badly designed?) forms, two elements have the same name. In this case, you can specify which one you want to fill.

Now, I won't explain all the functions, but here are the ones I wrote:

Function ClickElementByTagName($tagName, [int] $position = 0)
{
  if ($global:doc -eq $null) {
    Write-Error "Document is null"
    break
  }
  $elements = @($global:doc.getElementsByTagName($tagName))
  if ($elements.Count -ne 0) {
    $elements[$position].Click()
    WaitForPage
  }
  else {
    Write-Error "Couldn't find element ""$tagName"" at position ""$position""";
    break
  }
}

Function ClickElementById($id)
{
  $element = $global:doc.getElementById($id)
  if ($element -ne $null) {
    $element.Click()
    WaitForPage
  }
  else {
    Write-Error "Couldn't find element with id ""$id"""
    break
  }
}

Function ClickElementByName($name, [int] $position = 0)
{
  if ($global:doc -eq $null) {
    Write-Error "Document is null"
    break
  }
  $elements = @($global:doc.getElementsByName($name))
  if ($elements.Count -ne 0) {
    $elements[$position].Click()
    WaitForPage
  }
  else {
    Write-Error "Couldn't find element with name ""$name"" at position ""$position"""
    break
  }
}

These are used to click on objects of the DOM in order to submit the form. These functions are not error proof, but as far as I used this stuff, I didn’t have issues.

A Little Example

Let’s write something very simple to test these functions. I will do a advanced search on google using these functions:

NavigateTo "http://www.google.com/advanced_search"
SetElementValueByName "as_oq" "Unisys Fenix PLDA"
SetElementValueByName "num" "30"
SetElementValueByName "lr" "lang_en"
ClickElementByName "btnG"

This gives you a little example of the kind of things you can do. It’s rather simple, but very powerful if you have repetitive tasks to on some web sites.

Download the full script here.



blog comments powered by Disqus