PageRenderTime 89ms CodeModel.GetById 41ms app.highlight 39ms RepoModel.GetById 2ms app.codeStats 0ms

/testing/selenium-core/scripts/ui-map-sample.js

http://datanucleus-appengine.googlecode.com/
JavaScript | 979 lines | 771 code | 86 blank | 122 comment | 25 complexity | 35566840e217d4aa68436828a373e3a2 MD5 | raw file
  1// sample UI element mapping definition. This is for http://alistapart.com/,
  2// a particularly well structured site on web design principles.
  3
  4
  5
  6// in general, the map should capture structural aspects of the system, instead
  7// of "content". In other words, interactive elements / assertible elements
  8// that can be counted on to always exist should be defined here. Content -
  9// for example text or a link that appears in a blog entry - is always liable
 10// to change, and will not be fun to represent in this way. You probably don't
 11// want to be testing specific content anyway.
 12
 13// create the UI mapping object. THIS IS THE MOST IMPORTANT PART - DON'T FORGET
 14// TO DO THIS! In order for it to come into play, a user extension must
 15// construct the map in this way.
 16var myMap = new UIMap();
 17
 18
 19
 20
 21// any values which may appear multiple times can be defined as variables here.
 22// For example, here we're enumerating a list of top level topics that will be
 23// used as default argument values for several UI elements. Check out how
 24// this variable is referenced further down.
 25var topics = [
 26    'Code',
 27    'Content',
 28    'Culture',
 29    'Design',
 30    'Process',
 31    'User Science'
 32];
 33
 34// map subtopics to their parent topics
 35var subtopics = {
 36    'Browsers':         'Code'
 37    , 'CSS':            'Code'
 38    , 'Flash':          'Code'
 39    , 'HTML and XHTML': 'Code'
 40    , 'Scripting':      'Code'
 41    , 'Server Side':    'Code'
 42    , 'XML':            'Code'
 43    , 'Brand Arts': 'Content'
 44    , 'Community':  'Content'
 45    , 'Writing':    'Content'
 46    , 'Industry':           'Culture'
 47    , 'Politics and Money': 'Culture'
 48    , 'State of the Web':   'Culture'
 49    , 'Graphic Design':        'Design'
 50    , 'User Interface Design': 'Design'
 51    , 'Typography':            'Design'
 52    , 'Layout':                'Design'
 53    , 'Business':                        'Process'
 54    , 'Creativity':                      'Process'
 55    , 'Project Management and Workflow': 'Process'
 56    , 'Accessibility':            'User Science'
 57    , 'Information Architecture': 'User Science'
 58    , 'Usability':                'User Science'
 59};
 60
 61
 62
 63// define UI elements common for all pages. This regular expression does the
 64// trick. '^' is automatically prepended, and '$' is automatically postpended.
 65// Please note that because the regular expression is being represented as a
 66// string, all backslashes must be escaped with an additional backslash. Also
 67// note that the URL being matched will always have any trailing forward slash
 68// stripped.
 69myMap.addPageset({
 70    name: 'allPages'
 71    , description: 'all alistapart.com pages'
 72    , pathRegexp: '.*'
 73});
 74myMap.addElement('allPages', {
 75    name: 'masthead'
 76    // the description should be short and to the point, usually no longer than
 77    // a single line
 78    , description: 'top level image link to site homepage'
 79    // make sure the function returns the XPath ... it's easy to leave out the
 80    // "return" statement by accident!
 81    , locator: "xpath=//*[@id='masthead']/a/img"
 82    , testcase1: {
 83        xhtml: '<h1 id="masthead"><a><img expected-result="1" /></a></h1>'
 84    }
 85});
 86myMap.addElement('allPages', {
 87    // be VERY CAREFUL to include commas in the correct place. Missing commas
 88    // and extra commas can cause lots of headaches when debugging map
 89    // definition files!!!
 90    name: 'current_issue'
 91    , description: 'top level link to issue currently being browsed'
 92    , locator: "//div[@id='ish']/a"
 93    , testcase1: {
 94        xhtml: '<div id="ish"><a expected-result="1"></a></div>'
 95    }
 96});
 97myMap.addElement('allPages', {
 98    name: 'section'
 99    , description: 'top level link to articles section'
100    , args: [
101        {
102            name: 'section'
103            , description: 'the name of the section'
104            , defaultValues: [
105                'articles'
106                , 'topics'
107                , 'about'
108                , 'contact'
109                , 'contribute'
110                , 'feed'
111            ]
112        }
113    ]
114    // getXPath has been deprecated by getLocator, but verify backward
115    // compatability here
116    , getXPath: function(args) {
117        return "//li[@id=" + args.section.quoteForXPath() + "]/a";
118    }
119    , testcase1: {
120        args: { section: 'feed' }
121        , xhtml: '<ul><li id="feed"><a expected-result="1" /></li></ul>'
122    }
123});
124myMap.addElement('allPages', {
125    name: 'search_box'
126    , description: 'site search input field'
127    // xpath has been deprecated by locator, but verify backward compatability
128    , xpath: "//input[@id='search']"
129    , testcase1: {
130        xhtml: '<input id="search" expected-result="1" />'
131    }
132});
133myMap.addElement('allPages', {
134    name: 'search_discussions'
135    , description: 'site search include discussions checkbox'
136    , locator: 'incdisc'
137    , testcase1: {
138        xhtml: '<input id="incdisc" expected-result="1" />'
139    }
140});
141myMap.addElement('allPages', {
142    name: 'search_submit'
143    , description: 'site search submission button'
144    , locator: 'submit'
145    , testcase1: {
146        xhtml: '<input id="submit" expected-result="1" />'
147    }
148});
149myMap.addElement('allPages', {
150    name: 'topics'
151    , description: 'sidebar links to topic categories'
152    , args: [
153        {
154            name: 'topic'
155            , description: 'the name of the topic'
156            , defaultValues: topics
157        }
158    ]
159    , getLocator: function(args) {
160        return "//div[@id='topiclist']/ul/li" +
161            "/a[text()=" + args.topic.quoteForXPath() + "]";
162    }
163    , testcase1: {
164        args: { topic: 'foo' }
165        , xhtml: '<div id="topiclist"><ul><li>'
166            + '<a expected-result="1">foo</a>'
167            + '</li></ul></div>'
168    }
169});
170myMap.addElement('allPages', {
171    name: 'copyright'
172    , description: 'footer link to copyright page'
173    , getLocator: function(args) { return "//span[@class='copyright']/a"; }
174    , testcase1: {
175        xhtml: '<span class="copyright"><a expected-result="1" /></span>'
176    }
177});
178
179
180
181// define UI elements for the homepage, i.e. "http://alistapart.com/", and
182// magazine issue pages, i.e. "http://alistapart.com/issues/234".
183myMap.addPageset({
184    name: 'issuePages'
185    , description: 'pages including magazine issues'
186    , pathRegexp: '(issues/.+)?'
187});
188myMap.addElement('issuePages', {
189    name: 'article'
190    , description: 'front or issue page link to article'
191    , args: [
192        {
193            name: 'index'
194            , description: 'the index of the article'
195            // an array of default values for the argument. A default
196            // value is one that is passed to the getXPath() method of
197            // the container UIElement object when trying to build an
198            // element locator.
199            //
200            // range() may be used to count easily. Remember though that
201            // the ending value does not include the right extreme; for
202            // example range(1, 5) counts from 1 to 4 only.
203            , defaultValues: range(1, 5)
204        }
205    ]
206    , getLocator: function(args) {
207        return "//div[@class='item'][" + args.index + "]/h4/a";
208    }
209});
210myMap.addElement('issuePages', {
211    name: 'author'
212    , description: 'article author link'
213    , args: [
214        {
215            name: 'index'
216            , description: 'the index of the author, by article'
217            , defaultValues: range(1, 5)
218        }
219    ]
220    , getLocator: function(args) {
221        return "//div[@class='item'][" + args.index + "]/h5/a";
222    }
223});
224myMap.addElement('issuePages', {
225    name: 'store'
226    , description: 'alistapart.com store link'
227    , locator: "//ul[@id='banners']/li/a[@title='ALA Store']/img"
228});
229myMap.addElement('issuePages', {
230    name: 'special_article'
231    , description: "editor's choice article link"
232    , locator: "//div[@id='choice']/h4/a"
233});
234myMap.addElement('issuePages', {
235    name: 'special_author'
236    , description: "author link of editor's choice article"
237    , locator: "//div[@id='choice']/h5/a"
238});
239
240
241
242// define UI elements for the articles page, i.e.
243// "http://alistapart.com/articles"
244myMap.addPageset({
245    name: 'articleListPages'
246    , description: 'page with article listings'
247    , paths: [ 'articles' ]
248});
249myMap.addElement('articleListPages', {
250    name: 'issue'
251    , description: 'link to issue'
252    , args: [
253        {
254            name: 'index'
255            , description: 'the index of the issue on the page'
256            , defaultValues: range(1, 10)
257        }
258    ]
259    , getLocator: function(args) {
260        return "//h2[@class='ishinfo'][" + args.index + ']/a';
261    }
262    , genericLocator: "//h2[@class='ishinfo']/a"
263});
264myMap.addElement('articleListPages', {
265    name: 'article'
266    , description: 'link to article, by issue and article number'
267    ,  args: [
268        {
269            name: 'issue_index'
270            , description: "the index of the article's issue on the page; "
271                + 'typically five per page'
272            , defaultValues: range(1, 6)
273        }
274        , {
275            name: 'article_index'
276            , description: 'the index of the article within the issue; '
277                + 'typically two per issue'
278            , defaultValues: range(1, 5)
279        }
280    ]
281    , getLocator: function(args) {
282        var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']'
283            + "/following-sibling::div[@class='item']"
284            + '[' + (args.article_index || 1) + "]/h3[@class='title']/a";
285        return xpath;
286    }
287    , genericLocator: "//h2[@class='ishinfo']"
288        + "/following-sibling::div[@class='item']/h3[@class='title']/a"
289});
290myMap.addElement('articleListPages', {
291    name: 'author'
292    , description: 'article author link, by issue and article'
293    , args: [
294        {
295            name: 'issue_index'
296            , description: "the index of the article's issue on the page; \
297typically five per page"
298            , defaultValues: range(1, 6)
299        }
300        , {
301            name: 'article_index'
302            , description: "the index of the article within the issue; \
303typically two articles per issue"
304            , defaultValues: range(1, 3)
305        }
306    ]
307    // this XPath uses the "following-sibling" axis. The div elements for
308    // the articles in an issue are not children, but siblings of the h2
309    // element identifying the article.
310    , getLocator: function(args) {
311        var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']'
312            + "/following-sibling::div[@class='item']"
313            + '[' + (args.article_index || 1) + "]/h4[@class='byline']/a";
314        return xpath;
315    }
316    , genericLocator: "//h2[@class='ishinfo']"
317        + "/following-sibling::div[@class='item']/h4[@class='byline']/a"
318});
319myMap.addElement('articleListPages', {
320    name: 'next_page'
321    , description: 'link to next page of articles (older)'
322    , locator: "//a[contains(text(),'Next page')]"
323});
324myMap.addElement('articleListPages', {
325    name: 'previous_page'
326    , description: 'link to previous page of articles (newer)'
327    , locator: "//a[contains(text(),'Previous page')]"
328});
329
330
331
332// define UI elements for specific article pages, i.e.
333// "http://alistapart.com/articles/culturalprobe"
334myMap.addPageset({
335    name: 'articlePages'
336    , description: 'pages for actual articles'
337    , pathRegexp: 'articles/.+'
338});
339myMap.addElement('articlePages', {
340    name: 'title'
341    , description: 'article title loop-link'
342    , locator: "//div[@id='content']/h1[@class='title']/a"
343});
344myMap.addElement('articlePages', {    
345    name: 'author'
346    , description: 'article author link'
347    , locator: "//div[@id='content']/h3[@class='byline']/a"
348});
349myMap.addElement('articlePages', {    
350    name: 'article_topics'
351    , description: 'links to topics under which article is published, before \
352article content'
353    , args: [
354        {
355            name: 'topic'
356            , description: 'the name of the topic'
357            , defaultValues: keys(subtopics)
358        }
359    ]
360    , getLocator: function(args) {
361        return "//ul[@id='metastuff']/li/a"
362            + "[@title=" + args.topic.quoteForXPath() + "]";
363    }
364});
365myMap.addElement('articlePages', {    
366    name: 'discuss'
367    , description: 'link to article discussion area, before article content'
368    , locator: "//ul[@id='metastuff']/li[@class='discuss']/p/a"
369});
370myMap.addElement('articlePages', {    
371    name: 'related_topics'
372    , description: 'links to topics under which article is published, after \
373article content'
374    , args: [
375        {
376            name: 'topic'
377            , description: 'the name of the topic'
378            , defaultValues: keys(subtopics)
379        }
380    ]
381    , getLocator: function(args) {
382        return "//div[@id='learnmore']/p/a"
383            + "[@title=" + args.topic.quoteForXPath() + "]";
384    }
385});
386myMap.addElement('articlePages', {    
387    name: 'join_discussion'
388    , description: 'link to article discussion area, after article content'
389    , locator: "//div[@class='discuss']/p/a"
390});
391
392
393
394myMap.addPageset({
395    name: 'topicListingPages'
396    , description: 'top level listing of topics'
397    , paths: [ 'topics' ]
398});
399myMap.addElement('topicListingPages', {
400    name: 'topic'
401    , description: 'link to topic category'
402    , args: [
403        {
404            name: 'topic'
405            , description: 'the name of the topic'
406            , defaultValues: topics
407        }
408    ]
409    , getLocator: function(args) {
410        return "//div[@id='content']/h2/a"
411            + "[text()=" + args.topic.quoteForXPath() + "]";
412    }
413});
414myMap.addElement('topicListingPages', {
415    name: 'subtopic'
416    , description: 'link to subtopic category'
417    , args: [
418        {
419            name: 'subtopic'
420            , description: 'the name of the subtopic'
421            , defaultValues: keys(subtopics)
422        }
423    ]
424    , getLocator: function(args) {
425        return "//div[@id='content']" +
426            "/descendant::a[text()=" + args.subtopic.quoteForXPath() + "]";
427    }
428});
429
430// the following few subtopic page UI elements are very similar. Define UI
431// elements for the code page, which is a subpage under topics, i.e.
432// "http://alistapart.com/topics/code/"
433myMap.addPageset({
434    name: 'subtopicListingPages' 
435    , description: 'pages listing subtopics'
436    , pathPrefix: 'topics/'
437    , paths: [
438        'code'
439        , 'content'
440        , 'culture'
441        , 'design'
442        , 'process'
443        , 'userscience'
444    ]
445});
446myMap.addElement('subtopicListingPages', {
447    name: 'subtopic'
448    , description: 'link to a subtopic category'
449    , args: [
450        {
451            name: 'subtopic'
452            , description: 'the name of the subtopic'
453            , defaultValues: keys(subtopics)
454        }
455    ]
456    , getLocator: function(args) {
457        return "//div[@id='content']/h2" +
458            "/a[text()=" + args.subtopic.quoteForXPath() + "]";
459    }
460});
461
462
463
464// subtopic articles page
465myMap.addPageset({
466    name: 'subtopicArticleListingPages'
467    , description: 'pages listing the articles for a given subtopic'
468    , pathRegexp: 'topics/[^/]+/.+'
469});
470myMap.addElement('subtopicArticleListingPages', {
471    name: 'article'
472    , description: 'link to a subtopic article'
473    , args: [
474        {
475            name: 'index'
476            , description: 'the index of the article'
477            , defaultValues: range(1, 51) // the range seems unlimited ...
478        }
479    ]
480    , getLocator: function(args) {
481        return "//div[@id='content']/div[@class='item']"
482            + "[" + args.index + "]/h3/a";
483    }
484    , testcase1: {
485        args: { index: 2 }
486        , xhtml: '<div id="content"><div class="item" /><div class="item">'
487            + '<h3><a expected-result="1" /></h3></div></div>'
488    }
489});
490myMap.addElement('subtopicArticleListingPages', {
491    name: 'author'
492    , description: "link to a subtopic article author's page"
493    , args: [
494        {
495            name: 'article_index'
496            , description: 'the index of the authored article'
497            , defaultValues: range(1, 51)
498        }
499        , {
500            name: 'author_index'
501            , description: 'the index of the author when there are multiple'
502            , defaultValues: range(1, 4)
503        }
504    ]
505    , getLocator: function(args) {
506        return "//div[@id='content']/div[@class='item'][" +
507            args.article_index + "]/h4/a[" +
508            (args.author_index ? args.author_index : '1') + ']';
509    }
510});
511myMap.addElement('subtopicArticleListingPages', {
512    name: 'issue'
513    , description: 'link to issue a subtopic article appears in'
514    , args: [
515        {
516            name: 'index'
517            , description: 'the index of the subtopic article'
518            , defaultValues: range(1, 51)
519        }
520    ]
521    , getLocator: function(args) {
522        return "//div[@id='content']/div[@class='item']"
523            + "[" + args.index + "]/h5/a";
524    }
525});
526
527
528
529myMap.addPageset({
530    name: 'aboutPages'
531    , description: 'the website about page'
532    , paths: [ 'about' ]
533});
534myMap.addElement('aboutPages', {
535    name: 'crew'
536    , description: 'link to site crew member bio or personal website'
537    , args: [
538        {
539            name: 'role'
540            , description: 'the role of the crew member'
541            , defaultValues: [
542                'ALA Crew'
543                , 'Support'
544                , 'Emeritus'
545            ]
546        }
547        , {
548            name: 'role_index'
549            , description: 'the index of the member within the role'
550            , defaultValues: range(1, 20)
551        }
552        , {
553            name: 'member_index'
554            , description: 'the index of the member within the role title'
555            , defaultValues: range(1, 5)
556        }
557    ]
558    , getLocator: function(args) {
559        // the first role is kind of funky, and requires a conditional to
560        // build the XPath correctly. Its header looks like this:
561        //
562        // <h3>
563        // <span class="caps">ALA 4</span>.0 <span class="caps">CREW</span>
564        // </h3>
565        //
566        // This kind of complexity is a little daunting, but you can see
567        // how the format can handle it relatively easily and concisely.
568        if (args.role == 'ALA Crew') {
569            var selector = "descendant::text()='CREW'";
570        }
571        else {
572            var selector = "text()=" + args.role.quoteForXPath();
573        }
574        var xpath =
575            "//div[@id='secondary']/h3[" + selector + ']' +
576            "/following-sibling::dl/dt[" + (args.role_index || 1) + ']' +
577            '/a[' + (args.member_index || '1') + ']';
578        return xpath;
579    }
580});
581
582
583
584myMap.addPageset({
585    name: 'searchResultsPages'
586    , description: 'pages listing search results'
587    , paths: [ 'search' ]
588});
589myMap.addElement('searchResultsPages', {
590    name: 'result_link'
591    , description: 'search result link'
592    , args: [
593        {
594            name: 'index'
595            , description: 'the index of the search result'
596            , defaultValues: range(1, 11)
597        }
598    ]
599    , getLocator: function(args) {
600        return "//div[@id='content']/ul[" + args.index + ']/li/h3/a';
601    }
602});
603myMap.addElement('searchResultsPages', {
604    name: 'more_results_link'
605    , description: 'next or previous results link at top or bottom of page'
606    , args: [
607        {
608            name: 'direction'
609            , description: 'next or previous results page'
610            // demonstrate a method which acquires default values from the
611            // document object. Such default values may contain EITHER commas
612            // OR equals signs, but NOT BOTH.
613            , getDefaultValues: function(inDocument) {
614                var defaultValues = [];
615                var divs = inDocument.getElementsByTagName('div');
616                for (var i = 0; i < divs.length; ++i) {
617                    if (divs[i].className == 'pages') {
618                        break;
619                    }
620                }
621                var links = divs[i].getElementsByTagName('a');
622                for (i = 0; i < links.length; ++i) {
623                    defaultValues.push(links[i].innerHTML
624                        .replace(/^\xab\s*/, "")
625                        .replace(/\s*\bb$/, "")
626                        .replace(/\s*\d+$/, ""));
627                }
628                return defaultValues;
629            }
630        }
631        , {
632            name: 'position'
633            , description: 'position of the link'
634            , defaultValues: ['top', 'bottom']
635        }
636    ]
637    , getLocator: function(args) {
638        return "//div[@id='content']/div[@class='pages']["
639            + (args.position == 'top' ? '1' : '2') + ']'
640            + "/a[contains(text(), "
641            + (args.direction ? args.direction.quoteForXPath() : undefined)
642            + ")]";
643    }
644});
645    
646    
647    
648myMap.addPageset({
649    name: 'commentsPages'
650    , description: 'pages listing comments made to an article'
651    , pathRegexp: 'comments/.+'
652});
653myMap.addElement('commentsPages', {
654    name: 'article_link'
655    , description: 'link back to the original article'
656    , locator: "//div[@id='content']/h1[@class='title']/a"
657});
658myMap.addElement('commentsPages', {
659    name: 'comment_link'
660    , description: 'same-page link to comment'
661    , args: [
662        {
663            name: 'index'
664            , description: 'the index of the comment'
665            , defaultValues: range(1, 11)
666        }
667    ]
668    , getLocator: function(args) {
669        return "//div[@class='content']/div[contains(@class, 'comment')]" +
670            '[' + args.index + ']/h4/a[2]';
671    }
672});
673myMap.addElement('commentsPages', {
674    name: 'paging_link'
675    , description: 'links to more pages of comments'
676    , args: [
677        {
678            name: 'dest'
679            , description: 'the destination page'
680            , defaultValues: ['next', 'prev'].concat(range(1, 16))
681        }
682        , {
683            name: 'position'
684            , description: 'position of the link'
685            , defaultValues: ['top', 'bottom']
686        }
687    ]
688    , getLocator: function(args) {
689        var dest = args.dest;
690        var xpath = "//div[@id='content']/div[@class='pages']" +
691            '[' + (args.position == 'top' ? '1' : '2') + ']/p';
692        if (dest == 'next' || dest == 'prev') {
693            xpath += "/a[contains(text(), " + dest.quoteForXPath() + ")]";
694        }
695        else {
696            xpath += "/a[text()=" + dest.quoteForXPath() + "]";
697        }
698        return xpath;
699    }
700});
701
702
703
704myMap.addPageset({
705    name: 'authorPages'
706    , description: 'personal pages for each author'
707    , pathRegexp: 'authors/[a-z]/.+'
708});
709myMap.addElement('authorPages', {
710    name: 'article'
711    , description: "link to article written by this author.\n"
712        + 'This description has a line break.'
713    , args: [
714        {
715            name: 'index'
716            , description: 'index of the article on the page'
717            , defaultValues: range(1, 11)
718        }
719    ]
720    , getLocator: function(args) {
721        var index = args.index;
722        // try out the CSS locator!
723        //return "//h4[@class='title'][" + index + "]/a";
724        return 'css=h4.title:nth-child(' + index + ') > a';
725    }
726    , testcase1: {
727        args: { index: '2' }
728        , xhtml: '<h4 class="title" /><h4 class="title">'
729            + '<a expected-result="1" /></h4>'
730    }
731});
732
733
734
735// test the offset locator. Something like the following can be recorded:
736// ui=qaPages::content()//a[contains(text(),'May I quote from your articles?')]
737myMap.addPageset({
738    name: 'qaPages'
739    , description: 'question and answer pages'
740    , pathRegexp: 'qa'
741});
742myMap.addElement('qaPages', {
743    name: 'content'
744    , description: 'the content pane containing the q&a entries'
745    , locator: "//div[@id='content' and "
746        + "child::h1[text()='Questions and Answers']]"
747    , getOffsetLocator: UIElement.defaultOffsetLocatorStrategy
748});
749myMap.addElement('qaPages', {
750    name: 'last_updated'
751    , description: 'displays the last update date'
752    // demonstrate calling getLocator() for another UI element within a
753    // getLocator(). The former must have already been added to the map. And
754    // obviously, you can't randomly combine different locator types!
755    , locator: myMap.getUIElement('qaPages', 'content').getLocator() + '/p/em'
756});
757
758
759
760//******************************************************************************
761
762var myRollupManager = new RollupManager();
763
764// though the description element is required, its content is free form. You
765// might want to create a documentation policy as given below, where the pre-
766// and post-conditions of the rollup are spelled out.
767//
768// To take advantage of a "heredoc" like syntax for longer descriptions,
769// add a backslash to the end of the current line and continue the string on
770// the next line.
771myRollupManager.addRollupRule({
772    name: 'navigate_to_subtopic_article_listing'
773    , description: 'drill down to the listing of articles for a given subtopic \
774from the section menu, then the topic itself.'
775    , pre: 'current page contains the section menu (most pages should)'
776    , post: 'navigated to the page listing all articles for a given subtopic'
777    , args: [
778        {
779            name: 'subtopic'
780            , description: 'the subtopic whose article listing to navigate to'
781            , exampleValues: keys(subtopics)
782        }
783    ]
784    , commandMatchers: [
785        {
786            command: 'clickAndWait'
787            , target: 'ui=allPages::section\\(section=topics\\)'
788            // must escape parentheses in the the above target, since the
789            // string is being used as a regular expression. Again, backslashes
790            // in strings must be escaped too.
791        }
792        , {
793            command: 'clickAndWait'
794            , target: 'ui=topicListingPages::topic\\(.+'
795        }
796        , {
797            command: 'clickAndWait'
798            , target: 'ui=subtopicListingPages::subtopic\\(.+'
799            , updateArgs: function(command, args) {
800                // don't bother stripping the "ui=" prefix from the locator
801                // here; we're just using UISpecifier to parse the args out
802                var uiSpecifier = new UISpecifier(command.target);
803                args.subtopic = uiSpecifier.args.subtopic;
804                return args;
805            }
806        }
807    ]
808    , getExpandedCommands: function(args) {
809        var commands = [];
810        var topic = subtopics[args.subtopic];
811        var subtopic = args.subtopic;
812        commands.push({
813            command: 'clickAndWait'
814            , target: 'ui=allPages::section(section=topics)'
815        });
816        commands.push({
817            command: 'clickAndWait'
818            , target: 'ui=topicListingPages::topic(topic=' + topic + ')'
819        });
820        commands.push({
821            command: 'clickAndWait'
822            , target: 'ui=subtopicListingPages::subtopic(subtopic=' + subtopic
823                + ')'
824        });
825        commands.push({
826            command: 'verifyLocation'
827            , target: 'regexp:.+/topics/.+/.+'
828        });
829        return commands;
830    }
831});
832
833
834
835myRollupManager.addRollupRule({
836    name: 'replace_click_with_clickAndWait'
837    , description: 'replaces commands where a click was detected with \
838clickAndWait instead'
839    , alternateCommand: 'clickAndWait'
840    , commandMatchers: [
841        {
842            command: 'click'
843            , target: 'ui=subtopicArticleListingPages::article\\(.+'
844        }
845    ]
846    , expandedCommands: []
847});
848
849
850
851myRollupManager.addRollupRule({
852    name: 'navigate_to_subtopic_article'
853    , description: 'navigate to an article listed under a subtopic.'
854    , pre: 'current page contains the section menu (most pages should)'
855    , post: 'navigated to an article page'
856    , args: [
857        {
858            name: 'subtopic'
859            , description: 'the subtopic whose article listing to navigate to'
860            , exampleValues: keys(subtopics)
861        }
862        , {
863            name: 'index'
864            , description: 'the index of the article in the listing'
865            , exampleValues: range(1, 11)
866        }
867    ]
868    , commandMatchers: [
869        {
870            command: 'rollup'
871            , target: 'navigate_to_subtopic_article_listing'
872            , value: 'subtopic\\s*=.+'
873            , updateArgs: function(command, args) {
874                var args1 = parse_kwargs(command.value);
875                args.subtopic = args1.subtopic;
876                return args;
877            }
878        }
879        , {
880            command: 'clickAndWait'
881            , target: 'ui=subtopicArticleListingPages::article\\(.+'
882            , updateArgs: function(command, args) {
883                var uiSpecifier = new UISpecifier(command.target);
884                args.index = uiSpecifier.args.index;
885                return args;
886            }
887        }
888    ]
889    /*
890    // this is pretty much equivalent to the commandMatchers immediately above.
891    // Seems more verbose and less expressive, doesn't it? But sometimes you
892    // might prefer the flexibility of a function.
893    , getRollup: function(commands) {
894        if (commands.length >= 2) {
895            command1 = commands[0];
896            command2 = commands[1];
897            var args1 = parse_kwargs(command1.value);
898            try {
899                var uiSpecifier = new UISpecifier(command2.target
900                    .replace(/^ui=/, ''));
901            }
902            catch (e) {
903                return false;
904            }
905            if (command1.command == 'rollup' &&
906                command1.target == 'navigate_to_subtopic_article_listing' &&
907                args1.subtopic &&
908                command2.command == 'clickAndWait' &&
909                uiSpecifier.pagesetName == 'subtopicArticleListingPages' &&
910                uiSpecifier.elementName == 'article') {
911                var args = {
912                    subtopic: args1.subtopic
913                    , index: uiSpecifier.args.index
914                };
915                return {
916                    command: 'rollup'
917                    , target: this.name
918                    , value: to_kwargs(args)
919                    , replacementIndexes: [ 0, 1 ]
920                };
921            }
922        }
923        return false;
924    }
925    */
926    , getExpandedCommands: function(args) {
927        var commands = [];
928        commands.push({
929            command: 'rollup'
930            , target: 'navigate_to_subtopic_article_listing'
931            , value: to_kwargs({ subtopic: args.subtopic })
932        });
933        var uiSpecifier = new UISpecifier(
934            'subtopicArticleListingPages'
935            , 'article'
936            , { index: args.index });
937        commands.push({
938            command: 'clickAndWait'
939            , target: 'ui=' + uiSpecifier.toString()
940        });
941        commands.push({
942            command: 'verifyLocation'
943            , target: 'regexp:.+/articles/.+'
944        });
945        return commands;
946    }
947});
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979