selenium-core-runner 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. data/Gemfile +9 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +30 -0
  5. data/app/controllers/selenium_core_runner/suites_controller.rb +19 -0
  6. data/app/views/selenium_core_runner/suites/index.html.erb +177 -0
  7. data/app/views/selenium_core_runner/suites/show.html.erb +0 -0
  8. data/config/routes.rb +6 -0
  9. data/lib/selenium-core-runner/engine.rb +19 -0
  10. data/lib/selenium-core-runner.rb +3 -0
  11. data/public/selenium-core-runner/Blank.html +7 -0
  12. data/public/selenium-core-runner/InjectedRemoteRunner.html +8 -0
  13. data/public/selenium-core-runner/RemoteRunner.html +101 -0
  14. data/public/selenium-core-runner/SeleniumLog.html +109 -0
  15. data/public/selenium-core-runner/TestPrompt.html +145 -0
  16. data/public/selenium-core-runner/TestRunner-splash.html +55 -0
  17. data/public/selenium-core-runner/TestRunner.hta +177 -0
  18. data/public/selenium-core-runner/TestRunner.html +177 -0
  19. data/public/selenium-core-runner/icons/all.png +0 -0
  20. data/public/selenium-core-runner/icons/continue.png +0 -0
  21. data/public/selenium-core-runner/icons/continue_disabled.png +0 -0
  22. data/public/selenium-core-runner/icons/pause.png +0 -0
  23. data/public/selenium-core-runner/icons/pause_disabled.png +0 -0
  24. data/public/selenium-core-runner/icons/selected.png +0 -0
  25. data/public/selenium-core-runner/icons/step.png +0 -0
  26. data/public/selenium-core-runner/icons/step_disabled.png +0 -0
  27. data/public/selenium-core-runner/iedoc-core.xml +1789 -0
  28. data/public/selenium-core-runner/iedoc.xml +1830 -0
  29. data/public/selenium-core-runner/lib/cssQuery/cssQuery-p.js +6 -0
  30. data/public/selenium-core-runner/lib/cssQuery/src/cssQuery-level2.js +142 -0
  31. data/public/selenium-core-runner/lib/cssQuery/src/cssQuery-level3.js +150 -0
  32. data/public/selenium-core-runner/lib/cssQuery/src/cssQuery-standard.js +53 -0
  33. data/public/selenium-core-runner/lib/cssQuery/src/cssQuery.js +356 -0
  34. data/public/selenium-core-runner/lib/prototype.js +2006 -0
  35. data/public/selenium-core-runner/lib/scriptaculous/builder.js +101 -0
  36. data/public/selenium-core-runner/lib/scriptaculous/controls.js +815 -0
  37. data/public/selenium-core-runner/lib/scriptaculous/dragdrop.js +915 -0
  38. data/public/selenium-core-runner/lib/scriptaculous/effects.js +958 -0
  39. data/public/selenium-core-runner/lib/scriptaculous/scriptaculous.js +47 -0
  40. data/public/selenium-core-runner/lib/scriptaculous/slider.js +283 -0
  41. data/public/selenium-core-runner/lib/scriptaculous/unittest.js +383 -0
  42. data/public/selenium-core-runner/lib/snapsie.js +91 -0
  43. data/public/selenium-core-runner/scripts/find_matching_child.js +69 -0
  44. data/public/selenium-core-runner/scripts/htmlutils.js +1623 -0
  45. data/public/selenium-core-runner/scripts/injection.html +72 -0
  46. data/public/selenium-core-runner/scripts/selenium-api.js +3240 -0
  47. data/public/selenium-core-runner/scripts/selenium-browserbot.js +2333 -0
  48. data/public/selenium-core-runner/scripts/selenium-browserdetect.js +153 -0
  49. data/public/selenium-core-runner/scripts/selenium-commandhandlers.js +379 -0
  50. data/public/selenium-core-runner/scripts/selenium-executionloop.js +175 -0
  51. data/public/selenium-core-runner/scripts/selenium-logging.js +148 -0
  52. data/public/selenium-core-runner/scripts/selenium-remoterunner.js +695 -0
  53. data/public/selenium-core-runner/scripts/selenium-testrunner.js +1362 -0
  54. data/public/selenium-core-runner/scripts/selenium-version.js +5 -0
  55. data/public/selenium-core-runner/scripts/ui-doc.html +803 -0
  56. data/public/selenium-core-runner/scripts/ui-element.js +1627 -0
  57. data/public/selenium-core-runner/scripts/ui-map-sample.js +979 -0
  58. data/public/selenium-core-runner/scripts/user-extensions.js +3 -0
  59. data/public/selenium-core-runner/scripts/user-extensions.js.sample +75 -0
  60. data/public/selenium-core-runner/scripts/xmlextras.js +153 -0
  61. data/public/selenium-core-runner/selenium-logo.png +0 -0
  62. data/public/selenium-core-runner/selenium-test.css +43 -0
  63. data/public/selenium-core-runner/selenium.css +316 -0
  64. data/public/selenium-core-runner/xpath/dom.js +566 -0
  65. data/public/selenium-core-runner/xpath/javascript-xpath-0.1.11.js +2816 -0
  66. data/public/selenium-core-runner/xpath/util.js +549 -0
  67. data/public/selenium-core-runner/xpath/xmltoken.js +149 -0
  68. data/public/selenium-core-runner/xpath/xpath.js +2481 -0
  69. metadata +121 -0
