blob: 7d486b743c0cc4b1846f76ef380e184b11b7efa7 [file] [log] [blame]
// Copyright 2012 Selenium committers
// Copyright 2012 Software Freedom Conservancy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Defines utilities for exchanging messages between the
* sandboxed SafariDriver injected script and its corresponding content page.
*/
goog.provide('safaridriver.inject.Encoder');
goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('bot.dom');
goog.require('bot.inject');
goog.require('bot.response');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('goog.string');
goog.require('safaridriver.inject.message.Encode');
goog.require('safaridriver.message.Response');
goog.require('webdriver.promise');
/**
* @param {!safaridriver.message.MessageTarget} messageTarget The message
* target to use.
* @constructor
*/
safaridriver.inject.Encoder = function(messageTarget) {
/**
* @type {!Object.<!webdriver.promise.Deferred>}
* @private
*/
this.pendingResponses_ = {};
messageTarget.on(safaridriver.message.Response.TYPE,
goog.bind(this.onResponse_, this));
};
/**
* Key used in an object literal to indicate it is the encoded representation of
* a DOM element. The corresponding property's value will be a CSS selector for
* the encoded elmeent.
*
* <p>Note, this constant is very intentionally initialized to a value other
* than the standard JSON wire protocol key for WebElements.
*
* @type {string}
* @const
* @private
*/
safaridriver.inject.Encoder.ENCODED_ELEMENT_KEY_ =
'ENCODED_' + bot.inject.ELEMENT_KEY;
/**
* Computes the canonical CSSLocator locator for an element.
* @param {!Element} element The element to compute a CSS selector for.
* @return {string} The element's CSS selector.
* @private
*/
safaridriver.inject.Encoder.getElementCssSelector_ = function(element) {
var path = '';
for (var current = element; current;
current = bot.dom.getParentElement(current)) {
var index = 1;
for (var sibling = current.previousSibling; sibling;
sibling = sibling.previousSibling) {
if (sibling.nodeType == goog.dom.NodeType.ELEMENT) {
index++;
}
}
var tmp = current.tagName.toLowerCase();
if (index > 1) {
tmp += ':nth-child(' + index + ')';
}
if (path == '') {
path = tmp + path;
} else {
path = tmp + ' > ' + path;
}
}
return path;
};
/**
* Encodes a value so it may be included in a message exchanged between the
* document and sandboxed injected script. Any DOM element references will
* be replaced with an object literal whose sole key is t
* @param {*} value The value to encode.
* @return {*} The encoded value. Note, when called from the SafariDriver
* extension's injected script, this value will _never_ be a
* {@link webdriver.promise.Promise}.
* @throws {Error} If the value is cannot be encoded (e.g. it is a function, or
* an array or object with a cyclical reference).
*/
safaridriver.inject.Encoder.prototype.encode = function(value) {
var type = goog.typeOf(value);
switch (type) {
case 'boolean':
case 'number':
case 'string':
return value;
case 'null':
case 'undefined':
return null;
case 'array':
return goog.array.map(/** @type {!Array} */ (value),
this.encode, this);
case 'object':
if (goog.dom.isElement(value)) {
if (value.ownerDocument !== document) {
return this.encodeElement_(/** @type {!Element} */ (value));
}
var encoded = {};
encoded[safaridriver.inject.Encoder.ENCODED_ELEMENT_KEY_] =
safaridriver.inject.Encoder.getElementCssSelector_(
/** @type {!Element} */ (value));
return encoded;
}
// Check for a NodeList.
if (goog.isArrayLike(value)) {
return goog.array.map(/** @type {!goog.array.ArrayLike} */ (value),
this.encode, this);
}
return goog.object.map(/** @type {!Object} */ (value),
this.encode, this);
case 'function':
return value.toString();
default:
throw Error('Invalid value type: ' + type + ' => ' + value);
}
};
/**
* @param {!Element} element The element to encode.
* @return {!webdriver.promise.Promise} A promise that will resolve to the
* JSON representation of a WebElement.
* @private
*/
safaridriver.inject.Encoder.prototype.encodeElement_ = function(element) {
var webElement = new webdriver.promise.Deferred();
var id = goog.string.getRandomString();
var css = safaridriver.inject.Encoder.getElementCssSelector_(element);
var message = new safaridriver.inject.message.Encode(id, css);
var doc = goog.dom.getOwnerDocument(element);
var win = /** @type {!Window} */ (goog.dom.getWindow(doc));
message.send(win);
this.pendingResponses_[id] = webElement;
return webElement.promise;
};
/**
* Decodes a value. Any object literals whose sole key is
* {@link safaridriver.inject.Encoder.ENCODED_ELEMENT_KEY_} will be considered
* an encoded reference to a DOM element. The corresponding value for this key
* will be used as an XPath selector to locate the element.
* @param {*} value The value to decode.
* @return {*} The decoded value.
* @throws {bot.Error} If an encoded DOM element cannot be located on the page.
* @throws {Error} If the value is an invalid type, or an array or object with
* cyclical references.
*/
safaridriver.inject.Encoder.prototype.decode = function(value) {
var type = goog.typeOf(value);
switch (type) {
case 'boolean':
case 'number':
case 'string':
return value;
case 'null':
case 'undefined':
return null;
case 'array':
return goog.array.map(/** @type {!Array} */ (value),
this.decode, this);
case 'object':
var obj = /** @type {!Object} */ (value);
var keys = Object.keys(obj);
if (keys.length == 1 &&
keys[0] === safaridriver.inject.Encoder.ENCODED_ELEMENT_KEY_) {
var css = value[safaridriver.inject.Encoder.ENCODED_ELEMENT_KEY_];
var element = bot.getDocument().querySelector(css);
if (!element) {
throw new bot.Error(bot.ErrorCode.STALE_ELEMENT_REFERENCE,
'Unable to locate encoded element: ' + css);
}
return element;
}
return goog.object.map(obj, this.decode, this);
case 'function':
default:
throw Error('Invalid value type: ' + type + ' => ' + value);
}
};
/**
* Handles response messages.
* @param {!safaridriver.message.Response} message The message.
* @private
*/
safaridriver.inject.Encoder.prototype.onResponse_ = function(message) {
if (message.isSameOrigin()) {
return;
}
var promise = this.pendingResponses_[message.getId()];
if (!promise) {
return;
}
delete this.pendingResponses_[message.getId()];
var response = message.getResponse();
try {
bot.response.checkResponse(response);
var value = this.decode(response['value']);
promise.resolve(value);
} catch (ex) {
promise.reject(ex);
}
};