// sample UI element mapping definition. This is for http://alistapart.com/,
// a particularly well structured site on web design principles.



// in general, the map should capture structural aspects of the system, instead
// of "content". In other words, interactive elements / assertible elements
// that can be counted on to always exist should be defined here. Content -
// for example text or a link that appears in a blog entry - is always liable
// to change, and will not be fun to represent in this way. You probably don't
// want to be testing specific content anyway.

// create the UI mapping object. THIS IS THE MOST IMPORTANT PART - DON'T FORGET
// TO DO THIS! In order for it to come into play, a user extension must
// construct the map in this way.
var myMap = new UIMap();




// any values which may appear multiple times can be defined as variables here.
// For example, here we're enumerating a list of top level topics that will be
// used as default argument values for several UI elements. Check out how
// this variable is referenced further down.
var topics = [
    'Code',
    'Content',
    'Culture',
    'Design',
    'Process',
    'User Science'
];

// map subtopics to their parent topics
var subtopics = {
    'Browsers':         'Code'
    , 'CSS':            'Code'
    , 'Flash':          'Code'
    , 'HTML and XHTML': 'Code'
    , 'Scripting':      'Code'
    , 'Server Side':    'Code'
    , 'XML':            'Code'
    , 'Brand Arts': 'Content'
    , 'Community':  'Content'
    , 'Writing':    'Content'
    , 'Industry':           'Culture'
    , 'Politics and Money': 'Culture'
    , 'State of the Web':   'Culture'
    , 'Graphic Design':        'Design'
    , 'User Interface Design': 'Design'
    , 'Typography':            'Design'
    , 'Layout':                'Design'
    , 'Business':                        'Process'
    , 'Creativity':                      'Process'
    , 'Project Management and Workflow': 'Process'
    , 'Accessibility':            'User Science'
    , 'Information Architecture': 'User Science'
    , 'Usability':                'User Science'
};



// define UI elements common for all pages. This regular expression does the
// trick. '^' is automatically prepended, and '$' is automatically postpended.
// Please note that because the regular expression is being represented as a
// string, all backslashes must be escaped with an additional backslash. Also
// note that the URL being matched will always have any trailing forward slash
// stripped.
myMap.addPageset({
    name: 'allPages'
    , description: 'all alistapart.com pages'
    , pathRegexp: '.*'
});
myMap.addElement('allPages', {
    name: 'masthead'
    // the description should be short and to the point, usually no longer than
    // a single line
    , description: 'top level image link to site homepage'
    // make sure the function returns the XPath ... it's easy to leave out the
    // "return" statement by accident!
    , locator: "xpath=//*[@id='masthead']/a/img"
    , testcase1: {
        xhtml: '<h1 id="masthead"><a><img expected-result="1" /></a></h1>'
    }
});
myMap.addElement('allPages', {
    // be VERY CAREFUL to include commas in the correct place. Missing commas
    // and extra commas can cause lots of headaches when debugging map
    // definition files!!!
    name: 'current_issue'
    , description: 'top level link to issue currently being browsed'
    , locator: "//div[@id='ish']/a"
    , testcase1: {
        xhtml: '<div id="ish"><a expected-result="1"></a></div>'
    }
});
myMap.addElement('allPages', {
    name: 'section'
    , description: 'top level link to articles section'
    , args: [
        {
            name: 'section'
            , description: 'the name of the section'
            , defaultValues: [
                'articles'
                , 'topics'
                , 'about'
                , 'contact'
                , 'contribute'
                , 'feed'
            ]
        }
    ]
    // getXPath has been deprecated by getLocator, but verify backward
    // compatability here
    , getXPath: function(args) {
        return "//li[@id=" + args.section.quoteForXPath() + "]/a";
    }
    , testcase1: {
        args: { section: 'feed' }
        , xhtml: '<ul><li id="feed"><a expected-result="1" /></li></ul>'
    }
});
myMap.addElement('allPages', {
    name: 'search_box'
    , description: 'site search input field'
    // xpath has been deprecated by locator, but verify backward compatability
    , xpath: "//input[@id='search']"
    , testcase1: {
        xhtml: '<input id="search" expected-result="1" />'
    }
});
myMap.addElement('allPages', {
    name: 'search_discussions'
    , description: 'site search include discussions checkbox'
    , locator: 'incdisc'
    , testcase1: {
        xhtml: '<input id="incdisc" expected-result="1" />'
    }
});
myMap.addElement('allPages', {
    name: 'search_submit'
    , description: 'site search submission button'
    , locator: 'submit'
    , testcase1: {
        xhtml: '<input id="submit" expected-result="1" />'
    }
});
myMap.addElement('allPages', {
    name: 'topics'
    , description: 'sidebar links to topic categories'
    , args: [
        {
            name: 'topic'
            , description: 'the name of the topic'
            , defaultValues: topics
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='topiclist']/ul/li" +
            "/a[text()=" + args.topic.quoteForXPath() + "]";
    }
    , testcase1: {
        args: { topic: 'foo' }
        , xhtml: '<div id="topiclist"><ul><li>'
            + '<a expected-result="1">foo</a>'
            + '</li></ul></div>'
    }
});
myMap.addElement('allPages', {
    name: 'copyright'
    , description: 'footer link to copyright page'
    , getLocator: function(args) { return "//span[@class='copyright']/a"; }
    , testcase1: {
        xhtml: '<span class="copyright"><a expected-result="1" /></span>'
    }
});



