Passed
Pull Request — develop (#92)
by Felipe
06:21
created

ui-element.js ➔ ... ➔ this.init   F

Complexity

Conditions 17
Paths 480

Size

Total Lines 89

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
nc 480
nop 1
dl 0
loc 89
rs 3.221
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ui-element.js ➔ ... ➔ this.init often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
//******************************************************************************
2
// Globals, including constants
3
4
var UI_GLOBAL = {
5
    UI_PREFIX: 'ui'
6
    , XHTML_DOCTYPE: '<!DOCTYPE html PUBLIC '
7
        + '"-//W3C//DTD XHTML 1.0 Strict//EN" '
8
        + '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
9
    , XHTML_XMLNS: 'http://www.w3.org/1999/xhtml'
10
};
11
12
//*****************************************************************************
13
// Exceptions
14
15
function UIElementException(message)
16
{
17
    this.message = message;
18
    this.name = 'UIElementException';
19
}
20
21
function UIArgumentException(message)
22
{
23
    this.message = message;
24
    this.name = 'UIArgumentException';
25
}
26
27
function PagesetException(message)
28
{
29
    this.message = message;
30
    this.name = 'PagesetException';
31
}
32
33
function UISpecifierException(message)
34
{
35
    this.message = message;
36
    this.name = 'UISpecifierException';
37
}
38
39
function CommandMatcherException(message)
40
{
41
    this.message = message;
42
    this.name = 'CommandMatcherException';
43
}
44
45
//*****************************************************************************
46
// UI-Element core
47
48
/**
49
 * The UIElement object. This has been crafted along with UIMap to make
50
 * specifying UI elements using JSON as simple as possible. Object construction
51
 * will fail if 1) a proper name isn't provided, 2) a faulty args argument is
52
 * given, or 3) getLocator() returns undefined for a valid permutation of
53
 * default argument values. See ui-doc.html for the documentation on the
54
 * builder syntax.
55
 *
56
 * @param uiElementShorthand  an object whose contents conform to the
57
 *                            UI-Element builder syntax.
58
 *
59
 * @return  a new UIElement object
60
 * @throws  UIElementException
61
 */
62
function UIElement(uiElementShorthand)
63
{
64
    // a shorthand object might look like:
65
    //
66
    // {
67
    //     name: 'topic'
68
    //     , description: 'sidebar links to topic categories'
69
    //     , args: [
70
    //         {
71
    //             name: 'name'
72
    //             , description: 'the name of the topic'
73
    //             , defaultValues: topLevelTopics
74
    //         }
75
    //     ]
76
    //     , getLocator: function(args) {
77
    //         return this._listXPath +
78
    //             "/a[text()=" + args.name.quoteForXPath() + "]";
79
    //     }
80
    //     , getGenericLocator: function() {
81
    //         return this._listXPath + '/a';
82
    //     }
83
    //     // maintain testcases for getLocator()
84
    //     , testcase1: {
85
    //         // defaultValues used if args not specified
86
    //         args: { name: 'foo' }
87
    //         , xhtml: '<div id="topiclist">'
88
    //             + '<ul><li><a expected-result="1">foo</a></li></ul>'
89
    //             + '</div>'
90
    //     }
91
    //     // set a local element variable
92
    //     , _listXPath: "//div[@id='topiclist']/ul/li"
93
    // }
94
    //
95
    // name cannot be null or an empty string. Enforce the same requirement for
96
    // the description.
97
    
98
    /**
99
     * Recursively returns all permutations of argument-value pairs, given
100
     * a list of argument definitions. Each argument definition will have
101
     * a set of default values to use in generating said pairs. If an argument
102
     * has no default values defined, it will not be included among the
103
     * permutations.
104
     *
105
     * @param args            a list of UIArguments
106
     * @param opt_inDocument  (optional)
107
     * @return      a list of associative arrays containing key value pairs
108
     */
109
    this.permuteArgs = function(args, opt_inDocument) {
110
        var permutations = [];
111
        for (var i = 0; i < args.length; ++i) {
112
            var arg = args[i];
113
            var defaultValues = (arguments.length > 1)
114
                ? arg.getDefaultValues(opt_inDocument)
115
                : arg.getDefaultValues();
116
            
117
            // skip arguments for which no default values are defined
118
            if (defaultValues.length == 0) {
119
                continue;
120
            }
121
            for (var j = 0; j < defaultValues.length; ++j) {
122
                var value = defaultValues[j];
123
                var nextPermutations = this.permuteArgs(args.slice(i+1));
124
                if (nextPermutations.length == 0) {
125
                    var permutation = {};
126
                    permutation[arg.name] = value + ''; // make into string
127
                    permutations.push(permutation);
128
                }
129
                else {
130
                    for (var k = 0; k < nextPermutations.length; ++k) {
131
                        nextPermutations[k][arg.name] = value + '';
132
                        permutations.push(nextPermutations[k]);
133
                    }
134
                }
135
            }
136
            break;
137
        }
138
        return permutations;
139
    }
140
    
141
    
142
    
143
    /**
144
     * Returns a list of all testcases for this UIElement.
145
     */
146
    this.getTestcases = function()
147
    {
148
        return this.testcases;
149
    }
150
    
151
    
152
    
153
    /**
154
     * Run all unit tests, stopping at the first failure, if any. Return true
155
     * if no failures encountered, false otherwise. See the following thread
156
     * regarding use of getElementById() on XML documents created by parsing
157
     * text via the DOMParser:
158
     *
159
     * http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/2b1b82b3c53a1282/
160
     */
161
    this.test = function()
162
    {
163
        var parser = new DOMParser();
164
        var testcases = this.getTestcases();
165
        testcaseLoop: for (var i = 0; i < testcases.length; ++i) {
166
            var testcase = testcases[i];
167
            var xhtml = UI_GLOBAL.XHTML_DOCTYPE + '<html xmlns="'
168
                + UI_GLOBAL.XHTML_XMLNS + '">' + testcase.xhtml + '</html>';
169
            var doc = parser.parseFromString(xhtml, "text/xml");
170
            if (doc.firstChild.nodeName == 'parsererror') {
171
                safe_alert('Error parsing XHTML in testcase "' + testcase.name
172
                    + '" for UI element "' + this.name + '": ' + "\n"
173
                    + doc.firstChild.firstChild.nodeValue);
174
            }
175
            
176
            // we're no longer using the default locators when testing, because
177
            // args is now required
178
            var locator = parse_locator(this.getLocator(testcase.args));
179
            var results;
180
            if (locator.type == 'xpath' || (locator.type == 'implicit' &&
181
                locator.string.substring(0, 2) == '//')) {
182
                // try using the javascript xpath engine to avoid namespace
183
                // issues. The xpath does have to be lowercase however, it
184
                // seems. 
185
                results = eval_xpath(locator.string, doc,
186
                    { allowNativeXpath: false, returnOnFirstMatch: true });
187
            }
188
            else {
189
                // piece the locator back together
190
                locator = (locator.type == 'implicit')
191
                    ? locator.string
192
                    : locator.type + '=' + locator.string;
193
                results = eval_locator(locator, doc);
194
            }
195
            if (results.length && results[0].hasAttribute('expected-result')) {
196
                continue testcaseLoop;
197
            }
198
            
199
            // testcase failed
200
            if (is_IDE()) {
201
                var msg = 'Testcase "' + testcase.name
202
                    + '" failed for UI element "' + this.name + '":';
203
                if (!results.length) {
204
                    msg += '\n"' + locator + '" did not match any elements!';
205
                }
206
                else {
207
                    msg += '\n' + results[0] + ' was not the expected result!';
208
                }
209
                safe_alert(msg);
210
            }
211
            return false;
212
        }
213
        return true;
214
    };
215
    
216
    
217
    
218
    /**
219
     * Creates a set of locators using permutations of default values for
220
     * arguments used in the locator construction. The set is returned as an
221
     * object mapping locators to key-value arguments objects containing the
222
     * values passed to getLocator() to create the locator.
223
     *
224
     * @param opt_inDocument (optional) the document object of the "current"
225
     *                       page when this method is invoked. Some arguments
226
     *                       may have default value lists that are calculated
227
     *                       based on the contents of the page.
228
     *
229
     * @return  a list of locator strings
230
     * @throws  UIElementException
231
     */
232
    this.getDefaultLocators = function(opt_inDocument) {
233
        var defaultLocators = {};
234
        if (this.args.length == 0) {
235
            defaultLocators[this.getLocator({})] = {};
236
        }
237
        else {
238
            var permutations = this.permuteArgs(this.args, opt_inDocument);
239
            if (permutations.length != 0) {
240
                for (var i = 0; i < permutations.length; ++i) {
241
                    var args = permutations[i];
242
                    var locator = this.getLocator(args);
243
                    if (!locator) {
244
                        throw new UIElementException('Error in UIElement(): '
245
                            + 'no getLocator return value for element "' + name
246
                            + '"');
247
                    }
248
                    defaultLocators[locator] = args;
249
                }
250
            }
251
            else {
252
                // try using no arguments. If it doesn't work, fine.
253
                try {
254
                    var locator = this.getLocator();
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable locator already seems to be declared on line 242. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
255
                    defaultLocators[locator] = {};
256
                }
257
                catch (e) {
258
                    safe_log('debug', e.message);
259
                }
260
            }
261
        }
262
        return defaultLocators;
263
    };
264
    
265
    
266
    
267
    /**
268
     * Validate the structure of the shorthand notation this object is being
269
     * initialized with. Throws an exception if there's a validation error.
270
     *
271
     * @param uiElementShorthand
272
     *
273
     * @throws  UIElementException
274
     */
275
    this.validate = function(uiElementShorthand)
276
    {
277
        var msg = "UIElement validation error:\n" + print_r(uiElementShorthand);
278
        if (!uiElementShorthand.name) {
279
            throw new UIElementException(msg + 'no name specified!');
280
        }
281
        if (!uiElementShorthand.description) {
282
            throw new UIElementException(msg + 'no description specified!');
283
        }
284
        if (!uiElementShorthand.locator
285
            && !uiElementShorthand.getLocator
286
            && !uiElementShorthand.xpath
287
            && !uiElementShorthand.getXPath) {
288
            throw new UIElementException(msg + 'no locator specified!');
289
        }
290
    };
291
    
292
    
293
    
294
    this.init = function(uiElementShorthand)
295
    {
296
        this.validate(uiElementShorthand);
297
        
298
        this.name = uiElementShorthand.name;
299
        this.description = uiElementShorthand.description;
300
        
301
        // construct a new getLocator() method based on the locator property,
302
        // or use the provided function. We're deprecating the xpath property
303
        // and getXPath() function, but still allow for them for backwards
304
        // compatability.
305
        if (uiElementShorthand.locator) {
306
            this.getLocator = function(args) {
0 ignored issues
show
Unused Code introduced by
The parameter args is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
307
                return uiElementShorthand.locator;
308
            };
309
        }
310
        else if (uiElementShorthand.getLocator) {
311
            this.getLocator = uiElementShorthand.getLocator;
312
        }
313
        else if (uiElementShorthand.xpath) {
314
            this.getLocator = function(args) {
0 ignored issues
show
Unused Code introduced by
The parameter args is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
315
                return uiElementShorthand.xpath;
316
            };
317
        }
318
        else {
319
            this.getLocator = uiElementShorthand.getXPath;
320
        }
321
        
322
        if (uiElementShorthand.genericLocator) {
323
            this.getGenericLocator = function() {
324
                return uiElementShorthand.genericLocator;
325
            };
326
        }
327
        else if (uiElementShorthand.getGenericLocator) {
328
            this.getGenericLocator = uiElementShorthand.getGenericLocator;
329
        }
330
        
331
        if (uiElementShorthand.getOffsetLocator) {
332
            this.getOffsetLocator = uiElementShorthand.getOffsetLocator;
333
        }
334
        
335
        // get the testcases and local variables
336
        this.testcases = [];
337
        var localVars = {};
338
        for (var attr in uiElementShorthand) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
339
            if (attr.match(/^testcase/)) {
340
                var testcase = uiElementShorthand[attr];
341
                if (uiElementShorthand.args &&
342
                    uiElementShorthand.args.length && !testcase.args) {
343
                    safe_alert('No args defined in ' + attr + ' for UI element '
344
                        + this.name + '! Skipping testcase.');
345
                    continue;
346
                } 
347
                testcase.name = attr;
348
                this.testcases.push(testcase);
349
            }
350
            else if (attr.match(/^_/)) {
351
                this[attr] = uiElementShorthand[attr];
352
                localVars[attr] = uiElementShorthand[attr];
353
            }
354
        }
355
        
356
        // create the arguments
357
        this.args = []
358
        this.argsOrder = [];
359
        if (uiElementShorthand.args) {
360
            for (var i = 0; i < uiElementShorthand.args.length; ++i) {
361
                var arg = new UIArgument(uiElementShorthand.args[i], localVars);
362
                this.args.push(arg);
363
                this.argsOrder.push(arg.name);
364
365
                // if an exception is thrown when invoking getDefaultValues()
366
                // with no parameters passed in, assume the method requires an
367
                // inDocument parameter, and thus may only be invoked at run
368
                // time. Mark the UI element object accordingly.
369
                try {
370
                    arg.getDefaultValues();
371
                }
372
                catch (e) {
373
                    this.isDefaultLocatorConstructionDeferred = true;
374
                }
375
            }
376
            
377
        }
378
        
379
        if (!this.isDefaultLocatorConstructionDeferred) {
380
            this.defaultLocators = this.getDefaultLocators();
381
        }
382
    };
383
    
384
    
385
    
386
    this.init(uiElementShorthand);
387
}
388
389
// hang this off the UIElement "namespace". This is a composite strategy.
390
UIElement.defaultOffsetLocatorStrategy = function(locatedElement, pageElement) {
391
    var strategies = [
392
        UIElement.linkXPathOffsetLocatorStrategy
393
        , UIElement.preferredAttributeXPathOffsetLocatorStrategy
394
        , UIElement.simpleXPathOffsetLocatorStrategy
395
    ];
396
    
397
    for (var i = 0; i < strategies.length; ++i) {
398
        var strategy = strategies[i];
399
        var offsetLocator = strategy(locatedElement, pageElement);
400
        
401
        if (offsetLocator) {
402
            return offsetLocator;
403
        }
404
    }
405
    
406
    return null;
407
};
408
409
UIElement.simpleXPathOffsetLocatorStrategy = function(locatedElement,
410
    pageElement)
