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).

image 

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?

10 comments:

ErikP said...

Pretty nice language, I like it!

Open source project?

Dan said...

I've worked as .net developer in a QA team a few years ago. We had also testers whos'e jobs was to click all day through the features, every release. At that time i didn't know about DSL but i thought how wonderfull would be to have some sort of language with only few commands, easy to learn for them, to use it to write tests.

This is a great example. I'll play with it.

Dan

Doug said...

Thanks for putting these together. I compile and get that this class cannot be found MAstDeserializer.

Torkel Ödegaard said...

Just remove that file from the solution, it is not need anymore. I most have removed just before I made the zip without saving the solution.

Joakim Sundén said...

Great! However, as you mention, it's probably a bit verbose. Do you really need the "the" word? I don't think "click the link with the text" is more readable to a domain expert then "click link with text". You should check out Dave Thomas' The 'Language' in Domain-Specific Language Doesn't Mean English (or French, or Japanese, or ...) if you haven't already.

Colin Jack said...

Quick question now you've used Oslo a good bit, how easy would it be to define the following in Oslo and then map it down to a domain model:

when fee_amount > 300 and owning_account_holders_balance > 5000:
....

This is utter nonsense but gut feeling wise how practical would such a DSL be with Oslo?

Torkel Ödegaard said...

@Colin,

It all dependends, the sentence you had there "when fee_amount > 300 and owning_account_holders_balance > 5000:"

It would be quite simple to define a grammar for, but what of more complex conditions, with grouping or / and boolean expressions, it can kind of quickly become very complex.

If there is conditional / flow logic I feel a internal DSL makes much more sense as you get so much for free when you do a internal DSL.

Colin Jack said...

@Torkel
Excellent ta, does make me wonder if Oslo is actually going to make much difference within enterprise development. Be interested to see how things take shape.

Alex said...

Great post. I guess giving an automation language an English-like syntax is okay (see the Apple Script). That's the same kind of syntax I would use :)

Pete said...

YES! A browser automation DSL like this would be _stupendously_ useful to our QA engineer, business analyst, and even business-people. Please continue! :-)

Pete