Implement AutomationNode.querySelector().

BUG=404710

Review URL: https://codereview.chromium.org/655273005

Cr-Commit-Position: refs/heads/master@{#302944}
diff --git a/chrome/browser/extensions/api/automation/automation_apitest.cc b/chrome/browser/extensions/api/automation/automation_apitest.cc
index 5d3bd57..8a3dac1 100644
--- a/chrome/browser/extensions/api/automation/automation_apitest.cc
+++ b/chrome/browser/extensions/api/automation/automation_apitest.cc
@@ -57,12 +57,6 @@
     host_resolver()->AddRule("*", embedded_test_server()->base_url().host());
   }
 
-  void LoadPage() {
-    StartEmbeddedTestServer();
-    const GURL url = GetURLForPath(kDomain, "/index.html");
-    ui_test_utils::NavigateToURL(browser(), url);
-  }
-
  public:
   virtual void SetUpInProcessBrowserTestFixture() override {
     ExtensionApiTest::SetUpInProcessBrowserTestFixture();
@@ -70,7 +64,9 @@
 };
 
 IN_PROC_BROWSER_TEST_F(AutomationApiTest, TestRendererAccessibilityEnabled) {
-  LoadPage();
+  StartEmbeddedTestServer();
+  const GURL url = GetURLForPath(kDomain, "/index.html");
+  ui_test_utils::NavigateToURL(browser(), url);
 
   ASSERT_EQ(1, browser()->tab_strip_model()->count());
   content::WebContents* const tab =
@@ -177,6 +173,13 @@
       << message_;
 }
 
+IN_PROC_BROWSER_TEST_F(AutomationApiTest, QuerySelector) {
+  StartEmbeddedTestServer();
+  ASSERT_TRUE(
+      RunExtensionSubtest("automation/tests/tabs", "queryselector.html"))
+      << message_;
+}
+
 static const int kPid = 1;
 static const int kTab0Rid = 1;
 static const int kTab1Rid = 2;
diff --git a/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc b/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc
index aa2aa584..79632d3b 100644
--- a/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc
+++ b/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc
@@ -6,7 +6,9 @@
 
 #include <vector>
 
+#include "base/strings/string16.h"
 #include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/accessibility/ax_tree_id_registry.h"
 #include "chrome/browser/extensions/api/automation_internal/automation_action_adapter.h"
 #include "chrome/browser/extensions/api/automation_internal/automation_util.h"
@@ -21,9 +23,11 @@
 #include "content/public/browser/browser_accessibility_state.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
+#include "content/public/browser/render_view_host.h"
 #include "content/public/browser/render_widget_host.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/web_contents.h"
+#include "extensions/common/extension_messages.h"
 #include "extensions/common/permissions/permissions_data.h"
 
 #if defined(OS_CHROMEOS)
@@ -36,14 +40,93 @@
 
 DEFINE_WEB_CONTENTS_USER_DATA_KEY(extensions::AutomationWebContentsObserver);
 