411
{
412
    if (is_ancestor(locatedElement, pageElement)) {
413
        var xpath = "";
414
        var recorder = Recorder.get(locatedElement.ownerDocument.defaultView);
415
        var locatorBuilders = recorder.locatorBuilders;
416
        var currentNode = pageElement;
417
        
418
        while (currentNode != null && currentNode != locatedElement) {
419
            xpath = locatorBuilders.relativeXPathFromParent(currentNode)
420
                + xpath;
421
            currentNode = currentNode.parentNode;
422
        }
423
        
424
        var results = eval_xpath(xpath, locatedElement.ownerDocument,
425
            { contextNode: locatedElement });
426
        
427
        if (results.length > 0 && results[0] == pageElement) {
428
            return xpath;
429
        }
430
    }
431
    
432
    return null;
433
};
434
435
UIElement.linkXPathOffsetLocatorStrategy = function(locatedElement, pageElement)
436
{
437
    if (pageElement.nodeName == 'A' && is_ancestor(locatedElement, pageElement))
438
    {
439
        var text = pageElement.textContent
440
            .replace(/^\s+/, "")
441
            .replace(/\s+$/, "");
442
        
443
        if (text) {
444
            var xpath = '/descendant::a[normalize-space()='
445
                + text.quoteForXPath() + ']';
446
            
447
            var results = eval_xpath(xpath, locatedElement.ownerDocument,
448
                { contextNode: locatedElement });
449
            
450
            if (results.length > 0 && results[0] == pageElement) {
451
                return xpath;
452
            }
453
        }
454
    }
455
    
456
    return null;
457
};
458
459
// compare to the "xpath:attributes" locator strategy defined in the IDE source
460
UIElement.preferredAttributeXPathOffsetLocatorStrategy =
461
    function(locatedElement, pageElement)