// define UI elements for the homepage, i.e. "http://alistapart.com/", and
// magazine issue pages, i.e. "http://alistapart.com/issues/234".
myMap.addPageset({
    name: 'issuePages'
    , description: 'pages including magazine issues'
    , pathRegexp: '(issues/.+)?'
});
myMap.addElement('issuePages', {
    name: 'article'
    , description: 'front or issue page link to article'
    , args: [
        {
            name: 'index'
            , description: 'the index of the article'
            // an array of default values for the argument. A default
            // value is one that is passed to the getXPath() method of
            // the container UIElement object when trying to build an
            // element locator.
            //
            // range() may be used to count easily. Remember though that
            // the ending value does not include the right extreme; for
            // example range(1, 5) counts from 1 to 4 only.
            , defaultValues: range(1, 5)
        }
    ]
    , getLocator: function(args) {
        return "//div[@class='item'][" + args.index + "]/h4/a";
    }
});
myMap.addElement('issuePages', {
    name: 'author'
    , description: 'article author link'
    , args: [
        {
            name: 'index'
            , description: 'the index of the author, by article'
            , defaultValues: range(1, 5)
        }
    ]
    , getLocator: function(args) {
        return "//div[@class='item'][" + args.index + "]/h5/a";
    }
});
myMap.addElement('issuePages', {
    name: 'store'
    , description: 'alistapart.com store link'
    , locator: "//ul[@id='banners']/li/a[@title='ALA Store']/img"
});
myMap.addElement('issuePages', {
    name: 'special_article'
    , description: "editor's choice article link"
    , locator: "//div[@id='choice']/h4/a"
});
myMap.addElement('issuePages', {
    name: 'special_author'
    , description: "author link of editor's choice article"
    , locator: "//div[@id='choice']/h5/a"
});



