org.openqa.selenium.htmlunit.HtmlUnitDriver Maven / Gradle / Ivy
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you 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.
package org.openqa.selenium.htmlunit;
import static org.openqa.selenium.remote.CapabilityType.SUPPORTS_FINDING_BY_CSS;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.HasCapabilities;
import org.openqa.selenium.InvalidCookieDomainException;
import org.openqa.selenium.InvalidSelectorException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoAlertPresentException;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NoSuchFrameException;
import org.openqa.selenium.NoSuchWindowException;
import org.openqa.selenium.Platform;
import org.openqa.selenium.Point;
import org.openqa.selenium.Proxy;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.UnableToSetCookieException;
import org.openqa.selenium.UnhandledAlertException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.HasInputDevices;
import org.openqa.selenium.interactions.Keyboard;
import org.openqa.selenium.interactions.Mouse;
import org.openqa.selenium.internal.FindsByClassName;
import org.openqa.selenium.internal.FindsByCssSelector;
import org.openqa.selenium.internal.FindsById;
import org.openqa.selenium.internal.FindsByLinkText;
import org.openqa.selenium.internal.FindsByName;
import org.openqa.selenium.internal.FindsByTagName;
import org.openqa.selenium.internal.FindsByXPath;
import org.openqa.selenium.internal.WrapsElement;
import org.openqa.selenium.logging.Logs;
import org.openqa.selenium.remote.BrowserType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.SessionNotFoundException;
import org.w3c.css.sac.CSSException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.CookieManager;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.InteractivePage;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ProxyConfig;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.StringWebResponse;
import com.gargoylesoftware.htmlunit.TopLevelWindow;
import com.gargoylesoftware.htmlunit.UnexpectedPage;
import com.gargoylesoftware.htmlunit.Version;
import com.gargoylesoftware.htmlunit.WaitingRefreshHandler;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebClientOptions;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.WebWindowEvent;
import com.gargoylesoftware.htmlunit.WebWindowListener;
import com.gargoylesoftware.htmlunit.WebWindowNotFoundException;
import com.gargoylesoftware.htmlunit.html.BaseFrameElement;
import com.gargoylesoftware.htmlunit.html.DomElement;
import com.gargoylesoftware.htmlunit.html.DomNode;
import com.gargoylesoftware.htmlunit.html.DomNodeList;
import com.gargoylesoftware.htmlunit.html.FrameWindow;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlElement;
import com.gargoylesoftware.htmlunit.html.HtmlHtml;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.host.Element;
import com.gargoylesoftware.htmlunit.javascript.host.Location;
import com.gargoylesoftware.htmlunit.javascript.host.html.DocumentProxy;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCollection;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.NativeArray;
import net.sourceforge.htmlunit.corejs.javascript.NativeObject;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
import net.sourceforge.htmlunit.corejs.javascript.Undefined;
/**
* An implementation of {@link WebDriver} that drives HtmlUnit,
* which is a headless (GUI-less) browser simulator.
* The main supported browsers are Chrome, Firefox and Internet Explorer.
*/
public class HtmlUnitDriver implements WebDriver, JavascriptExecutor,
FindsById, FindsByLinkText, FindsByXPath, FindsByName, FindsByCssSelector,
FindsByTagName, FindsByClassName, HasCapabilities, HasInputDevices {
private WebClient webClient;
private WebWindow currentWindow;
private HtmlUnitAlert alert;
// Fictive position just to implement the API
private Point windowPosition = new Point(0, 0);
private Dimension initialWindowDimension;
private boolean enableJavascript;
private ProxyConfig proxyConfig;
private long implicitWait = 0;
private long scriptTimeout = 0;
private HtmlUnitKeyboard keyboard;
private HtmlUnitMouse mouse;
private boolean gotPage;
public static final String INVALIDXPATHERROR = "The xpath expression '%s' cannot be evaluated";
public static final String INVALIDSELECTIONERROR =
"The xpath expression '%s' selected an object of type '%s' instead of a WebElement";
/**
* Constructs a new instance with JavaScript disabled,
* and the {@link BrowserVersion#getDefault() default} BrowserVersion.
*/
public HtmlUnitDriver() {
this(false);
}
/**
* Constructs a new instance, specify JavaScript support
* and using the {@link BrowserVersion#getDefault() default} BrowserVersion.
*
* @param enableJavascript whether to enable JavaScript support or not
*/
public HtmlUnitDriver(boolean enableJavascript) {
this(BrowserVersion.getDefault(), enableJavascript);
}
/**
* Constructs a new instance with the specified {@link BrowserVersion} and the JavaScript support.
*
* @param version the browser version to use
* @param enableJavascript whether to enable JavaScript support or not
*/
public HtmlUnitDriver(BrowserVersion version, boolean enableJavascript) {
this(version);
setJavascriptEnabled(enableJavascript);
}
/**
* Constructs a new instance with the specified {@link BrowserVersion}.
*
* @param version the browser version to use
*/
public HtmlUnitDriver(BrowserVersion version) {
webClient = createWebClient(version);
alert = new HtmlUnitAlert(this);
currentWindow = webClient.getCurrentWindow();
initialWindowDimension = new Dimension(currentWindow.getOuterWidth(), currentWindow.getOuterHeight());
webClient.addWebWindowListener(new WebWindowListener() {
@Override
public void webWindowOpened(WebWindowEvent webWindowEvent) {
// Ignore
}
@Override
public void webWindowContentChanged(WebWindowEvent event) {
if (event.getWebWindow() != currentWindow) {
return;
}
// Do we need to pick some new default content?
switchToDefaultContentOfWindow(currentWindow);
}
@Override
public void webWindowClosed(WebWindowEvent event) {
// Check if the event window refers to us or one of our parent windows
// setup the currentWindow appropriately if necessary
WebWindow curr = currentWindow;
do {
// Instance equality is okay in this case
if (curr == event.getWebWindow()) {
currentWindow = currentWindow.getTopWindow();
return;
}
curr = curr.getParentWindow();
} while (curr != currentWindow.getTopWindow());
}
});
// Now put us on the home page, like a real browser
get(webClient.getOptions().getHomePage());
gotPage = false;
resetKeyboardAndMouseState();
}
/**
* Note: There are two configuration modes for the HtmlUnitDriver using this constructor.
*
* - The first is where the browserName is "chrome", "firefox" or "internet explorer"
* and browserVersion denotes the desired version.
* - The second one is where the browserName is "htmlunit" and the browserVersion
* denotes the required browser AND its version. In this mode the browserVersion could be
* "chrome" for Chrome, "firefox-38" for Firefox 38 or "internet explorer-11" for IE 11.
*
* The Remote WebDriver uses the second mode - the first mode is deprecated and should not be used.
*
* @param capabilities desired capabilities requested for the htmlunit driver session
*/
public HtmlUnitDriver(Capabilities capabilities) {
this(determineBrowserVersion(capabilities));
setJavascriptEnabled(capabilities.isJavascriptEnabled());
setProxySettings(Proxy.extractFrom(capabilities));
}
public HtmlUnitDriver(Capabilities desiredCapabilities, Capabilities requiredCapabilities) {
this(new DesiredCapabilities(desiredCapabilities, requiredCapabilities));
}
// Package visibility for testing
static BrowserVersion determineBrowserVersion(Capabilities capabilities) {
String browserName = null;
String browserVersion = null;
String rawVersion = capabilities.getVersion();
String[] splitVersion = rawVersion == null ? new String[0] : rawVersion.split("-");
if (splitVersion.length > 1) {
browserVersion = splitVersion[1];
browserName = splitVersion[0];
} else {
browserName = capabilities.getVersion();
browserVersion = "";
}
// This is for backwards compatibility - in case there are users who are trying to
// configure the HtmlUnitDriver by using the c'tor with capabilities.
if (!BrowserType.HTMLUNIT.equals(capabilities.getBrowserName())) {
browserName = capabilities.getBrowserName();
browserVersion = capabilities.getVersion();
}
if (BrowserType.FIREFOX.equals(browserName)) {
// Try and convert the version
try {
int version = Integer.parseInt(browserVersion);
switch (version) {
case 31:
return BrowserVersion.FIREFOX_31;
default:
return BrowserVersion.FIREFOX_38;
}
} catch (NumberFormatException e) {
return BrowserVersion.FIREFOX_38;
}
}
if (BrowserType.CHROME.equals(browserName)) {
return BrowserVersion.CHROME;
}
if (BrowserType.IE.equals(browserName)) {
// Try and convert the version
try {
int version = Integer.parseInt(browserVersion);
switch (version) {
case 8:
return BrowserVersion.INTERNET_EXPLORER_8;
default:
return BrowserVersion.INTERNET_EXPLORER_11;
}
} catch (NumberFormatException e) {
return BrowserVersion.INTERNET_EXPLORER_11;
}
}
return BrowserVersion.getDefault();
}
private WebClient createWebClient(BrowserVersion version) {
WebClient client = newWebClient(version);
WebClientOptions options = client.getOptions();
options.setHomePage(WebClient.URL_ABOUT_BLANK.toString());
options.setThrowExceptionOnFailingStatusCode(false);
options.setPrintContentOnFailingStatusCode(false);
options.setJavaScriptEnabled(enableJavascript);
options.setRedirectEnabled(true);
options.setUseInsecureSSL(true);
// Ensure that we've set the proxy if necessary
if (proxyConfig != null) {
options.setProxyConfig(proxyConfig);
}
client.setRefreshHandler(new WaitingRefreshHandler());
return modifyWebClient(client);
}
/**
* Get the simulated {@code BrowserVersion}.
* @return the used {@code BrowserVersion}
*/
public BrowserVersion getBrowserVersion() {
return webClient.getBrowserVersion();
}
/**
* Create the underlying WebClient, but don't set any fields on it.
*
* @param version Which browser to emulate
* @return a new instance of WebClient.
*/
protected WebClient newWebClient(BrowserVersion version) {
return new WebClient(version);
}
/**
* Child classes can override this method to customize the WebClient that the HtmlUnit driver
* uses.
*
* @param client The client to modify
* @return The modified client
*/
protected WebClient modifyWebClient(WebClient client) {
// Does nothing here to be overridden.
return client;
}
/**
* Set proxy for WebClient using Proxy.
*
* @param proxy The proxy preferences.
*/
public void setProxySettings(Proxy proxy) {
if (proxy == null || proxy.getProxyType() == Proxy.ProxyType.UNSPECIFIED) {
return;
}
switch (proxy.getProxyType()) {
case MANUAL:
ArrayList noProxyHosts = new ArrayList<>();
String noProxy = proxy.getNoProxy();
if (noProxy != null && !noProxy.equals("")) {
String[] hosts = noProxy.split(",");
for (int i = 0; i < hosts.length; i++) {
if (hosts[i].trim().length() > 0) {
noProxyHosts.add(hosts[i].trim());
}
}
}
String httpProxy = proxy.getHttpProxy();
if (httpProxy != null && !httpProxy.equals("")) {
String host = httpProxy;
int port = 0;
int index = httpProxy.indexOf(":");
if (index != -1) {
host = httpProxy.substring(0, index);
port = Integer.parseInt(httpProxy.substring(index + 1));
}
setHTTPProxy(host, port, noProxyHosts);
}
String socksProxy = proxy.getSocksProxy();
if (socksProxy != null && !socksProxy.equals("")) {
String host = socksProxy;
int port = 0;
int index = socksProxy.indexOf(":");
if (index != -1) {
host = socksProxy.substring(0, index);
port = Integer.parseInt(socksProxy.substring(index + 1));
}
setSocksProxy(host, port, noProxyHosts);
}
// sslProxy is not supported/implemented
// ftpProxy is not supported/implemented
break;
case PAC:
String pac = proxy.getProxyAutoconfigUrl();
if (pac != null && !pac.equals("")) {
setAutoProxy(pac);
}
break;
}
}
/**
* Sets HTTP proxy for WebClient
*
* @param host The hostname of HTTP proxy
* @param port The port of HTTP proxy, 0 means HTTP proxy w/o port
*/
public void setProxy(String host, int port) {
setHTTPProxy(host, port, null);
}
/**
* Sets HTTP proxy for WebClient with bypass proxy hosts
*
* @param host The hostname of HTTP proxy
* @param port The port of HTTP proxy, 0 means HTTP proxy w/o port
* @param noProxyHosts The list of hosts which need to bypass HTTP proxy
*/
public void setHTTPProxy(String host, int port, ArrayList noProxyHosts) {
proxyConfig = new ProxyConfig();
proxyConfig.setProxyHost(host);
proxyConfig.setProxyPort(port);
if (noProxyHosts != null && noProxyHosts.size() > 0) {
for (String noProxyHost : noProxyHosts) {
proxyConfig.addHostsToProxyBypass(noProxyHost);
}
}
getWebClient().getOptions().setProxyConfig(proxyConfig);
}
/**
* Sets SOCKS proxy for WebClient
*
* @param host The hostname of SOCKS proxy
* @param port The port of SOCKS proxy, 0 means HTTP proxy w/o port
*/
public void setSocksProxy(String host, int port) {
setSocksProxy(host, port, null);
}
/**
* Sets SOCKS proxy for WebClient with bypass proxy hosts
*
* @param host The hostname of SOCKS proxy
* @param port The port of SOCKS proxy, 0 means HTTP proxy w/o port
* @param noProxyHosts The list of hosts which need to bypass SOCKS proxy
*/
public void setSocksProxy(String host, int port, ArrayList noProxyHosts) {
proxyConfig = new ProxyConfig();
proxyConfig.setProxyHost(host);
proxyConfig.setProxyPort(port);
proxyConfig.setSocksProxy(true);
if (noProxyHosts != null && noProxyHosts.size() > 0) {
for (String noProxyHost : noProxyHosts) {
proxyConfig.addHostsToProxyBypass(noProxyHost);
}
}
getWebClient().getOptions().setProxyConfig(proxyConfig);
}
/**
* Sets Proxy Autoconfiguration URL for WebClient
*
* @param autoProxyUrl The Proxy Autoconfiguration URL
*/
public void setAutoProxy(String autoProxyUrl) {
proxyConfig = new ProxyConfig();
proxyConfig.setProxyAutoConfigUrl(autoProxyUrl);
getWebClient().getOptions().setProxyConfig(proxyConfig);
}
@Override
public Capabilities getCapabilities() {
DesiredCapabilities capabilities = DesiredCapabilities.htmlUnit();
capabilities.setPlatform(Platform.getCurrent());
capabilities.setJavascriptEnabled(isJavascriptEnabled());
capabilities.setVersion(Version.getProductVersion());
capabilities.setCapability(SUPPORTS_FINDING_BY_CSS, true);
return capabilities;
}
@Override
public void get(String url) {
// Prevent the malformed url exception.
if (WebClient.URL_ABOUT_BLANK.toString().equals(url)) {
get(WebClient.URL_ABOUT_BLANK);
return;
}
URL fullUrl;
try {
fullUrl = new URL(url);
} catch (Exception e) {
throw new WebDriverException(e);
}
get(fullUrl);
}
/**
* Allows HtmlUnit's about:blank to be loaded in the constructor, and may be useful for other
* tests?
*
* @param fullUrl The URL to visit
*/
protected void get(URL fullUrl) {
try {
getWebClient().getPage(fullUrl);
// A "get" works over the entire page
currentWindow = getCurrentWindow().getTopWindow();
} catch (UnknownHostException e) {
getCurrentWindow().getTopWindow().setEnclosedPage(new UnexpectedPage(
new StringWebResponse("Unknown host", fullUrl),
getCurrentWindow().getTopWindow()
));
} catch (ConnectException e) {
// This might be expected
} catch (SocketTimeoutException e) {
throw new TimeoutException(e);
} catch (Exception e) {
throw new WebDriverException(e);
}
gotPage = true;
pickWindow();
resetKeyboardAndMouseState();
}
private void resetKeyboardAndMouseState() {
keyboard = new HtmlUnitKeyboard(this);
mouse = new HtmlUnitMouse(keyboard);
}
protected void pickWindow() {
// TODO(simon): HtmlUnit tries to track the current window as the frontmost. We don't
if (currentWindow == null) {
currentWindow = getWebClient().getCurrentWindow();
}
}
@Override
public String getCurrentUrl() {
getWebClient(); // check that session is active
Page page = getCurrentWindow().getTopWindow().getEnclosedPage();
if (page == null) {
return null;
}
URL url = page.getUrl();
if (url == null) {
return null;
}
return url.toString();
}
@Override
public String getTitle() {
if (alert.getCurrentQueue() != null && alert.getCurrentQueue().peek() != null) {
throw new UnhandledAlertException("Alert found", alert.getCurrentQueue().peek());
}
Page page = lastPage();
if (page == null || !(page instanceof HtmlPage)) {
return null; // no page so there is no title
}
if (getCurrentWindow() instanceof FrameWindow) {
page = getCurrentWindow().getTopWindow().getEnclosedPage();
}
return ((HtmlPage) page).getTitleText();
}
@Override
public WebElement findElement(By by) {
return findElement(by, this);
}
@Override
public List findElements(By by) {
return findElements(by, this);
}
@Override
public String getPageSource() {
Page page = lastPage();
if (page == null) {
return null;
}
if (page instanceof SgmlPage) {
return ((SgmlPage) page).asXml();
}
WebResponse response = page.getWebResponse();
return response.getContentAsString();
}
@Override
public void close() {
getWebClient(); // check that session is active
WebWindow thisWindow = getCurrentWindow(); // check that the current window is active
if (getWebClient().getWebWindows().size() == 1) {
// closing the last window is equivalent to quit
quit();
} else {
if (thisWindow != null) {
alert.close();
((TopLevelWindow) thisWindow.getTopWindow()).close();
}
if (getWebClient().getWebWindows().size() == 0) {
quit();
}
}
}
@Override
public void quit() {
if (webClient != null) {
alert.close();
webClient.close();
webClient = null;
}
currentWindow = null;
}
@Override
public Set getWindowHandles() {
final Set allHandles = Sets.newHashSet();
for (final WebWindow window : getWebClient().getTopLevelWindows()) {
allHandles.add(String.valueOf(System.identityHashCode(window)));
}
return allHandles;
}
@Override
public String getWindowHandle() {
WebWindow topWindow = getCurrentWindow().getTopWindow();
if (topWindow.isClosed()) {
throw new NoSuchWindowException("Window is closed");
}
return String.valueOf(System.identityHashCode(topWindow));
}
@Override
public Object executeScript(String script, final Object... args) {
HtmlPage page = getPageToInjectScriptInto();
script = "function() {" + script + "\n};";
ScriptResult result = page.executeJavaScript(script);
Function func = (Function) result.getJavaScriptResult();
Object[] parameters = convertScriptArgs(page, args);
try {
result = page.executeJavaScriptFunctionIfPossible(
func,
(ScriptableObject) getCurrentWindow().getScriptObject(),
parameters,
page.getDocumentElement());
} catch (Throwable ex) {
throw new WebDriverException(ex);
}
return parseNativeJavascriptResult(result);
}
@Override
public Object executeAsyncScript(String script, Object... args) {
HtmlPage page = getPageToInjectScriptInto();
args = convertScriptArgs(page, args);
Object result = new AsyncScriptExecutor(page, scriptTimeout)
.execute(script, args);
return parseNativeJavascriptResult(result);
}
private Object[] convertScriptArgs(HtmlPage page, final Object[] args) {
final Scriptable scope = (Scriptable) page.getEnclosingWindow().getScriptObject();
final Object[] parameters = new Object[args.length];
final ContextAction action = new ContextAction() {
@Override
public Object run(final Context context) {
for (int i = 0; i < args.length; i++) {
parameters[i] = parseArgumentIntoJavascriptParameter(context, scope, args[i]);
}
return null;
}
};
getWebClient().getJavaScriptEngine().getContextFactory().call(action);
return parameters;
}
private HtmlPage getPageToInjectScriptInto() {
if (!isJavascriptEnabled()) {
throw new UnsupportedOperationException(
"Javascript is not enabled for this HtmlUnitDriver instance");
}
final Page lastPage = lastPage();
if (!(lastPage instanceof HtmlPage)) {
throw new UnsupportedOperationException("Cannot execute JS against a plain text page");
} else if (!gotPage) {
// just to make ExecutingJavascriptTest.testShouldThrowExceptionIfExecutingOnNoPage happy
// but does this limitation make sense?
throw new WebDriverException("Can't execute JavaScript before a page has been loaded!");
}
return (HtmlPage) lastPage;
}
private Object parseArgumentIntoJavascriptParameter(
Context context, Scriptable scope, Object arg) {
while (arg instanceof WrapsElement) {
arg = ((WrapsElement) arg).getWrappedElement();
}
if (!(arg instanceof HtmlUnitWebElement ||
arg instanceof HtmlElement || // special case the underlying type
arg instanceof Number ||
arg instanceof String ||
arg instanceof Boolean ||
arg.getClass().isArray() ||
arg instanceof Collection> ||
arg instanceof Map, ?>)) {
throw new IllegalArgumentException(
"Argument must be a string, number, boolean or WebElement: " +
arg + " (" + arg.getClass() + ")");
}
if (arg instanceof HtmlUnitWebElement) {
HtmlUnitWebElement webElement = (HtmlUnitWebElement) arg;
assertElementNotStale(webElement.getElement());
return webElement.getElement().getScriptObject();
} else if (arg instanceof HtmlElement) {
HtmlElement element = (HtmlElement) arg;
assertElementNotStale(element);
return element.getScriptObject();
} else if (arg instanceof Collection>) {
List