+namespace extensions {
+
 namespace {
 const int kDesktopTreeID = 0;
 const char kCannotRequestAutomationOnPage[] =
     "Cannot request automation tree on url \"*\". "
     "Extension manifest must request permission to access this host.";
-}  // namespace
+const char kRendererDestroyed[] = "The tab was closed.";
+const char kNoMainFrame[] = "No main frame.";
+const char kNoDocument[] = "No document.";
+const char kNodeDestroyed[] =
+    "querySelector sent on node which is no longer in the tree.";
 
-namespace extensions {
+// Handles sending and receiving IPCs for a single querySelector request. On
+// creation, sends the request IPC, and is destroyed either when the response is
+// received or the renderer is destroyed.
+class QuerySelectorHandler : public content::WebContentsObserver {
+ public:
+  QuerySelectorHandler(
+      content::WebContents* web_contents,
+      int request_id,
+      int acc_obj_id,
+      const base::string16& query,
+      const extensions::AutomationInternalQuerySelectorFunction::Callback&
+          callback)
+      : content::WebContentsObserver(web_contents),
+        request_id_(request_id),
+        callback_(callback) {
+    content::RenderViewHost* rvh = web_contents->GetRenderViewHost();
+
+    rvh->Send(new ExtensionMsg_AutomationQuerySelector(
+        rvh->GetRoutingID(), request_id, acc_obj_id, query));
+  }
+
+  ~QuerySelectorHandler() override {}
+
+  bool OnMessageReceived(const IPC::Message& message) override {
+    if (message.type() != ExtensionHostMsg_AutomationQuerySelector_Result::ID)
+      return false;
+
+    // There may be several requests in flight; check this response matches.
+    int message_request_id = 0;
+    PickleIterator iter(message);
+    if (!message.ReadInt(&iter, &message_request_id))
+      return false;
+
+    if (message_request_id != request_id_)
+      return false;
+
+    IPC_BEGIN_MESSAGE_MAP(QuerySelectorHandler, message)
+      IPC_MESSAGE_HANDLER(ExtensionHostMsg_AutomationQuerySelector_Result,
+                          OnQueryResponse)
+    IPC_END_MESSAGE_MAP()
+    return true;
+  }
+
+  void WebContentsDestroyed() override {
+    callback_.Run(kRendererDestroyed, 0);
+    delete this;
+  }
+
+ private:
+  void OnQueryResponse(int request_id,
+                       ExtensionHostMsg_AutomationQuerySelector_Error error,
+                       int result_acc_obj_id) {
+    std::string error_string;
+    switch (error.value) {
+    case ExtensionHostMsg_AutomationQuerySelector_Error::kNone:
+      error_string = "";
+      break;
+    case ExtensionHostMsg_AutomationQuerySelector_Error::kNoMainFrame:
+      error_string = kNoMainFrame;
+      break;
+    case ExtensionHostMsg_AutomationQuerySelector_Error::kNoDocument:
+      error_string = kNoDocument;
+      break;
+    case ExtensionHostMsg_AutomationQuerySelector_Error::kNodeDestroyed:
+      error_string = kNodeDestroyed;
+      break;
+    }
+    callback_.Run(error_string, result_acc_obj_id);
+    delete this;
+  }
+
+  int request_id_;
+  const extensions::AutomationInternalQuerySelectorFunction::Callback callback_;
+};
 
 bool CanRequestAutomation(const Extension* extension,
                           const AutomationInfo* automation_info,
@@ -64,6 +147,35 @@
       extension, url, url, tab_id, process_id, &unused_error);
 }
 
+// Helper class that implements an action adapter for a |RenderFrameHost|.
+class RenderFrameHostActionAdapter : public AutomationActionAdapter {
+ public:
+  explicit RenderFrameHostActionAdapter(content::RenderFrameHost* rfh)
+      : rfh_(rfh) {}
+
+  virtual ~RenderFrameHostActionAdapter() {}
+
+  // AutomationActionAdapter implementation.
+  void DoDefault(int32 id) override { rfh_->AccessibilityDoDefaultAction(id); }
+
+  void Focus(int32 id) override { rfh_->AccessibilitySetFocus(id); }
+
+  void MakeVisible(int32 id) override {
+    rfh_->AccessibilityScrollToMakeVisible(id, gfx::Rect());
+  }
+
+  void SetSelection(int32 id, int32 start, int32 end) override {
+    rfh_->AccessibilitySetTextSelection(id, start, end);
+  }
+
+ private:
+  content::RenderFrameHost* rfh_;
+
+  DISALLOW_COPY_AND_ASSIGN(RenderFrameHostActionAdapter);
+};
+
+}  // namespace
+
 // Helper class that receives accessibility data from |WebContents|.
 class AutomationWebContentsObserver
     : public content::WebContentsObserver,
@@ -101,33 +213,6 @@
   DISALLOW_COPY_AND_ASSIGN(AutomationWebContentsObserver);
 };
 
-// Helper class that implements an action adapter for a |RenderFrameHost|.
-class RenderFrameHostActionAdapter : public AutomationActionAdapter {
- public:
-  explicit RenderFrameHostActionAdapter(content::RenderFrameHost* rfh)
-      : rfh_(rfh) {}
-
-  virtual ~RenderFrameHostActionAdapter() {}
-
-  // AutomationActionAdapter implementation.
-  void DoDefault(int32 id) override { rfh_->AccessibilityDoDefaultAction(id); }
-
-  void Focus(int32 id) override { rfh_->AccessibilitySetFocus(id); }
-
-  void MakeVisible(int32 id) override {
-    rfh_->AccessibilityScrollToMakeVisible(id, gfx::Rect());
-  }
-
-  void SetSelection(int32 id, int32 start, int32 end) override {
-    rfh_->AccessibilitySetTextSelection(id, start, end);
-  }
-
- private:
-  content::RenderFrameHost* rfh_;
-
-  DISALLOW_COPY_AND_ASSIGN(RenderFrameHostActionAdapter);
-};
-
 ExtensionFunction::ResponseAction
 AutomationInternalEnableTabFunction::Run() {
   const AutomationInfo* automation_info = AutomationInfo::Get(extension());
@@ -276,4 +361,52 @@
 #endif  // defined(OS_CHROMEOS)
 }
 
