selenium-core-runner 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +9 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +30 -0
- data/app/controllers/selenium_core_runner/suites_controller.rb +19 -0
- data/app/views/selenium_core_runner/suites/index.html.erb +177 -0
- data/app/views/selenium_core_runner/suites/show.html.erb +0 -0
- data/config/routes.rb +6 -0
- data/lib/selenium-core-runner/engine.rb +19 -0
- data/lib/selenium-core-runner.rb +3 -0
- data/public/selenium-core-runner/Blank.html +7 -0
- data/public/selenium-core-runner/InjectedRemoteRunner.html +8 -0
- data/public/selenium-core-runner/RemoteRunner.html +101 -0
- data/public/selenium-core-runner/SeleniumLog.html +109 -0
- data/public/selenium-core-runner/TestPrompt.html +145 -0
- data/public/selenium-core-runner/TestRunner-splash.html +55 -0
- data/public/selenium-core-runner/TestRunner.hta +177 -0
- data/public/selenium-core-runner/TestRunner.html +177 -0
- data/public/selenium-core-runner/icons/all.png +0 -0
- data/public/selenium-core-runner/icons/continue.png +0 -0
- data/public/selenium-core-runner/icons/continue_disabled.png +0 -0
- data/public/selenium-core-runner/icons/pause.png +0 -0
- data/public/selenium-core-runner/icons/pause_disabled.png +0 -0
- data/public/selenium-core-runner/icons/selected.png +0 -0
- data/public/selenium-core-runner/icons/step.png +0 -0
- data/public/selenium-core-runner/icons/step_disabled.png +0 -0
- data/public/selenium-core-runner/iedoc-core.xml +1789 -0
- data/public/selenium-core-runner/iedoc.xml +1830 -0
- data/public/selenium-core-runner/lib/cssQuery/cssQuery-p.js +6 -0
- data/public/selenium-core-runner/lib/cssQuery/src/cssQuery-level2.js +142 -0
- data/public/selenium-core-runner/lib/cssQuery/src/cssQuery-level3.js +150 -0
- data/public/selenium-core-runner/lib/cssQuery/src/cssQuery-standard.js +53 -0
- data/public/selenium-core-runner/lib/cssQuery/src/cssQuery.js +356 -0
- data/public/selenium-core-runner/lib/prototype.js +2006 -0
- data/public/selenium-core-runner/lib/scriptaculous/builder.js +101 -0
- data/public/selenium-core-runner/lib/scriptaculous/controls.js +815 -0
- data/public/selenium-core-runner/lib/scriptaculous/dragdrop.js +915 -0
- data/public/selenium-core-runner/lib/scriptaculous/effects.js +958 -0
- data/public/selenium-core-runner/lib/scriptaculous/scriptaculous.js +47 -0
- data/public/selenium-core-runner/lib/scriptaculous/slider.js +283 -0
- data/public/selenium-core-runner/lib/scriptaculous/unittest.js +383 -0
- data/public/selenium-core-runner/lib/snapsie.js +91 -0
- data/public/selenium-core-runner/scripts/find_matching_child.js +69 -0
- data/public/selenium-core-runner/scripts/htmlutils.js +1623 -0
- data/public/selenium-core-runner/scripts/injection.html +72 -0
- data/public/selenium-core-runner/scripts/selenium-api.js +3240 -0
- data/public/selenium-core-runner/scripts/selenium-browserbot.js +2333 -0
- data/public/selenium-core-runner/scripts/selenium-browserdetect.js +153 -0
- data/public/selenium-core-runner/scripts/selenium-commandhandlers.js +379 -0
- data/public/selenium-core-runner/scripts/selenium-executionloop.js +175 -0
- data/public/selenium-core-runner/scripts/selenium-logging.js +148 -0
- data/public/selenium-core-runner/scripts/selenium-remoterunner.js +695 -0
- data/public/selenium-core-runner/scripts/selenium-testrunner.js +1362 -0
- data/public/selenium-core-runner/scripts/selenium-version.js +5 -0
- data/public/selenium-core-runner/scripts/ui-doc.html +803 -0
- data/public/selenium-core-runner/scripts/ui-element.js +1627 -0
- data/public/selenium-core-runner/scripts/ui-map-sample.js +979 -0
- data/public/selenium-core-runner/scripts/user-extensions.js +3 -0
- data/public/selenium-core-runner/scripts/user-extensions.js.sample +75 -0
- data/public/selenium-core-runner/scripts/xmlextras.js +153 -0
- data/public/selenium-core-runner/selenium-logo.png +0 -0
- data/public/selenium-core-runner/selenium-test.css +43 -0
- data/public/selenium-core-runner/selenium.css +316 -0
- data/public/selenium-core-runner/xpath/dom.js +566 -0
- data/public/selenium-core-runner/xpath/javascript-xpath-0.1.11.js +2816 -0
- data/public/selenium-core-runner/xpath/util.js +549 -0
- data/public/selenium-core-runner/xpath/xmltoken.js +149 -0
- data/public/selenium-core-runner/xpath/xpath.js +2481 -0
- 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
|
+
|