// define UI elements for the articles page, i.e.
// "http://alistapart.com/articles"
myMap.addPageset({
    name: 'articleListPages'
    , description: 'page with article listings'
    , paths: [ 'articles' ]
});
myMap.addElement('articleListPages', {
    name: 'issue'
    , description: 'link to issue'
    , args: [
        {
            name: 'index'
            , description: 'the index of the issue on the page'
            , defaultValues: range(1, 10)
        }
    ]
    , getLocator: function(args) {
        return "//h2[@class='ishinfo'][" + args.index + ']/a';
    }
    , genericLocator: "//h2[@class='ishinfo']/a"
});
myMap.addElement('articleListPages', {
    name: 'article'
    , description: 'link to article, by issue and article number'
    ,  args: [
        {
            name: 'issue_index'
            , description: "the index of the article's issue on the page; "
                + 'typically five per page'
            , defaultValues: range(1, 6)
        }
        , {
            name: 'article_index'
            , description: 'the index of the article within the issue; '
                + 'typically two per issue'
            , defaultValues: range(1, 5)
        }
    ]
    , getLocator: function(args) {
        var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']'
            + "/following-sibling::div[@class='item']"
            + '[' + (args.article_index || 1) + "]/h3[@class='title']/a";
        return xpath;
    }
    , genericLocator: "//h2[@class='ishinfo']"
        + "/following-sibling::div[@class='item']/h3[@class='title']/a"
});
myMap.addElement('articleListPages', {
    name: 'author'
    , description: 'article author link, by issue and article'
    , args: [
        {
            name: 'issue_index'
            , description: "the index of the article's issue on the page; \
typically five per page"
            , defaultValues: range(1, 6)
        }
        , {
            name: 'article_index'
            , description: "the index of the article within the issue; \
typically two articles per issue"
            , defaultValues: range(1, 3)
        }
    ]
    // this XPath uses the "following-sibling" axis. The div elements for
    // the articles in an issue are not children, but siblings of the h2
    // element identifying the article.
    , getLocator: function(args) {
        var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']'
            + "/following-sibling::div[@class='item']"
            + '[' + (args.article_index || 1) + "]/h4[@class='byline']/a";
        return xpath;
    }
    , genericLocator: "//h2[@class='ishinfo']"
        + "/following-sibling::div[@class='item']/h4[@class='byline']/a"
});
myMap.addElement('articleListPages', {
    name: 'next_page'
    , description: 'link to next page of articles (older)'
    , locator: "//a[contains(text(),'Next page')]"
});
myMap.addElement('articleListPages', {
    name: 'previous_page'
    , description: 'link to previous page of articles (newer)'
    , locator: "//a[contains(text(),'Previous page')]"
});



// define UI elements for specific article pages, i.e.
// "http://alistapart.com/articles/culturalprobe"
myMap.addPageset({
    name: 'articlePages'
    , description: 'pages for actual articles'
    , pathRegexp: 'articles/.+'
});
myMap.addElement('articlePages', {
    name: 'title'
    , description: 'article title loop-link'
    , locator: "//div[@id='content']/h1[@class='title']/a"
});
myMap.addElement('articlePages', {    
    name: 'author'
    , description: 'article author link'
    , locator: "//div[@id='content']/h3[@class='byline']/a"
});
myMap.addElement('articlePages', {    
    name: 'article_topics'
    , description: 'links to topics under which article is published, before \
article content'
    , args: [
        {
            name: 'topic'
            , description: 'the name of the topic'
            , defaultValues: keys(subtopics)
        }
    ]
    , getLocator: function(args) {
        return "//ul[@id='metastuff']/li/a"
            + "[@title=" + args.topic.quoteForXPath() + "]";
    }
});
myMap.addElement('articlePages', {    
    name: 'discuss'
    , description: 'link to article discussion area, before article content'
    , locator: "//ul[@id='metastuff']/li[@class='discuss']/p/a"
});
myMap.addElement('articlePages', {    
    name: 'related_topics'
    , description: 'links to topics under which article is published, after \
article content'
    , args: [
        {
            name: 'topic'
            , description: 'the name of the topic'
            , defaultValues: keys(subtopics)
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='learnmore']/p/a"
            + "[@title=" + args.topic.quoteForXPath() + "]";
    }
});
myMap.addElement('articlePages', {    
    name: 'join_discussion'
    , description: 'link to article discussion area, after article content'
    , locator: "//div[@class='discuss']/p/a"
});



