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