462
{
463
    // this is an ordered listing of single attributes
464
    var preferredAttributes =  [
465
        'name'
466
        , 'value'
467
        , 'type'
468
        , 'action'
469
        , 'alt'
470
        , 'title'
471
        , 'class'
472
        , 'src'
473
        , 'href'
474
        , 'onclick'
475
    ];
476
    
477
    if (is_ancestor(locatedElement, pageElement)) {
478
        var xpathBase = '/descendant::' + pageElement.nodeName.toLowerCase();
479
        
480
        for (var i = 0; i < preferredAttributes.length; ++i) {
481
            var name = preferredAttributes[i];
482
            var value = pageElement.getAttribute(name);
483
            
484
            if (value) {
485
                var xpath = xpathBase + '[@' + name + '='
486
                    + value.quoteForXPath() + ']';
487
                    
488
                var results = eval_xpath(xpath, locatedElement.ownerDocument,
489
                    { contextNode: locatedElement });
490
                
491
                if (results.length > 0 && results[0] == pageElement) {
492
                    return xpath;
493
                }
494
            }
495
        }
496
    }
497
    
498
    return null;
499
};
500
501
502
503
/**
504
 * Constructs a UIArgument. This is mostly for checking that the values are
505
 * valid.
506
 *
507
 * @param uiArgumentShorthand
508
 * @param localVars
509
 *
510
 * @throws  UIArgumentException
511
 */
512
function UIArgument(uiArgumentShorthand, localVars)
513
{
514
    /**
515
     * @param uiArgumentShorthand
516
     *
517
     * @throws  UIArgumentException
518
     */
519
    this.validate = function(uiArgumentShorthand)
520
    {
521
        var msg = "UIArgument validation error:\n"
522
            + print_r(uiArgumentShorthand);
523
        
524
        // try really hard to throw an exception!
525
        if (!uiArgumentShorthand.name) {
526
            throw new UIArgumentException(msg + 'no name specified!');
527
        }
528
        if (!uiArgumentShorthand.description) {
529
            throw new UIArgumentException(msg + 'no description specified!');
530
        }
531
        if (!uiArgumentShorthand.defaultValues &&
532
            !uiArgumentShorthand.getDefaultValues) {
533
            throw new UIArgumentException(msg + 'no default values specified!');
534
        }
535
    };
536
    
537
    
538
    
539
    /**
540
     * @param uiArgumentShorthand
541
     * @param localVars            a list of local variables
542
     */
543
    this.init = function(uiArgumentShorthand, localVars)
544
    {
545
        this.validate(uiArgumentShorthand);
546
        
547
        this.name = uiArgumentShorthand.name;
548
        this.description = uiArgumentShorthand.description;
549
        
550
        if (uiArgumentShorthand.defaultValues) {
551
            var defaultValues = uiArgumentShorthand.defaultValues;
552
            this.getDefaultValues =
553
                function() { return defaultValues; }
554
        }
555
        else {
556
            this.getDefaultValues = uiArgumentShorthand.getDefaultValues;
557
        }
558
        
559
        for (var name in localVars) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
560
            this[name] = localVars[name];
561
        }
562
    }
563
    
564
    
565
    
566
    this.init(uiArgumentShorthand, localVars);
567
}
568
569
570
571
/**
572
 * The UISpecifier constructor is overloaded. If less than three arguments are
573
 * provided, the first argument will be considered a UI specifier string, and
574
 * will be split out accordingly. Otherwise, the first argument will be
575
 * considered the path.
576
 *
577
 * @param uiSpecifierStringOrPagesetName  a UI specifier string, or the pageset
578
 *                                        name of the UI specifier
579
 * @param elementName  the name of the element
580
 * @param args         an object associating keys to values
581
 *
582
 * @return  new UISpecifier object
583
 */
