Skip to content

Instantly share code, notes, and snippets.

@amitzur
Last active February 18, 2019 12:40
Show Gist options
  • Save amitzur/72de1fb442faaee6a56877e36cc26548 to your computer and use it in GitHub Desktop.
Save amitzur/72de1fb442faaee6a56877e36cc26548 to your computer and use it in GitHub Desktop.
Webdriver bug

Both chromedriver and geckodriver have strange behavior: When executing javascript via the driver, objects in the return value that have the property nodeType with values 1 or 9 (Element or Document types) are converted to Element representations.

Another way to show this, is by starting the driver, then creating a session by POST'ing to http://localhost:9515/session and then executing javascript by POST'ing to http://localhost:9515/session/<session_id>/execute/sync. The response is something like:

{
  "ELEMENT": "0.6880568807801719-1"
}

This behavior isn't documented in the spec: https://w3c.github.io/webdriver/

const {Capabilities, Builder} = require('selenium-webdriver')
const driver = new Builder().withCapabilities(Capabilities.chrome()).build()
driver
.executeScript('return {nodeType: 1}')
.then(result => {
console.log(result)
return driver.quit()
})
.catch(ex => {
console.log('err', ex)
})
@benjamingr
Copy link

benjamingr commented Feb 18, 2019

The tl;dr; is that in kCallFunctionScript which both executeScript and getNode by node ID there is incorrect code reuse. It's a bug in ChromeDriver you're welcome to fix it:

 if (nodeType == NodeType.ELEMENT || nodeType == NodeType.DOCUMENT || (SHADOW_DOM_ENABLED && value instanceof ShadowRoot)) {
      var wrapped = {};
      var root = getNodeRootThroughAnyShadows(value);
      wrapped[ELEMENT_KEY] = getPageCache(root, w3cEnabled).storeItem(value);
      return wrapped;
  }

executeScript does:

return web_view->CallFunction(session->GetCurrentFrameId(),
                                  "function(){" + script + "}", *args, value);

Which does:


