/testing/selenium-core/scripts/ui-element.js
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