584
function UISpecifier(uiSpecifierStringOrPagesetName, elementName, args)
585
{
586
    /**
587
     * Initializes this object from a UI specifier string of the form:
588
     *
589
     *     pagesetName::elementName(arg1=value1, arg2=value2, ...)
590
     *
591
     * into its component parts, and returns them as an object.
592
     *
593
     * @return  an object containing the components of the UI specifier
594
     * @throws  UISpecifierException
595
     */
596
    this._initFromUISpecifierString = function(uiSpecifierString) {
597
        var matches = /^(.*)::([^\(]+)\((.*)\)$/.exec(uiSpecifierString);
598
        if (matches == null) {
599
            throw new UISpecifierException('Error in '
600
                + 'UISpecifier._initFromUISpecifierString(): "'
601
                + this.string + '" is not a valid UI specifier string');
602
        }
603
        this.pagesetName = matches[1];
604
        this.elementName = matches[2];
605
        this.args = (matches[3]) ? parse_kwargs(matches[3]) : {};
606
    };
607
    
608
    
609
    
610
    /**
611
     * Override the toString() method to return the UI specifier string when
612
     * evaluated in a string context. Combines the UI specifier components into
613
     * a canonical UI specifier string and returns it.
614
     *
615
     * @return   a UI specifier string
616
     */
617
    this.toString = function() {
618
        // empty string is acceptable for the path, but it must be defined
619
        if (this.pagesetName == undefined) {
620
            throw new UISpecifierException('Error in UISpecifier.toString(): "'
621
                + this.pagesetName + '" is not a valid UI specifier pageset '
622
                + 'name');
623
        }
624
        if (!this.elementName) {
625
            throw new UISpecifierException('Error in UISpecifier.unparse(): "'
626
                + this.elementName + '" is not a valid UI specifier element '
627
                + 'name');
628
        }
629
        if (!this.args) {
630
            throw new UISpecifierException('Error in UISpecifier.unparse(): "'
631
                + this.args + '" are not valid UI specifier args');
632
        }
633
        
634
        uiElement = UIMap.getInstance()
0 ignored issues
show
Bug introduced by
The variable uiElement seems to be never declared. Assigning variables without defining them first makes them global. If this was intended, consider making it explicit like using window.uiElement.
Loading history...
635
            .getUIElement(this.pagesetName, this.elementName);
636
        if (uiElement != null) {
637
            var kwargs = to_kwargs(this.args, uiElement.argsOrder);
638
        }
639
        else {
640
            // probably under unit test
641
            var kwargs = to_kwargs(this.args);
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable kwargs already seems to be declared on line 637. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
642
        }
643
        
644
        return this.pagesetName + '::' + this.elementName + '(' + kwargs + ')';
645
    };
646
    
647
    // construct the object
648
    if (arguments.length < 2) {
649
        this._initFromUISpecifierString(uiSpecifierStringOrPagesetName);
650
    }
651
    else {
652
        this.pagesetName = uiSpecifierStringOrPagesetName;
653
        this.elementName = elementName;
654
        this.args = (args) ? clone(args) : {};
655
    }
656
}
657
658
659
660
function Pageset(pagesetShorthand)
661
{
662
    /**
663
     * Returns true if the page is included in this pageset, false otherwise.
664
     * The page is specified by a document object.
665
     *
666
     * @param inDocument  the document object representing the page
667
     */
668
    this.contains = function(inDocument)
669
    {
670
        var urlParts = parseUri(unescape(inDocument.location.href));
671
        var path = urlParts.path
672
            .replace(/^\//, "")
673
            .replace(/\/$/, "");
674
        if (!this.pathRegexp.test(path)) {
675
            return false;
676
        }
677
        for (var paramName in this.paramRegexps) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
678
            var paramRegexp = this.paramRegexps[paramName];
679
            if (!paramRegexp.test(urlParts.queryKey[paramName])) {
680
                return false;
681
            }
682
        }
683
        if (!this.pageContent(inDocument)) {
684
            return false;
685
        }
686
        
687
        return true;
688
    }
689
    
690
    
691
    
692
    this.getUIElements = function()
693
    {
694
        var uiElements = [];
695
        for (var uiElementName in this.uiElements) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
696
            uiElements.push(this.uiElements[uiElementName]);
697
        }
698
        return uiElements;
699
    };
700
    
701
    
702
    
703
    /**
704
     * Returns a list of UI specifier string stubs representing all UI elements
705
     * for this pageset. Stubs contain all required arguments, but leave
706
     * argument values blank. Each element stub is paired with the element's
707
     * description.
708
     *
709
     * @return  a list of UI specifier string stubs
710
     */
711
    this.getUISpecifierStringStubs = function()
712
    {
713
        var stubs = [];
714
        for (var name in this.uiElements) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
715
            var uiElement = this.uiElements[name];
716
            var args = {};
717
            for (var i = 0; i < uiElement.args.length; ++i) {
718
                args[uiElement.args[i].name] = '';
719
            }
720
            var uiSpecifier = new UISpecifier(this.name, uiElement.name, args);
721
            stubs.push([
722
                UI_GLOBAL.UI_PREFIX + '=' + uiSpecifier.toString()
723
                , uiElement.description
724
            ]);
725
        }
726
        return stubs;
727
    }
728
    
729
    
730
    
731
    /**
732
     * Throws an exception on validation failure.
733
     */
734
    this._validate = function(pagesetShorthand)
735
    {
736
        var msg = "Pageset validation error:\n"
737
            + print_r(pagesetShorthand);
738
        if (!pagesetShorthand.name) {
739
            throw new PagesetException(msg + 'no name specified!');
740
        }
741
        if (!pagesetShorthand.description) {
742
            throw new PagesetException(msg + 'no description specified!');
743
        }
744
        if (!pagesetShorthand.paths &&
745
            !pagesetShorthand.pathRegexp &&
746
            !pagesetShorthand.pageContent) {
747
            throw new PagesetException(msg
748
                + 'no path, pathRegexp, or pageContent specified!');
749
        }
750
    };
751
    
752
    
753
    
754
    this.init = function(pagesetShorthand)
755
    {
756
        this._validate(pagesetShorthand);
757
        
758
        this.name = pagesetShorthand.name;
759
        this.description = pagesetShorthand.description;
760
        
761
        var pathPrefixRegexp = pagesetShorthand.pathPrefix
762
            ? RegExp.escape(pagesetShorthand.pathPrefix) : "";
763
        var pathRegexp = '^' + pathPrefixRegexp;
764
        
765
        if (pagesetShorthand.paths != undefined) {
766
            pathRegexp += '(?:';
767
            for (var i = 0; i < pagesetShorthand.paths.length; ++i) {
768
                if (i > 0) {
769
                    pathRegexp += '|';
770
                }
771
                pathRegexp += RegExp.escape(pagesetShorthand.paths[i]);
772
            }
773
            pathRegexp += ')$';
774
        }
775
        else if (pagesetShorthand.pathRegexp) {
776
            pathRegexp += '(?:' + pagesetShorthand.pathRegexp + ')$';
777
        }
778
779
        this.pathRegexp = new RegExp(pathRegexp);
780
        this.paramRegexps = {};
781
        for (var paramName in pagesetShorthand.paramRegexps) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
782
            this.paramRegexps[paramName] =
783
                new RegExp(pagesetShorthand.paramRegexps[paramName]);
784
        }
785
        this.pageContent = pagesetShorthand.pageContent ||
786
            function() { return true; };
787
        this.uiElements = {};
788
    };
789
    
790
    
791
    
792
    this.init(pagesetShorthand);
793
}
794
795
796
797
/**
798
 * Construct the UI map object, and return it. Once the object is instantiated,
799
 * it binds to a global variable and will not leave scope.
800
 *
801
 * @return  new UIMap object
802
 */
