Skip to content

Commit

Permalink
Implement getAttribute for W3C-compliant remote ends.
Browse files Browse the repository at this point in the history
Fix docs for WebElement.getAttribute to attempt to better reflect reality.

This shim is supposed to replicate the OSS behavior, but in terms of
W3C-compliant "Get Element Attribute" and "Get Element Property"
commands. (See #2301.)
  • Loading branch information
juangj committed Aug 11, 2016
1 parent c2ff597 commit 4b46465
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 17 deletions.
35 changes: 22 additions & 13 deletions java/client/src/org/openqa/selenium/WebElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,28 +87,37 @@ public interface WebElement extends SearchContext, TakesScreenshot {
String getTagName();

/**
* Get the value of a the given attribute of the element. Will return the current value, even if
* this has been modified after the page has been loaded. More exactly, this method will return
* the value of the given attribute, unless that attribute is not present, in which case the value
* of the property with the same name is returned (for example for the "value" property of a
* textarea element). If neither value is set, null is returned. The "style" attribute is
* converted as best can be to a text representation with a trailing semi-colon. The following are
* deemed to be "boolean" attributes, and will return either "true" or null:
*
* async, autofocus, autoplay, checked, compact, complete, controls, declare, defaultchecked,
* Get the value of the given attribute of the element. Will return the current value, even if
* this has been modified after the page has been loaded.
*
* <p>More exactly, this method will return the value of the property with the given name, if it
* exists. If it does not, then the value of the attribute with the given name is returned. If
* neither exists, null is returned.
*
* <p>The "style" attribute is converted as best can be to a text representation with a trailing
* semi-colon.
*
* <p>The following are deemed to be "boolean" attributes, and will return either "true" or null:
*
* <p>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
* seeking, selected, truespeed, willvalidate
*
* Finally, the following commonly mis-capitalized attribute/property names are evaluated as
* <p>Finally, the following commonly mis-capitalized attribute/property names are evaluated as
* expected:
*
* <ul>
* <li>"class"
* <li>"readonly"
* <li>If the given name is "class", the "className" property is returned.
* <li>If the given name is "readonly", the "readOnly" property is returned.
* </ul>
*
* <i>Note:</i> The reason for this behavior is that users frequently confuse attributes and
* properties. If you need to do something more precise, e.g., refer to an attribute even when a
* property of the same name exists, then you should evaluate Javascript to obtain the result
* you desire.
*
* @param name The name of the attribute.
* @return The attribute/property's current value or null if the value is not set.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public interface DriverCommand {
String GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = "getElementLocationOnceScrolledIntoView";
String GET_ELEMENT_SIZE = "getElementSize";
String GET_ELEMENT_ATTRIBUTE = "getElementAttribute";
String GET_ELEMENT_PROPERTY = "getElementProperty";
String GET_ELEMENT_VALUE_OF_CSS_PROPERTY = "getElementValueOfCssProperty";
String ELEMENT_EQUALS = "elementEquals";

Expand Down
93 changes: 89 additions & 4 deletions java/client/src/org/openqa/selenium/remote/RemoteWebElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.openqa.selenium.remote;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import org.openqa.selenium.Beta;
import org.openqa.selenium.By;
Expand Down Expand Up @@ -48,12 +49,21 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class RemoteWebElement implements WebElement, FindsByLinkText, FindsById, FindsByName,
FindsByTagName, FindsByClassName, FindsByCssSelector,
FindsByXPath, WrapsDriver, Locatable, HasIdentity,
TakesScreenshot {

private static final Set<String> BOOLEAN_ATTRIBUTES = ImmutableSet.of(
"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", "truespeed", "willvalidate");

private String foundBy;
protected String id;
protected RemoteWebDriver parent;
Expand Down Expand Up @@ -146,13 +156,88 @@ public String getTagName() {
}

public String getAttribute(String name) {
Object value =
if (parent.getW3CStandardComplianceLevel() == 0) {
return stringValueOf(
execute(DriverCommand.GET_ELEMENT_ATTRIBUTE, ImmutableMap.of("id", id, "name", name))
.getValue());
}

// Return a normalized version of the element's inline style. This excludes, e.g., properties
// inherited from parents or set via style sheets. This inline style is generally what people
// want when they ask for the "style" attribute; there is a separate WebDriver command for
// inspecting computed CSS properties.
if (name.equals("style")) {
return stringValueOf(parent.executeScript("return arguments[0].style.cssText", this));
}

// Special cases for selectable elements, i.e., radio buttons, checkboxes, and select options.
// When a user calls getAttribute("checked"), they generally want to know whether the element
// is *currently* checked, as opposed to the value of the "checked" attribute, which indicates
// the element's default checkedness.
if ("checked".equalsIgnoreCase(name) || "selected".equalsIgnoreCase(name)) {
String tagName = getTagName();
if ("option".equalsIgnoreCase(tagName)) {
return isSelected() ? "true" : null;
}
if ("input".equalsIgnoreCase(tagName)) {
String type = getAttribute("type");
if ("checkbox".equalsIgnoreCase(type) || "radio".equalsIgnoreCase(type)) {
return isSelected() ? "true" : null;
}
}
}

if (BOOLEAN_ATTRIBUTES.contains(name)) {
return getElementAttribute(name) != null ? "true" : null;
}

String attrValue = getElementProperty(name);
if ((attrValue != null && !attrValue.isEmpty()) || name.equals("value")) {
return attrValue;
}

attrValue = getElementAttribute(name);
if (attrValue != null) {
// We might be receiving a default value of the attribute (e.g., getting the "src" attribute
// for an <img> tag that lacks a src will return the empty string). If the attribute is
// absent, we are supposed to return null.
Object hasAttr =
parent.executeScript("return arguments[0].hasAttribute(arguments[1])", this, name);
if (hasAttr instanceof Boolean && !((Boolean) hasAttr)) {
attrValue = null;
}
}

return attrValue;
}

private String getElementAttribute(String name) {
return stringValueOf(
execute(DriverCommand.GET_ELEMENT_ATTRIBUTE, ImmutableMap.of("id", id, "name", name))
.getValue();
if (value == null) {
.getValue());
}

private String getElementProperty(String name) {
try {
return stringValueOf(
execute(DriverCommand.GET_ELEMENT_PROPERTY, ImmutableMap.of("id", id, "name", name))
.getValue());
} catch (WebDriverException e) {
// In Firefox 48, Marionette returns a malformed response if the property is missing.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1281397
// Delete this after we stop supporting Firefox 48.
if (e.getMessage().contains("Failed to find value field")) {
return null;
}
throw e;
}
}

private static String stringValueOf(Object o) {
if (o == null) {
return null;
}
return String.valueOf(value);
return String.valueOf(o);
}

public boolean isSelected() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public JsonHttpCommandCodec() {
defineCommand(FIND_ELEMENTS, post("/session/:sessionId/elements"));
defineCommand(GET_ACTIVE_ELEMENT, post("/session/:sessionId/element/active"));
defineCommand(GET_ELEMENT_ATTRIBUTE, get("/session/:sessionId/element/:id/attribute/:name"));
defineCommand(GET_ELEMENT_PROPERTY, get("/session/:sessionId/element/:id/property/:name"));
defineCommand(CLICK_ELEMENT, post("/session/:sessionId/element/:id/click"));
defineCommand(CLEAR_ELEMENT, post("/session/:sessionId/element/:id/clear"));
defineCommand(
Expand Down

2 comments on commit 4b46465

@shs96c
Copy link
Member

@shs96c shs96c commented on 4b46465 Aug 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have an atom for this….

@juangj
Copy link
Contributor Author

@juangj juangj commented on 4b46465 Aug 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shrug I was following the lead of a573338 and 941ddb9. If we would rather implement this by injecting the atom in each of the client languages, that's fine, too.

Please sign in to comment.