Using PowerShell and WatiN (PowerWatin?)

This is an update to a previous article.

Someone asked (on Twitter) about using WatiN from PowerShell, and pointed to this old post by Scott Hanselman saying he was having the same problems … so I wrote this to help them out:

WatiN requires -STA mode

Note: WatiN requires Single Threaded Apartment mode, so you need to be using PowerShell 2.0 (currently in CTP3) in order for any of this to work, and you need to pass the -STA parameter to PowerShell. Regardless, I thought I’d throw two tips out here:

Don’t use LoadFile, use LoadFrom

I’m not 100% sure when it’s appropriate to use LoadFile in PowerShell, but I can tell you that if your assembly is in a folder with a bunch of other assemblies upon which it depends … you need to use [Reflection.Assembly]::LoadFrom( $path ) instead — because the .net loader will be able to find the dependencies.

Generate some functions to help yourself out

It’s trivial to do code-generation in PowerShell, and WatiN is not friendly to the PowerShell syntax, so you’re going to want to generate a bunch of them. To get you started, here’s a set of Find-* functions to let you find each type of element that WatiN recognizes and automates… by name, id, class, style … well, by any attribute, really:


Function Start-Watin {
## CHANGE this to point to your WatiN.Core.dll
$WatinPath = Convert-Path (Resolve-Path "$(Split-Path $Profile)\Libraries\WatiN.Core.dll")
## Load the assembly
$global:watin = [Reflection.Assembly]::LoadFrom( $WatinPath )
## Create an initial window (I'm creating IE, but WatiN handles Firefox too)
$global:ie = new-object WatiN.Core.IE("http://www.google.com")
## Generate Find-Button, Find-TextField, etc:
$ie | Get-Member -Type Method |
      Where-Object {
         $type = $_.Definition.Split(" ")[0]
         [WatiN.Core.Element].IsAssignableFrom( ([type]$type) )
      } | ForEach-Object {
         New-Item -Path "Function:Find-$($_.Name)" -Value $( iex @"
{
Param(`$Attribute, [regex]`$value)
`$ie.$($_.Name)( ([Watin.Core.Find]::By( `$Attribute, `$value)) )
}
"
@      )
      }
}

With just that little bit of code, I generated all of these functions:

  • Find-Area
  • Find-Button
  • Find-CheckBox
  • Find-Div
  • Find-Element
  • Find-FileUpload
  • Find-Form
  • Find-Image
  • Find-Label
  • Find-Link
  • Find-Para
  • Find-RadioButton
  • Find-SelectList
  • Find-Span
  • Find-Table
  • Find-TableBody
  • Find-TableCell
  • Find-TableRow
  • Find-TextField

And each function takes the name of an attribute, like say: Alt, Class, For, Id, Index, Name, Src, Style, Text, Title, Url, Value … or just “Element” ... and a regular expression to match against the value of that attribute, and finds the control(s) that match. So for instance … since I’ve got a browser open to Google, I could trigger a search for my name by just doing:


Find-TextField Name q | % { $_.TypeText( "Joel Bennett" ) }
Find-Button Name btnG | % { $_.Click() }
 

Drat, the lawyer and that pottery guy still beat me out, maybe I should stop using “Jaykul” everywhere and start using my real name more …

[new] Edit: Another example

Someone asked in the comments about how to parse a table, and I thought I’d post an example, because it’s pretty cool how well PowerShell works for stuff like this. Lets say I wanted to scan down a column in a table, and depending on the values, return a value from another column. For an example, I’ll take a table off Wikipedia which lists multi-protocol IM clients, and try to return the names of the clients which support IRC.


## Using the function defined earlier ...
Start-Watin
## Now navigate to the page
$ie.Goto( "http://en.wikipedia.org/wiki/Multiprotocol_instant_messaging_application" )
## Find that first table (yeah, you just have to know the ID):
$table = Find-Table Id sortable_table_id_0

## we COULD do this to figure out which column is the IRC column:
## It's hard because there's no mapping for TH in WatiN
## Personally, I think they should show up in .TableCells
## -1 for the "th" element that's missing from TableRow[1..n].TableCells
$column=-1;
$table.TableRows[0].Elements | Where-Object {$_.TagName -eq "th"} |
   ForEach-Object{ if($_.Text -match "IRC"){ break } $column++ }

## Now we loop all rows, and examine that column
## If we find a "Yes", print out the table header for that row:
$table.TableRows | Where-Object { $_.TableCells[$column] -match "Yes" } |
   ForEach-Object { $_.Elements } |
   Where-Object   { $_.TagName -eq "th"} |
   ForEach-Object { $_.Text }
 

Clearly WatiN needs to fix the missin <th> TableHeader problem, but other than that, this wasn’t too painful. Of course, it would be a lot easier if we could cast ScriptBlocks as the generic Predicate<T> that the various .Find methods accept, but if I try this, it just freezes up and never returns:

$table.TableRows[0].Element( {$arg[0].TagName -eq "th"} )

Future Work

You could easily take this a lot further: dump those functions into a file for reusing next time (so you don’t have to regenerate them each time), and in fact, it would be relatively simple to make functions for each of the methods on each of the types we found above, so you would have a Click-Button function, and a TypeText-TextField function which could take those same search parameters (attribute and regular expression) and you could even generate appropriate parameters to take, for example, the text to type. In fact, you could generate a whole DSL for yourself … this guy did it using MGrammar … but why not PowerShell. ;)

Similar Posts:

8 thoughts on “Using PowerShell and WatiN (PowerWatin?)”

  1. I don’t do web testing anymore, but if someone wants to send me a hefty bounty, I could probably be convinced to take this further another day — it’s pretty cool, and would coincide fairly well to my WASP project. ;)

  2. Hello Joel,

    having been enthusiastic of the way to generate a bunch of functions from the DLL, I tried to reproduce your results.
    Naively I copied the code into the ISE and called “Start-Watin” with no functions being generated:

    ___________________________________________________________________________________________________________________________________
    PS C:\Dokumente und Einstellungen\Schulte> dir Function:F*

    CommandType Name Definition
    —————- —— —————
    Function F: Set-Location F:

    OK … I did find out, that the functions have been defined inside the “Start-Watin” function itself and that they are out of scope when that function ends.

    That brings up the question: Is there a way to let the generated functions “survive” the end of the generator function? .. something like the $global: prefix?

    ( I’m starting to learn PS … and I hope the question is not too silly :-)

    kind regards, Klaus

    1. Oh my goodness, yes. That was not a silly question at all. It should have said New-Item -Path "Function:Global:Find-… to start with, and I’ve changed it to that now.

      Nice catch. Originally I had written it as a SCRIPT file, which I could dot-source, and I missed that when I converted it into a function. Incidentally, as a Bonus Tip: you can dot-source functions.

  3. Hi Joel. Good stuff, last year we actually worked on an engine that would push all Watin calls to an STA thread running in the background, and then pull the results of the call back to our original thread….it was quite the nightmare, and while it worked, it was buggy.

    Needless to say, I am excited for the possibilities in Powershell CTP3.

    I messed around with your code and tried some of my own, and the one thing I’m noticing is that attempting to find an element that isn’t there takes a very long time to fail …sometimes it doesn’t come back at all. It just sits there. I’m wondering if this is related to powershell itself. Perhaps the way that Powershell handles STA results in some weird threading issue which doesn’t allow the timeout to happen? Have you seen this as well?

    Thanks,
    Adam

    1. I haven’t messed with it very much … I assume that Find*ing by anything other than id or index is going to take awhile, particularly if you’re looking at an element-type that there are a lot of (say, paragraphs, or divs … or whatever).

      But hey, I dono :) — Like I said, I haven’t worked on Web testing in ages, and I haven’t used WatiN for anything but playing with it from PowerShell to write this post as an answer to a Twitter question :-P

  4. I love this article. It’s a great primer! Jaykul, how do you parse a column in table list on a website?

    1. Well, WatiN doesn’t expose the columns, only the rows (because it just mirrors the HTML). So you have to iterate on the TableRows … just for fun, I’ll add an example to the main article, and re-publish it.

Comments are closed.