803
function UIMap()
804
{
805
    // the singleton pattern, split into two parts so that "new" can still
806
    // be used, in addition to "getInstance()"
807
    UIMap.self = this;
808
    
809
    // need to attach variables directly to the Editor object in order for them
810
    // to be in scope for Editor methods
811
    if (is_IDE()) {
812
        Editor.uiMap = this;
813
        Editor.UI_PREFIX = UI_GLOBAL.UI_PREFIX;
814
    }
815
    
816
    this.pagesets = new Object();
817
    
818
    
819
    
820
    /**
821
     * pageset[pagesetName]
822
     *   regexp
823
     *   elements[elementName]
824
     *     UIElement
825
     */
826
    this.addPageset = function(pagesetShorthand)
827
    {
828
        try {
829
            var pageset = new Pageset(pagesetShorthand);
830
        }
831
        catch (e) {
832
            safe_alert("Could not create pageset from shorthand:\n"
833
                + print_r(pagesetShorthand) + "\n" + e.message);
834
            return false;
835
        }
836
        
837
        if (this.pagesets[pageset.name]) {
838
            safe_alert('Could not add pageset "' + pageset.name
839
                + '": a pageset with that name already exists!');
840
            return false;
841
        }
842
        
843
        this.pagesets[pageset.name] = pageset;
844
        return true;
845
    };
846
    
847
    
848
    
849
    /**
850
     * @param pagesetName
851
     * @param uiElementShorthand  a representation of a UIElement object in
852
     *                            shorthand JSON.
853
     */
854
    this.addElement = function(pagesetName, uiElementShorthand)
855
    {
856
        try {
857
            var uiElement = new UIElement(uiElementShorthand);
858
        }
859
        catch (e) {
860
            safe_alert("Could not create UI element from shorthand:\n"
861
                + print_r(uiElementShorthand) + "\n" + e.message);
862
            return false;
863
        }
864
        
865
        // run the element's unit tests only for the IDE, and only when the
866
        // IDE is starting. Make a rough guess as to the latter condition.
867
        if (is_IDE() && !editor.selDebugger && !uiElement.test()) {
868
            safe_alert('Could not add UI element "' + uiElement.name
869
                + '": failed testcases!');
870
            return false;
871
        }
872
        
873
        try {
874
            this.pagesets[pagesetName].uiElements[uiElement.name] = uiElement;
875
        }
876
        catch (e) {
877
            safe_alert("Could not add UI element '" + uiElement.name
878
                + "' to pageset '" + pagesetName + "':\n" + e.message);
879
            return false;
880
        }
881
        
882
        return true;
883
    };
884
    
885
    
886
    
887
    /**
888
     * Returns the pageset for a given UI specifier string.
889
     *
890
     * @param uiSpecifierString
891
     * @return  a pageset object
892
     */
893
    this.getPageset = function(uiSpecifierString)
894
    {
895
        try {
896
            var uiSpecifier = new UISpecifier(uiSpecifierString);
897
            return this.pagesets[uiSpecifier.pagesetName];
898
        }
899
        catch (e) {
900
            return null;
901
        }
902
    }
903
    
904
    
905
    
906
    /**
907
     * Returns the UIElement that a UISpecifierString or pageset and element
908
     * pair refer to.
909
     *
910
     * @param pagesetNameOrUISpecifierString
911
     * @return  a UIElement, or null if none is found associated with
912
     *          uiSpecifierString
913
     */
914
    this.getUIElement = function(pagesetNameOrUISpecifierString, uiElementName)
915
    {
916
        var pagesetName = pagesetNameOrUISpecifierString;
917
        if (arguments.length == 1) {
918
            var uiSpecifierString = pagesetNameOrUISpecifierString;
919
            try {
920
                var uiSpecifier = new UISpecifier(uiSpecifierString);
921
                pagesetName = uiSpecifier.pagesetName;
922
                var uiElementName = uiSpecifier.elementName;
923
            }
924
            catch (e) {
925
                return null;
926
            }
927
        }
928
        try {
929
            return this.pagesets[pagesetName].uiElements[uiElementName];
930
        }
931
        catch (e) {
932
            return null;
933
        }
934
    };
935
    
936
    
937
    
938
    /**
939
     * Returns a list of pagesets that "contains" the provided page,
940
     * represented as a document object. Containership is defined by the
941
     * Pageset object's contain() method.
942
     *
943
     * @param inDocument  the page to get pagesets for
944
     * @return            a list of pagesets
945
     */
946
    this.getPagesetsForPage = function(inDocument)
947
    {
948
        var pagesets = [];
949
        for (var pagesetName in this.pagesets) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
950
            var pageset = this.pagesets[pagesetName];
951
            if (pageset.contains(inDocument)) {
952
                pagesets.push(pageset);
953
            }
954
        }
955
        return pagesets;
956
    };
957
    
958
    
959
    
960
    /**
961
     * Returns a list of all pagesets.
962
     *
963
     * @return  a list of pagesets
964
     */
965
    this.getPagesets = function()
966
    {
967
        var pagesets = [];
968
        for (var pagesetName in this.pagesets) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
969
            pagesets.push(this.pagesets[pagesetName]);
970
        }
971
        return pagesets;
972
    };
973
    
974
    
975
    
976
    /**
977
     * Returns a list of elements on a page that a given UI specifier string,
978
     * maps to. If no elements are mapped to, returns an empty list..
979
     *
980
     * @param   uiSpecifierString  a String that specifies a UI element with
981
     *                             attendant argument values
982
     * @param   inDocument         the document object the specified UI element
983
     *                             appears in
984
     * @return                     a potentially-empty list of elements
985
     *                             specified by uiSpecifierString
986
     */
987
    this.getPageElements = function(uiSpecifierString, inDocument)
988
    {
989
        var locator = this.getLocator(uiSpecifierString);
990
        var results = locator ? eval_locator(locator, inDocument) : [];
991
        return results;
992
    };
993
    
994
    
995
    
996
    /**
997
     * Returns the locator string that a given UI specifier string maps to, or
998
     * null if it cannot be mapped.
999
     *
1000
     * @param uiSpecifierString
1001
     */
1002
    this.getLocator = function(uiSpecifierString)
1003
    {
1004
        try {
1005
            var uiSpecifier = new UISpecifier(uiSpecifierString);
1006
        }
1007
        catch (e) {
1008
            safe_alert('Could not create UISpecifier for string "'
1009
                + uiSpecifierString + '": ' + e.message);
1010
            return null;
1011
        }
1012
        
1013
        var uiElement = this.getUIElement(uiSpecifier.pagesetName,
1014
            uiSpecifier.elementName);
1015
        try {
1016
            return uiElement.getLocator(uiSpecifier.args);
1017
        }
1018
        catch (e) {
1019
            return null;
1020
        }
1021
    }
1022
    
1023
    
1024
    
1025
    /**
1026
     * Finds and returns a UI specifier string given an element and the page
1027
     * that it appears on.
1028
     *
1029
     * @param pageElement  the document element to map to a UI specifier
1030
     * @param inDocument   the document the element appears in
1031
     * @return             a UI specifier string, or false if one cannot be
1032
     *                     constructed
1033
     */
1034
    this.getUISpecifierString = function(pageElement, inDocument)