@@ -0,0 +1,1627 @@
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();
255
+ defaultLocators[locator] = {};
256
+ }
257
+ catch (e) {
258
+ safe_log('debug', e.message);
259
+ }
260
+ }
261
+ }
262
+ return defaultLocators;
263
+ };
264
+
265
+
266
+
267
+ /**
268
+ * Validate the structure of the shorthand notation this object is being
269
+ * initialized with. Throws an exception if there's a validation error.
270
+ *
271
+ * @param uiElementShorthand
272
+ *
273
+ * @throws UIElementException
274
+ */
275
+ this.validate = function(uiElementShorthand)
276
+ {
277
+ var msg = "UIElement validation error:\n" + print_r(uiElementShorthand);
278
+ if (!uiElementShorthand.name) {
279
+ throw new UIElementException(msg + 'no name specified!');
280
+ }
281
+ if (!uiElementShorthand.description) {
282
+ throw new UIElementException(msg + 'no description specified!');
283
+ }
284
+ if (!uiElementShorthand.locator
285
+ && !uiElementShorthand.getLocator
286
+ && !uiElementShorthand.xpath
287
+ && !uiElementShorthand.getXPath) {
288
+ throw new UIElementException(msg + 'no locator specified!');
289
+ }
290
+ };
291
+
292
+
293
+
294
+ this.init = function(uiElementShorthand)
295
+ {
296
+ this.validate(uiElementShorthand);
297
+
298
+ this.name = uiElementShorthand.name;
299
+ this.description = uiElementShorthand.description;
300
+
301
+ // construct a new getLocator() method based on the locator property,
302
+ // or use the provided function. We're deprecating the xpath property
303
+ // and getXPath() function, but still allow for them for backwards
304
+ // compatability.
305
+ if (uiElementShorthand.locator) {
306
+ this.getLocator = function(args) {
307
+ return uiElementShorthand.locator;
308
+ };
309
+ }
310
+ else if (uiElementShorthand.getLocator) {
311
+ this.getLocator = uiElementShorthand.getLocator;
312
+ }
313
+ else if (uiElementShorthand.xpath) {
314
+ this.getLocator = function(args) {
315
+ return uiElementShorthand.xpath;
316
+ };
317
+ }
318
+ else {
319
+ this.getLocator = uiElementShorthand.getXPath;
320
+ }
321
+
322
+ if (uiElementShorthand.genericLocator) {
323
+ this.getGenericLocator = function() {
324
+ return uiElementShorthand.genericLocator;
325
+ };
326
+ }
327
+ else if (uiElementShorthand.getGenericLocator) {
328
+ this.getGenericLocator = uiElementShorthand.getGenericLocator;
329
+ }
330
+
331
+ if (uiElementShorthand.getOffsetLocator) {
332
+ this.getOffsetLocator = uiElementShorthand.getOffsetLocator;
333
+ }
334
+
335
+ // get the testcases and local variables
336
+ this.testcases = [];
337
+ var localVars = {};
338
+ for (var attr in uiElementShorthand) {
339
+ if (attr.match(/^testcase/)) {
340
+ var testcase = uiElementShorthand[attr];
341
+ if (uiElementShorthand.args &&
342
+ uiElementShorthand.args.length && !testcase.args) {
343
+ safe_alert('No args defined in ' + attr + ' for UI element '
344
+ + this.name + '! Skipping testcase.');
345
+ continue;
346
+ }
347
+ testcase.name = attr;
348
+ this.testcases.push(testcase);
349
+ }
350
+ else if (attr.match(/^_/)) {
351
+ this[attr] = uiElementShorthand[attr];
352
+ localVars[attr] = uiElementShorthand[attr];
353
+ }
354
+ }
355
+
356
+ // create the arguments
357
+ this.args = []
358
+ this.argsOrder = [];
359
+ if (uiElementShorthand.args) {
360
+ for (var i = 0; i < uiElementShorthand.args.length; ++i) {
361
+ var arg = new UIArgument(uiElementShorthand.args[i], localVars);
362
+ this.args.push(arg);
363
+ this.argsOrder.push(arg.name);
364
+
365
+ // if an exception is thrown when invoking getDefaultValues()
366
+ // with no parameters passed in, assume the method requires an
367
+ // inDocument parameter, and thus may only be invoked at run
368
+ // time. Mark the UI element object accordingly.
369
+ try {
370
+ arg.getDefaultValues();
371
+ }
372
+ catch (e) {
373
+ this.isDefaultLocatorConstructionDeferred = true;
374
+ }
375
+ }
376
+
377
+ }
378
+
379
+ if (!this.isDefaultLocatorConstructionDeferred) {
380
+ this.defaultLocators = this.getDefaultLocators();
381
+ }
382
+ };
383
+
384
+
385
+
386
+ this.init(uiElementShorthand);
387
+ }
388
+
389
+ // hang this off the UIElement "namespace". 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) {
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()
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);
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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];
1571
+ command.result = '';
1572
+ if (rollup.replacementIndexes.indexOf(k) != -1) {
1573
+ command.selectedForReplacement = true;
1574
+ }
1575
+ editor.view.rowUpdated(replacementIndex);
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
+