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