1035
    {
1036
        var is_fuzzy_match =
1037
            BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match;
1038
        var pagesets = this.getPagesetsForPage(inDocument);
1039
        
1040
        for (var i = 0; i < pagesets.length; ++i) {
1041
            var pageset = pagesets[i];
1042
            var uiElements = pageset.getUIElements();
1043
            
1044
            for (var j = 0; j < uiElements.length; ++j) {
1045
                var uiElement = uiElements[j];
1046
                
1047
                // first test against the generic locator, if there is one.
1048
                // This should net some performance benefit when recording on
1049
                // more complicated pages.
1050
                if (uiElement.getGenericLocator) {
1051
                    var passedTest = false;
1052
                    var results =
1053
                        eval_locator(uiElement.getGenericLocator(), inDocument);
1054
                    for (var i = 0; i < results.length; ++i) {
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable i already seems to be declared on line 1040. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
1055
                        if (results[i] == pageElement) {
1056
                            passedTest = true;
1057
                            break;
1058
                        }
1059
                    }
1060
                    if (!passedTest) {
1061
                        continue;
1062
                    }
1063
                }
1064
                
1065
                var defaultLocators;
1066
                if (uiElement.isDefaultLocatorConstructionDeferred) {
1067
                    defaultLocators = uiElement.getDefaultLocators(inDocument);
1068
                }
1069
                else {
1070
                    defaultLocators = uiElement.defaultLocators;
1071
                }
1072
                
1073
                //safe_alert(print_r(uiElement.defaultLocators));
1074
                for (var locator in defaultLocators) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
1075
                    var locatedElements = eval_locator(locator, inDocument);
1076
                    if (locatedElements.length) {
1077
                        var locatedElement = locatedElements[0];
1078
                    }
1079
                    else {
1080
                        continue;
1081
                    }
1082
                    
1083
                    // use a heuristic to determine whether the element
1084
                    // specified is the "same" as the element we're matching
1085
                    if (is_fuzzy_match) {
1086
                        if (is_fuzzy_match(locatedElement, pageElement)) {
1087
                            return UI_GLOBAL.UI_PREFIX + '=' +
1088
                                new UISpecifier(pageset.name, uiElement.name,
1089
                                    defaultLocators[locator]);
1090
                        }
1091
                    }
1092
                    else {
1093
                        if (locatedElement == pageElement) {
1094
                            return UI_GLOBAL.UI_PREFIX + '=' +
1095
                                new UISpecifier(pageset.name, uiElement.name,
1096
                                    defaultLocators[locator]);
1097
                        }
1098
                    }
1099
                    
1100
                    // ok, matching the element failed. See if an offset
1101
                    // locator can complete the match.
1102
                    if (uiElement.getOffsetLocator) {
1103
                        for (var k = 0; k < locatedElements.length; ++k) {
1104
                            var offsetLocator = uiElement
1105
                                .getOffsetLocator(locatedElements[k], pageElement);
1106
                            if (offsetLocator) {
1107
                                return UI_GLOBAL.UI_PREFIX + '=' +
1108
                                    new UISpecifier(pageset.name,
1109
                                        uiElement.name,
1110
                                        defaultLocators[locator])
1111
                                    + '->' + offsetLocator;
1112
                            }
1113
                        }
1114
                    }
1115
                }
1116
            }
1117
        }
1118
        return false;
1119
    };
1120
    
1121
    
1122
    
1123
    /**
1124
     * Returns a sorted list of UI specifier string stubs representing possible
1125
     * UI elements for all pagesets, paired the their descriptions. Stubs
1126
     * contain all required arguments, but leave argument values blank.
1127
     *
1128
     * @return  a list of UI specifier string stubs
1129
     */
1130
    this.getUISpecifierStringStubs = function() {
1131
        var stubs = [];
1132
        var pagesets = this.getPagesets();
1133
        for (var i = 0; i < pagesets.length; ++i) {
1134
            stubs = stubs.concat(pagesets[i].getUISpecifierStringStubs());
1135
        }
1136
        stubs.sort(function(a, b) {
1137
            if (a[0] < b[0]) {
1138
                return -1;
1139
            }
1140
            return a[0] == b[0] ? 0 : 1;
1141
        });
1142
        return stubs;
1143
    }
1144
}
1145
1146
UIMap.getInstance = function() {
1147
    return (UIMap.self == null) ? new UIMap() : UIMap.self;
1148
}
1149
1150
//******************************************************************************
1151
// Rollups
1152
1153
/**
1154
 * The Command object isn't available in the Selenium RC. We introduce an
1155
 * object with the identical constructor. In the IDE, this will be redefined,
1156
 * which is just fine.
1157
 *
1158
 * @param command
1159
 * @param target
1160
 * @param value
1161
 */
1162
if (typeof(Command) == 'undefined') {
1163
    function Command(command, target, value) {
0 ignored issues
show
Bug introduced by
The function Command is declared conditionally. This is not supported by all runtimes. Consider moving it to root scope or using var Command = function() { /* ... */ }; instead.
Loading history...
1164
        this.command = command != null ? command : '';
1165
        this.target = target != null ? target : '';
1166
        this.value = value != null ? value : '';
1167
    }
1168
}
1169
1170
1171
1172
/**
1173
 * A CommandMatcher object matches commands during the application of a
1174
 * RollupRule. It's specified with a shorthand format, for example:
1175
 *
1176
 *  new CommandMatcher({
1177
 *      command: 'click'
1178
 *      , target: 'ui=allPages::.+'
1179
 *  })
1180
 *
1181
 * which is intended to match click commands whose target is an element in the
1182
 * allPages PageSet. The matching expressions are given as regular expressions;
1183
 * in the example above, the command must be "click"; "clickAndWait" would be
1184
 * acceptable if 'click.*' were used. Here's a more complete example:
1185
 *
1186
 *  new CommandMatcher({
1187
 *      command: 'type'
1188
 *      , target: 'ui=loginPages::username()'
1189
 *      , value: '.+_test'
1190
 *      , updateArgs: function(command, args) {
1191
 *          args.username = command.value;
1192
 *      }
1193
 *  })
1194
 *
1195
 * Here, the command and target are fixed, but there is variability in the 
1196
 * value of the command. When a command matches, the username is saved to the
1197
 * arguments object.
1198
 */