+// static
+int AutomationInternalQuerySelectorFunction::query_request_id_counter_ = 0;
+
+ExtensionFunction::ResponseAction
+AutomationInternalQuerySelectorFunction::Run() {
+  const AutomationInfo* automation_info = AutomationInfo::Get(extension());
+  EXTENSION_FUNCTION_VALIDATE(automation_info);
+
+  using api::automation_internal::QuerySelector::Params;
+  scoped_ptr<Params> params(Params::Create(*args_));
+  EXTENSION_FUNCTION_VALIDATE(params.get());
+
+  if (params->args.tree_id == kDesktopTreeID) {
+    return RespondNow(
+        Error("querySelector queries may not be used on the desktop."));
+  }
+  AXTreeIDRegistry::FrameID frame_id =
+      AXTreeIDRegistry::GetInstance()->GetFrameID(params->args.tree_id);
+  content::RenderFrameHost* rfh =
+      content::RenderFrameHost::FromID(frame_id.first, frame_id.second);
+  if (!rfh)
+    return RespondNow(Error("querySelector query sent on destroyed tree."));
+
+  content::WebContents* contents =
+      content::WebContents::FromRenderFrameHost(rfh);
+
+  int request_id = query_request_id_counter_++;
+  base::string16 selector = base::UTF8ToUTF16(params->args.selector);
+
+  // QuerySelectorHandler handles IPCs and deletes itself on completion.
+  new QuerySelectorHandler(
+      contents, request_id, params->args.automation_node_id, selector,
+      base::Bind(&AutomationInternalQuerySelectorFunction::OnResponse, this));
+
+  return RespondLater();
+}
+
+void AutomationInternalQuerySelectorFunction::OnResponse(
+    const std::string& error,
+    int result_acc_obj_id) {
+  if (!error.empty()) {
+    Respond(Error(error));
+    return;
+  }
+
+  Respond(OneArgument(new base::FundamentalValue(result_acc_obj_id)));
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/automation_internal/automation_internal_api.h b/chrome/browser/extensions/api/automation_internal/automation_internal_api.h
index 557b6d1..5761d26 100644
--- a/chrome/browser/extensions/api/automation_internal/automation_internal_api.h
+++ b/chrome/browser/extensions/api/automation_internal/automation_internal_api.h
@@ -77,6 +77,28 @@
   ResponseAction Run() override;
 };
 
+class AutomationInternalQuerySelectorFunction
+    : public UIThreadExtensionFunction {
+  DECLARE_EXTENSION_FUNCTION("automationInternal.querySelector",
+                             AUTOMATIONINTERNAL_ENABLEDESKTOP)
+
+ public:
+  typedef base::Callback<void(const std::string& error,
+                              int result_acc_obj_id)> Callback;
+
+ protected:
+  ~AutomationInternalQuerySelectorFunction() override {}
+
+  ResponseAction Run() override;
+
+ private:
+  void OnResponse(const std::string& error, int result_acc_obj_id);
+
+  // Used for assigning a unique ID to each request so that the response can be
+  // routed appropriately.
+  static int query_request_id_counter_;
+};
+
 }  // namespace extensions
 
 #endif  // CHROME_BROWSER_EXTENSIONS_API_AUTOMATION_INTERNAL_AUTOMATION_INTERNAL_API_H_
diff --git a/chrome/common/extensions/api/automation.idl b/chrome/common/extensions/api/automation.idl
index 015af4b..9da10e0 100644
--- a/chrome/common/extensions/api/automation.idl
+++ b/chrome/common/extensions/api/automation.idl
@@ -220,6 +220,9 @@
     long height;
   };
 
+  // Called when the result for a <code>query</code> is available.
+  callback QueryCallback = void(AutomationNode node);
+
   // An event in the Automation tree.
   [nocompile, noinline_doc] dictionary AutomationEvent {
     // The $(ref:automation.AutomationNode) to which the event was targeted.
@@ -289,6 +292,22 @@
     // Removes a listener for the given event type and event phase.
     static void removeEventListener(
         EventType eventType, AutomationListener listener, boolean capture);
+
+    // Gets the first node in this node's subtree which matches the given CSS
+    // selector and is within the same DOM context.
+    //
+    // If this node doesn't correspond directly with an HTML node in the DOM,
+    // querySelector will be run on this node's nearest HTML node ancestor. Note
+    // that this may result in the query returning a node which is not a
+    // descendant of this node.
+    //
+    // If the selector matches a node which doesn't directly correspond to an
+    // automation node (for example an element within an ARIA widget, where the
+    // ARIA widget forms one node of the automation tree, or an element which
+    // is hidden from accessibility via hiding it using CSS or using
+    // aria-hidden), this will return the nearest ancestor which does correspond
+    // to an automation node.
+    static void querySelector(DOMString selector, QueryCallback callback);
   };
 
   // Called when the <code>AutomationNode</code> for the page is available.
