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