blob: 22d68d973eaf97f5c0e07f7bd203b972d9d9fed2 [file] [log] [blame]
// Copyright 2010 WebDriver committers
// Copyright 2010 Google Inc.
//
// 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 Atoms-based implementation of the webelement interface.
*/
goog.provide('webdriver.atoms.element');
goog.require('bot.Keyboard.Keys');
goog.require('bot.action');
goog.require('bot.dom');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.math');
goog.require('goog.string');
goog.require('goog.style');
goog.require('webdriver.Key');
/**
* @param {!Element} element The element to use.
* @return {boolean} Whether the element is checked or selected.
*/
webdriver.atoms.element.isSelected = function(element) {
// Although this method looks unloved, its compiled form is used by
// the Chrome and OperaDrivers.
if (!bot.dom.isSelectable(element)) {
return false;
}
return bot.dom.isSelected(element);
};
/**
* Common aliases for properties. This maps names that users use to the correct
* property name.
*
* @const
* @private
*/
webdriver.atoms.element.PROPERTY_ALIASES_ = {
'class': 'className',
'readonly': 'readOnly'
};
/**
* Used to determine whether we should return a boolean value from getAttribute.
* These are all extracted from the WHATWG spec:
*
* http://www.whatwg.org/specs/web-apps/current-work/
*
* These must all be lower-case.
*
* @const
* @private
*/
webdriver.atoms.element.BOOLEAN_PROPERTIES_ = [
'async',
'autofocus',
'autoplay',
'checked',
'compact',
'complete',
'controls',
'declare',
'defaultchecked',
'defaultselected',
'defer',
'disabled',
'draggable',
'ended',
'formnovalidate',
'hidden',
'indeterminate',
'iscontenteditable',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nohref',
'noresize',
'noshade',
'novalidate',
'nowrap',
'open',
'paused',
'pubdate',
'readonly',
'required',
'reversed',
'scoped',
'seamless',
'seeking',
'selected',
'spellcheck',
'truespeed',
'willvalidate'
];
/**
* Get the value of the given property or attribute. If the "attribute" is for
* a boolean property, we return null in the case where the value is false. If
* the attribute name is "style" an attempt to convert that style into a string
* is done.
*
* @param {!Element} element The element to use.
* @param {string} attribute The name of the attribute to look up.
* @return {?string} The string value of the attribute or property, or null.
*/
webdriver.atoms.element.getAttribute = function(element, attribute) {
var value = null;
var name = attribute.toLowerCase();
if ('style' == attribute.toLowerCase()) {
value = element.style;
if (value && !goog.isString(value)) {
value = value.cssText;
}
return (/** @type {?string} */value);
}
if (('selected' == name || 'checked' == name) &&
bot.dom.isSelectable(element)) {
return bot.dom.isSelected(element) ? 'true' : null;
}
// Our tests suggest that returning the attribute is desirable for
// the href attribute of <a> tags and the src attribute of <img> tags,
// but we normally attempt to get the property value before the attribute.
var isLink = bot.dom.isElement(element, goog.dom.TagName.A);
var isImg = bot.dom.isElement(element, goog.dom.TagName.IMG);
// Although the attribute matters, the property is consistent. Return that in
// preference to the attribute for links and images.
if ((isImg && name == 'src') || (isLink && name == 'href')) {
value = bot.dom.getAttribute(element, name);
if (value) {
// We want the full URL if present
value = bot.dom.getProperty(element, name);
}
return (/** @type {?string} */value);
}
var propName = webdriver.atoms.element.PROPERTY_ALIASES_[attribute] ||
attribute;
if (goog.array.contains(webdriver.atoms.element.BOOLEAN_PROPERTIES_, name)) {
value = !goog.isNull(bot.dom.getAttribute(element, attribute)) ||
bot.dom.getProperty(element, propName);
return value ? 'true' : null;
}
var property;
try {
property = bot.dom.getProperty(element, propName);
} catch (e) {
// Leaves property undefined or null
}
// 1- Call getAttribute if getProperty fails,
// i.e. property is null or undefined.
// This happens for event handlers in Firefox.
// For example, calling getProperty for 'onclick' would
// fail while getAttribute for 'onclick' will succeed and
// return the JS code of the handler.
//
// 2- When property is an object we fall back to the
// actual attribute instead.
// See issue http://code.google.com/p/selenium/issues/detail?id=966
if (!goog.isDefAndNotNull(property) || goog.isObject(property)) {
value = bot.dom.getAttribute(element, attribute);
} else {
value = property;
}
// The empty string is a valid return value.
return goog.isDefAndNotNull(value) ? value.toString() : null;
};
/**
* Get the location of the element in page space, if it's displayed.
*
* @param {!Element} element The element to get the location for.
* @return {goog.math.Rect} The bounding rectangle of the element.
*/
webdriver.atoms.element.getLocation = function(element) {
if (!bot.dom.isShown(element)) {
return null;
}
return goog.style.getBounds(element);
};
/**
* @param {Node} element The element to use.
* @return {boolean} Whether the element is in the HEAD tag.
* @private
*/
webdriver.atoms.element.isInHead_ = function(element) {
while (element) {
if (element.tagName && element.tagName.toLowerCase() == 'head') {
return true;
}
try {
element = element.parentNode;
} catch (e) {
// Fine. the DOM has dispeared from underneath us
return false;
}
}
return false;
};
/**
* @param {!Element} element The element to get the text from.
* @return {string} The visible text or an empty string.
*/
webdriver.atoms.element.getText = function(element) {
if (webdriver.atoms.element.isInHead_(element)) {
var doc = goog.dom.getOwnerDocument(element);
if (element.tagName.toUpperCase() == goog.dom.TagName.TITLE &&
goog.dom.getWindow(doc) == bot.getWindow().top) {
return goog.string.trim((/** @type {string} */doc.title));
}
return '';
}
return bot.dom.getVisibleText(element);
};
/**
* Types keys on the given {@code element} with a virtual keyboard. Converts
* special characters from the WebDriver JSON wire protocol to the appropriate
* {@link bot.Keyboard.Key} value.
*
* @param {!Element} element The element to type upon.
* @param {!Array.<string>} keys The keys to type on the element.
* @param {bot.Keyboard=} opt_keyboard Keyboard to use; if not provided,
* constructs one.
* @see bot.action.type
* @see http://code.google.com/p/selenium/wiki/JsonWireProtocol
*/
webdriver.atoms.element.type = function(element, keys, opt_keyboard) {
// Convert to bot.Keyboard.Key values.
/** @type {!Array.<!Array.<(string|!bot.Keyboard.Key)>>} */
var convertedSequences = [];
/** @type {!Array.<(string|!bot.Keyboard.Key)>} */
var current = [];
convertedSequences.push(current);
goog.array.forEach(keys, function(sequence) {
goog.array.forEach(sequence.split(''), function(key) {
if (isWebDriverKey(key)) {
var webdriverKey = webdriver.atoms.element.type.JSON_TO_KEY_MAP_[key];
// goog.isNull uses ==, which accepts undefined.
if (webdriverKey === null) {
// bot.action.type does not support a "null" key, so we have to
// terminate the entire sequence to release modifier keys.
convertedSequences.push(current = []);
} else if (goog.isDef(webdriverKey)) {
current.push(webdriverKey);
} else {
throw Error('Unsupported WebDriver key: \\u' +
key.charCodeAt(0).toString(16));
}
} else {
// Handle common aliases.
switch (key) {
case '\n':
current.push(bot.Keyboard.Keys.ENTER);
break;
case '\t':
current.push(bot.Keyboard.Keys.TAB);
break;
case '\b':
current.push(bot.Keyboard.Keys.BACKSPACE);
break;
default:
current.push(key);
break;
}
}
});
});
goog.array.forEach(convertedSequences, function(sequence) {
bot.action.type(element, sequence, opt_keyboard);
});
function isWebDriverKey(c) {
return '\uE000' <= c && c <= '\uE03D';
}
};
/**
* Maps JSON wire protocol values to their {@link bot.Keyboard.Key} counterpart.
* @type {!Object.<bot.Keyboard.Key>}
* @const
* @private
*/
webdriver.atoms.element.type.JSON_TO_KEY_MAP_ = {};
goog.scope(function() {
var map = webdriver.atoms.element.type.JSON_TO_KEY_MAP_;
var key = webdriver.Key;
var botKey = bot.Keyboard.Keys;
map[key.NULL] = null;
map[key.BACK_SPACE] = botKey.BACKSPACE;
map[key.TAB] = botKey.TAB;
map[key.RETURN] = botKey.ENTER;
// This not correct, but most browsers will do the right thing.
map[key.ENTER] = botKey.ENTER;
map[key.SHIFT] = botKey.SHIFT;
map[key.CONTROL] = botKey.CONTROL;
map[key.ALT] = botKey.ALT;
map[key.PAUSE] = botKey.PAUSE;
map[key.ESCAPE] = botKey.ESC;
map[key.SPACE] = botKey.SPACE;
map[key.PAGE_UP] = botKey.PAGE_UP;
map[key.PAGE_DOWN] = botKey.PAGE_DOWN;
map[key.END] = botKey.END;
map[key.HOME] = botKey.HOME;
map[key.LEFT] = botKey.LEFT;
map[key.UP] = botKey.UP;
map[key.RIGHT] = botKey.RIGHT;
map[key.DOWN] = botKey.DOWN;
map[key.INSERT] = botKey.INSERT;
map[key.DELETE] = botKey.DELETE;
map[key.SEMICOLON] = botKey.SEMICOLON;
map[key.EQUALS] = botKey.EQUALS;
map[key.NUMPAD0] = botKey.NUM_ZERO;
map[key.NUMPAD1] = botKey.NUM_ONE;
map[key.NUMPAD2] = botKey.NUM_TWO;
map[key.NUMPAD3] = botKey.NUM_THREE;
map[key.NUMPAD4] = botKey.NUM_FOUR;
map[key.NUMPAD5] = botKey.NUM_FIVE;
map[key.NUMPAD6] = botKey.NUM_SIX;
map[key.NUMPAD7] = botKey.NUM_SEVEN;
map[key.NUMPAD8] = botKey.NUM_EIGHT;
map[key.NUMPAD9] = botKey.NUM_NINE;
map[key.MULTIPLY] = botKey.NUM_MULTIPLY;
map[key.ADD] = botKey.NUM_PLUS;
map[key.SUBTRACT] = botKey.NUM_MINUS;
map[key.DECIMAL] = botKey.NUM_PERIOD;
map[key.DIVIDE] = botKey.NUM_DIVISION;
map[key.SEPARATOR] = botKey.SEPARATOR;
map[key.F1] = botKey.F1;
map[key.F2] = botKey.F2;
map[key.F3] = botKey.F3;
map[key.F4] = botKey.F4;
map[key.F5] = botKey.F5;
map[key.F6] = botKey.F6;
map[key.F7] = botKey.F7;
map[key.F8] = botKey.F8;
map[key.F9] = botKey.F9;
map[key.F10] = botKey.F10;
map[key.F11] = botKey.F11;
map[key.F12] = botKey.F12;
map[key.META] = botKey.META;
}); // goog.scope