selenium-webdriver 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. data/chrome/prebuilt/Win32/Release/npchromedriver.dll +0 -0
  2. data/chrome/prebuilt/x64/Release/npchromedriver.dll +0 -0
  3. data/chrome/src/extension/background.js +1 -0
  4. data/chrome/src/extension/content_script.js +182 -177
  5. data/chrome/src/extension/manifest-nonwin.json +1 -1
  6. data/chrome/src/extension/manifest-win.json +1 -1
  7. data/chrome/src/rb/lib/selenium/webdriver/chrome/bridge.rb +9 -3
  8. data/chrome/src/rb/lib/selenium/webdriver/chrome/launcher.rb +1 -1
  9. data/common/src/js/core/Blank.html +7 -0
  10. data/common/src/js/core/InjectedRemoteRunner.html +8 -0
  11. data/common/src/js/core/RemoteRunner.html +101 -0
  12. data/common/src/js/core/SeleniumLog.html +109 -0
  13. data/common/src/js/core/TestPrompt.html +145 -0
  14. data/common/src/js/core/TestRunner-splash.html +55 -0
  15. data/common/src/js/core/TestRunner.html +177 -0
  16. data/common/src/js/core/icons/all.png +0 -0
  17. data/common/src/js/core/icons/continue.png +0 -0
  18. data/common/src/js/core/icons/continue_disabled.png +0 -0
  19. data/common/src/js/core/icons/pause.png +0 -0
  20. data/common/src/js/core/icons/pause_disabled.png +0 -0
  21. data/common/src/js/core/icons/selected.png +0 -0
  22. data/common/src/js/core/icons/step.png +0 -0
  23. data/common/src/js/core/icons/step_disabled.png +0 -0
  24. data/common/src/js/core/lib/cssQuery/cssQuery-p.js +6 -0
  25. data/common/src/js/core/lib/cssQuery/src/cssQuery-level2.js +142 -0
  26. data/common/src/js/core/lib/cssQuery/src/cssQuery-level3.js +150 -0
  27. data/common/src/js/core/lib/cssQuery/src/cssQuery-standard.js +53 -0
  28. data/common/src/js/core/lib/cssQuery/src/cssQuery.js +356 -0
  29. data/common/src/js/core/lib/prototype.js +2006 -0
  30. data/common/src/js/core/lib/scriptaculous/builder.js +101 -0
  31. data/common/src/js/core/lib/scriptaculous/controls.js +815 -0
  32. data/common/src/js/core/lib/scriptaculous/dragdrop.js +915 -0
  33. data/common/src/js/core/lib/scriptaculous/effects.js +958 -0
  34. data/common/src/js/core/lib/scriptaculous/scriptaculous.js +47 -0
  35. data/common/src/js/core/lib/scriptaculous/slider.js +283 -0
  36. data/common/src/js/core/lib/scriptaculous/unittest.js +383 -0
  37. data/common/src/js/core/lib/snapsie.js +91 -0
  38. data/common/src/js/core/scripts/find_matching_child.js +69 -0
  39. data/common/src/js/core/scripts/htmlutils.js +1623 -0
  40. data/common/src/js/core/scripts/injection.html +72 -0
  41. data/common/src/js/core/scripts/selenium-api.js +3294 -0
  42. data/common/src/js/core/scripts/selenium-browserbot.js +2430 -0
  43. data/common/src/js/core/scripts/selenium-browserdetect.js +153 -0
  44. data/common/src/js/core/scripts/selenium-commandhandlers.js +379 -0
  45. data/common/src/js/core/scripts/selenium-executionloop.js +175 -0
  46. data/common/src/js/core/scripts/selenium-logging.js +148 -0
  47. data/common/src/js/core/scripts/selenium-remoterunner.js +695 -0
  48. data/common/src/js/core/scripts/selenium-testrunner.js +1362 -0
  49. data/common/src/js/core/scripts/selenium-version.js +5 -0
  50. data/common/src/js/core/scripts/ui-doc.html +808 -0
  51. data/common/src/js/core/scripts/ui-element.js +1644 -0
  52. data/common/src/js/core/scripts/ui-map-sample.js +979 -0
  53. data/common/src/js/core/scripts/user-extensions.js +3 -0
  54. data/common/src/js/core/scripts/user-extensions.js.sample +75 -0
  55. data/common/src/js/core/scripts/xmlextras.js +153 -0
  56. data/common/src/js/core/selenium-logo.png +0 -0
  57. data/common/src/js/core/selenium-test.css +43 -0
  58. data/common/src/js/core/selenium.css +316 -0
  59. data/common/src/js/core/xpath/dom.js +566 -0
  60. data/common/src/js/core/xpath/javascript-xpath-0.1.11.js +2816 -0
  61. data/common/src/js/core/xpath/util.js +549 -0
  62. data/common/src/js/core/xpath/xmltoken.js +149 -0
  63. data/common/src/js/core/xpath/xpath.js +2481 -0
  64. data/common/src/js/jsunit/app/css/jsUnitStyle.css +50 -0
  65. data/common/src/js/jsunit/app/css/readme +10 -0
  66. data/common/src/js/jsunit/app/emptyPage.html +11 -0
  67. data/common/src/js/jsunit/app/jsUnitCore.js +534 -0
  68. data/common/src/js/jsunit/app/jsUnitMockTimeout.js +81 -0
  69. data/common/src/js/jsunit/app/jsUnitTestManager.js +705 -0
  70. data/common/src/js/jsunit/app/jsUnitTestSuite.js +44 -0
  71. data/common/src/js/jsunit/app/jsUnitTracer.js +102 -0
  72. data/common/src/js/jsunit/app/jsUnitVersionCheck.js +59 -0
  73. data/common/src/js/jsunit/app/main-counts-errors.html +12 -0
  74. data/common/src/js/jsunit/app/main-counts-failures.html +13 -0
  75. data/common/src/js/jsunit/app/main-counts-runs.html +13 -0
  76. data/common/src/js/jsunit/app/main-counts.html +21 -0
  77. data/common/src/js/jsunit/app/main-data.html +178 -0
  78. data/common/src/js/jsunit/app/main-errors.html +23 -0
  79. data/common/src/js/jsunit/app/main-frame.html +19 -0
  80. data/common/src/js/jsunit/app/main-loader.html +45 -0
  81. data/common/src/js/jsunit/app/main-progress.html +25 -0
  82. data/common/src/js/jsunit/app/main-results.html +67 -0
  83. data/common/src/js/jsunit/app/main-status.html +13 -0
  84. data/common/src/js/jsunit/app/testContainer.html +16 -0
  85. data/common/src/js/jsunit/app/testContainerController.html +77 -0
  86. data/common/src/js/jsunit/app/xbDebug.js +306 -0
  87. data/common/src/js/jsunit/changelog.txt +60 -0
  88. data/common/src/js/jsunit/css/jsUnitStyle.css +83 -0
  89. data/common/src/js/jsunit/images/green.gif +0 -0
  90. data/common/src/js/jsunit/images/logo_jsunit.gif +0 -0
  91. data/common/src/js/jsunit/images/powerby-transparent.gif +0 -0
  92. data/common/src/js/jsunit/images/red.gif +0 -0
  93. data/common/src/js/jsunit/licenses/JDOM_license.txt +56 -0
  94. data/common/src/js/jsunit/licenses/Jetty_license.html +213 -0
  95. data/common/src/js/jsunit/licenses/MPL-1.1.txt +470 -0
  96. data/common/src/js/jsunit/licenses/gpl-2.txt +340 -0
  97. data/common/src/js/jsunit/licenses/index.html +141 -0
  98. data/common/src/js/jsunit/licenses/lgpl-2.1.txt +504 -0
  99. data/common/src/js/jsunit/licenses/mpl-tri-license-c.txt +35 -0
  100. data/common/src/js/jsunit/licenses/mpl-tri-license-html.txt +35 -0
  101. data/common/src/js/jsunit/readme.txt +19 -0
  102. data/common/src/js/jsunit/testRunner.html +167 -0
  103. data/common/src/js/jsunit/version.txt +1 -0
  104. data/common/src/rb/README +29 -0
  105. data/common/src/rb/lib/selenium/webdriver/driver.rb +124 -12
  106. data/common/src/rb/lib/selenium/webdriver/element.rb +119 -3
  107. data/common/src/rb/lib/selenium/webdriver/error.rb +1 -2
  108. data/common/src/rb/lib/selenium/webdriver/find.rb +19 -2
  109. data/common/src/rb/lib/selenium/webdriver/keys.rb +5 -1
  110. data/common/src/rb/lib/selenium/webdriver/navigation.rb +8 -4
  111. data/common/src/rb/lib/selenium/webdriver/platform.rb +4 -2
  112. data/common/src/rb/lib/selenium/webdriver/target_locator.rb +18 -0
  113. data/firefox/prebuilt/Win32/Release/webdriver-firefox.dll +0 -0
  114. data/firefox/prebuilt/linux/Release/libwebdriver-firefox.so +0 -0
  115. data/firefox/prebuilt/linux/Release/x_ignore_nofocus.so +0 -0
  116. data/firefox/prebuilt/linux64/Release/libwebdriver-firefox.so +0 -0
  117. data/firefox/prebuilt/linux64/Release/x_ignore_nofocus.so +0 -0
  118. data/firefox/src/extension/components/utils.js +13 -2
  119. data/firefox/src/extension/install.rdf +1 -1
  120. data/firefox/src/rb/lib/selenium/webdriver/firefox.rb +3 -2
  121. data/firefox/src/rb/lib/selenium/webdriver/firefox/binary.rb +1 -7
  122. data/firefox/src/rb/lib/selenium/webdriver/firefox/bridge.rb +11 -4
  123. data/firefox/src/rb/lib/selenium/webdriver/firefox/profile.rb +56 -15
  124. data/firefox/src/rb/lib/selenium/webdriver/firefox/util.rb +1 -1
  125. data/jobbie/prebuilt/Win32/Release/InternetExplorerDriver.dll +0 -0
  126. data/jobbie/prebuilt/x64/Release/InternetExplorerDriver.dll +0 -0
  127. data/jobbie/src/rb/lib/selenium/webdriver/ie/bridge.rb +10 -5
  128. data/jobbie/src/rb/lib/selenium/webdriver/ie/util.rb +9 -10
  129. data/remote/client/src/rb/lib/selenium/webdriver/remote/bridge.rb +4 -2
  130. data/remote/client/src/rb/lib/selenium/webdriver/remote/capabilities.rb +23 -23
  131. metadata +103 -6
  132. data/jobbie/prebuilt/Win32/Release/webdriver-ie-test.dll +0 -0
  133. data/jobbie/prebuilt/Win32/Release/webdriver-ie.dll +0 -0
  134. data/jobbie/prebuilt/x64/Release/webdriver-ie-test.dll +0 -0
  135. data/jobbie/prebuilt/x64/Release/webdriver-ie.dll +0 -0
