selenium-core-runner 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
+