Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/atoms/inject.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview Browser atom for injecting JavaScript into the page under
20
* test. There is no point in using this atom directly from JavaScript.
21
* Instead, it is intended to be used in its compiled form when injecting
22
* script from another language (e.g. C++).
23
*
24
* TODO: Add an example
25
*/
26
27
goog.provide('bot.inject');
28
goog.provide('bot.inject.cache');
29
30
goog.require('bot');
31
goog.require('bot.Error');
32
goog.require('bot.ErrorCode');
33
goog.require('bot.json');
34
/**
35
* @suppress {extraRequire} Used as a forward declaration which causes
36
* compilation errors if missing.
37
*/
38
goog.require('bot.response.ResponseObject');
39
goog.require('goog.array');
40
goog.require('goog.dom.NodeType');
41
goog.require('goog.object');
42
goog.require('goog.userAgent');
43
44
45
/**
46
* Type definition for the WebDriver's JSON wire protocol representation
47
* of a DOM element.
48
* @typedef {{ELEMENT: string}}
49
* @see bot.inject.ELEMENT_KEY
50
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
51
*/
52
bot.inject.JsonElement;
53
54
55
/**
56
* Type definition for a cached Window object that can be referenced in
57
* WebDriver's JSON wire protocol. Note, this is a non-standard
58
* representation.
59
* @typedef {{WINDOW: string}}
60
* @see bot.inject.WINDOW_KEY
61
*/
62
bot.inject.JsonWindow;
63
64
65
/**
66
* Key used to identify DOM elements in the WebDriver wire protocol.
67
* @type {string}
68
* @const
69
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
70
*/
71
bot.inject.ELEMENT_KEY = 'ELEMENT';
72
73
74
/**
75
* Key used to identify Window objects in the WebDriver wire protocol.
76
* @type {string}
77
* @const
78
*/
79
bot.inject.WINDOW_KEY = 'WINDOW';
80
81
82
/**
83
* Converts an element to a JSON friendly value so that it can be
84
* stringified for transmission to the injector. Values are modified as
85
* follows:
86
* <ul>
87
* <li>booleans, numbers, strings, and null are returned as is</li>
88
* <li>undefined values are returned as null</li>
89
* <li>functions are returned as a string</li>
90
* <li>each element in an array is recursively processed</li>
91
* <li>DOM Elements are wrapped in object-literals as dictated by the
92
* WebDriver wire protocol</li>
93
* <li>all other objects will be treated as hash-maps, and will be
94
* recursively processed for any string and number key types (all
95
* other key types are discarded as they cannot be converted to JSON).
96
* </ul>
97
*
98
* @param {*} value The value to make JSON friendly.
99
* @return {*} The JSON friendly value.
100
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
101
*/
102
bot.inject.wrapValue = function (value) {
103
var _wrap = function (value, seen) {
104
switch (goog.typeOf(value)) {
105
case 'string':
106
case 'number':
107
case 'boolean':
108
return value;
109
110
case 'function':
111
return value.toString();
112
113
case 'array':
114
return goog.array.map(/**@type {IArrayLike}*/(value),
115
function (v) { return _wrap(v, seen); });
116
117
case 'object':
118
// Since {*} expands to {Object|boolean|number|string|undefined}, the
119
// JSCompiler complains that it is too broad a type for the remainder of
120
// this block where {!Object} is expected. Downcast to prevent generating
121
// a ton of compiler warnings.
122
value = /**@type {!Object}*/ (value);
123
if (seen.indexOf(value) >= 0) {
124
throw new bot.Error(bot.ErrorCode.JAVASCRIPT_ERROR,
125
'Recursive object cannot be transferred');
126
}
127
128
// Sniff out DOM elements. We're using duck-typing instead of an
129
// instanceof check since the instanceof might not always work
130
// (e.g. if the value originated from another Firefox component)
131
if (goog.object.containsKey(value, 'nodeType') &&
132
(value['nodeType'] == goog.dom.NodeType.ELEMENT ||
133
value['nodeType'] == goog.dom.NodeType.DOCUMENT)) {
134
var ret = {};
135
ret[bot.inject.ELEMENT_KEY] =
136
bot.inject.cache.addElement(/**@type {!Element}*/(value));
137
return ret;
138
}
139
140
// Check if this is a Window
141
if (goog.object.containsKey(value, 'document')) {
142
var ret = {};
143
ret[bot.inject.WINDOW_KEY] =
144
bot.inject.cache.addElement(/**@type{!Window}*/(value));
145
return ret;
146
}
147
148
seen.push(value);
149
if (goog.isArrayLike(value)) {
150
return goog.array.map(/**@type {IArrayLike}*/(value),
151
function (v) { return _wrap(v, seen); });
152
}
153
154
var filtered = goog.object.filter(value, function (val, key) {
155
return goog.isNumber(key) || goog.isString(key);
156
});
157
return goog.object.map(filtered, function (v) { return _wrap(v, seen); });
158
159
default: // goog.typeOf(value) == 'undefined' || 'null'
160
return null;
161
}
162
};
163
return _wrap(value, []);
164
};
165
166
167
/**
168
* Unwraps any DOM element's encoded in the given `value`.
169
* @param {*} value The value to unwrap.
170
* @param {Document=} opt_doc The document whose cache to retrieve wrapped
171
* elements from. Defaults to the current document.
172
* @return {*} The unwrapped value.
173
*/
174
bot.inject.unwrapValue = function (value, opt_doc) {
175
if (goog.isArray(value)) {
176
return goog.array.map(/**@type {IArrayLike}*/(value),
177
function (v) { return bot.inject.unwrapValue(v, opt_doc); });
178
} else if (goog.isObject(value)) {
179
if (typeof value == 'function') {
180
return value;
181
}
182
183
if (goog.object.containsKey(value, bot.inject.ELEMENT_KEY)) {
184
return bot.inject.cache.getElement(value[bot.inject.ELEMENT_KEY],
185
opt_doc);
186
}
187
188
if (goog.object.containsKey(value, bot.inject.WINDOW_KEY)) {
189
return bot.inject.cache.getElement(value[bot.inject.WINDOW_KEY],
190
opt_doc);
191
}
192
193
return goog.object.map(value, function (val) {
194
return bot.inject.unwrapValue(val, opt_doc);
195
});
196
}
197
return value;
198
};
199
200
201
/**
202
* Recompiles `fn` in the context of another window so that the
203
* correct symbol table is used when the function is executed. This
204
* function assumes the `fn` can be decompiled to its source using
205
* `Function.prototype.toString` and that it only refers to symbols
206
* defined in the target window's context.
207
*
208
* @param {!(Function|string)} fn Either the function that should be
209
* recompiled, or a string defining the body of an anonymous function
210
* that should be compiled in the target window's context.
211
* @param {!Window} theWindow The window to recompile the function in.
212
* @return {!Function} The recompiled function.
213
* @private
214
*/
215
bot.inject.recompileFunction_ = function (fn, theWindow) {
216
if (goog.isString(fn)) {
217
try {
218
return new theWindow['Function'](fn);
219
} catch (ex) {
220
// Try to recover if in IE5-quirks mode
221
// Need to initialize the script engine on the passed-in window
222
if (goog.userAgent.IE && theWindow.execScript) {
223
theWindow.execScript(';');
224
return new theWindow['Function'](fn);
225
}
226
throw ex;
227
}
228
}
229
return theWindow == window ? fn : new theWindow['Function'](
230
'return (' + fn + ').apply(null,arguments);');
231
};
232
233
234
/**
235
* Executes an injected script. This function should never be called from
236
* within JavaScript itself. Instead, it is used from an external source that
237
* is injecting a script for execution.
238
*
239
* <p/>For example, in a WebDriver Java test, one might have:
240
* <pre><code>
241
* Object result = ((JavascriptExecutor) driver).executeScript(
242
* "return arguments[0] + arguments[1];", 1, 2);
243
* </code></pre>
244
*
245
* <p/>Once transmitted to the driver, this command would be injected into the
246
* page for evaluation as:
247
* <pre><code>
248
* bot.inject.executeScript(
249
* function() {return arguments[0] + arguments[1];},
250
* [1, 2]);
251
* </code></pre>
252
*
253
* <p/>The details of how this actually gets injected for evaluation is left
254
* as an implementation detail for clients of this library.
255
*
256
* @param {!(Function|string)} fn Either the function to execute, or a string
257
* defining the body of an anonymous function that should be executed. This
258
* function should only contain references to symbols defined in the context
259
* of the target window (`opt_window`). Any references to symbols
260
* defined in this context will likely generate a ReferenceError.
261
* @param {Array.<*>} args An array of wrapped script arguments, as defined by
262
* the WebDriver wire protocol.
263
* @param {boolean=} opt_stringify Whether the result should be returned as a
264
* serialized JSON string.
265
* @param {!Window=} opt_window The window in whose context the function should
266
* be invoked; defaults to the current window.
267
* @return {!(string|bot.response.ResponseObject)} The response object. If
268
* opt_stringify is true, the result will be serialized and returned in
269
* string format.
270
*/
271
bot.inject.executeScript = function (fn, args, opt_stringify, opt_window) {
272
var win = opt_window || bot.getWindow();
273
var ret;
274
try {
275
fn = bot.inject.recompileFunction_(fn, win);
276
var unwrappedArgs = /**@type {Object}*/ (bot.inject.unwrapValue(args,
277
win.document));
278
ret = bot.inject.wrapResponse(fn.apply(null, unwrappedArgs));
279
} catch (ex) {
280
ret = bot.inject.wrapError(ex);
281
}
282
return opt_stringify ? bot.json.stringify(ret) : ret;
283
};
284
285
286
/**
287
* Executes an injected script, which is expected to finish asynchronously
288
* before the given `timeout`. When the script finishes or an error
289
* occurs, the given `onDone` callback will be invoked. This callback
290
* will have a single argument, a {@link bot.response.ResponseObject} object.
291
*
292
* The script signals its completion by invoking a supplied callback given
293
* as its last argument. The callback may be invoked with a single value.
294
*
295
* The script timeout event will be scheduled with the provided window,
296
* ensuring the timeout is synchronized with that window's event queue.
297
* Furthermore, asynchronous scripts do not work across new page loads; if an
298
* "unload" event is fired on the window while an asynchronous script is
299
* pending, the script will be aborted and an error will be returned.
300
*
301
* Like `bot.inject.executeScript`, this function should only be called
302
* from an external source. It handles wrapping and unwrapping of input/output
303
* values.
304
*
305
* @param {(!Function|string)} fn Either the function to execute, or a string
306
* defining the body of an anonymous function that should be executed. This
307
* function should only contain references to symbols defined in the context
308
* of the target window (`opt_window`). Any references to symbols
309
* defined in this context will likely generate a ReferenceError.
310
* @param {Array.<*>} args An array of wrapped script arguments, as defined by
311
* the WebDriver wire protocol.
312
* @param {number} timeout The amount of time, in milliseconds, the script
313
* should be permitted to run; must be non-negative.
314
* @param {function(string)|function(!bot.response.ResponseObject)} onDone
315
* The function to call when the given `fn` invokes its callback,
316
* or when an exception or timeout occurs. This will always be called.
317
* @param {boolean=} opt_stringify Whether the result should be returned as a
318
* serialized JSON string.
319
* @param {!Window=} opt_window The window to synchronize the script with;
320
* defaults to the current window.
321
*/
322
bot.inject.executeAsyncScript = function (fn, args, timeout, onDone,
323
opt_stringify, opt_window) {
324
var win = opt_window || window;
325
var timeoutId;
326
var responseSent = false;
327
328
function sendResponse(status, value) {
329
if (!responseSent) {
330
if (win.removeEventListener) {
331
win.removeEventListener('unload', onunload, true);
332
} else {
333
win.detachEvent('onunload', onunload);
334
}
335
336
win.clearTimeout(timeoutId);
337
if (status != bot.ErrorCode.SUCCESS) {
338
var err = new bot.Error(status, value.message || value + '');
339
err.stack = value.stack;
340
value = bot.inject.wrapError(err);
341
} else {
342
value = bot.inject.wrapResponse(value);
343
}
344
onDone(opt_stringify ? bot.json.stringify(value) : value);
345
responseSent = true;
346
}
347
}
348
var sendError = goog.partial(sendResponse, bot.ErrorCode.UNKNOWN_ERROR);
349
350
if (win.closed) {
351
sendError('Unable to execute script; the target window is closed.');
352
return;
353
}
354
355
fn = bot.inject.recompileFunction_(fn, win);
356
357
args = /** @type {Array.<*>} */ (bot.inject.unwrapValue(args, win.document));
358
args.push(goog.partial(sendResponse, bot.ErrorCode.SUCCESS));
359
360
if (win.addEventListener) {
361
win.addEventListener('unload', onunload, true);
362
} else {
363
win.attachEvent('onunload', onunload);
364
}
365
366
var startTime = goog.now();
367
try {
368
fn.apply(win, args);
369
370
// Register our timeout *after* the function has been invoked. This will
371
// ensure we don't timeout on a function that invokes its callback after
372
// a 0-based timeout.
373
timeoutId = win.setTimeout(function () {
374
sendResponse(bot.ErrorCode.SCRIPT_TIMEOUT,
375
Error('Timed out waiting for asynchronous script result ' +
376
'after ' + (goog.now() - startTime) + ' ms'));
377
}, Math.max(0, timeout));
378
} catch (ex) {
379
sendResponse(ex.code || bot.ErrorCode.UNKNOWN_ERROR, ex);
380
}
381
382
function onunload() {
383
sendResponse(bot.ErrorCode.UNKNOWN_ERROR,
384
Error('Detected a page unload event; asynchronous script ' +
385
'execution does not work across page loads.'));
386
}
387
};
388
389
390
/**
391
* Wraps the response to an injected script that executed successfully so it
392
* can be JSON-ified for transmission to the process that injected this
393
* script.
394
* @param {*} value The script result.
395
* @return {{status:bot.ErrorCode,value:*}} The wrapped value.
396
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#responses
397
*/
398
bot.inject.wrapResponse = function (value) {
399
return {
400
'status': bot.ErrorCode.SUCCESS,
401
'value': bot.inject.wrapValue(value)
402
};
403
};
404
405
406
/**
407
* Wraps a JavaScript error in an object-literal so that it can be JSON-ified
408
* for transmission to the process that injected this script.
409
* @param {Error} err The error to wrap.
410
* @return {{status:bot.ErrorCode,value:*}} The wrapped error object.
411
* @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#failed-commands
412
*/
413
bot.inject.wrapError = function (err) {
414
// TODO: Parse stackTrace
415
return {
416
'status': goog.object.containsKey(err, 'code') ?
417
err['code'] : bot.ErrorCode.UNKNOWN_ERROR,
418
// TODO: Parse stackTrace
419
'value': {
420
'message': err.message
421
}
422
};
423
};
424
425
426
/**
427
* The property key used to store the element cache on the DOCUMENT node
428
* when it is injected into the page. Since compiling each browser atom results
429
* in a different symbol table, we must use this known key to access the cache.
430
* This ensures the same object is used between injections of different atoms.
431
* @private {string}
432
* @const
433
*/
434
bot.inject.cache.CACHE_KEY_ = '$wdc_';
435
436
437
/**
438
* The prefix for each key stored in an cache.
439
* @type {string}
440
* @const
441
*/
442
bot.inject.cache.ELEMENT_KEY_PREFIX = ':wdc:';
443
444
445
/**
446
* Retrieves the cache object for the given window. Will initialize the cache
447
* if it does not yet exist.
448
* @param {Document=} opt_doc The document whose cache to retrieve. Defaults to
449
* the current document.
450
* @return {Object.<string, (Element|Window)>} The cache object.
451
* @private
452
*/
453
bot.inject.cache.getCache_ = function (opt_doc) {
454
var doc = opt_doc || document;
455
var cache = doc[bot.inject.cache.CACHE_KEY_];
456
if (!cache) {
457
cache = doc[bot.inject.cache.CACHE_KEY_] = {};
458
// Store the counter used for generated IDs in the cache so that it gets
459
// reset whenever the cache does.
460
cache.nextId = goog.now();
461
}
462
// Sometimes the nextId does not get initialized and returns NaN
463
// TODO: Generate UID on the fly instead.
464
if (!cache.nextId) {
465
cache.nextId = goog.now();
466
}
467
return cache;
468
};
469
470
471
/**
472
* Adds an element to its ownerDocument's cache.
473
* @param {(Element|Window)} el The element or Window object to add.
474
* @return {string} The key generated for the cached element.
475
*/
476
bot.inject.cache.addElement = function (el) {
477
// Check if the element already exists in the cache.
478
var cache = bot.inject.cache.getCache_(el.ownerDocument);
479
var id = goog.object.findKey(cache, function (value) {
480
return value == el;
481
});
482
if (!id) {
483
id = bot.inject.cache.ELEMENT_KEY_PREFIX + cache.nextId++;
484
cache[id] = el;
485
}
486
return id;
487
};
488
489
490
/**
491
* Retrieves an element from the cache. Will verify that the element is
492
* still attached to the DOM before returning.
493
* @param {string} key The element's key in the cache.
494
* @param {Document=} opt_doc The document whose cache to retrieve the element
495
* from. Defaults to the current document.
496
* @return {Element|Window} The cached element.
497
*/
498
bot.inject.cache.getElement = function (key, opt_doc) {
499
key = decodeURIComponent(key);
500
var doc = opt_doc || document;
501
var cache = bot.inject.cache.getCache_(doc);
502
if (!goog.object.containsKey(cache, key)) {
503
// Throw STALE_ELEMENT_REFERENCE instead of NO_SUCH_ELEMENT since the
504
// key may have been defined by a prior document's cache.
505
throw new bot.Error(bot.ErrorCode.STALE_ELEMENT_REFERENCE,
506
'Element does not exist in cache');
507
}
508
509
var el = cache[key];
510
511
// If this is a Window check if it's closed
512
if (goog.object.containsKey(el, 'setInterval')) {
513
if (el.closed) {
514
delete cache[key];
515
throw new bot.Error(bot.ErrorCode.NO_SUCH_WINDOW,
516
'Window has been closed.');
517
}
518
return el;
519
}
520
521
// Make sure the element is still attached to the DOM before returning.
522
var node = el;
523
while (node) {
524
if (node == doc.documentElement) {
525
return el;
526
}
527
if (node.host && node.nodeType === 11) {
528
node = node.host;
529
}
530
node = node.parentNode;
531
}
532
delete cache[key];
533
throw new bot.Error(bot.ErrorCode.STALE_ELEMENT_REFERENCE,
534
'Element is no longer attached to the DOM');
535
};
536
537