@@ -0,0 +1,1644 @@
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 inDocument the document object to pass to the getDefaultValues()
107
+ * method of each argument.
108
+ *
109
+ * @return a list of associative arrays containing key value pairs
110
+ */
111
+ this.permuteArgs = function(args, inDocument) {
112
+ if (args.length == 0) {
113
+ return [];
114
+ }
115
+
116
+ var permutations = [];
117
+ var arg = args[0];
118
+ var remainingArgs = args.slice(1);
119
+ var subsequentPermutations = this.permuteArgs(remainingArgs,
120
+ inDocument);
121
+ var defaultValues = arg.getDefaultValues(inDocument);
122
+
123
+ // skip arguments for which no default values are defined. If the
124
+ // argument is a required one, then no permutations are possible.
125
+ if (defaultValues.length == 0) {
126
+ if (arg.required) {
127
+ return [];
128
+ }
129
+ else {
130
+ return subsequentPermutations;
131
+ }
132
+ }
133
+
134
+ for (var i = 0; i < defaultValues.length; ++i) {
135
+ var value = defaultValues[i];
136
+ var permutation;
137
+
138
+ if (subsequentPermutations.length == 0) {
139
+ permutation = {};
140
+ permutation[arg.name] = value + "";
141
+ permutations.push(permutation);
142
+ }
143
+ else {
144
+ for (var j = 0; j < subsequentPermutations.length; ++j) {
145
+ permutation = clone(subsequentPermutations[j]);
146
+ permutation[arg.name] = value + "";
147
+ permutations.push(permutation);
148
+ }
149
+ }
150
+ }
151
+
152
+ return permutations;
153
+ }
154
+
155
+
156
+
157
+ /**
158
+ * Returns a list of all testcases for this UIElement.
159
+ */
160
+ this.getTestcases = function()
161
+ {
162
+ return this.testcases;
163
+ }
164
+
165
+
166
+
167
+ /**
168
+ * Run all unit tests, stopping at the first failure, if any. Return true
169
+ * if no failures encountered, false otherwise. See the following thread
170
+ * regarding use of getElementById() on XML documents created by parsing
171
+ * text via the DOMParser:
172
+ *
173
+ * http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/2b1b82b3c53a1282/
174
+ */
175
+ this.test = function()
176
+ {
177
+ var parser = new DOMParser();
178
+ var testcases = this.getTestcases();
179
+ testcaseLoop: for (var i = 0; i < testcases.length; ++i) {
180
+ var testcase = testcases[i];
181
+ var xhtml = UI_GLOBAL.XHTML_DOCTYPE + '<html xmlns="'
182
+ + UI_GLOBAL.XHTML_XMLNS + '">' + testcase.xhtml + '</html>';
183
+ var doc = parser.parseFromString(xhtml, "text/xml");
184
+ if (doc.firstChild.nodeName == 'parsererror') {
185
+ safe_alert('Error parsing XHTML in testcase "' + testcase.name
186
+ + '" for UI element "' + this.name + '": ' + "\n"
187
+ + doc.firstChild.firstChild.nodeValue);
188
+ }
189
+
190
+ // we're no longer using the default locators when testing, because
191
+ // args is now required
192
+ var locator = parse_locator(this.getLocator(testcase.args));
193
+ var results;
194
+ if (locator.type == 'xpath' || (locator.type == 'implicit' &&
195
+ locator.string.substring(0, 2) == '//')) {
196
+ // try using the javascript xpath engine to avoid namespace
197
+ // issues. The xpath does have to be lowercase however, it
198
+ // seems.
199
+ results = eval_xpath(locator.string, doc,
200
+ { allowNativeXpath: false, returnOnFirstMatch: true });
201
+ }
202
+ else {
203
+ // piece the locator back together
204
+ locator = (locator.type == 'implicit')
205
+ ? locator.string
206
+ : locator.type + '=' + locator.string;
207
+ results = eval_locator(locator, doc);
208
+ }
209
+ if (results.length && results[0].hasAttribute('expected-result')) {
210
+ continue testcaseLoop;
211
+ }
212
+
213
+ // testcase failed
214
+ if (is_IDE()) {
215
+ var msg = 'Testcase "' + testcase.name
216
+ + '" failed for UI element "' + this.name + '":';
217
+ if (!results.length) {
218
+ msg += '\n"' + locator + '" did not match any elements!';
219
+ }
220
+ else {
221
+ msg += '\n' + results[0] + ' was not the expected result!';
222
+ }
223
+ safe_alert(msg);
224
+ }
225
+ return false;
226
+ }
227
+ return true;
228
+ };
229
+
230
+
231
+
232
+ /**
233
+ * Creates a set of locators using permutations of default values for
234
+ * arguments used in the locator construction. The set is returned as an
235
+ * object mapping locators to key-value arguments objects containing the
236
+ * values passed to getLocator() to create the locator.
237
+ *
238
+ * @param opt_inDocument (optional) the document object of the "current"
239
+ * page when this method is invoked. Some arguments
240
+ * may have default value lists that are calculated
241
+ * based on the contents of the page.
242
+ *
243
+ * @return a list of locator strings
244
+ * @throws UIElementException
245
+ */
246
+ this.getDefaultLocators = function(opt_inDocument) {
247
+ var defaultLocators = {};
248
+ if (this.args.length == 0) {
249
+ defaultLocators[this.getLocator({})] = {};
250
+ }
251
+ else {
252
+ var permutations = this.permuteArgs(this.args, opt_inDocument);
253
+ if (permutations.length != 0) {
254
+ for (var i = 0; i < permutations.length; ++i) {
255
+ var args = permutations[i];
256
+ var locator = this.getLocator(args);
257
+ if (!locator) {
258
+ throw new UIElementException('Error in UIElement(): '
259
+ + 'no getLocator return value for element "' + name
260
+ + '"');
261
+ }
262
+ defaultLocators[locator] = args;
263
+ }
264
+ }
265
+ else {
266
+ // try using no arguments. Parse the locator to make sure it's
267
+ // really good. If it doesn't work, fine.
268
+ try {
269
+ var locator = this.getLocator();
270
+ parse_locator(locator);
271
+ defaultLocators[locator] = {};
272
+ }
273
+ catch (e) {
274
+ safe_log('debug', e.message);
275
+ }
276
+ }
277
+ }
278
+ return defaultLocators;
279
+ };
280
+
281
+
282
+
283
+ /**
284
+ * Validate the structure of the shorthand notation this object is being
285
+ * initialized with. Throws an exception if there's a validation error.
286
+ *
287
+ * @param uiElementShorthand
288
+ *
289
+ * @throws UIElementException
290
+ */
291
+ this.validate = function(uiElementShorthand)
292
+ {
293
+ var msg = "UIElement validation error:\n" + print_r(uiElementShorthand);
294
+ if (!uiElementShorthand.name) {
295
+ throw new UIElementException(msg + 'no name specified!');
296
+ }
297
+ if (!uiElementShorthand.description) {
298
+ throw new UIElementException(msg + 'no description specified!');
299
+ }
300
+ if (!uiElementShorthand.locator
301
+ && !uiElementShorthand.getLocator
302
+ && !uiElementShorthand.xpath
303
+ && !uiElementShorthand.getXPath) {
304
+ throw new UIElementException(msg + 'no locator specified!');
305
+ }
306
+ };
307
+
308
+
309
+
310
+ this.init = function(uiElementShorthand)
311
+ {
312
+ this.validate(uiElementShorthand);
313
+
314
+ this.name = uiElementShorthand.name;
315
+ this.description = uiElementShorthand.description;
316
+
317
+ // construct a new getLocator() method based on the locator property,
318
+ // or use the provided function. We're deprecating the xpath property
319
+ // and getXPath() function, but still allow for them for backwards
320
+ // compatability.
321
+ if (uiElementShorthand.locator) {
322
+ this.getLocator = function(args) {
323
+ return uiElementShorthand.locator;
324
+ };
325
+ }
326
+ else if (uiElementShorthand.getLocator) {
327
+ this.getLocator = uiElementShorthand.getLocator;
328
+ }
329
+ else if (uiElementShorthand.xpath) {
330
+ this.getLocator = function(args) {
331
+ return uiElementShorthand.xpath;
332
+ };
333
+ }
334
+ else {
335
+ this.getLocator = uiElementShorthand.getXPath;
336
+ }
337
+
338
+ if (uiElementShorthand.genericLocator) {
339
+ this.getGenericLocator = function() {
340
+ return uiElementShorthand.genericLocator;
341
+ };
342
+ }
343
+ else if (uiElementShorthand.getGenericLocator) {
344
+ this.getGenericLocator = uiElementShorthand.getGenericLocator;
345
+ }
346
+
347
+ if (uiElementShorthand.getOffsetLocator) {
348
+ this.getOffsetLocator = uiElementShorthand.getOffsetLocator;
349
+ }
350
+
351
+ // get the testcases and local variables
352
+ this.testcases = [];
353
+ var localVars = {};
354
+ for (var attr in uiElementShorthand) {
355
+ if (attr.match(/^testcase/)) {
356
+ var testcase = uiElementShorthand[attr];
357
+ if (uiElementShorthand.args &&
358
+ uiElementShorthand.args.length && !testcase.args) {
359
+ safe_alert('No args defined in ' + attr + ' for UI element '
360
+ + this.name + '! Skipping testcase.');
361
+ continue;
362
+ }
363
+ testcase.name = attr;
364
+ this.testcases.push(testcase);
365
+ }
366
+ else if (attr.match(/^_/)) {
367
+ this[attr] = uiElementShorthand[attr];
368
+ localVars[attr] = uiElementShorthand[attr];
369
+ }
370
+ }
371
+
372
+ // create the arguments
373
+ this.args = []
374
+ this.argsOrder = [];
375
+ if (uiElementShorthand.args) {
376
+ for (var i = 0; i < uiElementShorthand.args.length; ++i) {
377
+ var arg = new UIArgument(uiElementShorthand.args[i], localVars);
378
+ this.args.push(arg);
379
+ this.argsOrder.push(arg.name);
380
+
381
+ // if an exception is thrown when invoking getDefaultValues()
382
+ // with no parameters passed in, assume the method requires an
383
+ // inDocument parameter, and thus may only be invoked at run
384
+ // time. Mark the UI element object accordingly.
385
+ try {
386
+ arg.getDefaultValues();
387
+ }
388
+ catch (e) {
389
+ this.isDefaultLocatorConstructionDeferred = true;
390
+ }
391
+ }
392
+
393
+ }
394
+
395
+ if (!this.isDefaultLocatorConstructionDeferred) {
396
+ this.defaultLocators = this.getDefaultLocators();
397
+ }
398
+ };
399
+
400
+
401
+
402
+ this.init(uiElementShorthand);
403
+ }
404
+
405
+ // hang this off the UIElement "namespace". This is a composite strategy.
406
+ UIElement.defaultOffsetLocatorStrategy = function(locatedElement, pageElement) {
407
+ var strategies = [
408
+ UIElement.linkXPathOffsetLocatorStrategy
409
+ , UIElement.preferredAttributeXPathOffsetLocatorStrategy
410
+ , UIElement.simpleXPathOffsetLocatorStrategy
411
+ ];
412
+
413
+ for (var i = 0; i < strategies.length; ++i) {
414
+ var strategy = strategies[i];
415
+ var offsetLocator = strategy(locatedElement, pageElement);
416
+
417
+ if (offsetLocator) {
418
+ return offsetLocator;
419
+ }
420
+ }
421
+
422
+ return null;
423
+ };
424
+
425
+ UIElement.simpleXPathOffsetLocatorStrategy = function(locatedElement,
426
+ pageElement)
427
+ {
428
+ if (is_ancestor(locatedElement, pageElement)) {
429
+ var xpath = "";
430
+ var recorder = Recorder.get(locatedElement.ownerDocument.defaultView);
431
+ var locatorBuilders = recorder.locatorBuilders;
432
+ var currentNode = pageElement;
433
+
434
+ while (currentNode != null && currentNode != locatedElement) {
435
+ xpath = locatorBuilders.relativeXPathFromParent(currentNode)
436
+ + xpath;
437
+ currentNode = currentNode.parentNode;
438
+ }
439
+
440
+ var results = eval_xpath(xpath, locatedElement.ownerDocument,
441
+ { contextNode: locatedElement });
442
+
443
+ if (results.length > 0 && results[0] == pageElement) {
444
+ return xpath;
445
+ }
446
+ }
447
+
448
+ return null;
449
+ };
450
+
451
+ UIElement.linkXPathOffsetLocatorStrategy = function(locatedElement, pageElement)
452
+ {
453
+ if (pageElement.nodeName == 'A' && is_ancestor(locatedElement, pageElement))
454
+ {
455
+ var text = pageElement.textContent
456
+ .replace(/^\s+/, "")
457
+ .replace(/\s+$/, "");
458
+
459
+ if (text) {
460
+ var xpath = '/descendant::a[normalize-space()='
461
+ + text.quoteForXPath() + ']';
462
+
463
+ var results = eval_xpath(xpath, locatedElement.ownerDocument,
464
+ { contextNode: locatedElement });
465
+
466
+ if (results.length > 0 && results[0] == pageElement) {
467
+ return xpath;
468
+ }
469
+ }
470
+ }
471
+
472
+ return null;
473
+ };
474
+
475
+ // compare to the "xpath:attributes" locator strategy defined in the IDE source
476
+ UIElement.preferredAttributeXPathOffsetLocatorStrategy =
477
+ function(locatedElement, pageElement)
478
+ {
479
+ // this is an ordered listing of single attributes
480
+ var preferredAttributes = [
481
+ 'name'
482
+ , 'value'
483
+ , 'type'
484
+ , 'action'
485
+ , 'alt'
486
+ , 'title'
487
+ , 'class'
488
+ , 'src'
489
+ , 'href'
490
+ , 'onclick'
491
+ ];
492
+
493
+ if (is_ancestor(locatedElement, pageElement)) {
494
+ var xpathBase = '/descendant::' + pageElement.nodeName.toLowerCase();
495
+
496
+ for (var i = 0; i < preferredAttributes.length; ++i) {
497
+ var name = preferredAttributes[i];
498
+ var value = pageElement.getAttribute(name);
499
+
500
+ if (value) {
501
+ var xpath = xpathBase + '[@' + name + '='
502
+ + value.quoteForXPath() + ']';
503
+
504
+ var results = eval_xpath(xpath, locatedElement.ownerDocument,
505
+ { contextNode: locatedElement });
506
+
507
+ if (results.length > 0 && results[0] == pageElement) {
508
+ return xpath;
509
+ }
510
+ }
511
+ }
512
+ }
513
+
514
+ return null;
515
+ };
516
+
517
+
518
+
519
+ /**
520
+ * Constructs a UIArgument. This is mostly for checking that the values are
521
+ * valid.
522
+ *
523
+ * @param uiArgumentShorthand
524
+ * @param localVars
525
+ *
526
+ * @throws UIArgumentException
527
+ */
528
+ function UIArgument(uiArgumentShorthand, localVars)
529
+ {
530
+ /**
531
+ * @param uiArgumentShorthand
532
+ *
533
+ * @throws UIArgumentException
534
+ */
535
+ this.validate = function(uiArgumentShorthand)
536
+ {
537
+ var msg = "UIArgument validation error:\n"
538
+ + print_r(uiArgumentShorthand);
539
+
540
+ // try really hard to throw an exception!
541
+ if (!uiArgumentShorthand.name) {
542
+ throw new UIArgumentException(msg + 'no name specified!');
543
+ }
544
+ if (!uiArgumentShorthand.description) {
545
+ throw new UIArgumentException(msg + 'no description specified!');
546
+ }
547
+ if (!uiArgumentShorthand.defaultValues &&
548
+ !uiArgumentShorthand.getDefaultValues) {
549
+ throw new UIArgumentException(msg + 'no default values specified!');
550
+ }
551
+ };
552
+
553
+
554
+
555
+ /**
556
+ * @param uiArgumentShorthand
557
+ * @param localVars a list of local variables
558
+ */
559
+ this.init = function(uiArgumentShorthand, localVars)
560
+ {
561
+ this.validate(uiArgumentShorthand);
562
+
563
+ this.name = uiArgumentShorthand.name;
564
+ this.description = uiArgumentShorthand.description;
565
+ this.required = uiArgumentShorthand.required || false;
566
+
567
+ if (uiArgumentShorthand.defaultValues) {
568
+ var defaultValues = uiArgumentShorthand.defaultValues;
569
+ this.getDefaultValues =
570
+ function() { return defaultValues; }
571
+ }
572
+ else {
573
+ this.getDefaultValues = uiArgumentShorthand.getDefaultValues;
574
+ }
575
+
576
+ for (var name in localVars) {
577
+ this[name] = localVars[name];
578
+ }
579
+ }
580
+
581
+
582
+
583
+ this.init(uiArgumentShorthand, localVars);
584
+ }
585
+
586
+
587
+
588
+ /**
589
+ * The UISpecifier constructor is overloaded. If less than three arguments are
590
+ * provided, the first argument will be considered a UI specifier string, and
591
+ * will be split out accordingly. Otherwise, the first argument will be
592
+ * considered the path.
593
+ *
594
+ * @param uiSpecifierStringOrPagesetName a UI specifier string, or the pageset
595
+ * name of the UI specifier
596
+ * @param elementName the name of the element
597
+ * @param args an object associating keys to values
598
+ *
599
+ * @return new UISpecifier object
600
+ */
601
+ function UISpecifier(uiSpecifierStringOrPagesetName, elementName, args)
602
+ {
603
+ /**
604
+ * Initializes this object from a UI specifier string of the form:
605
+ *
606
+ * pagesetName::elementName(arg1=value1, arg2=value2, ...)
607
+ *
608
+ * into its component parts, and returns them as an object.
609
+ *
610
+ * @return an object containing the components of the UI specifier
611
+ * @throws UISpecifierException
612
+ */
613
+ this._initFromUISpecifierString = function(uiSpecifierString) {
614
+ var matches = /^(.*)::([^\(]+)\((.*)\)$/.exec(uiSpecifierString);
615
+ if (matches == null) {
616
+ throw new UISpecifierException('Error in '
617
+ + 'UISpecifier._initFromUISpecifierString(): "'
618
+ + this.string + '" is not a valid UI specifier string');
619
+ }
620
+ this.pagesetName = matches[1];
621
+ this.elementName = matches[2];
622
+ this.args = (matches[3]) ? parse_kwargs(matches[3]) : {};
623
+ };
624
+
625
+
626
+
627
+ /**
628
+ * Override the toString() method to return the UI specifier string when
629
+ * evaluated in a string context. Combines the UI specifier components into
630
+ * a canonical UI specifier string and returns it.
631
+ *
632
+ * @return a UI specifier string
633
+ */
634
+ this.toString = function() {
635
+ // empty string is acceptable for the path, but it must be defined
636
+ if (this.pagesetName == undefined) {
637
+ throw new UISpecifierException('Error in UISpecifier.toString(): "'
638
+ + this.pagesetName + '" is not a valid UI specifier pageset '
639
+ + 'name');
640
+ }
641
+ if (!this.elementName) {
642
+ throw new UISpecifierException('Error in UISpecifier.unparse(): "'
643
+ + this.elementName + '" is not a valid UI specifier element '
644
+ + 'name');
645
+ }
646
+ if (!this.args) {
647
+ throw new UISpecifierException('Error in UISpecifier.unparse(): "'
648
+ + this.args + '" are not valid UI specifier args');
649
+ }
650
+
651
+ uiElement = UIMap.getInstance()
652
+ .getUIElement(this.pagesetName, this.elementName);
653
+ if (uiElement != null) {
654
+ var kwargs = to_kwargs(this.args, uiElement.argsOrder);
655
+ }
656
+ else {
657
+ // probably under unit test
658
+ var kwargs = to_kwargs(this.args);
659
+ }
660
+
661
+ return this.pagesetName + '::' + this.elementName + '(' + kwargs + ')';
662
+ };
663
+
664
+ // construct the object
665
+ if (arguments.length < 2) {
666
+ this._initFromUISpecifierString(uiSpecifierStringOrPagesetName);
667
+ }
668
+ else {
669
+ this.pagesetName = uiSpecifierStringOrPagesetName;
670
+ this.elementName = elementName;
671
+ this.args = (args) ? clone(args) : {};
672
+ }
673
+ }
674
+
675
+
676
+
677
+ function Pageset(pagesetShorthand)
678
+ {
679
+ /**
680
+ * Returns true if the page is included in this pageset, false otherwise.
681
+ * The page is specified by a document object.
682
+ *
683
+ * @param inDocument the document object representing the page
684
+ */
685
+ this.contains = function(inDocument)
686
+ {
687
+ var urlParts = parseUri(unescape(inDocument.location.href));
688
+ var path = urlParts.path
689
+ .replace(/^\//, "")
690
+ .replace(/\/$/, "");
691
+ if (!this.pathRegexp.test(path)) {
692
+ return false;
693
+ }
694
+ for (var paramName in this.paramRegexps) {
695
+ var paramRegexp = this.paramRegexps[paramName];
696
+ if (!paramRegexp.test(urlParts.queryKey[paramName])) {
697
+ return false;
698
+ }
699
+ }
700
+ if (!this.pageContent(inDocument)) {
701
+ return false;
702
+ }
703
+
704
+ return true;
705
+ }
706
+
707
+
708
+
709
+ this.getUIElements = function()
710
+ {
711
+ var uiElements = [];
712
+ for (var uiElementName in this.uiElements) {
713
+ uiElements.push(this.uiElements[uiElementName]);
714
+ }
715
+ return uiElements;
716
+ };
717
+
718
+
719
+
720
+ /**
721
+ * Returns a list of UI specifier string stubs representing all UI elements
722
+ * for this pageset. Stubs contain all required arguments, but leave
723
+ * argument values blank. Each element stub is paired with the element's
724
+ * description.
725
+ *
726
+ * @return a list of UI specifier string stubs
727
+ */
728
+ this.getUISpecifierStringStubs = function()
729
+ {
730
+ var stubs = [];
731
+ for (var name in this.uiElements) {
732
+ var uiElement = this.uiElements[name];
733
+ var args = {};
734
+ for (var i = 0; i < uiElement.args.length; ++i) {
735
+ args[uiElement.args[i].name] = '';
736
+ }
737
+ var uiSpecifier = new UISpecifier(this.name, uiElement.name, args);
738
+ stubs.push([
739
+ UI_GLOBAL.UI_PREFIX + '=' + uiSpecifier.toString()
740
+ , uiElement.description
741
+ ]);
742
+ }
743
+ return stubs;
744
+ }
745
+
746
+
747
+
748
+ /**
749
+ * Throws an exception on validation failure.
750
+ */
751
+ this._validate = function(pagesetShorthand)
752
+ {
753
+ var msg = "Pageset validation error:\n"
754
+ + print_r(pagesetShorthand);
755
+ if (!pagesetShorthand.name) {
756
+ throw new PagesetException(msg + 'no name specified!');
757
+ }
758
+ if (!pagesetShorthand.description) {
759
+ throw new PagesetException(msg + 'no description specified!');
760
+ }
761
+ if (!pagesetShorthand.paths &&
762
+ !pagesetShorthand.pathRegexp &&
763
+ !pagesetShorthand.pageContent) {
764
+ throw new PagesetException(msg
765
+ + 'no path, pathRegexp, or pageContent specified!');
766
+ }
767
+ };
768
+
769
+
770
+
771
+ this.init = function(pagesetShorthand)
772
+ {
773
+ this._validate(pagesetShorthand);
774
+
775
+ this.name = pagesetShorthand.name;
776
+ this.description = pagesetShorthand.description;
777
+
778
+ var pathPrefixRegexp = pagesetShorthand.pathPrefix
779
+ ? RegExp.escape(pagesetShorthand.pathPrefix) : "";
780
+ var pathRegexp = '^' + pathPrefixRegexp;
781
+
782
+ if (pagesetShorthand.paths != undefined) {
783
+ pathRegexp += '(?:';
784
+ for (var i = 0; i < pagesetShorthand.paths.length; ++i) {
785
+ if (i > 0) {
786
+ pathRegexp += '|';
787
+ }
788
+ pathRegexp += RegExp.escape(pagesetShorthand.paths[i]);
789
+ }
790
+ pathRegexp += ')$';
791
+ }
792
+ else if (pagesetShorthand.pathRegexp) {
793
+ pathRegexp += '(?:' + pagesetShorthand.pathRegexp + ')$';
794
+ }
795
+
796
+ this.pathRegexp = new RegExp(pathRegexp);
797
+ this.paramRegexps = {};
798
+ for (var paramName in pagesetShorthand.paramRegexps) {
799
+ this.paramRegexps[paramName] =
800
+ new RegExp(pagesetShorthand.paramRegexps[paramName]);
801
+ }
802
+ this.pageContent = pagesetShorthand.pageContent ||
803
+ function() { return true; };
804
+ this.uiElements = {};
805
+ };
806
+
807
+
808
+
809
+ this.init(pagesetShorthand);
810
+ }
811
+
812
+
813
+
814
+ /**
815
+ * Construct the UI map object, and return it. Once the object is instantiated,
816
+ * it binds to a global variable and will not leave scope.
817
+ *
818
+ * @return new UIMap object
819
+ */
820
+ function UIMap()
821
+ {
822
+ // the singleton pattern, split into two parts so that "new" can still
823
+ // be used, in addition to "getInstance()"
824
+ UIMap.self = this;
825
+
826
+ // need to attach variables directly to the Editor object in order for them
827
+ // to be in scope for Editor methods
828
+ if (is_IDE()) {
829
+ Editor.uiMap = this;
830
+ Editor.UI_PREFIX = UI_GLOBAL.UI_PREFIX;
831
+ }
832
+
833
+ this.pagesets = new Object();
834
+
835
+
836
+
837
+ /**
838
+ * pageset[pagesetName]
839
+ * regexp
840
+ * elements[elementName]
841
+ * UIElement
842
+ */
843
+ this.addPageset = function(pagesetShorthand)
844
+ {
845
+ try {
846
+ var pageset = new Pageset(pagesetShorthand);
847
+ }
848
+ catch (e) {
849
+ safe_alert("Could not create pageset from shorthand:\n"
850
+ + print_r(pagesetShorthand) + "\n" + e.message);
851
+ return false;
852
+ }
853
+
854
+ if (this.pagesets[pageset.name]) {
855
+ safe_alert('Could not add pageset "' + pageset.name
856
+ + '": a pageset with that name already exists!');
857
+ return false;
858
+ }
859
+
860
+ this.pagesets[pageset.name] = pageset;
861
+ return true;
862
+ };
863
+
864
+
865
+
866
+ /**
867
+ * @param pagesetName
868
+ * @param uiElementShorthand a representation of a UIElement object in
869
+ * shorthand JSON.
870
+ */
871
+ this.addElement = function(pagesetName, uiElementShorthand)
872
+ {
873
+ try {
874
+ var uiElement = new UIElement(uiElementShorthand);
875
+ }
876
+ catch (e) {
877
+ safe_alert("Could not create UI element from shorthand:\n"
878
+ + print_r(uiElementShorthand) + "\n" + e.message);
879
+ return false;
880
+ }
881
+
882
+ // run the element's unit tests only for the IDE, and only when the
883
+ // IDE is starting. Make a rough guess as to the latter condition.
884
+ if (is_IDE() && !editor.selDebugger && !uiElement.test()) {
885
+ safe_alert('Could not add UI element "' + uiElement.name
886
+ + '": failed testcases!');
887
+ return false;
888
+ }
889
+
890
+ try {
891
+ this.pagesets[pagesetName].uiElements[uiElement.name] = uiElement;
892
+ }
893
+ catch (e) {
894
+ safe_alert("Could not add UI element '" + uiElement.name
895
+ + "' to pageset '" + pagesetName + "':\n" + e.message);
896
+ return false;
897
+ }
898
+
899
+ return true;
900
+ };
901
+
902
+
903
+
904
+ /**
905
+ * Returns the pageset for a given UI specifier string.
906
+ *
907
+ * @param uiSpecifierString
908
+ * @return a pageset object
909
+ */
910
+ this.getPageset = function(uiSpecifierString)
911
+ {
912
+ try {
913
+ var uiSpecifier = new UISpecifier(uiSpecifierString);
914
+ return this.pagesets[uiSpecifier.pagesetName];
915
+ }
916
+ catch (e) {
917
+ return null;
918
+ }
919
+ }
920
+
921
+
922
+
923
+ /**
924
+ * Returns the UIElement that a UISpecifierString or pageset and element
925
+ * pair refer to.
926
+ *
927
+ * @param pagesetNameOrUISpecifierString
928
+ * @return a UIElement, or null if none is found associated with
929
+ * uiSpecifierString
930
+ */
931
+ this.getUIElement = function(pagesetNameOrUISpecifierString, uiElementName)
932
+ {
933
+ var pagesetName = pagesetNameOrUISpecifierString;
934
+ if (arguments.length == 1) {
935
+ var uiSpecifierString = pagesetNameOrUISpecifierString;
936
+ try {
937
+ var uiSpecifier = new UISpecifier(uiSpecifierString);
938
+ pagesetName = uiSpecifier.pagesetName;
939
+ var uiElementName = uiSpecifier.elementName;
940
+ }
941
+ catch (e) {
942
+ return null;
943
+ }
944
+ }
945
+ try {
946
+ return this.pagesets[pagesetName].uiElements[uiElementName];
947
+ }
948
+ catch (e) {
949
+ return null;
950
+ }
951
+ };
952
+
953
+
954
+
955
+ /**
956
+ * Returns a list of pagesets that "contains" the provided page,
957
+ * represented as a document object. Containership is defined by the
958
+ * Pageset object's contain() method.
959
+ *
960
+ * @param inDocument the page to get pagesets for
961
+ * @return a list of pagesets
962
+ */
963
+ this.getPagesetsForPage = function(inDocument)
964
+ {
965
+ var pagesets = [];
966
+ for (var pagesetName in this.pagesets) {
967
+ var pageset = this.pagesets[pagesetName];
968
+ if (pageset.contains(inDocument)) {
969
+ pagesets.push(pageset);
970
+ }
971
+ }
972
+ return pagesets;
973
+ };
974
+
975
+
976
+
977
+ /**
978
+ * Returns a list of all pagesets.
979
+ *
980
+ * @return a list of pagesets
981
+ */
982
+ this.getPagesets = function()
983
+ {
984
+ var pagesets = [];
985
+ for (var pagesetName in this.pagesets) {
986
+ pagesets.push(this.pagesets[pagesetName]);
987
+ }
988
+ return pagesets;
989
+ };
990
+
991
+
992
+
993
+ /**
994
+ * Returns a list of elements on a page that a given UI specifier string,
995
+ * maps to. If no elements are mapped to, returns an empty list..
996
+ *
997
+ * @param uiSpecifierString a String that specifies a UI element with
998
+ * attendant argument values
999
+ * @param inDocument the document object the specified UI element
1000
+ * appears in
1001
+ * @return a potentially-empty list of elements
1002
+ * specified by uiSpecifierString
1003
+ */
1004
+ this.getPageElements = function(uiSpecifierString, inDocument)
1005
+ {
1006
+ var locator = this.getLocator(uiSpecifierString);
1007
+ var results = locator ? eval_locator(locator, inDocument) : [];
1008
+ return results;
1009
+ };
1010
+
1011
+
1012
+
1013
+ /**
1014
+ * Returns the locator string that a given UI specifier string maps to, or
1015
+ * null if it cannot be mapped.
1016
+ *
1017
+ * @param uiSpecifierString
1018
+ */
1019
+ this.getLocator = function(uiSpecifierString)
1020
+ {
1021
+ try {
1022
+ var uiSpecifier = new UISpecifier(uiSpecifierString);
1023
+ }
1024
+ catch (e) {
1025
+ safe_alert('Could not create UISpecifier for string "'
1026
+ + uiSpecifierString + '": ' + e.message);
1027
+ return null;
1028
+ }
1029
+
1030
+ var uiElement = this.getUIElement(uiSpecifier.pagesetName,
1031
+ uiSpecifier.elementName);
1032
+ try {
1033
+ return uiElement.getLocator(uiSpecifier.args);
1034
+ }
1035
+ catch (e) {
1036
+ return null;
1037
+ }
1038
+ }
1039
+
1040
+
1041
+
1042
+ /**
1043
+ * Finds and returns a UI specifier string given an element and the page
1044
+ * that it appears on.
1045
+ *
1046
+ * @param pageElement the document element to map to a UI specifier
1047
+ * @param inDocument the document the element appears in
1048
+ * @return a UI specifier string, or false if one cannot be
1049
+ * constructed
1050
+ */
1051
+ this.getUISpecifierString = function(pageElement, inDocument)
1052
+ {
1053
+ var is_fuzzy_match =
1054
+ BrowserBot.prototype.locateElementByUIElement.is_fuzzy_match;
1055
+ var pagesets = this.getPagesetsForPage(inDocument);
1056
+
1057
+ for (var i = 0; i < pagesets.length; ++i) {
1058
+ var pageset = pagesets[i];
1059
+ var uiElements = pageset.getUIElements();
1060
+
1061
+ for (var j = 0; j < uiElements.length; ++j) {
1062
+ var uiElement = uiElements[j];
1063
+
1064
+ // first test against the generic locator, if there is one.
1065
+ // This should net some performance benefit when recording on
1066
+ // more complicated pages.
1067
+ if (uiElement.getGenericLocator) {
1068
+ var passedTest = false;
1069
+ var results =
1070
+ eval_locator(uiElement.getGenericLocator(), inDocument);
1071
+ for (var i = 0; i < results.length; ++i) {
1072
+ if (results[i] == pageElement) {
1073
+ passedTest = true;
1074
+ break;
1075
+ }
1076
+ }
1077
+ if (!passedTest) {
1078
+ continue;
1079
+ }
1080
+ }
1081
+
1082
+ var defaultLocators;
1083
+ if (uiElement.isDefaultLocatorConstructionDeferred) {
1084
+ defaultLocators = uiElement.getDefaultLocators(inDocument);
1085
+ }
1086
+ else {
1087
+ defaultLocators = uiElement.defaultLocators;
1088
+ }
1089
+
1090
+ //safe_alert(print_r(uiElement.defaultLocators));
1091
+ for (var locator in defaultLocators) {
1092
+ var locatedElements = eval_locator(locator, inDocument);
1093
+ if (locatedElements.length) {
1094
+ var locatedElement = locatedElements[0];
1095
+ }
1096
+ else {
1097
+ continue;
1098
+ }
1099
+
1100
+ // use a heuristic to determine whether the element
1101
+ // specified is the "same" as the element we're matching
1102
+ if (is_fuzzy_match) {
1103
+ if (is_fuzzy_match(locatedElement, pageElement)) {
1104
+ return UI_GLOBAL.UI_PREFIX + '=' +
1105
+ new UISpecifier(pageset.name, uiElement.name,
1106
+ defaultLocators[locator]);
1107
+ }
1108
+ }
1109
+ else {
1110
+ if (locatedElement == pageElement) {
1111
+ return UI_GLOBAL.UI_PREFIX + '=' +
1112
+ new UISpecifier(pageset.name, uiElement.name,
1113
+ defaultLocators[locator]);
1114
+ }
1115
+ }
1116
+
1117
+ // ok, matching the element failed. See if an offset
1118
+ // locator can complete the match.
1119
+ if (uiElement.getOffsetLocator) {
1120
+ for (var k = 0; k < locatedElements.length; ++k) {
1121
+ var offsetLocator = uiElement
1122
+ .getOffsetLocator(locatedElements[k], pageElement);
1123
+ if (offsetLocator) {
1124
+ return UI_GLOBAL.UI_PREFIX + '=' +
1125
+ new UISpecifier(pageset.name,
1126
+ uiElement.name,
1127
+ defaultLocators[locator])
1128
+ + '->' + offsetLocator;
1129
+ }
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ return false;
1136
+ };
1137
+
1138
+
1139
+
1140
+ /**
1141
+ * Returns a sorted list of UI specifier string stubs representing possible
1142
+ * UI elements for all pagesets, paired the their descriptions. Stubs
1143
+ * contain all required arguments, but leave argument values blank.
1144
+ *
1145
+ * @return a list of UI specifier string stubs
1146
+ */
1147
+ this.getUISpecifierStringStubs = function() {
1148
+ var stubs = [];
1149
+ var pagesets = this.getPagesets();
1150
+ for (var i = 0; i < pagesets.length; ++i) {
1151
+ stubs = stubs.concat(pagesets[i].getUISpecifierStringStubs());
1152
+ }
1153
+ stubs.sort(function(a, b) {
1154
+ if (a[0] < b[0]) {
1155
+ return -1;
1156
+ }
1157
+ return a[0] == b[0] ? 0 : 1;
1158
+ });
1159
+ return stubs;
1160
+ }
1161
+ }
1162
+
1163
+ UIMap.getInstance = function() {
1164
+ return (UIMap.self == null) ? new UIMap() : UIMap.self;
1165
+ }
1166
+
1167
+ //******************************************************************************
1168
+ // Rollups
1169
+
1170
+ /**
1171
+ * The Command object isn't available in the Selenium RC. We introduce an
1172
+ * object with the identical constructor. In the IDE, this will be redefined,
1173
+ * which is just fine.
1174
+ *
1175
+ * @param command
1176
+ * @param target
1177
+ * @param value
1178
+ */
1179
+ if (typeof(Command) == 'undefined') {
1180
+ function Command(command, target, value) {
1181
+ this.command = command != null ? command : '';
1182
+ this.target = target != null ? target : '';
1183
+ this.value = value != null ? value : '';
1184
+ }
1185
+ }
1186
+
1187
+
1188
+
1189
+ /**
1190
+ * A CommandMatcher object matches commands during the application of a
1191
+ * RollupRule. It's specified with a shorthand format, for example:
1192
+ *
1193
+ * new CommandMatcher({
1194
+ * command: 'click'
1195
+ * , target: 'ui=allPages::.+'
1196
+ * })
1197
+ *
1198
+ * which is intended to match click commands whose target is an element in the
1199
+ * allPages PageSet. The matching expressions are given as regular expressions;
1200
+ * in the example above, the command must be "click"; "clickAndWait" would be
1201
+ * acceptable if 'click.*' were used. Here's a more complete example:
1202
+ *
1203
+ * new CommandMatcher({
1204
+ * command: 'type'
1205
+ * , target: 'ui=loginPages::username()'
1206
+ * , value: '.+_test'
1207
+ * , updateArgs: function(command, args) {
1208
+ * args.username = command.value;
1209
+ * }
1210
+ * })
1211
+ *
1212
+ * Here, the command and target are fixed, but there is variability in the
1213
+ * value of the command. When a command matches, the username is saved to the
1214
+ * arguments object.
1215
+ */
1216
+ function CommandMatcher(commandMatcherShorthand)
1217
+ {
1218
+ /**
1219
+ * Ensure the shorthand notation used to initialize the CommandMatcher has
1220
+ * all required values.
1221
+ *
1222
+ * @param commandMatcherShorthand an object containing information about
1223
+ * the CommandMatcher
1224
+ */
1225
+ this.validate = function(commandMatcherShorthand) {
1226
+ var msg = "CommandMatcher validation error:\n"
1227
+ + print_r(commandMatcherShorthand);
1228
+ if (!commandMatcherShorthand.command) {
1229
+ throw new CommandMatcherException(msg + 'no command specified!');
1230
+ }
1231
+ if (!commandMatcherShorthand.target) {
1232
+ throw new CommandMatcherException(msg + 'no target specified!');
1233
+ }
1234
+ if (commandMatcherShorthand.minMatches &&
1235
+ commandMatcherShorthand.maxMatches &&
1236
+ commandMatcherShorthand.minMatches >
1237
+ commandMatcherShorthand.maxMatches) {
1238
+ throw new CommandMatcherException(msg + 'minMatches > maxMatches!');
1239
+ }
1240
+ };
1241
+
1242
+ /**
1243
+ * Initialize this object.
1244
+ *
1245
+ * @param commandMatcherShorthand an object containing information used to
1246
+ * initialize the CommandMatcher
1247
+ */
1248
+ this.init = function(commandMatcherShorthand) {
1249
+ this.validate(commandMatcherShorthand);
1250
+
1251
+ this.command = commandMatcherShorthand.command;
1252
+ this.target = commandMatcherShorthand.target;
1253
+ this.value = commandMatcherShorthand.value || null;
1254
+ this.minMatches = commandMatcherShorthand.minMatches || 1;
1255
+ this.maxMatches = commandMatcherShorthand.maxMatches || 1;
1256
+ this.updateArgs = commandMatcherShorthand.updateArgs ||
1257
+ function(command, args) { return args; };
1258
+ };
1259
+
1260
+ /**
1261
+ * Determines whether a given command matches. Updates args by "reference"
1262
+ * and returns true if it does; return false otherwise.
1263
+ *
1264
+ * @param command the command to attempt to match
1265
+ */
1266
+ this.isMatch = function(command) {
1267
+ var re = new RegExp('^' + this.command + '$');
1268
+ if (! re.test(command.command)) {
1269
+ return false;
1270
+ }
1271
+ re = new RegExp('^' + this.target + '$');
1272
+ if (! re.test(command.target)) {
1273
+ return false;
1274
+ }
1275
+ if (this.value != null) {
1276
+ re = new RegExp('^' + this.value + '$');
1277
+ if (! re.test(command.value)) {
1278
+ return false;
1279
+ }
1280
+ }
1281
+
1282
+ // okay, the command matches
1283
+ return true;
1284
+ };
1285
+
1286
+ // initialization
1287
+ this.init(commandMatcherShorthand);
1288
+ }
1289
+
1290
+
1291
+
1292
+ function RollupRuleException(message)
1293
+ {
1294
+ this.message = message;
1295
+ this.name = 'RollupRuleException';
1296
+ }
1297
+
1298
+ function RollupRule(rollupRuleShorthand)
1299
+ {
1300
+ /**
1301
+ * Ensure the shorthand notation used to initialize the RollupRule has all
1302
+ * required values.
1303
+ *
1304
+ * @param rollupRuleShorthand an object containing information about the
1305
+ * RollupRule
1306
+ */
1307
+ this.validate = function(rollupRuleShorthand) {
1308
+ var msg = "RollupRule validation error:\n"
1309
+ + print_r(rollupRuleShorthand);
1310
+ if (!rollupRuleShorthand.name) {
1311
+ throw new RollupRuleException(msg + 'no name specified!');
1312
+ }
1313
+ if (!rollupRuleShorthand.description) {
1314
+ throw new RollupRuleException(msg + 'no description specified!');
1315
+ }
1316
+ // rollupRuleShorthand.args is optional
1317
+ if (!rollupRuleShorthand.commandMatchers &&
1318
+ !rollupRuleShorthand.getRollup) {
1319
+ throw new RollupRuleException(msg
1320
+ + 'no command matchers specified!');
1321
+ }
1322
+ if (!rollupRuleShorthand.expandedCommands &&
1323
+ !rollupRuleShorthand.getExpandedCommands) {
1324
+ throw new RollupRuleException(msg
1325
+ + 'no expanded commands specified!');
1326
+ }
1327
+
1328
+ return true;
1329
+ };
1330
+
1331
+ /**
1332
+ * Initialize this object.
1333
+ *
1334
+ * @param rollupRuleShorthand an object containing information used to
1335
+ * initialize the RollupRule
1336
+ */
1337
+ this.init = function(rollupRuleShorthand) {
1338
+ this.validate(rollupRuleShorthand);
1339
+
1340
+ this.name = rollupRuleShorthand.name;
1341
+ this.description = rollupRuleShorthand.description;
1342
+ this.pre = rollupRuleShorthand.pre || '';
1343
+ this.post = rollupRuleShorthand.post || '';
1344
+ this.alternateCommand = rollupRuleShorthand.alternateCommand;
1345
+ this.args = rollupRuleShorthand.args || [];
1346
+
1347
+ if (rollupRuleShorthand.commandMatchers) {
1348
+ // construct the rule from the list of CommandMatchers
1349
+ this.commandMatchers = [];
1350
+ var matchers = rollupRuleShorthand.commandMatchers;
1351
+ for (var i = 0; i < matchers.length; ++i) {
1352
+ if (matchers[i].updateArgs && this.args.length == 0) {
1353
+ // enforce metadata for arguments
1354
+ var msg = "RollupRule validation error:\n"
1355
+ + print_r(rollupRuleShorthand)
1356
+ + 'no argument metadata provided!';
1357
+ throw new RollupRuleException(msg);
1358
+ }
1359
+ this.commandMatchers.push(new CommandMatcher(matchers[i]));
1360
+ }
1361
+
1362
+ // returns false if the rollup doesn't match, or a rollup command
1363
+ // if it does. If returned, the command contains the
1364
+ // replacementIndexes property, which indicates which commands it
1365
+ // substitutes for.
1366
+ this.getRollup = function(commands) {
1367
+ // this is a greedy matching algorithm
1368
+ var replacementIndexes = [];
1369
+ var commandMatcherQueue = this.commandMatchers;
1370
+ var matchCount = 0;
1371
+ var args = {};
1372
+ for (var i = 0, j = 0; i < commandMatcherQueue.length;) {
1373
+ var matcher = commandMatcherQueue[i];
1374
+ if (j >= commands.length) {
1375
+ // we've run out of commands! If the remaining matchers
1376
+ // do not have minMatches requirements, this is a
1377
+ // match. Otherwise, it's not.
1378
+ if (matcher.minMatches > 0) {
1379
+ return false;
1380
+ }
1381
+ ++i;
1382
+ matchCount = 0; // unnecessary, but let's be consistent
1383
+ }
1384
+ else {
1385
+ if (matcher.isMatch(commands[j])) {
1386
+ ++matchCount;
1387
+ if (matchCount == matcher.maxMatches) {
1388
+ // exhausted this matcher's matches ... move on
1389
+ // to next matcher
1390
+ ++i;
1391
+ matchCount = 0;
1392
+ }
1393
+ args = matcher.updateArgs(commands[j], args);
1394
+ replacementIndexes.push(j);
1395
+ ++j; // move on to next command
1396
+ }
1397
+ else {
1398
+ //alert(matchCount + ', ' + matcher.minMatches);
1399
+ if (matchCount < matcher.minMatches) {
1400
+ return false;
1401
+ }
1402
+ // didn't match this time, but we've satisfied the
1403
+ // requirements already ... move on to next matcher
1404
+ ++i;
1405
+ matchCount = 0;
1406
+ // still gonna look at same command
1407
+ }
1408
+ }
1409
+ }
1410
+
1411
+ var rollup;
1412
+ if (this.alternateCommand) {
1413
+ rollup = new Command(this.alternateCommand,
1414
+ commands[0].target, commands[0].value);
1415
+ }
1416
+ else {
1417
+ rollup = new Command('rollup', this.name);
1418
+ rollup.value = to_kwargs(args);
1419
+ }
1420
+ rollup.replacementIndexes = replacementIndexes;
1421
+ return rollup;
1422
+ };
1423
+ }
1424
+ else {
1425
+ this.getRollup = function(commands) {
1426
+ var result = rollupRuleShorthand.getRollup(commands);
1427
+ if (result) {
1428
+ var rollup = new Command(
1429
+ result.command
1430
+ , result.target
1431
+ , result.value
1432
+ );
1433
+ rollup.replacementIndexes = result.replacementIndexes;
1434
+ return rollup;
1435
+ }
1436
+ return false;
1437
+ };
1438
+ }
1439
+
1440
+ this.getExpandedCommands = function(kwargs) {
1441
+ var commands = [];
1442
+ var expandedCommands = (rollupRuleShorthand.expandedCommands
1443
+ ? rollupRuleShorthand.expandedCommands
1444
+ : rollupRuleShorthand.getExpandedCommands(
1445
+ parse_kwargs(kwargs)));
1446
+ for (var i = 0; i < expandedCommands.length; ++i) {
1447
+ var command = expandedCommands[i];
1448
+ commands.push(new Command(
1449
+ command.command
1450
+ , command.target
1451
+ , command.value
1452
+ ));
1453
+ }
1454
+ return commands;
1455
+ };
1456
+ };
1457
+
1458
+ this.init(rollupRuleShorthand);
1459
+ }
1460
+
1461
+
1462
+
1463
+ /**
1464
+ *
1465
+ */
1466
+ function RollupManager()
1467
+ {
1468
+ // singleton pattern
1469
+ RollupManager.self = this;
1470
+
1471
+ this.init = function()
1472
+ {
1473
+ this.rollupRules = {};
1474
+ if (is_IDE()) {
1475
+ Editor.rollupManager = this;
1476
+ }
1477
+ };
1478
+
1479
+ /**
1480
+ * Adds a new RollupRule to the repository. Returns true on success, or
1481
+ * false if the rule couldn't be added.
1482
+ *
1483
+ * @param rollupRuleShorthand shorthand JSON specification of the new
1484
+ * RollupRule, possibly including CommandMatcher
1485
+ * shorthand too.
1486
+ * @return true if the rule was added successfully,
1487
+ * false otherwise.
1488
+ */
1489
+ this.addRollupRule = function(rollupRuleShorthand)
1490
+ {
1491
+ try {
1492
+ var rule = new RollupRule(rollupRuleShorthand);
1493
+ this.rollupRules[rule.name] = rule;
1494
+ }
1495
+ catch(e) {
1496
+ smart_alert("Could not create RollupRule from shorthand:\n\n"
1497
+ + e.message);
1498
+ return false;
1499
+ }
1500
+ return true;
1501
+ };
1502
+
1503
+ /**
1504
+ * Returns a RollupRule by name.
1505
+ *
1506
+ * @param rollupName the name of the rule to fetch
1507
+ * @return the RollupRule, or null if it isn't found.
1508
+ */
1509
+ this.getRollupRule = function(rollupName)
1510
+ {
1511
+ return (this.rollupRules[rollupName] || null);
1512
+ };
1513
+
1514
+ /**
1515
+ * Returns a list of name-description pairs for use in populating the
1516
+ * auto-populated target dropdown in the IDE. Rules that have an alternate
1517
+ * command defined are not included in the list, as they are not bona-fide
1518
+ * rollups.
1519
+ *
1520
+ * @return a list of name-description pairs
1521
+ */
1522
+ this.getRollupRulesForDropdown = function()
1523
+ {
1524
+ var targets = [];
1525
+ var names = keys(this.rollupRules).sort();
1526
+ for (var i = 0; i < names.length; ++i) {
1527
+ var name = names[i];
1528
+ if (this.rollupRules[name].alternateCommand) {
1529
+ continue;
1530
+ }
1531
+ targets.push([ name, this.rollupRules[name].description ]);
1532
+ }
1533
+ return targets;
1534
+ };
1535
+
1536
+ /**
1537
+ * Applies all rules to the current editor commands, asking the user in
1538
+ * each case if it's okay to perform the replacement. The rules are applied
1539
+ * repeatedly until there are no more matches. The algorithm should
1540
+ * remember when the user has declined a replacement, and not ask to do it
1541
+ * again.
1542
+ *
1543
+ * @return the list of commands with rollup replacements performed
1544
+ */
1545
+ this.applyRollupRules = function()
1546
+ {
1547
+ var commands = editor.getTestCase().commands;
1548
+ var blacklistedRollups = {};
1549
+
1550
+ // so long as rollups were performed, we need to keep iterating through
1551
+ // the commands starting at the beginning, because further rollups may
1552
+ // potentially be applied on the newly created ones.
1553
+ while (true) {
1554
+ var performedRollup = false;
1555
+ for (var i = 0; i < commands.length; ++i) {
1556
+ // iterate through commands
1557
+ for (var rollupName in this.rollupRules) {
1558
+ var rule = this.rollupRules[rollupName];
1559
+ var rollup = rule.getRollup(commands.slice(i));
1560
+ if (rollup) {
1561
+ // since we passed in a sliced version of the commands
1562
+ // array to the getRollup() method, we need to re-add
1563
+ // the offset to the replacementIndexes
1564
+ var k = 0;
1565
+ for (; k < rollup.replacementIndexes.length; ++k) {
1566
+ rollup.replacementIndexes[k] += i;
1567
+ }
1568
+
1569
+ // build the confirmation message
1570
+ var msg = "Perform the following command rollup?\n\n";
1571
+ for (k = 0; k < rollup.replacementIndexes.length; ++k) {
1572
+ var replacementIndex = rollup.replacementIndexes[k];
1573
+ var command = commands[replacementIndex];
1574
+ msg += '[' + replacementIndex + ']: ';
1575
+ msg += command + "\n";
1576
+ }
1577
+ msg += "\n";
1578
+ msg += rollup;
1579
+
1580
+ // check against blacklisted rollups
1581
+ if (blacklistedRollups[msg]) {
1582
+ continue;
1583
+ }
1584
+
1585
+ // highlight the potentially replaced rows
1586
+ for (k = 0; k < commands.length; ++k) {
1587
+ var command = commands[k];
1588
+ command.result = '';
1589
+ if (rollup.replacementIndexes.indexOf(k) != -1) {
1590
+ command.selectedForReplacement = true;
1591
+ }
1592
+ editor.view.rowUpdated(replacementIndex);
1593
+ }
1594
+
1595
+ // get confirmation from user
1596
+ if (confirm(msg)) {
1597
+ // perform rollup
1598
+ var deleteRanges = [];
1599
+ var replacementIndexes = rollup.replacementIndexes;
1600
+ for (k = 0; k < replacementIndexes.length; ++k) {
1601
+ // this is expected to be list of ranges. A
1602
+ // range has a start, and a list of commands.
1603
+ // The deletion only checks the length of the
1604
+ // command list.
1605
+ deleteRanges.push({
1606
+ start: replacementIndexes[k]
1607
+ , commands: [ 1 ]
1608
+ });
1609
+ }
1610
+ editor.view.executeAction(new TreeView
1611
+ .DeleteCommandAction(editor.view,deleteRanges));
1612
+ editor.view.insertAt(i, rollup);
1613
+
1614
+ performedRollup = true;
1615
+ }
1616
+ else {
1617
+ // cleverly remember not to try this rollup again
1618
+ blacklistedRollups[msg] = true;
1619
+ }
1620
+
1621
+ // unhighlight
1622
+ for (k = 0; k < commands.length; ++k) {
1623
+ commands[k].selectedForReplacement = false;
1624
+ editor.view.rowUpdated(k);
1625
+ }
1626
+ }
1627
+ }
1628
+ }
1629
+ if (!performedRollup) {
1630
+ break;
1631
+ }
1632
+ }
1633
+ return commands;
1634
+ };
1635
+
1636
+ this.init();
1637
+ }
1638
+
1639
+ RollupManager.getInstance = function() {
1640
+ return (RollupManager.self == null)
1641
+ ? new RollupManager()
1642
+ : RollupManager.self;
1643
+ }
1644
+