diff --git a/chrome/common/extensions/api/automation_internal.idl b/chrome/common/extensions/api/automation_internal.idl
index 9c2a089..7b9063fe7 100644
--- a/chrome/common/extensions/api/automation_internal.idl
+++ b/chrome/common/extensions/api/automation_internal.idl
@@ -81,6 +81,13 @@
     long endIndex;
   };
 
+  // Arguments for the querySelector function.
+  dictionary QuerySelectorRequiredParams {
+    long treeID;
+    long automationNodeID;
+    DOMString selector;
+  };
+
   // Returns the accessibility tree id of the web contents who's accessibility
   // was enabled using enableTab().
   callback EnableTabCallback = void(long tree_id);
@@ -88,6 +95,9 @@
   // Callback called when enableDesktop() returns.
   callback EnableDesktopCallback = void();
 
+  // Callback called when querySelector() returns.
+  callback QuerySelectorCallback = void(long resultAutomationNodeID);
+
   interface Functions {
     // Enable automation of the tab with the given id, or the active tab if no
     // tab id is given, and retrieves accessibility tree id for use in
@@ -103,6 +113,10 @@
     // Performs an action on an automation node.
     static void performAction(PerformActionRequiredParams args,
                               object opt_args);
+
+    // Performs a query selector query.
+    static void querySelector(QuerySelectorRequiredParams args,
+                              QuerySelectorCallback callback);
   };
 
   interface Events {
diff --git a/chrome/renderer/resources/extensions/automation/automation_node.js b/chrome/renderer/resources/extensions/automation/automation_node.js
index 459fa5b..bc57029 100644
--- a/chrome/renderer/resources/extensions/automation/automation_node.js
+++ b/chrome/renderer/resources/extensions/automation/automation_node.js
@@ -93,6 +93,14 @@
                           endIndex: endIndex });
   },
 
+  querySelector: function(selector, callback) {
+    automationInternal.querySelector(
+      { treeID: this.rootImpl.treeID,
+        automationNodeID: this.id,
+        selector: selector },
+      this.querySelectorCallback_.bind(this, callback));
+  },
+
   addEventListener: function(eventType, callback, capture) {
     this.removeEventListener(eventType, callback);
     if (!this.listeners[eventType])
@@ -140,11 +148,14 @@
   },
 
   toString: function() {
-    return 'node id=' + this.id +
+    var impl = privates(this).impl;
+    if (!impl)
+      impl = this;
+    return 'node id=' + impl.id +
         ' role=' + this.role +
         ' state=' + $JSON.stringify(this.state) +
-        ' parentID=' + this.parentID +
-        ' childIds=' + $JSON.stringify(this.childIds) +
+        ' parentID=' + impl.parentID +
+        ' childIds=' + $JSON.stringify(impl.childIds) +
         ' attributes=' + $JSON.stringify(this.attributes);
   },
 
@@ -213,6 +224,23 @@
                                        automationNodeID: this.id,
                                        actionType: actionType },
                                      opt_args || {});
+  },
+
+  querySelectorCallback_: function(userCallback, resultAutomationNodeID) {
+    // resultAutomationNodeID could be zero or undefined or (unlikely) null;
+    // they all amount to the same thing here, which is that no node was
+    // returned.
+    if (!resultAutomationNodeID) {
+      userCallback(null);
+      return;
+    }
+    var resultNode = this.rootImpl.get(resultAutomationNodeID);
+    if (!resultNode) {
+      logging.WARNING('Query selector result not in tree: ' +
+                      resultAutomationNodeID);
+      userCallback(null);
+    }
+    userCallback(resultNode);
   }
 };
 
@@ -615,6 +643,7 @@
                                                 'setSelection',
                                                 'addEventListener',
                                                 'removeEventListener',