1199
function CommandMatcher(commandMatcherShorthand)
1200
{
1201
    /**
1202
     * Ensure the shorthand notation used to initialize the CommandMatcher has
1203
     * all required values.
1204
     *
1205
     * @param commandMatcherShorthand  an object containing information about
1206
     *                                 the CommandMatcher
1207
     */
1208
    this.validate = function(commandMatcherShorthand) {
1209
        var msg = "CommandMatcher validation error:\n"
1210
            + print_r(commandMatcherShorthand);
1211
        if (!commandMatcherShorthand.command) {
1212
            throw new CommandMatcherException(msg + 'no command specified!');
1213
        }
1214
        if (!commandMatcherShorthand.target) {
1215
            throw new CommandMatcherException(msg + 'no target specified!');
1216
        }
1217
        if (commandMatcherShorthand.minMatches &&
1218
            commandMatcherShorthand.maxMatches &&
1219
            commandMatcherShorthand.minMatches >
1220
            commandMatcherShorthand.maxMatches) {
1221
            throw new CommandMatcherException(msg + 'minMatches > maxMatches!');
1222
        }
1223
    };
1224
1225
    /**
1226
     * Initialize this object.
1227
     *
1228
     * @param commandMatcherShorthand  an object containing information used to
1229
     *                                 initialize the CommandMatcher
1230
     */
1231
    this.init = function(commandMatcherShorthand) {
1232
        this.validate(commandMatcherShorthand);
1233
        
1234
        this.command = commandMatcherShorthand.command;
1235
        this.target = commandMatcherShorthand.target;
1236
        this.value = commandMatcherShorthand.value || null;
1237
        this.minMatches = commandMatcherShorthand.minMatches || 1;
1238
        this.maxMatches = commandMatcherShorthand.maxMatches || 1;
1239
        this.updateArgs = commandMatcherShorthand.updateArgs ||
1240
            function(command, args) { return args; };
1241
    };
1242
    
1243
    /**
1244
     * Determines whether a given command matches. Updates args by "reference"
1245
     * and returns true if it does; return false otherwise.
1246
     *
1247
     * @param command  the command to attempt to match
1248
     */
1249
    this.isMatch = function(command) {
1250
        var re = new RegExp('^' + this.command + '$');
1251
        if (! re.test(command.command)) {
1252
            return false;
1253
        }
1254
        re = new RegExp('^' + this.target + '$');
1255
        if (! re.test(command.target)) {
1256
            return false;
1257
        }
1258
        if (this.value != null) {
1259
            re = new RegExp('^' + this.value + '$');
1260
            if (! re.test(command.value)) {
1261
                return false;
1262
            }
1263
        }
1264
        
1265
        // okay, the command matches
1266
        return true;
1267
    };
1268
    
1269
    // initialization
1270
    this.init(commandMatcherShorthand);
1271
}
1272
1273
1274
1275
function RollupRuleException(message)
1276
{
1277
    this.message = message;
1278
    this.name = 'RollupRuleException';
1279
}
1280
1281
function RollupRule(rollupRuleShorthand)
1282
{
1283
    /**
1284
     * Ensure the shorthand notation used to initialize the RollupRule has all
1285
     * required values.
1286
     *
1287
     * @param rollupRuleShorthand  an object containing information about the
1288
     *                             RollupRule
1289
     */
1290
    this.validate = function(rollupRuleShorthand) {
1291
        var msg = "RollupRule validation error:\n"
1292
            + print_r(rollupRuleShorthand);
1293
        if (!rollupRuleShorthand.name) {
1294
            throw new RollupRuleException(msg + 'no name specified!');
1295
        }
1296
        if (!rollupRuleShorthand.description) {
1297
            throw new RollupRuleException(msg + 'no description specified!');
1298
        }
1299
        // rollupRuleShorthand.args is optional
1300
        if (!rollupRuleShorthand.commandMatchers &&
1301
            !rollupRuleShorthand.getRollup) {
1302
            throw new RollupRuleException(msg
1303
                + 'no command matchers specified!');
1304
        }
1305
        if (!rollupRuleShorthand.expandedCommands &&
1306
            !rollupRuleShorthand.getExpandedCommands) {
1307
            throw new RollupRuleException(msg
1308
                + 'no expanded commands specified!');
1309
        }
1310
        
1311
        return true;
1312
    };
1313
1314
    /**
1315
     * Initialize this object.
1316
     *
1317
     * @param rollupRuleShorthand  an object containing information used to
1318
     *                             initialize the RollupRule
1319
     */
1320
    this.init = function(rollupRuleShorthand) {
1321
        this.validate(rollupRuleShorthand);
1322
        
1323
        this.name = rollupRuleShorthand.name;
1324
        this.description = rollupRuleShorthand.description;
1325
        this.pre = rollupRuleShorthand.pre || '';
1326
        this.post = rollupRuleShorthand.post || '';
1327
        this.alternateCommand = rollupRuleShorthand.alternateCommand;
1328
        this.args = rollupRuleShorthand.args || [];
1329
        
1330
        if (rollupRuleShorthand.commandMatchers) {
1331
            // construct the rule from the list of CommandMatchers
1332
            this.commandMatchers = [];
1333
            var matchers = rollupRuleShorthand.commandMatchers;
1334
            for (var i = 0; i < matchers.length; ++i) {
1335
                if (matchers[i].updateArgs && this.args.length == 0) {
1336
                    // enforce metadata for arguments
1337
                    var msg = "RollupRule validation error:\n"
1338
                        + print_r(rollupRuleShorthand)
1339
                        + 'no argument metadata provided!';
1340
                    throw new RollupRuleException(msg);
1341
                }
1342
                this.commandMatchers.push(new CommandMatcher(matchers[i]));
1343
            }
1344
            
1345
            // returns false if the rollup doesn't match, or a rollup command
1346
            // if it does. If returned, the command contains the
1347
            // replacementIndexes property, which indicates which commands it
1348
            // substitutes for.
1349
            this.getRollup = function(commands) {
1350
                // this is a greedy matching algorithm
1351
                var replacementIndexes = [];
1352
                var commandMatcherQueue = this.commandMatchers;
1353
                var matchCount = 0;
1354
                var args = {};
1355
                for (var i = 0, j = 0; i < commandMatcherQueue.length;) {
1356
                    var matcher = commandMatcherQueue[i];
1357
                    if (j >= commands.length) {
1358
                        // we've run out of commands! If the remaining matchers
1359
                        // do not have minMatches requirements, this is a
1360
                        // match. Otherwise, it's not.
1361
                        if (matcher.minMatches > 0) {
1362
                            return false;
1363
                        }
1364
                        ++i;
1365
                        matchCount = 0; // unnecessary, but let's be consistent
1366
                    }
1367
                    else {
1368
                        if (matcher.isMatch(commands[j])) {
1369
                            ++matchCount;
1370
                            if (matchCount == matcher.maxMatches) {
1371
                                // exhausted this matcher's matches ... move on
1372
                                // to next matcher
1373
                                ++i;
1374
                                matchCount = 0;
1375
                            }
1376
                            args = matcher.updateArgs(commands[j], args);
1377
                            replacementIndexes.push(j);
1378
                            ++j; // move on to next command
1379
                        }
1380
                        else {
1381
                            //alert(matchCount + ', ' + matcher.minMatches);
1382
                            if (matchCount < matcher.minMatches) {
1383
                                return false;
1384
                            }
1385
                            // didn't match this time, but we've satisfied the
1386
                            // requirements already ... move on to next matcher
1387
                            ++i;
1388
                            matchCount = 0;
1389
                            // still gonna look at same command
1390
                        }
1391
                    }
1392
                }
1393
                
1394
                var rollup;
1395
                if (this.alternateCommand) {
1396
                    rollup = new Command(this.alternateCommand,
1397
                        commands[0].target, commands[0].value);
1398
                }
1399
                else {
1400
                    rollup = new Command('rollup', this.name);
1401
                    rollup.value = to_kwargs(args);
1402
                }
1403
                rollup.replacementIndexes = replacementIndexes;
1404
                return rollup;
1405
            };
1406
        }
1407
        else {
1408
            this.getRollup = function(commands) {
1409
                var result = rollupRuleShorthand.getRollup(commands);
1410
                if (result) {
1411
                    var rollup = new Command(
1412
                        result.command
1413
                        , result.target
1414
                        , result.value
1415
                    );
1416
                    rollup.replacementIndexes = result.replacementIndexes;
1417
                    return rollup;
1418
                }
1419
                return false;
1420
            };
1421
        }
1422
        
1423
        this.getExpandedCommands = function(kwargs) {
1424
            var commands = [];
1425
            var expandedCommands = (rollupRuleShorthand.expandedCommands
1426
                ? rollupRuleShorthand.expandedCommands
1427
                : rollupRuleShorthand.getExpandedCommands(
1428
                    parse_kwargs(kwargs)));
1429
            for (var i = 0; i < expandedCommands.length; ++i) {
1430
                var command = expandedCommands[i];
1431
                commands.push(new Command(
1432
                    command.command
1433
                    , command.target
1434
                    , command.value
1435
                ));
1436
            }
1437
            return commands;
1438
        };
1439
    };
1440
    
1441
    this.init(rollupRuleShorthand);
1442
}
1443
1444
1445
1446
/**
1447
 *
1448
 */
