My first try with writing a DSL for WatiN was such fun experience that I decided to have another go. I wanted to try to create something with a little more natural sentence like syntax. Here are some tests showing of the new syntax:
--- Can filter by author ---
goto address "http://demo.codesaga.com/".
click the link with the text "MvcContrib".
select "torkel" from the list with the id #author-fitler.
page should contain the text "Filtering view by author torkel".
--- Can filter by date ---
goto address "http://demo.codesaga.com".
click the link with the text "xUnit".
set focus to the textbox with the id #date-filter.
click the link with the text "2".
page should contain the text "Filtering view by date".
--- Can expand diff in changsest view (via ajax) ---
goto address "http://demo.codesaga.com/history/xUnit?cs=25434".
click the element with the class name @cs-item-diff.
page should contain the element with the class name @code-cell, wait for
it 3 seconds.
The added verbosity might be to much for programmers but the point of making something like this more readable is to make acceptance tests understandable by non-programmers. I am not saying that acceptance tests is something that shouldn't involve developers. But having them accessible to for non-programmers can be very valuable. I am not sure why, I kind of like the verbosity in this case (I usually don't). It would be very easy to make some words optional so one can write "click #edit" as a shortening of "click the link with the id #edit".
The MGrammar for this language:
module CodingInstinct {
import Language;
import Microsoft.Languages;
export BrowserLang;
language BrowserLang {
syntax Main = t:Test* => t;
syntax Test = name:TestName
a:ActionList => Test { Name { name }, ActionList { a } };
syntax ActionList
= item:Action => [item]
| list:ActionList item:Action => [valuesof(list), item];
syntax Action = a:ActionDef "." => a;
syntax ActionDef
= a:GotoAction => a
| a:ClickAction => a
| a:SelectAction => a
| a:TextAssert => a
| a:ElementAssert => a
| a:TypeAction => a
| a:SetFocusAction => a;
syntax GotoAction = "goto" "address"? theUrl:StringLiteral
=> GotoAction { Url { theUrl } };
syntax ClickAction = "click" "the"? ("link" | "element")? ec:ElementConstraint
=> ClickAction { Constraint { ec } };
syntax TypeAction = "type" value:StringLiteral "into" "the" "textbox" ec:ElementConstraint
=> TypeAction { Value { value> }, Constraint { ec } };
syntax SelectAction = "select" value:StringLiteral "from" "the" "list" ec:ElementConstraint
=> SelectAction { Value { value }, Constraint { ec } };
syntax TextAssert = "page should contain" "the" "text" text:StringLiteral
=> TextAssert { Value { text } };
syntax ElementAssert = "page should contain" "the" "element" ec:ElementConstraint
wait:ElementWait?
=> ElementAssert { Constraint { ec }, Wait { wait } };
syntax ElementWait = "wait" "for" "it" sec:Base.Digits ("second" | "seconds")
=> sec;
syntax SetFocusAction = "set" "focus" "to" "the" "textbox" ec:ElementConstraint
=> SetFocusAction { Constraint { ec } };
syntax ElementConstraint
= "with" "the" "text" name:StringLiteral => TextConstraint { Value { name } }
| "with" "the" "id" name:ElementId => IdConstraint { Value { name } }
| "with" "the" "class" "name" name:ElementClass => ClassConstraint { Value { name } };
token TestName = "--- " (Base.Letter|Base.Whitespace)+ " ---";
token ElementId = '#' (Base.Letter|'-'|'_')+;
token ElementClass = '@' (Base.Letter|'-'|'_')+;
interleave Skippable
= Base.Whitespace+
| Language.Grammar.Comment
| Base.NewLine
| ",";
syntax StringLiteral
= val:Language.Grammar.TextLiteral => val;
}
}
Another improvement in this new DSL syntax is the format for specifying an element or class name. In the above grammar these are defined as tokens, where ids begin with # and class names with @ followed by any word. The nice thing with a token is that you can add a Classification attribute to it where you specify what token category it belongs to. Classification names are linked to font and color styles (i.e. syntax highlighting).
To get the DSL to actually execute you need the MGraph node tree that the parser spits. The MGraph is not something that you want work with directly as it is pretty low level. When I did the first version of this WatiN DSL I spent the majority of the time figuring out how to parse and deserialize the MGraph into a custom set of AST classes. In the process I wrote a very basic generic MGraph -> .NET classes deserializer.
Luckily, as Don Box pointed out in the comments to my previous post, SpankyJ has written a much better deserializer that converts the MGraph into Xaml via an MGraphXamlReader. It was very easy to switch to his implementation as he had some useful method extensions on the DynamicParser.
Example:
DynamicParser parser = LoadExampleGrammar();
var xamlMap = new Dictionary<Identifier, Type>
{ { "Person", typeof(Person) } };
var people = parser.Parse<List<object>>(testInput, xamlMap);
But having to define the mapping between MGraph node names and .NET classes manually like this was something I did not like. I wanted something with a more convention based approach. Roger Alsing is also doing some work with MGrammar and he gave me this great piece of code which I modified slightly:
public Dictionary<Identifier, Type> GetTypeMap()
{
return Assembly
.GetExecutingAssembly()
.GetTypes()
.Where(t => t.Namespace.StartsWith("WatinDsl.Ast"))
.Where(t => !t.IsAbstract)
.ToDictionary
(
t => (Identifier)t.Name,
t => t
);
}
Pretty simple code really, it just creates a dictionary of all the non abstract types in the namespace WatinDsl.Ast.
I basically rewrote the AST for this new version, now most actions have a Constraint property that determines what element the action is targeting. Here is an sample:
public class ClickAction : IAction
{
public IElementConstraint Constraint { get; set; }
public void Execute(IBrowser browser)
{
browser.Element(Constraint.Get()).Click();
}
}
public class IdConstraint : IElementConstraint
{
public string Value { get; set; }
public AttributeConstraint Get()
{
return Find.ById(Value.Substring(1));
}
}
For the full code: WatinDsl_2.zip
This is still just an experimental spike for learning MGrammar, but it is also an interesting scenario for exploring the potential in a browser automation language. Is a browser automation language, like the one I have created, something that you would find useful? What would your syntax look like?
No comments:
Post a Comment