| //****************************************************************************** | |
| // Globals, including constants | |
| var UI_GLOBAL = { | |
| UI_PREFIX: 'ui' | |
| , XHTML_DOCTYPE: '<!DOCTYPE html PUBLIC ' | |
| + '"-//W3C//DTD XHTML 1.0 Strict//EN" ' | |
| + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' | |
| , XHTML_XMLNS: 'http://www.w3.org/1999/xhtml' | |
| }; | |
| //***************************************************************************** | |
| // Exceptions | |
| function UIElementException(message) | |
| { | |
| this.message = message; | |
| this.name = 'UIElementException'; | |
| } | |
| function UIArgumentException(message) | |
| { | |
| this.message = message; | |
| this.name = 'UIArgumentException'; | |
| } | |
| function PagesetException(message) | |
| { | |
| this.message = message; | |
| this.name = 'PagesetException'; | |
| } | |
| function UISpecifierException(message) | |
| { | |
| this.message = message; | |
| this.name = 'UISpecifierException'; | |
| } | |
| function CommandMatcherException(message) | |
| { | |
| this.message = message; | |
| this.name = 'CommandMatcherException'; | |
| } | |
| //***************************************************************************** | |
| // UI-Element core | |
| /** | |
| * The UIElement object. This has been crafted along with UIMap to make | |
| * specifying UI elements using JSON as simple as possible. Object construction | |
| * will fail if 1) a proper name isn't provided, 2) a faulty args argument is | |
| * given, or 3) getLocator() returns undefined for a valid permutation of | |
| * default argument values. See ui-doc.html for the documentation on the | |
| * builder syntax. | |
| * | |
| * @param uiElementShorthand an object whose contents conform to the | |
| * UI-Element builder syntax. | |
| * | |
| * @return a new UIElement object | |
| * @throws UIElementException | |
| */ | |
| function UIElement(uiElementShorthand) | |
| { | |
| // a shorthand object might look like: | |
| // | |
| // { | |
| // name: 'topic' | |
| // , description: 'sidebar links to topic categories' | |
| // , args: [ | |
| // { | |
| // name: 'name' | |
| // , description: 'the name of the topic' | |
| // , defaultValues: topLevelTopics | |
| // } | |
| // ] | |
| // , getLocator: function(args) { | |
| // return this._listXPath + | |
| // "/a[text()=" + args.name.quoteForXPath() + "]"; | |
| // } | |
| // , getGenericLocator: function() { | |
| // return this._listXPath + '/a'; | |
| // } | |
| // // maintain testcases for getLocator() | |
| // , testcase1: { | |
| // // defaultValues used if args not specified | |
| // args: { name: 'foo' } | |
| // , xhtml: '<div id="topiclist">' | |
| // + '<ul><li><a expected-result="1">foo</a></li></ul>' | |
| // + '</div>' | |
| // } | |
| // // set a local element variable | |
| // , _listXPath: "//div[@id='topiclist']/ul/li" | |
| // } | |
| // | |
| // name cannot be null or an empty string. Enforce the same requirement for | |
| // the description. | |
| /** | |
| * Recursively returns all permutations of argument-value pairs, given | |
| * a list of argument definitions. Each argument definition will have | |
| * a set of default values to use in generating said pairs. If an argument | |
| * has no default values defined, it will not be included among the | |
| * permutations. | |
| * | |
| * @param args a list of UIArguments | |
| * @param inDocument the document object to pass to the getDefaultValues() | |
| * method of each argument. | |
| * | |
| * @return a list of associative arrays containing key value pairs | |
| */ | |
| this.permuteArgs = function(args, inDocument) { | |
| if (args.length == 0) { | |
| return []; | |
| } | |
| var permutations = []; | |
| var arg = args[0]; | |
| var remainingArgs = args.slice(1); | |
| var subsequentPermutations = this.permuteArgs(remainingArgs, | |
| inDocument); | |
| var defaultValues = arg.getDefaultValues(inDocument); | |
| // skip arguments for which no default values are defined. If the | |
| // argument is a required one, then no permutations are possible. | |
| if (defaultValues.length == 0) { | |
| if (arg.required) { | |
| return []; | |
| } | |
| else { | |
| return subsequentPermutations; | |
| } | |
| } | |
| for (var i = 0; i < defaultValues.length; ++i) { | |
| var value = defaultValues[i]; | |
| var permutation; | |
| if (subsequentPermutations.length == 0) { | |
| permutation = {}; | |
| permutation[arg.name] = value + ""; | |
| permutations.push(permutation); | |
| } | |
| else { | |
| for (var j = 0; j < subsequentPermutations.length; ++j) { | |
| permutation = clone(subsequentPermutations[j]); | |
| permutation[arg.name] = value + ""; | |
| permutations.push(permutation); | |
| } | |
| } | |
| } | |
| return permutations; | |
| } | |
| /** | |
| * Returns a list of all testcases for this UIElement. | |
| */ | |
| this.getTestcases = function() | |
| { | |
| return this.testcases; | |
| } | |
| /** | |
| * Run all unit tests, stopping at the first failure, if any. Return true | |
| * if no failures encountered, false otherwise. See the following thread | |
| * regarding use of getElementById() on XML documents created by parsing | |
| * text via the DOMParser: | |
| * | |
| * http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/2b1b82b3c53a1282/ | |
| */ | |
| this.test = function() | |
| { | |
| var parser = new DOMParser(); | |
| var testcases = this.getTestcases(); | |
| testcaseLoop: for (var i = 0; i < testcases.length; ++i) { | |
| var testcase = testcases[i]; | |
| var xhtml = UI_GLOBAL.XHTML_DOCTYPE + '<html xmlns="' | |
| + UI_GLOBAL.XHTML_XMLNS + '">' + testcase.xhtml + '</html>'; | |
| var doc = parser.parseFromString(xhtml, "text/xml"); | |
| if (doc.firstChild.nodeName == 'parsererror') { | |
| safe_alert('Error parsing XHTML in testcase "' + testcase.name | |
| + '" for UI element "' + this.name + '": ' + "\n" | |
| + doc.firstChild.firstChild.nodeValue); | |
| } | |
| // we're no longer using the default locators when testing, because | |
| // args is now required | |
| var locator = parse_locator(this.getLocator(testcase.args)); | |
| var results; | |
| if (locator.type == 'xpath' || (locator.type == 'implicit' && | |
| locator.string.substring(0, 2) == '//')) { | |
| // try using the javascript xpath engine to avoid namespace | |
| // issues. The xpath does have to be lowercase however, it | |
| // seems. | |
| results = eval_xpath(locator.string, doc, | |
| { allowNativeXpath: false, returnOnFirstMatch: true }); | |
| } | |
| else { | |
| // piece the locator back together | |
| locator = (locator.type == 'implicit') | |
| ? locator.string | |
| : locator.type + '=' + locator.string; | |
| results = eval_locator(locator, doc); | |
| } | |
| if (results.length && results[0].hasAttribute('expected-result')) { | |
| continue testcaseLoop; | |
| } | |
| // testcase failed | |
| if (is_IDE()) { | |
| var msg = 'Testcase "' + testcase.name | |
| + '" failed for UI element "' + this.name + '":'; | |
| if (!results.length) { | |
| msg += '\n"' + (locator.string || locator) + '" did not match any elements!'; | |
| } | |
| else { | |
| msg += '\n' + results[0] + ' was not the expected result!'; | |
| } | |
| safe_alert(msg); | |
| } | |
| return false; | |
| } | |
| return true; | |
| }; | |
| /** | |
| * Creates a set of locators using permutations of default values for | |
| * arguments used in the locator construction. The set is returned as an | |
| * object mapping locators to key-value arguments objects containing the | |
| * values passed to getLocator() to create the locator. | |
| * | |
| * @param opt_inDocument (optional) the document object of the "current" | |
| * page when this method is invoked. Some arguments | |
| * may have default value lists that are calculated | |
| * based on the contents of the page. | |
| * | |
| * @return a list of locator strings | |
| * @throws UIElementException | |
| */ | |
| this.getDefaultLocators = function(opt_inDocument) { | |
| var defaultLocators = {}; | |
| if (this.args.length == 0) { | |
| defaultLocators[this.getLocator({})] = {}; | |
| } | |
| else { | |
| var permutations = this.permuteArgs(this.args, opt_inDocument); | |
| if (permutations.length != 0) { | |
| for (var i = 0; i < permutations.length; ++i) { | |
| var args = permutations[i]; | |
| var locator = this.getLocator(args); | |
| if (!locator) { | |
| throw new UIElementException('Error in UIElement(): ' | |
| + 'no getLocator return value for element "' + name | |
| + '"'); | |
| } | |
| defaultLocators[locator] = args; | |
| } | |
| } | |
| else { | |
| // try using no arguments. Parse the locator to make sure it's | |
| // really good. If it doesn't work, fine. | |
| try { | |
| var locator = this.getLocator(); | |
| parse_locator(locator); | |
| defaultLocators[locator] = {}; | |
| } | |
| catch (e) { | |
| safe_log('debug', e.message); | |
| } | |
| } | |
| } | |
| return defaultLocators; | |
| }; | |
| /** | |
| * Validate the structure of the shorthand notation this object is being | |
| * initialized with. Throws an exception if there's a validation error. | |
| * | |
| * @param uiElementShorthand | |
| * | |
| * @throws UIElementException | |
| */ | |
| this.validate = function(uiElementShorthand) | |
| { | |
| var msg = "UIElement validation error:\n" + print_r(uiElementShorthand); | |
| if (!uiElementShorthand.name) { | |
| throw new UIElementException(msg + 'no name specified!'); | |
| } | |
| if (!uiElementShorthand.description) { | |
| throw new UIElementException(msg + 'no description specified!'); | |
| } | |
| if (!uiElementShorthand.locator | |
| && !uiElementShorthand.getLocator | |
| && !uiElementShorthand.xpath | |
| && !uiElementShorthand.getXPath) { | |
| throw new UIElementException(msg + 'no locator specified!'); | |
| } | |
| }; | |
| this.init = function(uiElementShorthand) | |
| { | |
| this.validate(uiElementShorthand); | |
| this.name = uiElementShorthand.name; | |
| this.description = uiElementShorthand.description; | |
| // construct a new getLocator() method based on the locator property, | |
| // or use the provided function. We're deprecating the xpath property | |
| // and getXPath() function, but still allow for them for backwards | |
| // compatability. | |
| if (uiElementShorthand.locator) { | |
| this.getLocator = function(args) { | |
| return uiElementShorthand.locator; | |
| }; | |
| } | |
| else if (uiElementShorthand.getLocator) { | |
| this.getLocator = uiElementShorthand.getLocator; | |
| } | |
| else if (uiElementShorthand.xpath) { | |
| this.getLocator = function(args) { | |
| return uiElementShorthand.xpath; | |
| }; | |
| } | |
| else { | |
| this.getLocator = uiElementShorthand.getXPath; | |
| } | |
| if (uiElementShorthand.genericLocator) { | |
| this.getGenericLocator = function() { | |
| return uiElementShorthand.genericLocator; | |
| }; | |
| } | |
| else if (uiElementShorthand.getGenericLocator) { | |
| this.getGenericLocator = uiElementShorthand.getGenericLocator; | |
| } | |
| if (uiElementShorthand.getOffsetLocator) { | |
| this.getOffsetLocator = uiElementShorthand.getOffsetLocator; | |
| } | |
| // get the testcases and local variables | |
| this.testcases = []; | |
| var localVars = {}; | |
| for (var attr in uiElementShorthand) { | |
| if (attr.match(/^testcase/)) { | |
| var testcase = uiElementShorthand[attr]; | |
| if (uiElementShorthand.args && | |
| uiElementShorthand.args.length && !testcase.args) { | |
| safe_alert('No args defined in ' + attr + ' for UI element ' | |
| + this.name + '! Skipping testcase.'); | |
| continue; | |
| } | |
| testcase.name = attr; | |
| this.testcases.push(testcase); | |
| } | |
| else if (attr.match(/^_/)) { | |
| this[attr] = uiElementShorthand[attr]; | |
| localVars[attr] = uiElementShorthand[attr]; | |
| } | |
| } | |
| // create the arguments | |
| this.args = [] | |
| this.argsOrder = []; | |
| if (uiElementShorthand.args) { | |
| for (var i = 0; i < uiElementShorthand.args.length; ++i) { | |
| var arg = new UIArgument(uiElementShorthand.args[i], localVars); | |
| this.args.push(arg); | |
| this.argsOrder.push(arg.name); | |
| // if an exception is thrown when invoking getDefaultValues() | |
| // with no parameters passed in, assume the method requires an | |
| // inDocument parameter, and thus may only be invoked at run | |
| // time. Mark the UI element object accordingly. | |
| try { | |
| arg.getDefaultValues(); | |
| } | |
| catch (e) { | |
| this.isDefaultLocatorConstructionDeferred = true; | |
| } | |
| } | |
| } | |
| if (!this.isDefaultLocatorConstructionDeferred) { | |
| this.defaultLocators = this.getDefaultLocators(); | |
| } | |
| }; | |
| this.init(uiElementShorthand); | |
| } | |
| // hang this off the UIElement "namespace". This is a composite strategy. | |
| UIElement.defaultOffsetLocatorStrategy = function(locatedElement, pageElement) { | |
| var strategies = [ | |
| UIElement.linkXPathOffsetLocatorStrategy | |
| , UIElement.preferredAttributeXPathOffsetLocatorStrategy | |
| , UIElement.simpleXPathOffsetLocatorStrategy | |
| ]; | |
| for (var i = 0; i < strategies.length; ++i) { | |
| var strategy = strategies[i]; | |
| var offsetLocator = strategy(locatedElement, pageElement); | |
| if (offsetLocator) { | |
| return offsetLocator; | |
| } | |
| } | |
| return null; | |
| }; | |
| UIElement.simpleXPathOffsetLocatorStrategy = function(locatedElement, | |
| pageElement) | |
| { | |
| if (is_ancestor(locatedElement, pageElement)) { | |
| var xpath = ""; | |
| var recorder = Recorder.get(locatedElement.ownerDocument.defaultView); | |
| var locatorBuilders = recorder.locatorBuilders; | |
| var currentNode = pageElement; | |
| while (currentNode != null && currentNode != locatedElement) { | |
| xpath = locatorBuilders.relativeXPathFromParent(currentNode) | |
| + xpath; | |
| currentNode = currentNode.parentNode; | |
| } | |
| var results = eval_xpath(xpath, locatedElement.ownerDocument, | |
| { contextNode: locatedElement }); | |
| if (results.length > 0 && results[0] == pageElement) { | |
| return xpath; | |
| } | |
| } | |
| return null; | |
| }; | |
| UIElement.linkXPathOffsetLocatorStrategy = function(locatedElement, pageElement) | |
| { | |
| if (pageElement.nodeName == 'A' && is_ancestor(locatedElement, pageElement)) | |
| { | |
| var text = pageElement.textContent | |
| .replace(/^\s+/, "") | |
| .replace(/\s+$/, ""); | |
| if (text) { | |
| var xpath = '/descendant::a[normalize-space()=' | |
| + text.quoteForXPath() + ']'; | |
| var results = eval_xpath(xpath, locatedElement.ownerDocument, | |
| { contextNode: locatedElement }); | |
| if (results.length > 0 && results[0] == pageElement) { | |
| return xpath; | |
| } | |
| } | |
| } | |
| return null; | |
| }; | |
| // compare to the "xpath:attributes" locator strategy defined in the IDE source | |
| UIElement.preferredAttributeXPathOffsetLocatorStrategy = | |
| function(locatedElement, pageElement) | |
| { | |
| // this is an ordered listing of single attributes | |
| var preferredAttributes = [ | |
| 'name' | |
| , 'value' | |
| , 'type' | |
| , 'action' | |
| , 'alt' | |
| , 'title' | |
| , 'class' | |
| , 'src' | |
| , 'href' | |
| , 'onclick' | |
| ]; | |
| if (is_ancestor(locatedElement, pageElement)) { | |
| var xpathBase = '/descendant::' + pageElement.nodeName.toLowerCase(); | |
| for (var i = 0; i < preferredAttributes.length; ++i) { | |
| var name = preferredAttributes[i]; | |
| var value = pageElement.getAttribute(name); | |
| if (value) { | |
| var xpath = xpathBase + '[@' + name + '=' | |
| + value.quoteForXPath() + ']'; | |
| var results = eval_xpath(xpath, locatedElement.ownerDocument, | |
| { contextNode: locatedElement }); | |
| if (results.length > 0 && results[0] == pageElement) { | |
| return xpath; | |
| } | |
| } | |
| } | |
| } | |
| return null; | |
| }; | |
| /** | |
| * Constructs a UIArgument. This is mostly for checking that the values are | |
| * valid. | |
| * | |
| * @param uiArgumentShorthand | |
| * @param localVars | |
| * | |
| * @throws UIArgumentException | |
| */ | |
| function UIArgument(uiArgumentShorthand, localVars) | |
| { | |
| /** | |
| * @param uiArgumentShorthand | |
| * | |
| * @throws UIArgumentException | |
| */ | |
| this.validate = function(uiArgumentShorthand) | |
| { | |
| var msg = "UIArgument validation error:\n" | |
| + print_r(uiArgumentShorthand); | |
| // try really hard to throw an exception! | |
| if (!uiArgumentShorthand.name) { | |
| throw new UIArgumentException(msg + 'no name specified!'); | |
| } | |
| if (!uiArgumentShorthand.description) { | |
| throw new UIArgumentException(msg + 'no description specified!'); | |
| } | |
| if (!uiArgumentShorthand.defaultValues && | |
| !uiArgumentShorthand.getDefaultValues) { | |
| throw new UIArgumentException(msg + 'no default values specified!'); | |
| } | |
| }; | |
| /** | |
| * @param uiArgumentShorthand | |
| * @param localVars a list of local variables | |
| */ | |
| this.init = function(uiArgumentShorthand, localVars) | |
| { | |
| this.validate(uiArgumentShorthand); | |
| this.name = uiArgumentShorthand.name; | |
| this.description = uiArgumentShorthand.description; | |
| this.required = uiArgumentShorthand.required || false; | |
| if (uiArgumentShorthand.defaultValues) { | |
| var defaultValues = uiArgumentShorthand.defaultValues; | |
| this.getDefaultValues = | |
| function() { return defaultValues; } | |
| } | |
| else { | |
| this.getDefaultValues = uiArgumentShorthand.getDefaultValues; | |
| } | |
| for (var name in localVars) { | |
| this[name] = localVars[name]; | |
| } | |
| } | |
| this.init(uiArgumentShorthand, localVars); | |
| } | |
| /** | |
| * The UISpecifier constructor is overloaded. If less than three arguments are | |
| * provided, the first argument will be considered a UI specifier string, and | |
| * will be split out accordingly. Otherwise, the first argument will be | |
| * considered the path. | |
| * | |
| * @param uiSpecifierStringOrPagesetName a UI specifier string, or the pageset | |
| * name of the UI specifier | |
| * @param elementName the name of the element | |
| * @param args an object associating keys to values | |
| * | |
| * @return new UISpecifier object | |
| */ | |
| function UISpecifier(uiSpecifierStringOrPagesetName, elementName, args) | |
| { | |
| /** | |
| * Initializes this object from a UI specifier string of the form: | |
| * | |
| * pagesetName::elementName(arg1=value1, arg2=value2, ...) | |
| * | |
| * into its component parts, and returns them as an object. | |
| * | |
| * @return an object containing the components of the UI specifier | |
| * @throws UISpecifierException | |
| */ | |
| this._initFromUISpecifierString = function(uiSpecifierString) { | |
| var matches = /^(.*)::([^\(]+)\((.*)\)$/.exec(uiSpecifierString); | |
| if (matches == null) { | |
| throw new UISpecifierException('Error in ' | |
| + 'UISpecifier._initFromUISpecifierString(): "' | |
| + this.string + '" is not a valid UI specifier string'); | |
| } | |
| this.pagesetName = matches[1]; | |
| this.elementName = matches[2]; | |
| this.args = (matches[3]) ? parse_kwargs(matches[3]) : {}; | |
| }; | |
| /** | |
| * Override the toString() method to return the UI specifier string when | |
| * evaluated in a string context. Combines the UI specifier components into | |
| * a canonical UI specifier string and returns it. | |
| * | |
| * @return a UI specifier string | |
| */ | |
| this.toString = function() { | |
| // empty string is acceptable for the path, but it must be defined | |
| if (this.pagesetName == undefined) { | |
| throw new UISpecifierException('Error in UISpecifier.toString(): "' | |
| + this.pagesetName + '" is not a valid UI specifier pageset ' | |
| + 'name'); | |
| } | |
| if (!this.elementName) { | |
| throw new UISpecifierException('Error in UISpecifier.unparse(): "' | |
| + this.elementName + '" is not a valid UI specifier element ' | |
| + 'name'); | |
| } | |
| if (!this.args) { | |
| throw new UISpecifierException('Error in UISpecifier.unparse(): "' | |
| + this.args + '" are not valid UI specifier args'); | |
| } | |
| uiElement = UIMap.getInstance() | |
| .getUIElement(this.pagesetName, this.elementName); | |
| if (uiElement != null) { | |
| var kwargs = to_kwargs(this.args, uiElement.argsOrder); | |
| } | |
| else { | |
| // probably under unit test | |
| var kwargs = to_kwargs(this.args); | |
| } | |
| return this.pagesetName + '::' + this.elementName + '(' + kwargs + ')'; | |
| }; | |
| // construct the object | |
| if (arguments.length < 2) { | |
| this._initFromUISpecifierString(uiSpecifierStringOrPagesetName); | |
| } | |
| else { | |
| this.pagesetName = uiSpecifierStringOrPagesetName; | |
| this.elementName = elementName; | |
| this.args = (args) ? clone(args) : {}; | |
| } | |
| } | |
| function Pageset(pagesetShorthand) | |
| { | |
| /** | |
| * Returns true if the page is included in this pageset, false otherwise. | |
| * The page is specified by a document object. | |
| * | |
| * @param inDocument the document object representing the page | |
| */ | |
| this.contains = function(inDocument) | |
| { | |
| var urlParts = parseUri(unescape(inDocument.location.href)); | |
| var path = urlParts.path | |
| .replace(/^\//, "") | |
| .replace(/\/$/, ""); | |
| if (!this.pathRegexp.test(path)) { | |
| return false; | |
| } | |
| for (var paramName in this.paramRegexps) { | |
| var paramRegexp = this.paramRegexps[paramName]; | |
| if (!paramRegexp.test(urlParts.queryKey[paramName])) { | |
| return false; | |
| } | |
| } | |
| if (!this.pageContent(inDocument)) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| this.getUIElements = function() | |
| { | |
| var uiElements = []; | |
| for (var uiElementName in this.uiElements) { | |
| uiElements.push(this.uiElements[uiElementName]); | |
| } | |
| return uiElements; | |
| }; | |
| /** | |
| * Returns a list of UI specifier string stubs representing all UI elements | |
| * for this pageset. Stubs contain all required arguments, but leave | |
| * argument values blank. Each element stub is paired with the element's | |
| * description. | |
| * | |
| * @return a list of UI specifier string stubs | |
| */ | |
| this.getUISpecifierStringStubs = function() | |
| { | |
| var stubs = []; | |
| for (var name in this.uiElements) { | |
| var uiElement = this.uiElements[name]; | |
| var args = {}; | |
| for (var i = 0; i < uiElement.args.length; ++i) { | |
| args[uiElement.args[i].name] = ''; | |
| } | |
| var uiSpecifier = new UISpecifier(this.name, uiElement.name, args); | |
| stubs.push([ | |
| UI_GLOBAL.UI_PREFIX + '=' + uiSpecifier.toString() | |
| , uiElement.description | |
| ]); | |
| } | |
| return stubs; | |
| } | |
| /** | |
| * Throws an exception on validation failure. | |
| */ | |
| this._validate = function(pagesetShorthand) | |
| { | |
| var msg = "Pageset validation error:\n" | |
| + print_r(pagesetShorthand); | |
| if (!pagesetShorthand.name) { | |
| throw new PagesetException(msg + 'no name specified!'); | |
| } | |
| if (!pagesetShorthand.description) { | |
| throw new PagesetException(msg + 'no description specified!'); | |
| } | |
| if (!pagesetShorthand.paths && | |
| !pagesetShorthand.pathRegexp && | |
| !pagesetShorthand.pageContent) { | |
| throw new PagesetException(msg | |
| + 'no path, pathRegexp, or pageContent specified!'); | |
| } | |
| }; | |
| this.init = function(pagesetShorthand) | |
| { | |
| this._validate(pagesetShorthand); | |
| this.name = pagesetShorthand.name; | |
| this.description = pagesetShorthand.description; | |
| var pathPrefixRegexp = pagesetShorthand.pathPrefix | |
| ? RegExp.escape(pagesetShorthand.pathPrefix) : ""; | |
| var pathRegexp = '^' + pathPrefixRegexp; | |
| if (pagesetShorthand.paths != undefined) { | |
| pathRegexp += '(?:'; | |
| for (var i = 0; i < pagesetShorthand.paths.length; ++i) { | |
| if (i > 0) { | |
| pathRegexp += '|'; | |
| } | |
| pathRegexp += RegExp.escape(pagesetShorthand.paths[i]); | |
| } | |
| pathRegexp += ')$'; | |
| } | |
| else if (pagesetShorthand.pathRegexp) { | |
| pathRegexp += '(?:' + pagesetShorthand.pathRegexp + ')$'; | |
| } | |
| this.pathRegexp = new RegExp(pathRegexp); | |
| this.paramRegexps = {}; | |
| for (var paramName in pagesetShorthand.paramRegexps) { | |
| this.paramRegexps[paramName] = | |
| new RegExp(pagesetShorthand.paramRegexps[paramName]); | |
| } | |
| this.pageContent = pagesetShorthand.pageContent || | |
| function() { return true; }; | |
| this.uiElements = {}; | |
| }; | |
| this.init(pagesetShorthand); | |
| } | |
| /** | |
| * Construct the UI map object, and return it. Once the object is instantiated, | |
| * it binds to a global variable and will not leave scope. | |
| * | |
| * @return new UIMap object | |
| */ | |
| function UIMap() | |
| { | |
| // the singleton pattern, split into two parts so that "new" can still | |
| // be used, in addition to "getInstance()" | |
| UIMap.self = this; | |
| // need to attach variables directly to the Editor object in order for them | |
| // to be in scope for Editor methods | |
| if (is_IDE()) { | |
| Editor.uiMap = this; | |
| Editor.UI_PREFIX = UI_GLOBAL.UI_PREFIX; | |
| } | |
| this.pagesets = new Object(); | |
| /** | |
| * pageset[pagesetName] | |
| * regexp | |
| * elements[elementName] | |
| * UIElement | |
| */ | |
| this.addPageset = function(pagesetShorthand) | |
| { | |
| try { | |
| var pageset = new Pageset(pagesetShorthand); | |
| } | |
| catch (e) { | |
| safe_alert("Could not create pageset from shorthand:\n" | |
| + print_r(pagesetShorthand) + "\n" + e.message); | |
| return false; | |
| } | |
| if (this.pagesets[pageset.name]) { | |
| safe_alert('Could not add pageset "' + pageset.name | |
| + '": a pageset with that name already exists!'); | |
| return false; | |
| } | |
| this.pagesets[pageset.name] = pageset; | |
| return true; | |
| }; | |
| /** | |
| * @param pagesetName | |
| * @param uiElementShorthand a representation of a UIElement object in | |
| * shorthand JSON. | |
| */ | |
| this.addElement = function(pagesetName, uiElementShorthand) | |
| { | |
| try { | |
| var uiElement = new UIElement(uiElementShorthand); | |
| } | |
| catch (e) { | |
| safe_alert("Could not create UI element from shorthand:\n" | |
| + print_r(uiElementShorthand) + "\n" + e.message); | |
| return false; | |
| } | |
| // run the element's unit tests only for the IDE, and only when the | |
| // IDE is starting. Make a rough guess as to the latter condition. | |
| if (is_IDE() && !editor.selDebugger && !uiElement.test()) { | |
| safe_alert('Could not add UI element "' + uiElement.name | |
| + '": failed testcases!'); | |
| return false; | |
| } | |
| try { | |
| this.pagesets[pagesetName].uiElements[uiElement.name] = uiElement; | |
| } | |
| catch (e) { | |
| safe_alert("Could not add UI element '" + uiElement.name | |
| + "' to pageset '" + pagesetName + "':\n" + e.message); | |
| return false; | |
| } | |
| return true; | |
| }; | |
| /** | |
| * Returns the pageset for a given UI specifier string. | |
| * | |
| * @param uiSpecifierString | |
| * @return a pageset object | |
| */ | |
| this.getPageset = function(uiSpecifierString) | |
| { | |
| try { | |
| var uiSpecifier = new UISpecifier(uiSpecifierString); | |
| return this.pagesets[uiSpecifier.pagesetName]; | |
| } | |
| catch (e) { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Returns the UIElement that a UISpecifierString or pageset and element | |
| * pair refer to. | |
| * | |
| * @param pagesetNameOrUISpecifierString | |
| * @return a UIElement, or null if none is found associated with | |
| * uiSpecifierString | |
| */ | |
| this.getUIElement = function(pagesetNameOrUISpecifierString, uiElementName) | |
| { | |
| var pagesetName = pagesetNameOrUISpecifierString; | |
| if (arguments.length == 1) { | |
| var uiSpecifierString = pagesetNameOrUISpecifierString; | |
| try { | |
| var uiSpecifier = new UISpecifier(uiSpecifierString); | |
| pagesetName = uiSpecifier.pagesetName; | |
| var uiElementName = uiSpecifier.elementName; | |
| } | |
| catch (e) { | |
| return null; | |
| } | |
| } | |
| try { | |
| return this.pagesets[pagesetName].uiElements[uiElementName]; | |
| } | |
| catch (e) { | |
| return null; | |
| } | |
| }; | |
| /** | |
| * Returns a list of pagesets that "contains" the provided page, | |
| * represented as a document object. Containership is defined by the | |
| * Pageset object's contain() method. | |
| * | |
| * @param inDocument the page to get pagesets for | |
| * @return a list of pagesets | |
| */ | |
| this.getPagesetsForPage = function(inDocument) | |
| { | |
| var pagesets = []; | |
| for (var pagesetName in this.pagesets) { | |
| var pageset = this.pagesets[pagesetName]; | |
| if (pageset.contains(inDocument)) { | |
| pagesets.push(pageset); | |
| } | |
| } | |
| return pagesets; | |
| }; | |
| /** | |
| * Returns a list of all pagesets. | |
| * | |
| * @return a list of pagesets | |
| */ | |
| this.getPagesets = function() | |
| { | |
| var pagesets = []; | |
| for (var pagesetName in this.pagesets) { | |
| pagesets.push(this.pagesets[pagesetName]); | |
| } | |
| return pagesets; | |
| }; | |
| /** | |
| * Returns a list of elements on a page that a given UI specifier string, | |
| * maps to. If no elements are mapped to, returns an empty list.. | |
| * | |
| * @param uiSpecifierString a String that specifies a UI element with | |
| * attendant argument values | |
| * @param inDocument the document object the specified UI element | |
| * appears in | |
| * @return a potentially-empty list of elements | |
| * specified by uiSpecifierString | |
| */ | |
| this.getPageElements = function(uiSpecifierString, inDocument) | |
| { | |
| var locator = this.getLocator(uiSpecifierString); | |
| var results = locator ? eval_locator(locator, inDocument) : []; | |
| return results; | |
| }; | |
| /** | |
| * Returns the locator string that a given UI specifier string maps to, or | |
| * null if it cannot be mapped. | |
| * | |
| * @param uiSpecifierString | |
| */ | |
| this.getLocator = function(uiSpecifierString) | |
| { | |
| try { | |
| var uiSpecifier = new UISpecifier(uiSpecifierString); | |
| } | |
| catch (e) { | |
| safe_alert('Could not create UISpecifier for string "' | |
| + uiSpecifierString + '": ' + e.message); | |
| return null; | |
| } | |
| var uiElement = this.getUIElement(uiSpecifier.pagesetName, | |
| uiSpecifier.elementName); | |
| try { | |
| return uiElement.getLocator(uiSpecifier.args); | |
| } | |
| catch (e) { | |
| return null; | |
| } | |
| } | |
| /** | |
| * Finds and returns a UI specifier string given an element and the page | |
| * that it appears on. | |
| * | |
| * @param pageElement the document element to map to a UI specifier | |
| * @param inDocument the document the element appears in | |
| * @return a UI specifier string, or false if one cannot be | |
| * constructed | |
| */ | |
| this.getUISpecifierString = function(pageElement, inDocument) | |
| { | |
| var is_fuzzy_match = | |
| BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match; | |
| var pagesets = this.getPagesetsForPage(inDocument); | |
| for (var i = 0; i < pagesets.length; ++i) { | |
| var pageset = pagesets[i]; | |
| var uiElements = pageset.getUIElements(); | |
| for (var j = 0; j < uiElements.length; ++j) { | |
| var uiElement = uiElements[j]; | |
| // first test against the generic locator, if there is one. | |
| // This should net some performance benefit when recording on | |
| // more complicated pages. | |
| if (uiElement.getGenericLocator) { | |
| var passedTest = false; | |
| var results = | |
| eval_locator(uiElement.getGenericLocator(), inDocument); | |
| for (var i = 0; i < results.length; ++i) { | |
| if (results[i] == pageElement) { | |
| passedTest = true; | |
| break; | |
| } | |
| } | |
| if (!passedTest) { | |
| continue; | |
| } | |
| } | |
| var defaultLocators; | |
| if (uiElement.isDefaultLocatorConstructionDeferred) { | |
| defaultLocators = uiElement.getDefaultLocators(inDocument); | |
| } | |
| else { | |
| defaultLocators = uiElement.defaultLocators; | |
| } | |
| //safe_alert(print_r(uiElement.defaultLocators)); | |
| for (var locator in defaultLocators) { | |
| var locatedElements = eval_locator(locator, inDocument); | |
| if (locatedElements.length) { | |
| var locatedElement = locatedElements[0]; | |
| } | |
| else { | |
| continue; | |
| } | |
| // use a heuristic to determine whether the element | |
| // specified is the "same" as the element we're matching | |
| if (is_fuzzy_match) { | |
| if (is_fuzzy_match(locatedElement, pageElement)) { | |
| return UI_GLOBAL.UI_PREFIX + '=' + | |
| new UISpecifier(pageset.name, uiElement.name, | |
| defaultLocators[locator]); | |
| } | |
| } | |
| else { | |
| if (locatedElement == pageElement) { | |
| return UI_GLOBAL.UI_PREFIX + '=' + | |
| new UISpecifier(pageset.name, uiElement.name, | |
| defaultLocators[locator]); | |
| } | |
| } | |
| // ok, matching the element failed. See if an offset | |
| // locator can complete the match. | |
| if (uiElement.getOffsetLocator) { | |
| for (var k = 0; k < locatedElements.length; ++k) { | |
| var offsetLocator = uiElement | |
| .getOffsetLocator(locatedElements[k], pageElement); | |
| if (offsetLocator) { | |
| return UI_GLOBAL.UI_PREFIX + '=' + | |
| new UISpecifier(pageset.name, | |
| uiElement.name, | |
| defaultLocators[locator]) | |
| + '->' + offsetLocator; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Returns a sorted list of UI specifier string stubs representing possible | |
| * UI elements for all pagesets, paired the their descriptions. Stubs | |
| * contain all required arguments, but leave argument values blank. | |
| * | |
| * @return a list of UI specifier string stubs | |
| */ | |
| this.getUISpecifierStringStubs = function() { | |
| var stubs = []; | |
| var pagesets = this.getPagesets(); | |
| for (var i = 0; i < pagesets.length; ++i) { | |
| stubs = stubs.concat(pagesets[i].getUISpecifierStringStubs()); | |
| } | |
| stubs.sort(function(a, b) { | |
| if (a[0] < b[0]) { | |
| return -1; | |
| } | |
| return a[0] == b[0] ? 0 : 1; | |
| }); | |
| return stubs; | |
| } | |
| } | |
| UIMap.getInstance = function() { | |
| return (UIMap.self == null) ? new UIMap() : UIMap.self; | |
| } | |
| //****************************************************************************** | |
| // Rollups | |
| /** | |
| * The Command object isn't available in the Selenium RC. We introduce an | |
| * object with the identical constructor. In the IDE, this will be redefined, | |
| * which is just fine. | |
| * | |
| * @param command | |
| * @param target | |
| * @param value | |
| */ | |
| if (typeof(Command) == 'undefined') { | |
| function Command(command, target, value) { | |
| this.command = command != null ? command : ''; | |
| this.target = target != null ? target : ''; | |
| this.value = value != null ? value : ''; | |
| } | |
| } | |
| /** | |
| * A CommandMatcher object matches commands during the application of a | |
| * RollupRule. It's specified with a shorthand format, for example: | |
| * | |
| * new CommandMatcher({ | |
| * command: 'click' | |
| * , target: 'ui=allPages::.+' | |
| * }) | |
| * | |
| * which is intended to match click commands whose target is an element in the | |
| * allPages PageSet. The matching expressions are given as regular expressions; | |
| * in the example above, the command must be "click"; "clickAndWait" would be | |
| * acceptable if 'click.*' were used. Here's a more complete example: | |
| * | |
| * new CommandMatcher({ | |
| * command: 'type' | |
| * , target: 'ui=loginPages::username()' | |
| * , value: '.+_test' | |
| * , updateArgs: function(command, args) { | |
| * args.username = command.value; | |
| * } | |
| * }) | |
| * | |
| * Here, the command and target are fixed, but there is variability in the | |
| * value of the command. When a command matches, the username is saved to the | |
| * arguments object. | |
| */ | |
| function CommandMatcher(commandMatcherShorthand) | |
| { | |
| /** | |
| * Ensure the shorthand notation used to initialize the CommandMatcher has | |
| * all required values. | |
| * | |
| * @param commandMatcherShorthand an object containing information about | |
| * the CommandMatcher | |
| */ | |
| this.validate = function(commandMatcherShorthand) { | |
| var msg = "CommandMatcher validation error:\n" | |
| + print_r(commandMatcherShorthand); | |
| if (!commandMatcherShorthand.command) { | |
| throw new CommandMatcherException(msg + 'no command specified!'); | |
| } | |
| if (!commandMatcherShorthand.target) { | |
| throw new CommandMatcherException(msg + 'no target specified!'); | |
| } | |
| if (commandMatcherShorthand.minMatches && | |
| commandMatcherShorthand.maxMatches && | |
| commandMatcherShorthand.minMatches > | |
| commandMatcherShorthand.maxMatches) { | |
| throw new CommandMatcherException(msg + 'minMatches > maxMatches!'); | |
| } | |
| }; | |
| /** | |
| * Initialize this object. | |
| * | |
| * @param commandMatcherShorthand an object containing information used to | |
| * initialize the CommandMatcher | |
| */ | |
| this.init = function(commandMatcherShorthand) { | |
| this.validate(commandMatcherShorthand); | |
| this.command = commandMatcherShorthand.command; | |
| this.target = commandMatcherShorthand.target; | |
| this.value = commandMatcherShorthand.value || null; | |
| this.minMatches = commandMatcherShorthand.minMatches || 1; | |
| this.maxMatches = commandMatcherShorthand.maxMatches || 1; | |
| this.updateArgs = commandMatcherShorthand.updateArgs || | |
| function(command, args) { return args; }; | |
| }; | |
| /** | |
| * Determines whether a given command matches. Updates args by "reference" | |
| * and returns true if it does; return false otherwise. | |
| * | |
| * @param command the command to attempt to match | |
| */ | |
| this.isMatch = function(command) { | |
| var re = new RegExp('^' + this.command + '$'); | |
| if (! re.test(command.command)) { | |
| return false; | |
| } | |
| re = new RegExp('^' + this.target + '$'); | |
| if (! re.test(command.target)) { | |
| return false; | |
| } | |
| if (this.value != null) { | |
| re = new RegExp('^' + this.value + '$'); | |
| if (! re.test(command.value)) { | |
| return false; | |
| } | |
| } | |
| // okay, the command matches | |
| return true; | |
| }; | |
| // initialization | |
| this.init(commandMatcherShorthand); | |
| } | |
| function RollupRuleException(message) | |
| { | |
| this.message = message; | |
| this.name = 'RollupRuleException'; | |
| } | |
| function RollupRule(rollupRuleShorthand) | |
| { | |
| /** | |
| * Ensure the shorthand notation used to initialize the RollupRule has all | |
| * required values. | |
| * | |
| * @param rollupRuleShorthand an object containing information about the | |
| * RollupRule | |
| */ | |
| this.validate = function(rollupRuleShorthand) { | |
| var msg = "RollupRule validation error:\n" | |
| + print_r(rollupRuleShorthand); | |
| if (!rollupRuleShorthand.name) { | |
| throw new RollupRuleException(msg + 'no name specified!'); | |
| } | |
| if (!rollupRuleShorthand.description) { | |
| throw new RollupRuleException(msg + 'no description specified!'); | |
| } | |
| // rollupRuleShorthand.args is optional | |
| if (!rollupRuleShorthand.commandMatchers && | |
| !rollupRuleShorthand.getRollup) { | |
| throw new RollupRuleException(msg | |
| + 'no command matchers specified!'); | |
| } | |
| if (!rollupRuleShorthand.expandedCommands && | |
| !rollupRuleShorthand.getExpandedCommands) { | |
| throw new RollupRuleException(msg | |
| + 'no expanded commands specified!'); | |
| } | |
| return true; | |
| }; | |
| /** | |
| * Initialize this object. | |
| * | |
| * @param rollupRuleShorthand an object containing information used to | |
| * initialize the RollupRule | |
| */ | |
| this.init = function(rollupRuleShorthand) { | |
| this.validate(rollupRuleShorthand); | |
| this.name = rollupRuleShorthand.name; | |
| this.description = rollupRuleShorthand.description; | |
| this.pre = rollupRuleShorthand.pre || ''; | |
| this.post = rollupRuleShorthand.post || ''; | |
| this.alternateCommand = rollupRuleShorthand.alternateCommand; | |
| this.args = rollupRuleShorthand.args || []; | |
| if (rollupRuleShorthand.commandMatchers) { | |
| // construct the rule from the list of CommandMatchers | |
| this.commandMatchers = []; | |
| var matchers = rollupRuleShorthand.commandMatchers; | |
| for (var i = 0; i < matchers.length; ++i) { | |
| if (matchers[i].updateArgs && this.args.length == 0) { | |
| // enforce metadata for arguments | |
| var msg = "RollupRule validation error:\n" | |
| + print_r(rollupRuleShorthand) | |
| + 'no argument metadata provided!'; | |
| throw new RollupRuleException(msg); | |
| } | |
| this.commandMatchers.push(new CommandMatcher(matchers[i])); | |
| } | |
| // returns false if the rollup doesn't match, or a rollup command | |
| // if it does. If returned, the command contains the | |
| // replacementIndexes property, which indicates which commands it | |
| // substitutes for. | |
| this.getRollup = function(commands) { | |
| // this is a greedy matching algorithm | |
| var replacementIndexes = []; | |
| var commandMatcherQueue = this.commandMatchers; | |
| var matchCount = 0; | |
| var args = {}; | |
| for (var i = 0, j = 0; i < commandMatcherQueue.length;) { | |
| var matcher = commandMatcherQueue[i]; | |
| if (j >= commands.length) { | |
| // we've run out of commands! If the remaining matchers | |
| // do not have minMatches requirements, this is a | |
| // match. Otherwise, it's not. | |
| if (matcher.minMatches > 0) { | |
| return false; | |
| } | |
| ++i; | |
| matchCount = 0; // unnecessary, but let's be consistent | |
| } | |
| else { | |
| if (matcher.isMatch(commands[j])) { | |
| ++matchCount; | |
| if (matchCount == matcher.maxMatches) { | |
| // exhausted this matcher's matches ... move on | |
| // to next matcher | |
| ++i; | |
| matchCount = 0; | |
| } | |
| args = matcher.updateArgs(commands[j], args); | |
| replacementIndexes.push(j); | |
| ++j; // move on to next command | |
| } | |
| else { | |
| //alert(matchCount + ', ' + matcher.minMatches); | |
| if (matchCount < matcher.minMatches) { | |
| return false; | |
| } | |
| // didn't match this time, but we've satisfied the | |
| // requirements already ... move on to next matcher | |
| ++i; | |
| matchCount = 0; | |
| // still gonna look at same command | |
| } | |
| } | |
| } | |
| var rollup; | |
| if (this.alternateCommand) { | |
| rollup = new Command(this.alternateCommand, | |
| commands[0].target, commands[0].value); | |
| } | |
| else { | |
| rollup = new Command('rollup', this.name); | |
| rollup.value = to_kwargs(args); | |
| } | |
| rollup.replacementIndexes = replacementIndexes; | |
| return rollup; | |
| }; | |
| } | |
| else { | |
| this.getRollup = function(commands) { | |
| var result = rollupRuleShorthand.getRollup(commands); | |
| if (result) { | |
| var rollup = new Command( | |
| result.command | |
| , result.target | |
| , result.value | |
| ); | |
| rollup.replacementIndexes = result.replacementIndexes; | |
| return rollup; | |
| } | |
| return false; | |
| }; | |
| } | |
| this.getExpandedCommands = function(kwargs) { | |
| var commands = []; | |
| var expandedCommands = (rollupRuleShorthand.expandedCommands | |
| ? rollupRuleShorthand.expandedCommands | |
| : rollupRuleShorthand.getExpandedCommands( | |
| parse_kwargs(kwargs))); | |
| for (var i = 0; i < expandedCommands.length; ++i) { | |
| var command = expandedCommands[i]; | |
| commands.push(new Command( | |
| command.command | |
| , command.target | |
| , command.value | |
| )); | |
| } | |
| return commands; | |
| }; | |
| }; | |
| this.init(rollupRuleShorthand); | |
| } | |
| /** | |
| * | |
| */ | |
| function RollupManager() | |
| { | |
| // singleton pattern | |
| RollupManager.self = this; | |
| this.init = function() | |
| { | |
| this.rollupRules = {}; | |
| if (is_IDE()) { | |
| Editor.rollupManager = this; | |
| } | |
| }; | |
| /** | |
| * Adds a new RollupRule to the repository. Returns true on success, or | |
| * false if the rule couldn't be added. | |
| * | |
| * @param rollupRuleShorthand shorthand JSON specification of the new | |
| * RollupRule, possibly including CommandMatcher | |
| * shorthand too. | |
| * @return true if the rule was added successfully, | |
| * false otherwise. | |
| */ | |
| this.addRollupRule = function(rollupRuleShorthand) | |
| { | |
| try { | |
| var rule = new RollupRule(rollupRuleShorthand); | |
| this.rollupRules[rule.name] = rule; | |
| } | |
| catch(e) { | |
| smart_alert("Could not create RollupRule from shorthand:\n\n" | |
| + e.message); | |
| return false; | |
| } | |
| return true; | |
| }; | |
| /** | |
| * Returns a RollupRule by name. | |
| * | |
| * @param rollupName the name of the rule to fetch | |
| * @return the RollupRule, or null if it isn't found. | |
| */ | |
| this.getRollupRule = function(rollupName) | |
| { | |
| return (this.rollupRules[rollupName] || null); | |
| }; | |
| /** | |
| * Returns a list of name-description pairs for use in populating the | |
| * auto-populated target dropdown in the IDE. Rules that have an alternate | |
| * command defined are not included in the list, as they are not bona-fide | |
| * rollups. | |
| * | |
| * @return a list of name-description pairs | |
| */ | |
| this.getRollupRulesForDropdown = function() | |
| { | |
| var targets = []; | |
| var names = keys(this.rollupRules).sort(); | |
| for (var i = 0; i < names.length; ++i) { | |
| var name = names[i]; | |
| if (this.rollupRules[name].alternateCommand) { | |
| continue; | |
| } | |
| targets.push([ name, this.rollupRules[name].description ]); | |
| } | |
| return targets; | |
| }; | |
| /** | |
| * Applies all rules to the current editor commands, asking the user in | |
| * each case if it's okay to perform the replacement. The rules are applied | |
| * repeatedly until there are no more matches. The algorithm should | |
| * remember when the user has declined a replacement, and not ask to do it | |
| * again. | |
| * | |
| * @return the list of commands with rollup replacements performed | |
| */ | |
| this.applyRollupRules = function() | |
| { | |
| var commands = editor.getTestCase().commands; | |
| var blacklistedRollups = {}; | |
| // so long as rollups were performed, we need to keep iterating through | |
| // the commands starting at the beginning, because further rollups may | |
| // potentially be applied on the newly created ones. | |
| while (true) { | |
| var performedRollup = false; | |
| for (var i = 0; i < commands.length; ++i) { | |
| // iterate through commands | |
| for (var rollupName in this.rollupRules) { | |
| var rule = this.rollupRules[rollupName]; | |
| var rollup = rule.getRollup(commands.slice(i)); | |
| if (rollup) { | |
| // since we passed in a sliced version of the commands | |
| // array to the getRollup() method, we need to re-add | |
| // the offset to the replacementIndexes | |
| var k = 0; | |
| for (; k < rollup.replacementIndexes.length; ++k) { | |
| rollup.replacementIndexes[k] += i; | |
| } | |
| // build the confirmation message | |
| var msg = "Perform the following command rollup?\n\n"; | |
| for (k = 0; k < rollup.replacementIndexes.length; ++k) { | |
| var replacementIndex = rollup.replacementIndexes[k]; | |
| var command = commands[replacementIndex]; | |
| msg += '[' + replacementIndex + ']: '; | |
| msg += command + "\n"; | |
| } | |
| msg += "\n"; | |
| msg += rollup; | |
| // check against blacklisted rollups | |
| if (blacklistedRollups[msg]) { | |
| continue; | |
| } | |
| // highlight the potentially replaced rows | |
| for (k = 0; k < commands.length; ++k) { | |
| var command = commands[k]; | |
| command.result = ''; | |
| if (rollup.replacementIndexes.indexOf(k) != -1) { | |
| command.selectedForReplacement = true; | |
| } | |
| editor.view.rowUpdated(replacementIndex); | |
| } | |
| // get confirmation from user | |
| if (confirm(msg)) { | |
| // perform rollup | |
| var deleteRanges = []; | |
| var replacementIndexes = rollup.replacementIndexes; | |
| for (k = 0; k < replacementIndexes.length; ++k) { | |
| // this is expected to be list of ranges. A | |
| // range has a start, and a list of commands. | |
| // The deletion only checks the length of the | |
| // command list. | |
| deleteRanges.push({ | |
| start: replacementIndexes[k] | |
| , commands: [ 1 ] | |
| }); | |
| } | |
| editor.view.executeAction(new TreeView | |
| .DeleteCommandAction(editor.view,deleteRanges)); | |
| editor.view.insertAt(i, rollup); | |
| performedRollup = true; | |
| } | |
| else { | |
| // cleverly remember not to try this rollup again | |
| blacklistedRollups[msg] = true; | |
| } | |
| // unhighlight | |
| for (k = 0; k < commands.length; ++k) { | |
| commands[k].selectedForReplacement = false; | |
| editor.view.rowUpdated(k); | |
| } | |
| } | |
| } | |
| } | |
| if (!performedRollup) { | |
| break; | |
| } | |
| } | |
| return commands; | |
| }; | |
| this.init(); | |
| } | |
| RollupManager.getInstance = function() { | |
| return (RollupManager.self == null) | |
| ? new RollupManager() | |
| : RollupManager.self; | |
| } | |