PageRenderTime 86ms CodeModel.GetById 14ms app.highlight 61ms RepoModel.GetById 2ms app.codeStats 0ms

/testing/selenium-core/scripts/ui-element.js

http://datanucleus-appengine.googlecode.com/
JavaScript | 1537 lines | 943 code | 173 blank | 421 comment | 194 complexity | dfc99c0b48766017c15a42f3f043f6a4 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1//******************************************************************************
   2// Globals, including constants
   3
   4var UI_GLOBAL = {
   5    UI_PREFIX: 'ui'
   6    , XHTML_DOCTYPE: '<!DOCTYPE html PUBLIC '
   7        + '"-//W3C//DTD XHTML 1.0 Strict//EN" '
   8        + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
   9    , XHTML_XMLNS: 'http://www.w3.org/1999/xhtml'
  10};
  11
  12//*****************************************************************************
  13// Exceptions
  14
  15function UIElementException(message)
  16{
  17    this.message = message;
  18    this.name = 'UIElementException';
  19}
  20
  21function UIArgumentException(message)
  22{
  23    this.message = message;
  24    this.name = 'UIArgumentException';
  25}
  26
  27function PagesetException(message)
  28{
  29    this.message = message;
  30    this.name = 'PagesetException';
  31}
  32
  33function UISpecifierException(message)
  34{
  35    this.message = message;
  36    this.name = 'UISpecifierException';
  37}
  38
  39function CommandMatcherException(message)
  40{
  41    this.message = message;
  42    this.name = 'CommandMatcherException';
  43}
  44
  45//*****************************************************************************
  46// UI-Element core
  47
  48/**
  49 * The UIElement object. This has been crafted along with UIMap to make
  50 * specifying UI elements using JSON as simple as possible. Object construction
  51 * will fail if 1) a proper name isn't provided, 2) a faulty args argument is
  52 * given, or 3) getLocator() returns undefined for a valid permutation of
  53 * default argument values. See ui-doc.html for the documentation on the
  54 * builder syntax.
  55 *
  56 * @param uiElementShorthand  an object whose contents conform to the
  57 *                            UI-Element builder syntax.
  58 *
  59 * @return  a new UIElement object
  60 * @throws  UIElementException
  61 */
  62function UIElement(uiElementShorthand)
  63{
  64    // a shorthand object might look like:
  65    //
  66    // {
  67    //     name: 'topic'
  68    //     , description: 'sidebar links to topic categories'
  69    //     , args: [
  70    //         {
  71    //             name: 'name'
  72    //             , description: 'the name of the topic'
  73    //             , defaultValues: topLevelTopics
  74    //         }
  75    //     ]
  76    //     , getLocator: function(args) {
  77    //         return this._listXPath +
  78    //             "/a[text()=" + args.name.quoteForXPath() + "]";
  79    //     }
  80    //     , getGenericLocator: function() {
  81    //         return this._listXPath + '/a';
  82    //     }
  83    //     // maintain testcases for getLocator()
  84    //     , testcase1: {
  85    //         // defaultValues used if args not specified
  86    //         args: { name: 'foo' }
  87    //         , xhtml: '<div id="topiclist">'
  88    //             + '<ul><li><a expected-result="1">foo</a></li></ul>'
  89    //             + '</div>'
  90    //     }
  91    //     // set a local element variable
  92    //     , _listXPath: "//div[@id='topiclist']/ul/li"
  93    // }
  94    //
  95    // name cannot be null or an empty string. Enforce the same requirement for
  96    // the description.
  97    
  98    /**
  99     * Recursively returns all permutations of argument-value pairs, given
 100     * a list of argument definitions. Each argument definition will have
 101     * a set of default values to use in generating said pairs. If an argument
 102     * has no default values defined, it will not be included among the
 103     * permutations.
 104     *
 105     * @param args            a list of UIArguments
 106     * @param opt_inDocument  (optional)
 107     * @return      a list of associative arrays containing key value pairs
 108     */
 109    this.permuteArgs = function(args, opt_inDocument) {
 110        var permutations = [];
 111        for (var i = 0; i < args.length; ++i) {
 112            var arg = args[i];
 113            var defaultValues = (arguments.length > 1)
 114                ? arg.getDefaultValues(opt_inDocument)
 115                : arg.getDefaultValues();
 116            
 117            // skip arguments for which no default values are defined
 118            if (defaultValues.length == 0) {
 119                continue;
 120            }
 121            for (var j = 0; j < defaultValues.length; ++j) {
 122                var value = defaultValues[j];
 123                var nextPermutations = this.permuteArgs(args.slice(i+1));
 124                if (nextPermutations.length == 0) {
 125                    var permutation = {};
 126                    permutation[arg.name] = value + ''; // make into string
 127                    permutations.push(permutation);
 128                }
 129                else {
 130                    for (var k = 0; k < nextPermutations.length; ++k) {
 131                        nextPermutations[k][arg.name] = value + '';
 132                        permutations.push(nextPermutations[k]);
 133                    }
 134                }
 135            }
 136            break;
 137        }
 138        return permutations;
 139    }
 140    
 141    
 142    
 143    /**
 144     * Returns a list of all testcases for this UIElement.
 145     */
 146    this.getTestcases = function()
 147    {
 148        return this.testcases;
 149    }
 150    
 151    
 152    
 153    /**
 154     * Run all unit tests, stopping at the first failure, if any. Return true
 155     * if no failures encountered, false otherwise. See the following thread
 156     * regarding use of getElementById() on XML documents created by parsing
 157     * text via the DOMParser:
 158     *
 159     * http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/2b1b82b3c53a1282/
 160     */
 161    this.test = function()
 162    {
 163        var parser = new DOMParser();
 164        var testcases = this.getTestcases();
 165        testcaseLoop: for (var i = 0; i < testcases.length; ++i) {
 166            var testcase = testcases[i];
 167            var xhtml = UI_GLOBAL.XHTML_DOCTYPE + '<html xmlns="'
 168                + UI_GLOBAL.XHTML_XMLNS + '">' + testcase.xhtml + '</html>';
 169            var doc = parser.parseFromString(xhtml, "text/xml");
 170            if (doc.firstChild.nodeName == 'parsererror') {
 171                safe_alert('Error parsing XHTML in testcase "' + testcase.name
 172                    + '" for UI element "' + this.name + '": ' + "\n"
 173                    + doc.firstChild.firstChild.nodeValue);
 174            }
 175            
 176            // we're no longer using the default locators when testing, because
 177            // args is now required
 178            var locator = parse_locator(this.getLocator(testcase.args));
 179            var results;
 180            if (locator.type == 'xpath' || (locator.type == 'implicit' &&
 181                locator.string.substring(0, 2) == '//')) {
 182                // try using the javascript xpath engine to avoid namespace
 183                // issues. The xpath does have to be lowercase however, it
 184                // seems. 
 185                results = eval_xpath(locator.string, doc,
 186                    { allowNativeXpath: false, returnOnFirstMatch: true });
 187            }
 188            else {
 189                // piece the locator back together
 190                locator = (locator.type == 'implicit')
 191                    ? locator.string
 192                    : locator.type + '=' + locator.string;
 193                results = eval_locator(locator, doc);
 194            }
 195            if (results.length && results[0].hasAttribute('expected-result')) {
 196                continue testcaseLoop;
 197            }
 198            
 199            // testcase failed
 200            if (is_IDE()) {
 201                var msg = 'Testcase "' + testcase.name
 202                    + '" failed for UI element "' + this.name + '":';
 203                if (!results.length) {
 204                    msg += '\n"' + locator + '" did not match any elements!';
 205                }
 206                else {
 207                    msg += '\n' + results[0] + ' was not the expected result!';
 208                }
 209                safe_alert(msg);
 210            }
 211            return false;
 212        }
 213        return true;
 214    };
 215    
 216    
 217    
 218    /**
 219     * Creates a set of locators using permutations of default values for
 220     * arguments used in the locator construction. The set is returned as an
 221     * object mapping locators to key-value arguments objects containing the
 222     * values passed to getLocator() to create the locator.
 223     *
 224     * @param opt_inDocument (optional) the document object of the "current"
 225     *                       page when this method is invoked. Some arguments
 226     *                       may have default value lists that are calculated
 227     *                       based on the contents of the page.
 228     *
 229     * @return  a list of locator strings
 230     * @throws  UIElementException
 231     */
 232    this.getDefaultLocators = function(opt_inDocument) {
 233        var defaultLocators = {};
 234        if (this.args.length == 0) {
 235            defaultLocators[this.getLocator({})] = {};
 236        }
 237        else {
 238            var permutations = this.permuteArgs(this.args, opt_inDocument);
 239            if (permutations.length != 0) {
 240                for (var i = 0; i < permutations.length; ++i) {
 241                    var args = permutations[i];
 242                    var locator = this.getLocator(args);
 243                    if (!locator) {
 244                        throw new UIElementException('Error in UIElement(): '
 245                            + 'no getLocator return value for element "' + name
 246                            + '"');
 247                    }
 248                    defaultLocators[locator] = args;
 249                }
 250            }
 251            else {
 252                // try using no arguments. If it doesn't work, fine.
 253                try {
 254                    var locator = this.getLocator();
 255                    defaultLocators[locator] = {};
 256                }
 257                catch (e) {
 258                    safe_log('debug', e.message);
 259                }
 260            }
 261        }
 262        return defaultLocators;
 263    };
 264    
 265    
 266    
 267    /**
 268     * Validate the structure of the shorthand notation this object is being
 269     * initialized with. Throws an exception if there's a validation error.
 270     *
 271     * @param uiElementShorthand
 272     *
 273     * @throws  UIElementException
 274     */
 275    this.validate = function(uiElementShorthand)
 276    {
 277        var msg = "UIElement validation error:\n" + print_r(uiElementShorthand);
 278        if (!uiElementShorthand.name) {
 279            throw new UIElementException(msg + 'no name specified!');
 280        }
 281        if (!uiElementShorthand.description) {
 282            throw new UIElementException(msg + 'no description specified!');
 283        }
 284        if (!uiElementShorthand.locator
 285            && !uiElementShorthand.getLocator
 286            && !uiElementShorthand.xpath
 287            && !uiElementShorthand.getXPath) {
 288            throw new UIElementException(msg + 'no locator specified!');
 289        }
 290    };
 291    
 292    
 293    
 294    this.init = function(uiElementShorthand)
 295    {
 296        this.validate(uiElementShorthand);
 297        
 298        this.name = uiElementShorthand.name;
 299        this.description = uiElementShorthand.description;
 300        
 301        // construct a new getLocator() method based on the locator property,
 302        // or use the provided function. We're deprecating the xpath property
 303        // and getXPath() function, but still allow for them for backwards
 304        // compatability.
 305        if (uiElementShorthand.locator) {
 306            this.getLocator = function(args) {
 307                return uiElementShorthand.locator;
 308            };
 309        }
 310        else if (uiElementShorthand.getLocator) {
 311            this.getLocator = uiElementShorthand.getLocator;
 312        }
 313        else if (uiElementShorthand.xpath) {
 314            this.getLocator = function(args) {
 315                return uiElementShorthand.xpath;
 316            };
 317        }
 318        else {
 319            this.getLocator = uiElementShorthand.getXPath;
 320        }
 321        
 322        if (uiElementShorthand.genericLocator) {
 323            this.getGenericLocator = function() {
 324                return uiElementShorthand.genericLocator;
 325            };
 326        }
 327        else if (uiElementShorthand.getGenericLocator) {
 328            this.getGenericLocator = uiElementShorthand.getGenericLocator;
 329        }
 330        
 331        if (uiElementShorthand.getOffsetLocator) {
 332            this.getOffsetLocator = uiElementShorthand.getOffsetLocator;
 333        }
 334        
 335        // get the testcases and local variables
 336        this.testcases = [];
 337        var localVars = {};
 338        for (var attr in uiElementShorthand) {
 339            if (attr.match(/^testcase/)) {
 340                var testcase = uiElementShorthand[attr];
 341                if (uiElementShorthand.args &&
 342                    uiElementShorthand.args.length && !testcase.args) {
 343                    safe_alert('No args defined in ' + attr + ' for UI element '
 344                        + this.name + '! Skipping testcase.');
 345                    continue;
 346                } 
 347                testcase.name = attr;
 348                this.testcases.push(testcase);
 349            }
 350            else if (attr.match(/^_/)) {
 351                this[attr] = uiElementShorthand[attr];
 352                localVars[attr] = uiElementShorthand[attr];
 353            }
 354        }
 355        
 356        // create the arguments
 357        this.args = []
 358        this.argsOrder = [];
 359        if (uiElementShorthand.args) {
 360            for (var i = 0; i < uiElementShorthand.args.length; ++i) {
 361                var arg = new UIArgument(uiElementShorthand.args[i], localVars);
 362                this.args.push(arg);
 363                this.argsOrder.push(arg.name);
 364
 365                // if an exception is thrown when invoking getDefaultValues()
 366                // with no parameters passed in, assume the method requires an
 367                // inDocument parameter, and thus may only be invoked at run
 368                // time. Mark the UI element object accordingly.
 369                try {
 370                    arg.getDefaultValues();
 371                }
 372                catch (e) {
 373                    this.isDefaultLocatorConstructionDeferred = true;
 374                }
 375            }
 376            
 377        }
 378        
 379        if (!this.isDefaultLocatorConstructionDeferred) {
 380            this.defaultLocators = this.getDefaultLocators();
 381        }
 382    };
 383    
 384    
 385    
 386    this.init(uiElementShorthand);
 387}
 388
 389// hang this off the UIElement "namespace"
 390UIElement.defaultOffsetLocatorStrategy = function(locatedElement, pageElement) {
 391    if (is_ancestor(locatedElement, pageElement)) {
 392        var offsetLocator;
 393        var recorder = Recorder.get(locatedElement.ownerDocument.defaultView);
 394        var builderNames = [
 395            'xpath:link'
 396            , 'xpath:img'
 397            , 'xpath:attributes'
 398            , 'xpath:idRelative'
 399            , 'xpath:href'
 400            , 'xpath:position'
 401        ];
 402        for (var i = 0; i < builderNames.length; ++i) {
 403            offsetLocator = recorder.locatorBuilders
 404                .buildWith(builderNames[i], pageElement, locatedElement);
 405            if (offsetLocator) {
 406                return offsetLocator;
 407            }
 408        }
 409    }
 410    return null;
 411};
 412
 413
 414
 415/**
 416 * Constructs a UIArgument. This is mostly for checking that the values are
 417 * valid.
 418 *
 419 * @param uiArgumentShorthand
 420 * @param localVars
 421 *
 422 * @throws  UIArgumentException
 423 */
 424function UIArgument(uiArgumentShorthand, localVars)
 425{
 426    /**
 427     * @param uiArgumentShorthand
 428     *
 429     * @throws  UIArgumentException
 430     */
 431    this.validate = function(uiArgumentShorthand)
 432    {
 433        var msg = "UIArgument validation error:\n"
 434            + print_r(uiArgumentShorthand);
 435        
 436        // try really hard to throw an exception!
 437        if (!uiArgumentShorthand.name) {
 438            throw new UIArgumentException(msg + 'no name specified!');
 439        }
 440        if (!uiArgumentShorthand.description) {
 441            throw new UIArgumentException(msg + 'no description specified!');
 442        }
 443        if (!uiArgumentShorthand.defaultValues &&
 444            !uiArgumentShorthand.getDefaultValues) {
 445            throw new UIArgumentException(msg + 'no default values specified!');
 446        }
 447    };
 448    
 449    
 450    
 451    /**
 452     * @param uiArgumentShorthand
 453     * @param localVars            a list of local variables
 454     */
 455    this.init = function(uiArgumentShorthand, localVars)
 456    {
 457        this.validate(uiArgumentShorthand);
 458        
 459        this.name = uiArgumentShorthand.name;
 460        this.description = uiArgumentShorthand.description;
 461        
 462        if (uiArgumentShorthand.defaultValues) {
 463            var defaultValues = uiArgumentShorthand.defaultValues;
 464            this.getDefaultValues =
 465                function() { return defaultValues; }
 466        }
 467        else {
 468            this.getDefaultValues = uiArgumentShorthand.getDefaultValues;
 469        }
 470        
 471        for (var name in localVars) {
 472            this[name] = localVars[name];
 473        }
 474    }
 475    
 476    
 477    
 478    this.init(uiArgumentShorthand, localVars);
 479}
 480
 481
 482
 483/**
 484 * The UISpecifier constructor is overloaded. If less than three arguments are
 485 * provided, the first argument will be considered a UI specifier string, and
 486 * will be split out accordingly. Otherwise, the first argument will be
 487 * considered the path.
 488 *
 489 * @param uiSpecifierStringOrPagesetName  a UI specifier string, or the pageset
 490 *                                        name of the UI specifier
 491 * @param elementName  the name of the element
 492 * @param args         an object associating keys to values
 493 *
 494 * @return  new UISpecifier object
 495 */
 496function UISpecifier(uiSpecifierStringOrPagesetName, elementName, args)
 497{
 498    /**
 499     * Initializes this object from a UI specifier string of the form:
 500     *
 501     *     pagesetName::elementName(arg1=value1, arg2=value2, ...)
 502     *
 503     * into its component parts, and returns them as an object.
 504     *
 505     * @return  an object containing the components of the UI specifier
 506     * @throws  UISpecifierException
 507     */
 508    this._initFromUISpecifierString = function(uiSpecifierString) {
 509        var matches = /^(.*)::([^\(]+)\((.*)\)$/.exec(uiSpecifierString);
 510        if (matches == null) {
 511            throw new UISpecifierException('Error in '
 512                + 'UISpecifier._initFromUISpecifierString(): "'
 513                + this.string + '" is not a valid UI specifier string');
 514        }
 515        this.pagesetName = matches[1];
 516        this.elementName = matches[2];
 517        this.args = (matches[3]) ? parse_kwargs(matches[3]) : {};
 518    };
 519    
 520    
 521    
 522    /**
 523     * Override the toString() method to return the UI specifier string when
 524     * evaluated in a string context. Combines the UI specifier components into
 525     * a canonical UI specifier string and returns it.
 526     *
 527     * @return   a UI specifier string
 528     */
 529    this.toString = function() {
 530        // empty string is acceptable for the path, but it must be defined
 531        if (this.pagesetName == undefined) {
 532            throw new UISpecifierException('Error in UISpecifier.toString(): "'
 533                + this.pagesetName + '" is not a valid UI specifier pageset '
 534                + 'name');
 535        }
 536        if (!this.elementName) {
 537            throw new UISpecifierException('Error in UISpecifier.unparse(): "'
 538                + this.elementName + '" is not a valid UI specifier element '
 539                + 'name');
 540        }
 541        if (!this.args) {
 542            throw new UISpecifierException('Error in UISpecifier.unparse(): "'
 543                + this.args + '" are not valid UI specifier args');
 544        }
 545        
 546        uiElement = UIMap.getInstance()
 547            .getUIElement(this.pagesetName, this.elementName);
 548        if (uiElement != null) {
 549            var kwargs = to_kwargs(this.args, uiElement.argsOrder);
 550        }
 551        else {
 552            // probably under unit test
 553            var kwargs = to_kwargs(this.args);
 554        }
 555        return this.pagesetName + '::' + this.elementName + '(' + kwargs + ')';
 556    };
 557    
 558    
 559    
 560    // construct the object
 561    if (arguments.length < 2) {
 562        this._initFromUISpecifierString(uiSpecifierStringOrPagesetName);
 563    }
 564    else {
 565        this.pagesetName = uiSpecifierStringOrPagesetName;
 566        this.elementName = elementName;
 567        this.args = (args) ? clone(args) : {};
 568    }
 569}
 570
 571
 572
 573function Pageset(pagesetShorthand)
 574{
 575    /**
 576     * Returns true if the page is included in this pageset, false otherwise.
 577     * The page is specified by a document object.
 578     *
 579     * @param inDocument  the document object representing the page
 580     */
 581    this.contains = function(inDocument)
 582    {
 583        var urlParts = parseUri(unescape(inDocument.location.href));
 584        var path = urlParts.path
 585            .replace(/^\//, "")
 586            .replace(/\/$/, "");
 587        if (!this.pathRegexp.test(path)) {
 588            return false;
 589        }
 590        for (var paramName in this.paramRegexps) {
 591            var paramRegexp = this.paramRegexps[paramName];
 592            if (!paramRegexp.test(urlParts.queryKey[paramName])) {
 593                return false;
 594            }
 595        }
 596        if (!this.pageContent(inDocument)) {
 597            return false;
 598        }
 599        
 600        return true;
 601    }
 602    
 603    
 604    
 605    this.getUIElements = function()
 606    {
 607        var uiElements = [];
 608        for (var uiElementName in this.uiElements) {
 609            uiElements.push(this.uiElements[uiElementName]);
 610        }
 611        return uiElements;
 612    };
 613    
 614    
 615    
 616    /**
 617     * Returns a list of UI specifier string stubs representing all UI elements
 618     * for this pageset. Stubs contain all required arguments, but leave
 619     * argument values blank. Each element stub is paired with the element's
 620     * description.
 621     *
 622     * @return  a list of UI specifier string stubs
 623     */
 624    this.getUISpecifierStringStubs = function()
 625    {
 626        var stubs = [];
 627        for (var name in this.uiElements) {
 628            var uiElement = this.uiElements[name];
 629            var args = {};
 630            for (var i = 0; i < uiElement.args.length; ++i) {
 631                args[uiElement.args[i].name] = '';
 632            }
 633            var uiSpecifier = new UISpecifier(this.name, uiElement.name, args);
 634            stubs.push([
 635                UI_GLOBAL.UI_PREFIX + '=' + uiSpecifier.toString()
 636                , uiElement.description
 637            ]);
 638        }
 639        return stubs;
 640    }
 641    
 642    
 643    
 644    /**
 645     * Throws an exception on validation failure.
 646     */
 647    this._validate = function(pagesetShorthand)
 648    {
 649        var msg = "Pageset validation error:\n"
 650            + print_r(pagesetShorthand);
 651        if (!pagesetShorthand.name) {
 652            throw new PagesetException(msg + 'no name specified!');
 653        }
 654        if (!pagesetShorthand.description) {
 655            throw new PagesetException(msg + 'no description specified!');
 656        }
 657        if (!pagesetShorthand.paths &&
 658            !pagesetShorthand.pathRegexp &&
 659            !pagesetShorthand.pageContent) {
 660            throw new PagesetException(msg
 661                + 'no path, pathRegexp, or pageContent specified!');
 662        }
 663    };
 664    
 665    
 666    
 667    this.init = function(pagesetShorthand)
 668    {
 669        this._validate(pagesetShorthand);
 670        
 671        this.name = pagesetShorthand.name;
 672        this.description = pagesetShorthand.description;
 673        
 674        var pathPrefixRegexp = pagesetShorthand.pathPrefix
 675            ? RegExp.escape(pagesetShorthand.pathPrefix) : "";
 676        var pathRegexp = '^' + pathPrefixRegexp;
 677        
 678        if (pagesetShorthand.paths != undefined) {
 679            pathRegexp += '(?:';
 680            for (var i = 0; i < pagesetShorthand.paths.length; ++i) {
 681                if (i > 0) {
 682                    pathRegexp += '|';
 683                }
 684                pathRegexp += RegExp.escape(pagesetShorthand.paths[i]);
 685            }
 686            pathRegexp += ')$';
 687        }
 688        else if (pagesetShorthand.pathRegexp) {
 689            pathRegexp += '(?:' + pagesetShorthand.pathRegexp + ')$';
 690        }
 691
 692        this.pathRegexp = new RegExp(pathRegexp);
 693        this.paramRegexps = {};
 694        for (var paramName in pagesetShorthand.paramRegexps) {
 695            this.paramRegexps[paramName] =
 696                new RegExp(pagesetShorthand.paramRegexps[paramName]);
 697        }
 698        this.pageContent = pagesetShorthand.pageContent ||
 699            function() { return true; };
 700        this.uiElements = {};
 701    };
 702    
 703    
 704    
 705    this.init(pagesetShorthand);
 706}
 707
 708
 709
 710/**
 711 * Construct the UI map object, and return it. Once the object is instantiated,
 712 * it binds to a global variable and will not leave scope.
 713 *
 714 * @return  new UIMap object
 715 */
 716function UIMap()
 717{
 718    // the singleton pattern, split into two parts so that "new" can still
 719    // be used, in addition to "getInstance()"
 720    UIMap.self = this;
 721    
 722    // need to attach variables directly to the Editor object in order for them
 723    // to be in scope for Editor methods
 724    if (is_IDE()) {
 725        Editor.uiMap = this;
 726        Editor.UI_PREFIX = UI_GLOBAL.UI_PREFIX;
 727    }
 728    
 729    this.pagesets = new Object();
 730    
 731    
 732    
 733    /**
 734     * pageset[pagesetName]
 735     *   regexp
 736     *   elements[elementName]
 737     *     UIElement
 738     */
 739    this.addPageset = function(pagesetShorthand)
 740    {
 741        try {
 742            var pageset = new Pageset(pagesetShorthand);
 743        }
 744        catch (e) {
 745            safe_alert("Could not create pageset from shorthand:\n"
 746                + print_r(pagesetShorthand) + "\n" + e.message);
 747            return false;
 748        }
 749        
 750        if (this.pagesets[pageset.name]) {
 751            safe_alert('Could not add pageset "' + pageset.name
 752                + '": a pageset with that name already exists!');
 753            return false;
 754        }
 755        
 756        this.pagesets[pageset.name] = pageset;
 757        return true;
 758    };
 759    
 760    
 761    
 762    /**
 763     * @param pagesetName
 764     * @param uiElementShorthand  a representation of a UIElement object in
 765     *                            shorthand JSON.
 766     */
 767    this.addElement = function(pagesetName, uiElementShorthand)
 768    {
 769        try {
 770            var uiElement = new UIElement(uiElementShorthand);
 771        }
 772        catch (e) {
 773            safe_alert("Could not create UI element from shorthand:\n"
 774                + print_r(uiElementShorthand) + "\n" + e.message);
 775            return false;
 776        }
 777        
 778        // run the element's unit tests only for the IDE, and only when the
 779        // IDE is starting. Make a rough guess as to the latter condition.
 780        if (is_IDE() && !editor.selDebugger && !uiElement.test()) {
 781            safe_alert('Could not add UI element "' + uiElement.name
 782                + '": failed testcases!');
 783            return false;
 784        }
 785        
 786        try {
 787            this.pagesets[pagesetName].uiElements[uiElement.name] = uiElement;
 788        }
 789        catch (e) {
 790            safe_alert("Could not add UI element '" + uiElement.name
 791                + "' to pageset '" + pagesetName + "':\n" + e.message);
 792            return false;
 793        }
 794        
 795        return true;
 796    };
 797    
 798    
 799    
 800    /**
 801     * Returns the pageset for a given UI specifier string.
 802     *
 803     * @param uiSpecifierString
 804     * @return  a pageset object
 805     */
 806    this.getPageset = function(uiSpecifierString)
 807    {
 808        try {
 809            var uiSpecifier = new UISpecifier(uiSpecifierString);
 810            return this.pagesets[uiSpecifier.pagesetName];
 811        }
 812        catch (e) {
 813            return null;
 814        }
 815    }
 816    
 817    
 818    
 819    /**
 820     * Returns the UIElement that a UISpecifierString or pageset and element
 821     * pair refer to.
 822     *
 823     * @param pagesetNameOrUISpecifierString
 824     * @return  a UIElement, or null if none is found associated with
 825     *          uiSpecifierString
 826     */
 827    this.getUIElement = function(pagesetNameOrUISpecifierString, uiElementName)
 828    {
 829        var pagesetName = pagesetNameOrUISpecifierString;
 830        if (arguments.length == 1) {
 831            var uiSpecifierString = pagesetNameOrUISpecifierString;
 832            try {
 833                var uiSpecifier = new UISpecifier(uiSpecifierString);
 834                pagesetName = uiSpecifier.pagesetName;
 835                var uiElementName = uiSpecifier.elementName;
 836            }
 837            catch (e) {
 838                return null;
 839            }
 840        }
 841        try {
 842            return this.pagesets[pagesetName].uiElements[uiElementName];
 843        }
 844        catch (e) {
 845            return null;
 846        }
 847    };
 848    
 849    
 850    
 851    /**
 852     * Returns a list of pagesets that "contains" the provided page,
 853     * represented as a document object. Containership is defined by the
 854     * Pageset object's contain() method.
 855     *
 856     * @param inDocument  the page to get pagesets for
 857     * @return            a list of pagesets
 858     */
 859    this.getPagesetsForPage = function(inDocument)
 860    {
 861        var pagesets = [];
 862        for (var pagesetName in this.pagesets) {
 863            var pageset = this.pagesets[pagesetName];
 864            if (pageset.contains(inDocument)) {
 865                pagesets.push(pageset);
 866            }
 867        }
 868        return pagesets;
 869    };
 870    
 871    
 872    
 873    /**
 874     * Returns a list of all pagesets.
 875     *
 876     * @return  a list of pagesets
 877     */
 878    this.getPagesets = function()
 879    {
 880        var pagesets = [];
 881        for (var pagesetName in this.pagesets) {
 882            pagesets.push(this.pagesets[pagesetName]);
 883        }
 884        return pagesets;
 885    };
 886    
 887    
 888    
 889    /**
 890     * Returns a list of elements on a page that a given UI specifier string,
 891     * maps to. If no elements are mapped to, returns an empty list..
 892     *
 893     * @param   uiSpecifierString  a String that specifies a UI element with
 894     *                             attendant argument values
 895     * @param   inDocument         the document object the specified UI element
 896     *                             appears in
 897     * @return                     a potentially-empty list of elements
 898     *                             specified by uiSpecifierString
 899     */
 900    this.getPageElements = function(uiSpecifierString, inDocument)
 901    {
 902        var locator = this.getLocator(uiSpecifierString);
 903        var results = locator ? eval_locator(locator, inDocument) : [];
 904        return results;
 905    };
 906    
 907    
 908    
 909    /**
 910     * Returns the locator string that a given UI specifier string maps to, or
 911     * null if it cannot be mapped.
 912     *
 913     * @param uiSpecifierString
 914     */
 915    this.getLocator = function(uiSpecifierString)
 916    {
 917        try {
 918            var uiSpecifier = new UISpecifier(uiSpecifierString);
 919        }
 920        catch (e) {
 921            safe_alert('Could not create UISpecifier for string "'
 922                + uiSpecifierString + '": ' + e.message);
 923            return null;
 924        }
 925        
 926        var uiElement = this.getUIElement(uiSpecifier.pagesetName,
 927            uiSpecifier.elementName);
 928        try {
 929            return uiElement.getLocator(uiSpecifier.args);
 930        }
 931        catch (e) {
 932            return null;
 933        }
 934    }
 935    
 936    
 937    
 938    /**
 939     * Finds and returns a UI specifier string given an element and the page
 940     * that it appears on.
 941     *
 942     * @param pageElement  the document element to map to a UI specifier
 943     * @param inDocument   the document the element appears in
 944     * @return             a UI specifier string, or false if one cannot be
 945     *                     constructed
 946     */
 947    this.getUISpecifierString = function(pageElement, inDocument)
 948    {
 949        var is_fuzzy_match =
 950            BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match;
 951        var pagesets = this.getPagesetsForPage(inDocument);
 952        for (var i = 0; i < pagesets.length; ++i) {
 953            var pageset = pagesets[i];
 954            var uiElements = pageset.getUIElements();
 955            for (var j = 0; j < uiElements.length; ++j) {
 956                var uiElement = uiElements[j];
 957                
 958                // first test against the generic locator, if there is one.
 959                // This should net some performance benefit when recording on
 960                // more complicated pages.
 961                if (uiElement.getGenericLocator) {
 962                    var passedTest = false;
 963                    var results =
 964                        eval_locator(uiElement.getGenericLocator(), inDocument);
 965                    for (var i = 0; i < results.length; ++i) {
 966                        if (results[i] == pageElement) {
 967                            passedTest = true;
 968                            break;
 969                        }
 970                    }
 971                    if (!passedTest) {
 972                        continue;
 973                    }
 974                }
 975                
 976                var defaultLocators;
 977                if (uiElement.isDefaultLocatorConstructionDeferred) {
 978                    defaultLocators = uiElement.getDefaultLocators(inDocument);
 979                }
 980                else {
 981                    defaultLocators = uiElement.defaultLocators;
 982                }
 983                
 984                //safe_alert(print_r(uiElement.defaultLocators));
 985                for (var locator in defaultLocators) {
 986                    var locatedElements = eval_locator(locator, inDocument);
 987                    if (locatedElements.length) {
 988                        var locatedElement = locatedElements[0];
 989                    }
 990                    else {
 991                        continue;
 992                    }
 993                    
 994                    // use a heuristic to determine whether the element
 995                    // specified is the "same" as the element we're matching
 996                    if (is_fuzzy_match) {
 997                        if (is_fuzzy_match(locatedElement, pageElement)) {
 998                            return UI_GLOBAL.UI_PREFIX + '=' +
 999                                new UISpecifier(pageset.name, uiElement.name,
1000                                    defaultLocators[locator]);
1001                        }
1002                    }
1003                    else {
1004                        if (locatedElement == pageElement) {
1005                            return UI_GLOBAL.UI_PREFIX + '=' +
1006                                new UISpecifier(pageset.name, uiElement.name,
1007                                    defaultLocators[locator]);
1008                        }
1009                    }
1010                    // ok, matching the element failed. See if an offset
1011                    // locator can complete the match.
1012                    if (uiElement.getOffsetLocator) {
1013                        for (var i = 0; i < locatedElements.length; ++i) {
1014                            var offsetLocator = uiElement
1015                                .getOffsetLocator(locatedElement, pageElement);
1016                            if (offsetLocator) {
1017                                return UI_GLOBAL.UI_PREFIX + '=' +
1018                                    new UISpecifier(pageset.name,
1019                                        uiElement.name,
1020                                        defaultLocators[locator])
1021                                    + '->' + offsetLocator;
1022                            }
1023                        }
1024                    }
1025                }
1026            }
1027        }
1028        return false;
1029    };
1030    
1031    
1032    
1033    /**
1034     * Returns a sorted list of UI specifier string stubs representing possible
1035     * UI elements for all pagesets, paired the their descriptions. Stubs
1036     * contain all required arguments, but leave argument values blank.
1037     *
1038     * @return  a list of UI specifier string stubs
1039     */
1040    this.getUISpecifierStringStubs = function() {
1041        var stubs = [];
1042        var pagesets = this.getPagesets();
1043        for (var i = 0; i < pagesets.length; ++i) {
1044            stubs = stubs.concat(pagesets[i].getUISpecifierStringStubs());
1045        }
1046        stubs.sort(function(a, b) {
1047            if (a[0] < b[0]) {
1048                return -1;
1049            }
1050            return a[0] == b[0] ? 0 : 1;
1051        });
1052        return stubs;
1053    }
1054}
1055
1056UIMap.getInstance = function() {
1057    return (UIMap.self == null) ? new UIMap() : UIMap.self;
1058}
1059
1060//******************************************************************************
1061// Rollups
1062
1063/**
1064 * The Command object isn't available in the Selenium RC. We introduce an
1065 * object with the identical constructor. In the IDE, this will be redefined,
1066 * which is just fine.
1067 *
1068 * @param command
1069 * @param target
1070 * @param value
1071 */
1072if (typeof(Command) == 'undefined') {
1073    function Command(command, target, value) {
1074        this.command = command != null ? command : '';
1075        this.target = target != null ? target : '';
1076        this.value = value != null ? value : '';
1077    }
1078}
1079
1080
1081
1082/**
1083 * A CommandMatcher object matches commands during the application of a
1084 * RollupRule. It's specified with a shorthand format, for example:
1085 *
1086 *  new CommandMatcher({
1087 *      command: 'click'
1088 *      , target: 'ui=allPages::.+'
1089 *  })
1090 *
1091 * which is intended to match click commands whose target is an element in the
1092 * allPages PageSet. The matching expressions are given as regular expressions;
1093 * in the example above, the command must be "click"; "clickAndWait" would be
1094 * acceptable if 'click.*' were used. Here's a more complete example:
1095 *
1096 *  new CommandMatcher({
1097 *      command: 'type'
1098 *      , target: 'ui=loginPages::username()'
1099 *      , value: '.+_test'
1100 *      , updateArgs: function(command, args) {
1101 *          args.username = command.value;
1102 *      }
1103 *  })
1104 *
1105 * Here, the command and target are fixed, but there is variability in the 
1106 * value of the command. When a command matches, the username is saved to the
1107 * arguments object.
1108 */
1109function CommandMatcher(commandMatcherShorthand)
1110{
1111    /**
1112     * Ensure the shorthand notation used to initialize the CommandMatcher has
1113     * all required values.
1114     *
1115     * @param commandMatcherShorthand  an object containing information about
1116     *                                 the CommandMatcher
1117     */
1118    this.validate = function(commandMatcherShorthand) {
1119        var msg = "CommandMatcher validation error:\n"
1120            + print_r(commandMatcherShorthand);
1121        if (!commandMatcherShorthand.command) {
1122            throw new CommandMatcherException(msg + 'no command specified!');
1123        }
1124        if (!commandMatcherShorthand.target) {
1125            throw new CommandMatcherException(msg + 'no target specified!');
1126        }
1127        if (commandMatcherShorthand.minMatches &&
1128            commandMatcherShorthand.maxMatches &&
1129            commandMatcherShorthand.minMatches >
1130            commandMatcherShorthand.maxMatches) {
1131            throw new CommandMatcherException(msg + 'minMatches > maxMatches!');
1132        }
1133    };
1134
1135    /**
1136     * Initialize this object.
1137     *
1138     * @param commandMatcherShorthand  an object containing information used to
1139     *                                 initialize the CommandMatcher
1140     */
1141    this.init = function(commandMatcherShorthand) {
1142        this.validate(commandMatcherShorthand);
1143        
1144        this.command = commandMatcherShorthand.command;
1145        this.target = commandMatcherShorthand.target;
1146        this.value = commandMatcherShorthand.value || null;
1147        this.minMatches = commandMatcherShorthand.minMatches || 1;
1148        this.maxMatches = commandMatcherShorthand.maxMatches || 1;
1149        this.updateArgs = commandMatcherShorthand.updateArgs ||
1150            function(command, args) { return args; };
1151    };
1152    
1153    /**
1154     * Determines whether a given command matches. Updates args by "reference"
1155     * and returns true if it does; return false otherwise.
1156     *
1157     * @param command  the command to attempt to match
1158     */
1159    this.isMatch = function(command) {
1160        var re = new RegExp('^' + this.command + '$');
1161        if (! re.test(command.command)) {
1162            return false;
1163        }
1164        re = new RegExp('^' + this.target + '$');
1165        if (! re.test(command.target)) {
1166            return false;
1167        }
1168        if (this.value != null) {
1169            re = new RegExp('^' + this.value + '$');
1170            if (! re.test(command.value)) {
1171                return false;
1172            }
1173        }
1174        
1175        // okay, the command matches
1176        return true;
1177    };
1178    
1179    // initialization
1180    this.init(commandMatcherShorthand);
1181}
1182
1183
1184
1185function RollupRuleException(message)
1186{
1187    this.message = message;
1188    this.name = 'RollupRuleException';
1189}
1190
1191function RollupRule(rollupRuleShorthand)
1192{
1193    /**
1194     * Ensure the shorthand notation used to initialize the RollupRule has all
1195     * required values.
1196     *
1197     * @param rollupRuleShorthand  an object containing information about the
1198     *                             RollupRule
1199     */
1200    this.validate = function(rollupRuleShorthand) {
1201        var msg = "RollupRule validation error:\n"
1202            + print_r(rollupRuleShorthand);
1203        if (!rollupRuleShorthand.name) {
1204            throw new RollupRuleException(msg + 'no name specified!');
1205        }
1206        if (!rollupRuleShorthand.description) {
1207            throw new RollupRuleException(msg + 'no description specified!');
1208        }
1209        // rollupRuleShorthand.args is optional
1210        if (!rollupRuleShorthand.commandMatchers &&
1211            !rollupRuleShorthand.getRollup) {
1212            throw new RollupRuleException(msg
1213                + 'no command matchers specified!');
1214        }
1215        if (!rollupRuleShorthand.expandedCommands &&
1216            !rollupRuleShorthand.getExpandedCommands) {
1217            throw new RollupRuleException(msg
1218                + 'no expanded commands specified!');
1219        }
1220        
1221        return true;
1222    };
1223
1224    /**
1225     * Initialize this object.
1226     *
1227     * @param rollupRuleShorthand  an object containing information used to
1228     *                             initialize the RollupRule
1229     */
1230    this.init = function(rollupRuleShorthand) {
1231        this.validate(rollupRuleShorthand);
1232        
1233        this.name = rollupRuleShorthand.name;
1234        this.description = rollupRuleShorthand.description;
1235        this.pre = rollupRuleShorthand.pre || '';
1236        this.post = rollupRuleShorthand.post || '';
1237        this.alternateCommand = rollupRuleShorthand.alternateCommand;
1238        this.args = rollupRuleShorthand.args || [];
1239        
1240        if (rollupRuleShorthand.commandMatchers) {
1241            // construct the rule from the list of CommandMatchers
1242            this.commandMatchers = [];
1243            var matchers = rollupRuleShorthand.commandMatchers;
1244            for (var i = 0; i < matchers.length; ++i) {
1245                if (matchers[i].updateArgs && this.args.length == 0) {
1246                    // enforce metadata for arguments
1247                    var msg = "RollupRule validation error:\n"
1248                        + print_r(rollupRuleShorthand)
1249                        + 'no argument metadata provided!';
1250                    throw new RollupRuleException(msg);
1251                }
1252                this.commandMatchers.push(new CommandMatcher(matchers[i]));
1253            }
1254            
1255            // returns false if the rollup doesn't match, or a rollup command
1256            // if it does. If returned, the command contains the
1257            // replacementIndexes property, which indicates which commands it
1258            // substitutes for.
1259            this.getRollup = function(commands) {
1260                // this is a greedy matching algorithm
1261                var replacementIndexes = [];
1262                var commandMatcherQueue = this.commandMatchers;
1263                var matchCount = 0;
1264                var args = {};
1265                for (var i = 0, j = 0; i < commandMatcherQueue.length;) {
1266                    var matcher = commandMatcherQueue[i];
1267                    if (j >= commands.length) {
1268                        // we've run out of commands! If the remaining matchers
1269                        // do not have minMatches requirements, this is a
1270                        // match. Otherwise, it's not.
1271                        if (matcher.minMatches > 0) {
1272                            return false;
1273                        }
1274                        ++i;
1275                        matchCount = 0; // unnecessary, but let's be consistent
1276                    }
1277                    else {
1278                        if (matcher.isMatch(commands[j])) {
1279                            ++matchCount;
1280                            if (matchCount == matcher.maxMatches) {
1281                                // exhausted this matcher's matches ... move on
1282                                // to next matcher
1283                                ++i;
1284                                matchCount = 0;
1285                            }
1286                            args = matcher.updateArgs(commands[j], args);
1287                            replacementIndexes.push(j);
1288                            ++j; // move on to next command
1289                        }
1290                        else {
1291                            //alert(matchCount + ', ' + matcher.minMatches);
1292                            if (matchCount < matcher.minMatches) {
1293                                return false;
1294                            }
1295                            // didn't match this time, but we've satisfied the
1296                            // requirements already ... move on to next matcher
1297                            ++i;
1298                            matchCount = 0;
1299                            // still gonna look at same command
1300                        }
1301                    }
1302                }
1303                
1304                var rollup;
1305                if (this.alternateCommand) {
1306                    rollup = new Command(this.alternateCommand,
1307                        commands[0].target, commands[0].value);
1308                }
1309                else {
1310                    rollup = new Command('rollup', this.name);
1311                    rollup.value = to_kwargs(args);
1312                }
1313                rollup.replacementIndexes = replacementIndexes;
1314                return rollup;
1315            };
1316        }
1317        else {
1318            this.getRollup = function(commands) {
1319                var result = rollupRuleShorthand.getRollup(commands);
1320                if (result) {
1321                    var rollup = new Command(
1322                        result.command
1323                        , result.target
1324                        , result.value
1325                    );
1326                    rollup.replacementIndexes = result.replacementIndexes;
1327                    return rollup;
1328                }
1329                return false;
1330            };
1331        }
1332        
1333        this.getExpandedCommands = function(kwargs) {
1334            var commands = [];
1335            var expandedCommands = (rollupRuleShorthand.expandedCommands
1336                ? rollupRuleShorthand.expandedCommands
1337                : rollupRuleShorthand.getExpandedCommands(
1338                    parse_kwargs(kwargs)));
1339            for (var i = 0; i < expandedCommands.length; ++i) {
1340                var command = expandedCommands[i];
1341                commands.push(new Command(
1342                    command.command
1343                    , command.target
1344                    , command.value
1345                ));
1346            }
1347            return commands;
1348        };
1349    };
1350    
1351    this.init(rollupRuleShorthand);
1352}
1353
1354
1355
1356/**
1357 *
1358 */
1359function RollupManager()
1360{
1361    // singleton pattern
1362    RollupManager.self = this;
1363    
1364    this.init = function()
1365    {
1366        this.rollupRules = {};
1367        if (is_IDE()) {
1368            Editor.rollupManager = this;
1369        }
1370    };
1371
1372    /**
1373     * Adds a new RollupRule to the repository. Returns true on success, or
1374     * false if the rule couldn't be added.
1375     *
1376     * @param rollupRuleShorthand  shorthand JSON specification of the new
1377     *                             RollupRule, possibly including CommandMatcher
1378     *                             shorthand too.
1379     * @return                     true if the rule was added successfully,
1380     *                             false otherwise.
1381     */
1382    this.addRollupRule = function(rollupRuleShorthand)
1383    {
1384        try {
1385            var rule = new RollupRule(rollupRuleShorthand);
1386            this.rollupRules[rule.name] = rule;
1387        }
1388        catch(e) {
1389            smart_alert("Could not create RollupRule from shorthand:\n\n"
1390                + e.message);
1391            return false;
1392        }
1393        return true;
1394    };
1395    
1396    /**
1397     * Returns a RollupRule by name.
1398     *
1399     * @param rollupName  the name of the rule to fetch
1400     * @return            the RollupRule, or null if it isn't found.
1401     */
1402    this.getRollupRule = function(rollupName)
1403    {
1404        return (this.rollupRules[rollupName] || null);
1405    };
1406    
1407    /**
1408     * Returns a list of name-description pairs for use in populating the
1409     * auto-populated target dropdown in the IDE. Rules that have an alternate
1410     * command defined are not included in the list, as they are not bona-fide
1411     * rollups.
1412     *
1413     * @return  a list of name-description pairs
1414     */
1415    this.getRollupRulesForDropdown = function()
1416    {
1417        var targets = [];
1418        var names = keys(this.rollupRules).sort();
1419        for (var i = 0; i < names.length; ++i) {
1420            var name = names[i];
1421            if (this.rollupRules[name].alternateCommand) {
1422                continue;
1423            }
1424            targets.push([ name, this.rollupRules[name].description ]);
1425        }
1426        return targets;
1427    };
1428    
1429    /**
1430     * Applies all rules to the current editor commands, asking the user in
1431     * each case if it's okay to perform the replacement. The rules are applied
1432     * repeatedly until there are no more matches. The algorithm should
1433     * remember when the user has declined a replacement, and not ask to do it
1434     * again.
1435     *
1436     * @return  the list of commands with rollup replacements performed
1437     */
1438    this.applyRollupRules = function()
1439    {
1440        var commands = editor.getTestCase().commands;
1441        var blacklistedRollups = {};
1442    
1443        // so long as rollups were performed, we need to keep iterating through
1444        // the commands starting at the beginning, because further rollups may
1445        // potentially be applied on the newly created ones.
1446        while (true) {
1447            var performedRollup = false;
1448            for (var i = 0; i < commands.length; ++i) {
1449                // iterate through commands
1450                for (var rollupName in th

Large files files are truncated, but you can click here to view the full file