1449
function RollupManager()
1450
{
1451
    // singleton pattern
1452
    RollupManager.self = this;
1453
    
1454
    this.init = function()
1455
    {
1456
        this.rollupRules = {};
1457
        if (is_IDE()) {
1458
            Editor.rollupManager = this;
1459
        }
1460
    };
1461
1462
    /**
1463
     * Adds a new RollupRule to the repository. Returns true on success, or
1464
     * false if the rule couldn't be added.
1465
     *
1466
     * @param rollupRuleShorthand  shorthand JSON specification of the new
1467
     *                             RollupRule, possibly including CommandMatcher
1468
     *                             shorthand too.
1469
     * @return                     true if the rule was added successfully,
1470
     *                             false otherwise.
1471
     */
1472
    this.addRollupRule = function(rollupRuleShorthand)
1473
    {
1474
        try {
1475
            var rule = new RollupRule(rollupRuleShorthand);
1476
            this.rollupRules[rule.name] = rule;
1477
        }
1478
        catch(e) {
1479
            smart_alert("Could not create RollupRule from shorthand:\n\n"
1480
                + e.message);
1481
            return false;
1482
        }
1483
        return true;
1484
    };
1485
    
1486
    /**
1487
     * Returns a RollupRule by name.
1488
     *
1489
     * @param rollupName  the name of the rule to fetch
1490
     * @return            the RollupRule, or null if it isn't found.
1491
     */
1492
    this.getRollupRule = function(rollupName)
1493
    {
1494
        return (this.rollupRules[rollupName] || null);
1495
    };
1496
    
1497
    /**
1498
     * Returns a list of name-description pairs for use in populating the
1499
     * auto-populated target dropdown in the IDE. Rules that have an alternate
1500
     * command defined are not included in the list, as they are not bona-fide
1501
     * rollups.
1502
     *
1503
     * @return  a list of name-description pairs
1504
     */
1505
    this.getRollupRulesForDropdown = function()
1506
    {
1507
        var targets = [];
1508
        var names = keys(this.rollupRules).sort();
1509
        for (var i = 0; i < names.length; ++i) {
1510
            var name = names[i];
1511
            if (this.rollupRules[name].alternateCommand) {
1512
                continue;
1513
            }
1514
            targets.push([ name, this.rollupRules[name].description ]);
1515
        }
1516
        return targets;
1517
    };
1518
    
1519
    /**
1520
     * Applies all rules to the current editor commands, asking the user in
1521
     * each case if it's okay to perform the replacement. The rules are applied
1522
     * repeatedly until there are no more matches. The algorithm should
1523
     * remember when the user has declined a replacement, and not ask to do it
1524
     * again.
1525
     *
1526
     * @return  the list of commands with rollup replacements performed
1527
     */
1528
    this.applyRollupRules = function()
1529
    {
1530
        var commands = editor.getTestCase().commands;
1531
        var blacklistedRollups = {};
1532
    
1533
        // so long as rollups were performed, we need to keep iterating through
1534
        // the commands starting at the beginning, because further rollups may
1535
        // potentially be applied on the newly created ones.
1536
        while (true) {
1537
            var performedRollup = false;
1538
            for (var i = 0; i < commands.length; ++i) {
1539
                // iterate through commands
1540
                for (var rollupName in this.rollupRules) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
1541
                    var rule = this.rollupRules[rollupName];
1542
                    var rollup = rule.getRollup(commands.slice(i));
1543
                    if (rollup) {
1544
                        // since we passed in a sliced version of the commands
1545
                        // array to the getRollup() method, we need to re-add 
1546
                        // the offset to the replacementIndexes
1547
                        var k = 0;
1548
                        for (; k < rollup.replacementIndexes.length; ++k) {
1549
                            rollup.replacementIndexes[k] += i;
1550
                        }
1551
                        
1552
                        // build the confirmation message
1553
                        var msg = "Perform the following command rollup?\n\n";
1554
                        for (k = 0; k < rollup.replacementIndexes.length; ++k) {
1555
                            var replacementIndex = rollup.replacementIndexes[k];
1556
                            var command = commands[replacementIndex];
1557
                            msg += '[' + replacementIndex + ']: ';
1558
                            msg += command + "\n";
1559
                        }
1560
                        msg += "\n";
1561
                        msg += rollup;
1562
                        
1563
                        // check against blacklisted rollups
1564
                        if (blacklistedRollups[msg]) {
1565
                            continue;
1566
                        }
1567
                        
1568
                        // highlight the potentially replaced rows
1569
                        for (k = 0; k < commands.length; ++k) {
1570
                            var command = commands[k];
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable command already seems to be declared on line 1556. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
1571
                            command.result = '';
1572
                            if (rollup.replacementIndexes.indexOf(k) != -1) {
1573
                                command.selectedForReplacement = true;
1574
                            }
1575
                            editor.view.rowUpdated(replacementIndex);
0 ignored issues
show
Bug introduced by
The variable replacementIndex seems to not be initialized for all possible execution paths. Are you sure rowUpdated handles undefined variables?
Loading history...
1576
                        }
1577
                        
1578
                        // get confirmation from user
1579
                        if (confirm(msg)) {
1580
                            // perform rollup
1581
                            var deleteRanges = [];
1582
                            var replacementIndexes = rollup.replacementIndexes;
1583
                            for (k = 0; k < replacementIndexes.length; ++k) {
1584
                                // this is expected to be list of ranges. A
1585
                                // range has a start, and a list of commands.
1586
                                // The deletion only checks the length of the
1587
                                // command list.
1588
                                deleteRanges.push({
1589
                                    start: replacementIndexes[k]
1590
                                    , commands: [ 1 ]
1591
                                });
1592
                            }
1593
                            editor.view.executeAction(new TreeView
1594
                                .DeleteCommandAction(editor.view,deleteRanges));
1595
                            editor.view.insertAt(i, rollup);
1596
                            
1597
                            performedRollup = true;
1598
                        }
1599
                        else {
1600
                            // cleverly remember not to try this rollup again
1601
                            blacklistedRollups[msg] = true;
1602
                        }
1603
                        
1604
                        // unhighlight
1605
                        for (k = 0; k < commands.length; ++k) {
1606
                            commands[k].selectedForReplacement = false;
1607
                            editor.view.rowUpdated(k);
1608
                        }
1609
                    }
1610
                }
1611
            }
1612
            if (!performedRollup) {
1613
                break;
1614
            }
1615
        }
1616
        return commands;
1617
    };
1618
    
1619
    this.init();
1620
}
1621
1622
RollupManager.getInstance = function() {
1623
    return (RollupManager.self == null)
1624
        ? new RollupManager()
1625
        : RollupManager.self;
1626
}
1627
1628