+                                                'querySelector',
                                                 'toJSON'],
                                     readonly: ['isRootNode',
                                                'role',
diff --git a/chrome/test/data/extensions/api_test/automation/sites/complex.html b/chrome/test/data/extensions/api_test/automation/sites/complex.html
new file mode 100644
index 0000000..8610fe5
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/automation/sites/complex.html
@@ -0,0 +1,23 @@
+<!--
+ * Copyright 2014 The Chromium Authors. All rights reserved.  Use of this
+ * source code is governed by a BSD-style license that can be found in the
+ * LICENSE file.
+-->
+<html>
+<head>
+<title>Automation Tests - Complex DOM</title>
+</head>
+<body>
+<!-- Using role=group just to force it to be a group in the a11y tree -->
+<div role="group">
+<h1>Google Chrome Terms of Service</h1>
+<p>These Terms of Service apply to the executable code version of Google Chrome. Source code for Google Chrome is available free of charge under open source software license agreements at http://code.google.com/chromium/terms.html.</p>
+</div>
+<div role="main">
+<p><strong>1. Your relationship with Google</strong></p>
+<p>1.1 Your use of Google’s products, software, services and web sites (referred to collectively as the “Services” in this document and excluding any services provided to you by Google under a separate written agreement) is subject to the terms of a legal agreement between you and Google. “Google” means Google Inc., whose principal place of business is at 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States. This document explains how the agreement is made up, and sets out some of the terms of that agreement.</p>
+</div>
+<button disabled><span id="span-in-button">Ok</span></button>
+<button>Cancel</button>
+</body>
+</html>
diff --git a/chrome/test/data/extensions/api_test/automation/sites/index.html b/chrome/test/data/extensions/api_test/automation/sites/index.html
index f8fc4f1..337dee51 100644
--- a/chrome/test/data/extensions/api_test/automation/sites/index.html
+++ b/chrome/test/data/extensions/api_test/automation/sites/index.html
@@ -8,7 +8,7 @@
 <title>Automation Tests</title>
 </head>
 <body>
-<button>Ok</button>
+<button><span id="span-in-button">Ok</span></button>
 <input type="text" aria-label="Username">
 <button>Cancel</button>
 </body>
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/close_tab.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/close_tab.js
index 42c769d..bb2d619 100644
--- a/chrome/test/data/extensions/api_test/automation/tests/tabs/close_tab.js
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/close_tab.js
@@ -4,7 +4,7 @@
 
 var allTests = [
   function testCloseTab() {
-    getUrlFromConfig(function(url) {
+    getUrlFromConfig('index.html', function(url) {
       chrome.tabs.create({'url': url}, function(tab) {
         chrome.automation.getTree(function(rootNode) {
           rootNode.addEventListener(EventType.destroyed, function() {
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/common.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/common.js
index af464a25..0a33500 100644
--- a/chrome/test/data/extensions/api_test/automation/tests/tabs/common.js
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/common.js
@@ -18,8 +18,9 @@
   });
 }
 
-function setUpAndRunTests(allTests) {
-  getUrlFromConfig(function(url) {
+function setUpAndRunTests(allTests, opt_path) {
+  var path = opt_path || 'index.html';
+  getUrlFromConfig(path, function(url) {
     createTab(url, function(unused_tab) {
       chrome.automation.getTree(function (returnedRootNode) {
         rootNode = returnedRootNode;
@@ -35,10 +36,10 @@
   });
 }
 
-function getUrlFromConfig(callback) {
+function getUrlFromConfig(path, callback) {
   chrome.test.getConfig(function(config) {
     assertTrue('testServer' in config, 'Expected testServer in config');
-    var url = 'http://a.com:PORT/index.html'
+    var url = ('http://a.com:PORT/' + path)
         .replace(/PORT/, config.testServer.port);
     callback(url)
   });
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/queryselector.html b/chrome/test/data/extensions/api_test/automation/tests/tabs/queryselector.html
new file mode 100644
index 0000000..f4e018cc
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/queryselector.html
@@ -0,0 +1,7 @@
+<!--
+ * Copyright 2014 The Chromium Authors. All rights reserved.  Use of this
+ * source code is governed by a BSD-style license that can be found in the
+ * LICENSE file.
+-->
+<script src="common.js"></script>
+<script src="queryselector.js"></script>
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/queryselector.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/queryselector.js
new file mode 100644
index 0000000..6259fad
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/queryselector.js
@@ -0,0 +1,82 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+var allTests = [
+  // Basic query from root node.
+  function testQuerySelector() {
+    var cancelButton = rootNode.lastChild().lastChild();
+    function assertCorrectResult(queryResult) {
+      assertEq(queryResult, cancelButton);
+      chrome.test.succeed();
+    }
+    rootNode.querySelector('body > button:nth-of-type(2)',
+                           assertCorrectResult);
+  },
+
+  function testQuerySelectorNoMatch() {
+    function assertCorrectResult(queryResult) {
+      assertEq(null, queryResult);
+      chrome.test.succeed();
+    }
+    rootNode.querySelector('#nonexistent',
+                           assertCorrectResult);
+  },
+
+  // Demonstrates that a query from a non-root element queries inside that
+  // element.
+  function testQuerySelectorFromMain() {
+    var main = rootNode.children()[1];
+    // paragraph inside "main" element - not the first <p> on the page
+    var p = main.firstChild();
+    function assertCorrectResult(queryResult) {
+      assertEq(queryResult, p);
+      chrome.test.succeed();
+    }
+    main.querySelector('p', assertCorrectResult);
+  },
+
+  // Demonstrates that a query for an element which is ignored for accessibility
+  // returns its nearest ancestor.
+  function testQuerySelectorForSpanInsideButtonReturnsButton() {
+    var okButton = rootNode.lastChild().firstChild();
+    function assertCorrectResult(queryResult) {
+      assertEq(queryResult, okButton);
+      chrome.test.succeed();
+    }
+    rootNode.querySelector('#span-in-button', assertCorrectResult);
+  },
+
+  // Demonstrates that querying from an anonymous node may have unexpected
+  // results.
+  function testQuerySelectorFromAnonymousGroup() {
+    var h1 = rootNode.firstChild().firstChild();
+    var group = rootNode.lastChild();
+    function assertCorrectResult(queryResult) {
+      assertEq(h1, queryResult);
+      chrome.test.succeed();
+    }
+    group.querySelector('h1', assertCorrectResult);
+  },
+
+  function testQuerySelectorFromRemovedNode() {
+    var group = rootNode.firstChild();
+    function assertCorrectResult(queryResult) {
+      assertEq(null, queryResult);
+      var errorMsg =
+          'querySelector sent on node which is no longer in the tree.';
+      assertEq(errorMsg, chrome.extension.lastError.message);
+      assertEq(errorMsg, chrome.runtime.lastError.message);
+
+      chrome.test.succeed();
+    }
+    function afterRemoveChild() {
+      group.querySelector('h1', assertCorrectResult);
+    }
+    chrome.tabs.executeScript(
+        { code: 'document.body.removeChild(document.body.firstElementChild)' },
+        afterRemoveChild);
+  }
+];
+
+setUpAndRunTests(allTests, 'complex.html');
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/tab_id.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/tab_id.js
index 0bf163c..c14659a 100644
--- a/chrome/test/data/extensions/api_test/automation/tests/tabs/tab_id.js
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/tab_id.js
@@ -1,3 +1,4 @@
+
 // Copyright 2014 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
@@ -22,7 +23,7 @@
 
 var allTests = [
   function testGetTabById() {
-    getUrlFromConfig(function(url) {
+    getUrlFromConfig('index.html', function(url) {
       // Keep the NTP as the active tab so that we know we're requesting the
       // tab by ID rather than just getting the active tab still.
       createBackgroundTab(url, function(tab) {
diff --git a/extensions/common/extension_messages.h b/extensions/common/extension_messages.h
index d2aadea6..45160b6 100644
--- a/extensions/common/extension_messages.h
+++ b/extensions/common/extension_messages.h
@@ -258,6 +258,14 @@
   int creation_flags;
 };
 
+struct ExtensionHostMsg_AutomationQuerySelector_Error {
+  enum Value { kNone, kNoMainFrame, kNoDocument, kNodeDestroyed };
+
+  ExtensionHostMsg_AutomationQuerySelector_Error() : value(kNone) {}
+
+  Value value;
+};
+
 namespace IPC {
 
 template <>
@@ -320,6 +328,14 @@
 
 #endif  // EXTENSIONS_COMMON_EXTENSION_MESSAGES_H_
 
+IPC_ENUM_TRAITS_MAX_VALUE(
+    ExtensionHostMsg_AutomationQuerySelector_Error::Value,
+    ExtensionHostMsg_AutomationQuerySelector_Error::kNodeDestroyed)
+
+IPC_STRUCT_TRAITS_BEGIN(ExtensionHostMsg_AutomationQuerySelector_Error)
+IPC_STRUCT_TRAITS_MEMBER(value)
+IPC_STRUCT_TRAITS_END()
+
 // Parameters structure for ExtensionMsg_UpdatePermissions.
 IPC_STRUCT_BEGIN(ExtensionMsg_UpdatePermissions_Params)
   IPC_STRUCT_MEMBER(std::string, extension_id)
@@ -750,3 +766,18 @@
                      std::string /* embedder_url */,
                      std::string /* mime_type */,
                      int /* element_instance_id */)
+
+// Sent when a query selector request is made from the automation API.
+// acc_obj_id is the accessibility tree ID of the starting element.
+IPC_MESSAGE_ROUTED3(ExtensionMsg_AutomationQuerySelector,
+                    int /* request_id */,
+                    int /* acc_obj_id */,
+                    base::string16 /* selector */)
+
+// Result of a query selector request.
+// result_acc_obj_id is the accessibility tree ID of the result element; 0
+// indicates no result.
+IPC_MESSAGE_ROUTED3(ExtensionHostMsg_AutomationQuerySelector_Result,
+                    int /* request_id */,
+                    ExtensionHostMsg_AutomationQuerySelector_Error /* error */,
+                    int /* result_acc_obj_id */)
diff --git a/extensions/extensions.gyp b/extensions/extensions.gyp
index 71e6aebc..bfc275f 100644
--- a/extensions/extensions.gyp
+++ b/extensions/extensions.gyp
@@ -829,6 +829,8 @@
         # Note: sources list duplicated in GN build.
         'renderer/activity_log_converter_strategy.cc',
         'renderer/activity_log_converter_strategy.h',
+        'renderer/api/automation/automation_api_helper.cc',
+        'renderer/api/automation/automation_api_helper.h',
         'renderer/api_activity_logger.cc',
         'renderer/api_activity_logger.h',
         'renderer/api_definitions_natives.cc',
diff --git a/extensions/renderer/BUILD.gn b/extensions/renderer/BUILD.gn
index 703ad5f..740f329 100644
--- a/extensions/renderer/BUILD.gn
+++ b/extensions/renderer/BUILD.gn
@@ -11,6 +11,8 @@
   sources = [
     "activity_log_converter_strategy.cc",
     "activity_log_converter_strategy.h",
+    "api/automation/automation_api_helper.cc",
+    "api/automation/automation_api_helper.h",
     "api_activity_logger.cc",
     "api_activity_logger.h",
     "api_definitions_natives.cc",
diff --git a/extensions/renderer/api/automation/automation_api_helper.cc b/extensions/renderer/api/automation/automation_api_helper.cc
new file mode 100644
index 0000000..ab10682
--- /dev/null
+++ b/extensions/renderer/api/automation/automation_api_helper.cc
@@ -0,0 +1,90 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "extensions/renderer/api/automation/automation_api_helper.h"
+
+#include "content/public/renderer/render_view.h"
+#include "extensions/common/extension_messages.h"
+#include "third_party/WebKit/public/web/WebAXObject.h"
+#include "third_party/WebKit/public/web/WebDocument.h"
+#include "third_party/WebKit/public/web/WebElement.h"
+#include "third_party/WebKit/public/web/WebExceptionCode.h"
+#include "third_party/WebKit/public/web/WebFrame.h"
+#include "third_party/WebKit/public/web/WebNode.h"
+#include "third_party/WebKit/public/web/WebView.h"
+
+namespace extensions {
+
+AutomationApiHelper::AutomationApiHelper(content::RenderView* render_view)
+    : content::RenderViewObserver(render_view) {
+}
+
+AutomationApiHelper::~AutomationApiHelper() {
+}
+
+bool AutomationApiHelper::OnMessageReceived(const IPC::Message& message) {
+  bool handled = true;
+  IPC_BEGIN_MESSAGE_MAP(AutomationApiHelper, message)
+    IPC_MESSAGE_HANDLER(ExtensionMsg_AutomationQuerySelector, OnQuerySelector)
+    IPC_MESSAGE_UNHANDLED(handled = false)
+  IPC_END_MESSAGE_MAP()
+  return handled;
+}
+
+void AutomationApiHelper::OnQuerySelector(int request_id,
+                                          int acc_obj_id,
+                                          const base::string16& selector) {
+  ExtensionHostMsg_AutomationQuerySelector_Error error;
+  if (!render_view() || !render_view()->GetWebView() ||
+      !render_view()->GetWebView()->mainFrame()) {
+    error.value = ExtensionHostMsg_AutomationQuerySelector_Error::kNoMainFrame;
+    Send(new ExtensionHostMsg_AutomationQuerySelector_Result(
+        routing_id(), request_id, error, 0));
+    return;
+  }
+  blink::WebDocument document =
+      render_view()->GetWebView()->mainFrame()->document();
+  if (document.isNull()) {
+      error.value =
+          ExtensionHostMsg_AutomationQuerySelector_Error::kNoDocument;
+    Send(new ExtensionHostMsg_AutomationQuerySelector_Result(
+        routing_id(), request_id, error, 0));
+    return;
+  }
+  blink::WebNode start_node = document;
+  if (acc_obj_id > 0) {
+    blink::WebAXObject start_acc_obj =
+        document.accessibilityObjectFromID(acc_obj_id);
+    if (start_acc_obj.isNull()) {
+      error.value =
+          ExtensionHostMsg_AutomationQuerySelector_Error::kNodeDestroyed;
+      Send(new ExtensionHostMsg_AutomationQuerySelector_Result(
+          routing_id(), request_id, error, 0));
+      return;
+    }
+
+    start_node = start_acc_obj.node();
+    while (start_node.isNull()) {
+      start_acc_obj = start_acc_obj.parentObject();
+      start_node = start_acc_obj.node();
+    }
+  }
+  blink::WebString web_selector(selector);
+  blink::WebExceptionCode ec = 0;
+  blink::WebElement result_element = start_node.querySelector(web_selector, ec);
+  int result_acc_obj_id = 0;
+  if (!ec && !result_element.isNull()) {
+    blink::WebAXObject result_acc_obj = result_element.accessibilityObject();
+    if (!result_acc_obj.isDetached()) {
+      while (result_acc_obj.accessibilityIsIgnored())
+        result_acc_obj = result_acc_obj.parentObject();
+
+      result_acc_obj_id = result_acc_obj.axID();
+    }
+  }
+  Send(new ExtensionHostMsg_AutomationQuerySelector_Result(
+      routing_id(), request_id, error, result_acc_obj_id));
+}
+
+}  // namespace extensions
diff --git a/extensions/renderer/api/automation/automation_api_helper.h b/extensions/renderer/api/automation/automation_api_helper.h
new file mode 100644
index 0000000..0f1c8b0
--- /dev/null
+++ b/extensions/renderer/api/automation/automation_api_helper.h
@@ -0,0 +1,33 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_API_HELPER_H_
+#define EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_API_HELPER_H_
+
+#include "base/strings/string16.h"
+#include "content/public/renderer/render_view_observer.h"
+
+namespace extensions {
+
+// Renderer-side implementation for chrome.automation API (for the few pieces
+// which aren't built in to the existing accessibility system).
+class AutomationApiHelper : public content::RenderViewObserver {
+ public:
+  explicit AutomationApiHelper(content::RenderView* render_view);
+  ~AutomationApiHelper() override;
+
+ private:
+  // RenderViewObserver implementation.
+  bool OnMessageReceived(const IPC::Message& message) override;
+
+  void OnQuerySelector(int acc_obj_id,
+                       int request_id,
+                       const base::string16& selector);
+
+  DISALLOW_COPY_AND_ASSIGN(AutomationApiHelper);
+};
+
+}  // namespace extensions
+
+#endif  // EXTENSIONS_RENDERER_API_AUTOMATION_AUTOMATION_API_HELPER_H_
diff --git a/extensions/renderer/extension_helper.cc b/extensions/renderer/extension_helper.cc
index f12b526e..fef0869 100644
--- a/extensions/renderer/extension_helper.cc
+++ b/extensions/renderer/extension_helper.cc
@@ -9,6 +9,7 @@
 #include "extensions/common/api/messaging/message.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension_messages.h"
+#include "extensions/renderer/api/automation/automation_api_helper.h"
 #include "extensions/renderer/console.h"
 #include "extensions/renderer/dispatcher.h"
 #include "extensions/renderer/messaging_bindings.h"
@@ -122,6 +123,8 @@
       view_type_(VIEW_TYPE_INVALID),
       tab_id_(-1),
       browser_window_id_(-1) {
+  // Lifecycle managed by RenderViewObserver.
+  new AutomationApiHelper(render_view);
 }
 
 ExtensionHelper::~ExtensionHelper() {
diff --git a/extensions/renderer/extension_helper.h b/extensions/renderer/extension_helper.h
index 7e660757..9a04142 100644
--- a/extensions/renderer/extension_helper.h
+++ b/extensions/renderer/extension_helper.h
@@ -7,6 +7,7 @@
 
 #include <vector>
 
+#include "base/memory/scoped_ptr.h"
 #include "content/public/common/console_message_level.h"
 #include "content/public/renderer/render_view_observer.h"
 #include "content/public/renderer/render_view_observer_tracker.h"
@@ -22,7 +23,9 @@
 }
 
 namespace extensions {
+class AutomationApiHelper;
 class Dispatcher;
+
 struct Message;
 
 // RenderView-level plumbing for extension features.