myMap.addPageset({
    name: 'topicListingPages'
    , description: 'top level listing of topics'
    , paths: [ 'topics' ]
});
myMap.addElement('topicListingPages', {
    name: 'topic'
    , description: 'link to topic category'
    , args: [
        {
            name: 'topic'
            , description: 'the name of the topic'
            , defaultValues: topics
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/h2/a"
            + "[text()=" + args.topic.quoteForXPath() + "]";
    }
});
myMap.addElement('topicListingPages', {
    name: 'subtopic'
    , description: 'link to subtopic category'
    , args: [
        {
            name: 'subtopic'
            , description: 'the name of the subtopic'
            , defaultValues: keys(subtopics)
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']" +
            "/descendant::a[text()=" + args.subtopic.quoteForXPath() + "]";
    }
});

// the following few subtopic page UI elements are very similar. Define UI
// elements for the code page, which is a subpage under topics, i.e.
// "http://alistapart.com/topics/code/"
myMap.addPageset({
    name: 'subtopicListingPages' 
    , description: 'pages listing subtopics'
    , pathPrefix: 'topics/'
    , paths: [
        'code'
        , 'content'
        , 'culture'
        , 'design'
        , 'process'
        , 'userscience'
    ]
});
myMap.addElement('subtopicListingPages', {
    name: 'subtopic'
    , description: 'link to a subtopic category'
    , args: [
        {
            name: 'subtopic'
            , description: 'the name of the subtopic'
            , defaultValues: keys(subtopics)
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/h2" +
            "/a[text()=" + args.subtopic.quoteForXPath() + "]";
    }
});



// subtopic articles page
myMap.addPageset({
    name: 'subtopicArticleListingPages'
    , description: 'pages listing the articles for a given subtopic'
    , pathRegexp: 'topics/[^/]+/.+'
});
myMap.addElement('subtopicArticleListingPages', {
    name: 'article'
    , description: 'link to a subtopic article'
    , args: [
        {
            name: 'index'
            , description: 'the index of the article'
            , defaultValues: range(1, 51) // the range seems unlimited ...
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/div[@class='item']"
            + "[" + args.index + "]/h3/a";
    }
    , testcase1: {
        args: { index: 2 }
        , xhtml: '<div id="content"><div class="item" /><div class="item">'
            + '<h3><a expected-result="1" /></h3></div></div>'
    }
});
myMap.addElement('subtopicArticleListingPages', {
    name: 'author'
    , description: "link to a subtopic article author's page"
    , args: [
        {
            name: 'article_index'
            , description: 'the index of the authored article'
            , defaultValues: range(1, 51)
        }
        , {
            name: 'author_index'
            , description: 'the index of the author when there are multiple'
            , defaultValues: range(1, 4)
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/div[@class='item'][" +
            args.article_index + "]/h4/a[" +
            (args.author_index ? args.author_index : '1') + ']';
    }
});
myMap.addElement('subtopicArticleListingPages', {
    name: 'issue'
    , description: 'link to issue a subtopic article appears in'
    , args: [
        {
            name: 'index'
            , description: 'the index of the subtopic article'
            , defaultValues: range(1, 51)
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/div[@class='item']"
            + "[" + args.index + "]/h5/a";
    }
});



myMap.addPageset({
    name: 'aboutPages'
    , description: 'the website about page'
    , paths: [ 'about' ]
});
myMap.addElement('aboutPages', {
    name: 'crew'
    , description: 'link to site crew member bio or personal website'
    , args: [
        {
            name: 'role'
            , description: 'the role of the crew member'
            , defaultValues: [
                'ALA Crew'
                , 'Support'
                , 'Emeritus'
            ]
        }
        , {
            name: 'role_index'
            , description: 'the index of the member within the role'
            , defaultValues: range(1, 20)
        }
        , {
            name: 'member_index'
            , description: 'the index of the member within the role title'
            , defaultValues: range(1, 5)
        }
    ]
    , getLocator: function(args) {
        // the first role is kind of funky, and requires a conditional to
        // build the XPath correctly. Its header looks like this:
        //
        // <h3>
        // <span class="caps">ALA 4</span>.0 <span class="caps">CREW</span>
        // </h3>
        //
        // This kind of complexity is a little daunting, but you can see
        // how the format can handle it relatively easily and concisely.
        if (args.role == 'ALA Crew') {
            var selector = "descendant::text()='CREW'";
        }
        else {
            var selector = "text()=" + args.role.quoteForXPath();
        }
        var xpath =
            "//div[@id='secondary']/h3[" + selector + ']' +
            "/following-sibling::dl/dt[" + (args.role_index || 1) + ']' +
            '/a[' + (args.member_index || '1') + ']';
        return xpath;
    }
});



myMap.addPageset({
    name: 'searchResultsPages'
    , description: 'pages listing search results'
    , paths: [ 'search' ]
});
myMap.addElement('searchResultsPages', {
    name: 'result_link'
    , description: 'search result link'
    , args: [
        {
            name: 'index'
            , description: 'the index of the search result'
            , defaultValues: range(1, 11)
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/ul[" + args.index + ']/li/h3/a';
    }
});
myMap.addElement('searchResultsPages', {
    name: 'more_results_link'
    , description: 'next or previous results link at top or bottom of page'
    , args: [
        {
            name: 'direction'
            , description: 'next or previous results page'
            // demonstrate a method which acquires default values from the
            // document object. Such default values may contain EITHER commas
            // OR equals signs, but NOT BOTH.
            , getDefaultValues: function(inDocument) {
                var defaultValues = [];
                var divs = inDocument.getElementsByTagName('div');
                for (var i = 0; i < divs.length; ++i) {
                    if (divs[i].className == 'pages') {
                        break;
                    }
                }
                var links = divs[i].getElementsByTagName('a');
                for (i = 0; i < links.length; ++i) {
                    defaultValues.push(links[i].innerHTML
                        .replace(/^\xab\s*/, "")
                        .replace(/\s*\bb$/, "")
                        .replace(/\s*\d+$/, ""));
                }
                return defaultValues;
            }
        }
        , {
            name: 'position'
            , description: 'position of the link'
            , defaultValues: ['top', 'bottom']
        }
    ]
    , getLocator: function(args) {
        return "//div[@id='content']/div[@class='pages']["
            + (args.position == 'top' ? '1' : '2') + ']'
            + "/a[contains(text(), "
            + (args.direction ? args.direction.quoteForXPath() : undefined)
            + ")]";
    }
});
    
    
    
myMap.addPageset({
    name: 'commentsPages'
    , description: 'pages listing comments made to an article'
    , pathRegexp: 'comments/.+'
});
myMap.addElement('commentsPages', {
    name: 'article_link'
    , description: 'link back to the original article'
    , locator: "//div[@id='content']/h1[@class='title']/a"
});
myMap.addElement('commentsPages', {
    name: 'comment_link'
    , description: 'same-page link to comment'
    , args: [
        {
            name: 'index'
            , description: 'the index of the comment'
            , defaultValues: range(1, 11)
        }
    ]
    , getLocator: function(args) {
        return "//div[@class='content']/div[contains(@class, 'comment')]" +
            '[' + args.index + ']/h4/a[2]';
    }
});
myMap.addElement('commentsPages', {
    name: 'paging_link'
    , description: 'links to more pages of comments'
    , args: [
        {
            name: 'dest'
            , description: 'the destination page'
            , defaultValues: ['next', 'prev'].concat(range(1, 16))
        }
        , {
            name: 'position'
            , description: 'position of the link'
            , defaultValues: ['top', 'bottom']
        }
    ]
    , getLocator: function(args) {
        var dest = args.dest;
        var xpath = "//div[@id='content']/div[@class='pages']" +
            '[' + (args.position == 'top' ? '1' : '2') + ']/p';
        if (dest == 'next' || dest == 'prev') {
            xpath += "/a[contains(text(), " + dest.quoteForXPath() + ")]";
        }
        else {
            xpath += "/a[text()=" + dest.quoteForXPath() + "]";
        }
        return xpath;
    }
});



myMap.addPageset({
    name: 'authorPages'
    , description: 'personal pages for each author'
    , pathRegexp: 'authors/[a-z]/.+'
});
myMap.addElement('authorPages', {
    name: 'article'
    , description: "link to article written by this author.\n"
        + 'This description has a line break.'
    , args: [
        {
            name: 'index'
            , description: 'index of the article on the page'
            , defaultValues: range(1, 11)
        }
    ]
    , getLocator: function(args) {
        var index = args.index;
        // try out the CSS locator!
        //return "//h4[@class='title'][" + index + "]/a";
        return 'css=h4.title:nth-child(' + index + ') > a';
    }
    , testcase1: {
        args: { index: '2' }
        , xhtml: '<h4 class="title" /><h4 class="title">'
            + '<a expected-result="1" /></h4>'
    }
});



// test the offset locator. Something like the following can be recorded:
// ui=qaPages::content()//a[contains(text(),'May I quote from your articles?')]
myMap.addPageset({
    name: 'qaPages'
    , description: 'question and answer pages'
    , pathRegexp: 'qa'
});
myMap.addElement('qaPages', {
    name: 'content'
    , description: 'the content pane containing the q&a entries'
    , locator: "//div[@id='content' and "
        + "child::h1[text()='Questions and Answers']]"
    , getOffsetLocator: UIElement.defaultOffsetLocatorStrategy
});
myMap.addElement('qaPages', {
    name: 'last_updated'
    , description: 'displays the last update date'
    // demonstrate calling getLocator() for another UI element within a
    // getLocator(). The former must have already been added to the map. And
    // obviously, you can't randomly combine different locator types!
    , locator: myMap.getUIElement('qaPages', 'content').getLocator() + '/p/em'
});



//******************************************************************************

var myRollupManager = new RollupManager();

// though the description element is required, its content is free form. You
// might want to create a documentation policy as given below, where the pre-
// and post-conditions of the rollup are spelled out.
//
// To take advantage of a "heredoc" like syntax for longer descriptions,
// add a backslash to the end of the current line and continue the string on
// the next line.
myRollupManager.addRollupRule({
    name: 'navigate_to_subtopic_article_listing'
    , description: 'drill down to the listing of articles for a given subtopic \
from the section menu, then the topic itself.'
    , pre: 'current page contains the section menu (most pages should)'
    , post: 'navigated to the page listing all articles for a given subtopic'
    , args: [
        {
            name: 'subtopic'
            , description: 'the subtopic whose article listing to navigate to'
            , exampleValues: keys(subtopics)
        }
    ]
    , commandMatchers: [
        {
            command: 'clickAndWait'
            , target: 'ui=allPages::section\\(section=topics\\)'
            // must escape parentheses in the the above target, since the
            // string is being used as a regular expression. Again, backslashes
            // in strings must be escaped too.
        }
        , {
            command: 'clickAndWait'
            , target: 'ui=topicListingPages::topic\\(.+'
        }
        , {
            command: 'clickAndWait'
            , target: 'ui=subtopicListingPages::subtopic\\(.+'
            , updateArgs: function(command, args) {
                // don't bother stripping the "ui=" prefix from the locator
                // here; we're just using UISpecifier to parse the args out
                var uiSpecifier = new UISpecifier(command.target);
                args.subtopic = uiSpecifier.args.subtopic;
                return args;
            }
        }
    ]
    , getExpandedCommands: function(args) {
        var commands = [];
        var topic = subtopics[args.subtopic];
        var subtopic = args.subtopic;
        commands.push({
            command: 'clickAndWait'
            , target: 'ui=allPages::section(section=topics)'
        });
        commands.push({
            command: 'clickAndWait'
            , target: 'ui=topicListingPages::topic(topic=' + topic + ')'
        });
        commands.push({
            command: 'clickAndWait'
            , target: 'ui=subtopicListingPages::subtopic(subtopic=' + subtopic
                + ')'
        });
        commands.push({
            command: 'verifyLocation'
            , target: 'regexp:.+/topics/.+/.+'
        });
        return commands;
    }
});



myRollupManager.addRollupRule({
    name: 'replace_click_with_clickAndWait'
    , description: 'replaces commands where a click was detected with \
clickAndWait instead'
    , alternateCommand: 'clickAndWait'
    , commandMatchers: [
        {
            command: 'click'
            , target: 'ui=subtopicArticleListingPages::article\\(.+'
        }
    ]
    , expandedCommands: []
});



myRollupManager.addRollupRule({
    name: 'navigate_to_subtopic_article'
    , description: 'navigate to an article listed under a subtopic.'
    , pre: 'current page contains the section menu (most pages should)'
    , post: 'navigated to an article page'
    , args: [
        {
            name: 'subtopic'
            , description: 'the subtopic whose article listing to navigate to'
            , exampleValues: keys(subtopics)
        }
        , {
            name: 'index'
            , description: 'the index of the article in the listing'
            , exampleValues: range(1, 11)
        }
    ]
    , commandMatchers: [
        {
            command: 'rollup'
            , target: 'navigate_to_subtopic_article_listing'
            , value: 'subtopic\\s*=.+'
            , updateArgs: function(command, args) {
                var args1 = parse_kwargs(command.value);
                args.subtopic = args1.subtopic;
                return args;
            }
        }
        , {
            command: 'clickAndWait'
            , target: 'ui=subtopicArticleListingPages::article\\(.+'
            , updateArgs: function(command, args) {
                var uiSpecifier = new UISpecifier(command.target);
                args.index = uiSpecifier.args.index;
                return args;
            }
        }
    ]
    /*
    // this is pretty much equivalent to the commandMatchers immediately above.
    // Seems more verbose and less expressive, doesn't it? But sometimes you
    // might prefer the flexibility of a function.
    , getRollup: function(commands) {
        if (commands.length >= 2) {
            command1 = commands[0];
            command2 = commands[1];
            var args1 = parse_kwargs(command1.value);
            try {
                var uiSpecifier = new UISpecifier(command2.target
                    .replace(/^ui=/, ''));
            }
            catch (e) {
                return false;
            }
            if (command1.command == 'rollup' &&
                command1.target == 'navigate_to_subtopic_article_listing' &&
                args1.subtopic &&
                command2.command == 'clickAndWait' &&
                uiSpecifier.pagesetName == 'subtopicArticleListingPages' &&
                uiSpecifier.elementName == 'article') {
                var args = {
                    subtopic: args1.subtopic
                    , index: uiSpecifier.args.index
                };
                return {
                    command: 'rollup'
                    , target: this.name
                    , value: to_kwargs(args)
                    , replacementIndexes: [ 0, 1 ]
                };
            }
        }
        return false;
    }
    */
    , getExpandedCommands: function(args) {
        var commands = [];
        commands.push({
            command: 'rollup'
            , target: 'navigate_to_subtopic_article_listing'
            , value: to_kwargs({ subtopic: args.subtopic })
        });
        var uiSpecifier = new UISpecifier(
            'subtopicArticleListingPages'
            , 'article'
            , { index: args.index });
        commands.push({
            command: 'clickAndWait'
            , target: 'ui=' + uiSpecifier.toString()
        });
        commands.push({
            command: 'verifyLocation'
            , target: 'regexp:.+/articles/.+'
        });
        return commands;
    }
});