Status WebViewImpl::CallFunction(const std::string& frame,
                                 const std::string& function,
                                 const base::ListValue& args,
                                 std::unique_ptr<base::Value>* result) {
  std::string json;
  base::JSONWriter::Write(args, &json);
  std::string w3c = w3c_compliant_ ? "true" : "false";
  // TODO(zachconrad): Second null should be array of shadow host ids.
  std::string expression = base::StringPrintf(
      "(%s).apply(null, [null, %s, %s, %s])",
      kCallFunctionScript,
      function.c_str(),
      json.c_str(),
      w3c.c_str());
  std::unique_ptr<base::Value> temp_result;
  Status status = EvaluateScript(frame, expression, &temp_result);
  if (status.IsError())
      return status;
  return internal::ParseCallFunctionResult(*temp_result, result);

Funnily, GetNodeIdFromFunction also does:


Status GetNodeIdFromFunction(DevToolsClient* client,
                             int context_id,
                             const std::string& function,
                             const base::ListValue& args,
                             bool* found_node,
                             int* node_id,
                             bool w3c_compliant) {
  std::string json;
  base::JSONWriter::Write(args, &json);
  std::string w3c = w3c_compliant ? "true" : "false";
  // TODO(zachconrad): Second null should be array of shadow host ids.
  std::string expression = base::StringPrintf(
      "(%s).apply(null, [null, %s, %s, %s, true])",
      kCallFunctionScript,
      function.c_str(),
      json.c_str(),
      w3c.c_str());

  bool got_object;
  std::string element_id;
  Status status = internal::EvaluateScriptAndGetObject(
      client, context_id, expression, &got_object, &element_id);
  if (status.IsError())
    return status;
  if (!got_object) {
    *found_node = false;
    return Status(kOk);
  }

This is funny, because kCallFunctionScript is:

const char kCallFunctionScript[] =
    "function() { // Copyright (c) 2012 The Chromium Authors. All rights reserved.\n"
    "// Use of this source code is governed by a BSD-style license that can be\n"
    "// found in the LICENSE file.\n"
    "\n"
    "/**\n"
    " * Enum for WebDriver status codes.\n"
    " * @enum {number}\n"
    " */\n"
    "var StatusCode = {\n"
    "  STALE_ELEMENT_REFERENCE: 10,\n"
    "  UNKNOWN_ERROR: 13,\n"
    "};\n"
    "\n"
    "/**\n"
    " * Enum for node types.\n"
    " * @enum {number}\n"
    " */\n"
    "var NodeType = {\n"
    "  ELEMENT: 1,\n"
    "  DOCUMENT: 9,\n"
    "};\n"
    "\n"
    "/**\n"
    " * Dictionary key to use for holding an element ID.\n"
    " * @const\n"
    " * @type {string}\n"
    " */\n"
    "var ELEMENT_KEY = 'ELEMENT';\n"
    "\n"
    "/**\n"
    " * True if using W3C Element references.\n"
    " * @const\n"
    " * @type {boolean}\n"
    " */\n"
    "var w3cEnabled = false;\n"
    "\n"
    "/**\n"
    " * True if shadow dom is enabled.\n"
    " * @const\n"
    " * @type {boolean}\n"
    " */\n"
    "var SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';\n"
    "\n"
    "/**\n"
    " * Generates a unique ID to identify an element.\n"
    " * @void\n"
    " * @return {string} Randomly generated ID.\n"
    " */\n"
    "function generateUUID() {\n"
    "  var array = new Uint8Array(16);\n"
    "  window.crypto.getRandomValues(array);\n"
    "  array[6] = 0x40 | (array[6] & 0x0f);\n"
    "  array[8] = 0x80 | (array[8] & 0x3f);\n"
    "\n"
    "  var UUID = \"\";\n"
    "  for (var i = 0; i < 16; i++) {\n"
    "    var temp = array[i].toString(16);\n"
    "    if (temp.length < 2)\n"
    "      temp = \"0\" + temp;\n"
    "    UUID += temp;\n"
    "    if (i == 3 || i == 5 || i == 7 || i == 9)\n"
    "      UUID += \"-\";\n"
    "  }\n"
    "  return UUID;\n"
    "};\n"
    "\n"
    "/**\n"
    " * A cache which maps IDs <-> cached objects for the purpose of identifying\n"
    " * a script object remotely. Uses UUIDs for identification.\n"
    " * @constructor\n"
    " */\n"
    "function CacheWithUUID() {\n"
    "  this.cache_ = {};\n"
    "}\n"
    "\n"
    "CacheWithUUID.prototype = {\n"
    "  /**\n"
    "   * Stores a given item in the cache and returns a unique UUID.\n"
    "   *\n"
    "   * @param {!Object} item The item to store in the cache.\n"
    "   * @return {number} The UUID for the cached item.\n"
    "   */\n"
    "  storeItem: function(item) {\n"
    "    for (var i in this.cache_) {\n"
    "      if (item == this.cache_[i])\n"
    "        return i;\n"
    "    }\n"
    "    var id = generateUUID();\n"
    "    this.cache_[id] = item;\n"
    "    return id;\n"
    "  },\n"
    "\n"
    "  /**\n"
    "   * Retrieves the cached object for the given ID.\n"
    "   *\n"
    "   * @param {number} id The ID for the cached item to retrieve.\n"
    "   * @return {!Object} The retrieved item.\n"
    "   */\n"
    "  retrieveItem: function(id) {\n"
    "    var item = this.cache_[id];\n"
    "    if (item)\n"
    "      return item;\n"
    "    var error = new Error('not in cache');\n"
    "    error.code = StatusCode.STALE_ELEMENT_REFERENCE;\n"
    "    error.message = 'element is not attached to the page document';\n"
    "    throw error;\n"
    "  },\n"
    "\n"
    "  /**\n"
    "   * Clears stale items from the cache.\n"
    "   */\n"
    "  clearStale: function() {\n"
    "    for (var id in this.cache_) {\n"
    "      var node = this.cache_[id];\n"
    "      if (!this.isNodeReachable_(node))\n"
    "        delete this.cache_[id];\n"
    "    }\n"
    "  },\n"
    "\n"
    "  /**\n"
    "    * @private\n"
    "    * @param {!Node} node The node to check.\n"
    "    * @return {boolean} If the nodes is reachable.\n"
    "    */\n"
    "  isNodeReachable_: function(node) {\n"
    "    var nodeRoot = getNodeRootThroughAnyShadows(node);\n"
    "    return (nodeRoot == document);\n"
    "  }\n"
    "\n"
    "\n"
    "};\n"
    "\n"
    "/**\n"
    " * A cache which maps IDs <-> cached objects for the purpose of identifying\n"
    " * a script object remotely.\n"
    " * @constructor\n"
    " */\n"
    "function Cache() {\n"
    "  this.cache_ = {};\n"
    "  this.nextId_ = 1;\n"
    "  this.idPrefix_ = Math.random().toString();\n"
    "}\n"
    "\n"
    "Cache.prototype = {\n"
    "\n"
    "  /**\n"
    "   * Stores a given item in the cache and returns a unique ID.\n"
    "   *\n"
    "   * @param {!Object} item The item to store in the cache.\n"
    "   * @return {number} The ID for the cached item.\n"
    "   */\n"
    "  storeItem: function(item) {\n"
    "    for (var i in this.cache_) {\n"
    "      if (item == this.cache_[i])\n"
    "        return i;\n"
    "    }\n"
    "    var id = this.idPrefix_  + '-' + this.nextId_;\n"
    "    this.cache_[id] = item;\n"
    "    this.nextId_++;\n"
    "    return id;\n"
    "  },\n"
    "\n"
    "  /**\n"
    "   * Retrieves the cached object for the given ID.\n"
    "   *\n"
    "   * @param {number} id The ID for the cached item to retrieve.\n"
    "   * @return {!Object} The retrieved item.\n"
    "   */\n"
    "  retrieveItem: function(id) {\n"
    "    var item = this.cache_[id];\n"
    "    if (item)\n"
    "      return item;\n"
    "    var error = new Error('not in cache');\n"
    "    error.code = StatusCode.STALE_ELEMENT_REFERENCE;\n"
    "    error.message = 'element is not attached to the page document';\n"
    "    throw error;\n"
    "  },\n"
    "\n"
    "  /**\n"
    "   * Clears stale items from the cache.\n"
    "   */\n"
    "  clearStale: function() {\n"
    "    for (var id in this.cache_) {\n"
    "      var node = this.cache_[id];\n"
    "      if (!this.isNodeReachable_(node))\n"
    "        delete this.cache_[id];\n"
    "    }\n"
    "  },\n"
    "\n"
    "  /**\n"
    "    * @private\n"
    "    * @param {!Node} node The node to check.\n"
    "    * @return {boolean} If the nodes is reachable.\n"
    "    */\n"
    "  isNodeReachable_: function(node) {\n"
    "    var nodeRoot = getNodeRootThroughAnyShadows(node);\n"
    "    return (nodeRoot == document);\n"
    "  }\n"
    "};\n"
    "\n"
    "/**\n"
    " * Returns the root element of the node.  Found by traversing parentNodes until\n"
    " * a node with no parent is found.  This node is considered the root.\n"
    " * @param {?Node} node The node to find the root element for.\n"
    " * @return {?Node} The root node.\n"
    " */\n"
    "function getNodeRoot(node) {\n"
    "  while (node && node.parentNode) {\n"
    "    node = node.parentNode;\n"
    "  }\n"
    "  return node;\n"
    "}\n"
    "\n"
    "/**\n"
    " * Returns the root element of the node, jumping up through shadow roots if\n"
    " * any are found.\n"
    " */\n"
    "function getNodeRootThroughAnyShadows(node) {\n"
    "  var root = getNodeRoot(node);\n"
    "  while (SHADOW_DOM_ENABLED && root instanceof ShadowRoot) {\n"
    "    root = getNodeRoot(root.host);\n"
    "  }\n"
    "  return root;\n"
    "}\n"
    "\n"
    "/**\n"
    " * Returns the global object cache for the page.\n"
    " * @param {Document=} opt_doc The document whose cache to retrieve. Defaults to\n"
    " *     the current document.\n"
    " * @return {!Cache} The page's object cache.\n"
    " */\n"
    "function getPageCache(opt_doc, opt_w3c) {\n"
    "  var doc = opt_doc || document;\n"
    "  var w3c = opt_w3c || false;\n"
    "  var key = '$cdc_asdjflasutopfhvcZLmcfl_';\n"
    "  if (w3c) {\n"
    "    if (!(key in doc))\n"
    "      doc[key] = new CacheWithUUID();\n"
    "    return doc[key];\n"
    "  } else {\n"
    "    if (!(key in doc))\n"
    "      doc[key] = new Cache();\n"
    "    return doc[key];\n"
    "  }\n"
    "}\n"
    "\n"
    "/**\n"
    " * Wraps the given value to be transmitted remotely by converting\n"
    " * appropriate objects to cached object IDs.\n"
    " *\n"
    " * @param {*} value The value to wrap.\n"
    " * @return {*} The wrapped value.\n"
    " */\n"
    "function wrap(value) {\n"
    "  // As of crrev.com/1316933002, typeof() for some elements will return\n"
    "  // 'function', not 'object'. So we need to check for both non-null objects, as\n"
    "  // well Elements that also happen to be callable functions (e.g. <embed> and\n"
    "  // <object> elements). Note that we can not use |value instanceof Object| here\n"
    "  // since this does not work with frames/iframes, for example\n"
    "  // frames[0].document.body instanceof Object == false even though\n"
    "  // typeof(frames[0].document.body) == 'object'.\n"
    "  if ((typeof(value) == 'object' && value != null) ||\n"
    "      (typeof(value) == 'function' && value.nodeName &&\n"
    "       value.nodeType == NodeType.ELEMENT)) {\n"
    "    var nodeType = value['nodeType'];\n"
    "    if (nodeType == NodeType.ELEMENT || nodeType == NodeType.DOCUMENT\n"
    "        || (SHADOW_DOM_ENABLED && value instanceof ShadowRoot)) {\n"
    "      var wrapped = {};\n"
    "      var root = getNodeRootThroughAnyShadows(value);\n"
    "      wrapped[ELEMENT_KEY] = getPageCache(root, w3cEnabled).storeItem(value);\n"
    "      return wrapped;\n"
    "    }\n"
    "\n"
    "    var obj;\n"
    "    if (typeof(value.length) == 'number') {\n"
    "      obj = [];\n"
    "      for (var i = 0; i < value.length; i++)\n"
    "        obj[i] = wrap(value[i]);\n"
    "    } else {\n"
    "      obj = {};\n"
    "      for (var prop in value)\n"
    "        obj[prop] = wrap(value[prop]);\n"
    "    }\n"
    "    return obj;\n"
    "  }\n"
    "  return value;\n"
    "}\n"
    "\n"
    "/**\n"
    " * Unwraps the given value by converting from object IDs to the cached\n"
    " * objects.\n"
    " *\n"
    " * @param {*} value The value to unwrap.\n"
    " * @param {Cache} cache The cache to retrieve wrapped elements from.\n"
    " * @return {*} The unwrapped value.\n"
    " */\n"
    "function unwrap(value, cache) {\n"
    "  if (typeof(value) == 'object' && value != null) {\n"
    "    if (ELEMENT_KEY in value)\n"
    "      return cache.retrieveItem(value[ELEMENT_KEY]);\n"
    "\n"
    "    var obj;\n"
    "    if (typeof(value.length) == 'number') {\n"
    "      obj = [];\n"
    "      for (var i = 0; i < value.length; i++)\n"
    "        obj[i] = unwrap(value[i], cache);\n"
    "    } else {\n"
    "      obj = {};\n"
    "      for (var prop in value)\n"
    "        obj[prop] = unwrap(value[prop], cache);\n"
    "    }\n"
    "    return obj;\n"
    "  }\n"
    "  return value;\n"
    "}\n"
    "\n"
    "/**\n"
    " * Calls a given function and returns its value.\n"
    " *\n"
    " * The inputs to and outputs of the function will be unwrapped and wrapped\n"
    " * respectively, unless otherwise specified. This wrapping involves converting\n"
    " * between cached object reference IDs and actual JS objects. The cache will\n"
    " * automatically be pruned each call to remove stale references.\n"
    " *\n"
    " * @param  {Array<string>} shadowHostIds The host ids of the nested shadow\n"
    " *     DOMs the function should be executed in the context of.\n"
    " * @param {function(...[*]) : *} func The function to invoke.\n"
    " * @param {!Array<*>} args The array of arguments to supply to the function,\n"
    " *     which will be unwrapped before invoking the function.\n"
    " * @param {boolean} w3c Whether to return a W3C compliant element reference.\n"
    " * @param {boolean=} opt_unwrappedReturn Whether the function's return value\n"
    " *     should be left unwrapped.\n"
    " * @return {*} An object containing a status and value property, where status\n"
    " *     is a WebDriver status code and value is the wrapped value. If an\n"
    " *     unwrapped return was specified, this will be the function's pure return\n"
    " *     value.\n"
    " */\n"
    "function callFunction(shadowHostIds, func, args, w3c, opt_unwrappedReturn) {\n"
    "  if (w3c) {\n"
    "    w3cEnabled = true;\n"
    "    ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';\n"
    "\n"
    "  }\n"
    "  var cache = getPageCache(null, w3cEnabled);\n"
    "  cache.clearStale();\n"
    "  if (shadowHostIds && SHADOW_DOM_ENABLED) {\n"
    "    for (var i = 0; i < shadowHostIds.length; i++) {\n"
    "      var host = cache.retrieveItem(shadowHostIds[i]);\n"
    "      // TODO(zachconrad): Use the olderShadowRoot API when available to check\n"
    "      // all of the shadow roots.\n"
    "      cache = getPageCache(host.webkitShadowRoot, w3cEnabled);\n"
    "      cache.clearStale();\n"
    "    }\n"
    "  }\n"
    "\n"
    "  if (opt_unwrappedReturn)\n"
    "    return func.apply(null, unwrap(args, cache));\n"
    "\n"
    "  var status = 0;\n"
    "  try {\n"
    "    var returnValue = wrap(func.apply(null, unwrap(args, cache)));\n"
    "  } catch (error) {\n"
    "    status = error.code || StatusCode.UNKNOWN_ERROR;\n"
    "    var returnValue = error.message;\n"
    "  }\n"
    "  return {\n"
    "      status: status,\n"
    "      value: returnValue\n"
    "  }\n"
    "}\n"
    "; return callFunction.apply(null, arguments) }\n";

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment