From 74ddb63b51aadad80a1f00e6fa6746eb31d3cc71 Mon Sep 17 00:00:00 2001
From: "jmikhail@google.com"
 <jmikhail@google.com@0039d316-1c4b-4281-b951-d872f2087c98>
Date: Thu, 23 Dec 2010 22:24:14 +0000
Subject: [PATCH] Implemnts the commands in webdriver to preform searching of
 elements on a page.   /session/:sessionId/timeouts/implicit_wait  
 /session/:sessionId/element   /session/:sessionId/elements  
 /session/:sessionId/element/:id/element  
 /session/:sessionId/element/:id/elements

BUG=none
TEST=webdriver_remote_tests.py

Review URL: http://codereview.chromium.org/3643002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@70107 0039d316-1c4b-4281-b951-d872f2087c98
---
 chrome/chrome_tests.gypi                      |   4 +
 .../commands/find_element_commands.cc         | 111 ++++++++++++++++++
 .../commands/find_element_commands.h          |  74 ++++++++++++
 .../commands/implicit_wait_command.cc         |  45 +++++++
 .../commands/implicit_wait_command.h          |  42 +++++++
 chrome/test/webdriver/server.cc               |  17 ++-
 .../test/webdriver/webdriver_remote_tests.py  |  74 +++++++++---
 .../py/selenium/remote/webdriver/WebDriver.py |  11 ++
 .../py/selenium/remote/webdriver/command.py   |   1 +
 .../remote/webdriver/remote_connection.py     |   1 +
 10 files changed, 362 insertions(+), 18 deletions(-)
 create mode 100644 chrome/test/webdriver/commands/find_element_commands.cc
 create mode 100644 chrome/test/webdriver/commands/find_element_commands.h
 create mode 100644 chrome/test/webdriver/commands/implicit_wait_command.cc
 create mode 100644 chrome/test/webdriver/commands/implicit_wait_command.h

diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index 60a768c3eeec5..23dde1c1a8e78 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -685,6 +685,10 @@
         'test/webdriver/commands/command.cc',
         'test/webdriver/commands/create_session.h',
         'test/webdriver/commands/create_session.cc',
+        'test/webdriver/commands/find_element_commands.h',
+        'test/webdriver/commands/find_element_commands.cc',
+        'test/webdriver/commands/implicit_wait_command.h',
+        'test/webdriver/commands/implicit_wait_command.cc',
         'test/webdriver/commands/execute_command.h',
         'test/webdriver/commands/execute_command.cc',
         'test/webdriver/commands/navigate_commands.h',
diff --git a/chrome/test/webdriver/commands/find_element_commands.cc b/chrome/test/webdriver/commands/find_element_commands.cc
new file mode 100644
index 0000000000000..acda1393ce887
--- /dev/null
+++ b/chrome/test/webdriver/commands/find_element_commands.cc
@@ -0,0 +1,111 @@
+// Copyright (c) 2010 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 "chrome/test/webdriver/commands/find_element_commands.h"
+
+#include <sstream>
+#include <string>
+
+#include "base/string_number_conversions.h"
+#include "base/utf_string_conversions.h"
+#include "base/values.h"
+#include "third_party/webdriver/atoms.h"
+#include "chrome/test/webdriver/error_codes.h"
+#include "chrome/test/webdriver/utility_functions.h"
+
+namespace webdriver {
+
+bool FindElementCommand::Init(Response* const response) {
+  if (!WebDriverCommand::Init(response)) {
+    SET_WEBDRIVER_ERROR(response, "Failure on Init for find element",
+                        kInternalServerError);
+    return false;
+  }
+
+  if (!GetStringASCIIParameter("using", &use_) ||
+      !GetStringASCIIParameter("value", &value_)) {
+    SET_WEBDRIVER_ERROR(response,
+        "Request is missing required 'using' and/or 'value' data", kBadRequest);
+    return false;
+  }
+
+  // TODO(jmikhail): The findElement(s) atom should handle this conversion.
+  if ("class name" == use_) {
+    use_ = "className";
+  } else if ("link text" == use_) {
+    use_ = "linkText";
+  } else if ("partial link text" == use_) {
+    use_ = "partialLinkText";
+  } else if ("tag name" == use_) {
+    use_ = "tagName";
+  }
+
+  // Searching under a custom root if the URL pattern is
+  // "/session/$session/element/$id/element(s)"
+  root_element_id_ = GetPathVariable(4);
+
+  return true;
+}
+
+void FindElementCommand::ExecutePost(Response* const response) {
+  scoped_ptr<ListValue> args(new ListValue());
+  DictionaryValue* locator = new DictionaryValue();
+  ErrorCode error;
+  std::wstring jscript;
+  Value* result = NULL;
+  bool done = false;
+
+  // Set the command we are using to locate the value beging searched for.
+  locator->SetString(use_, value_);
+  args->Append(locator);
+  args->Append(root_element_id_.size() == 0 ? Value::CreateNullValue() :
+      WebDriverCommand::GetElementIdAsDictionaryValue(root_element_id_));
+  if (find_one_element_) {
+    jscript = build_atom(FIND_ELEMENT, sizeof FIND_ELEMENT);
+    jscript.append(L"var result = findElement(arguments[0], arguments[1]);")
+           .append(L"if (!result) {")
+           .append(L"var e = Error('Unable to locate element');")
+           .append(L"e.code = ")
+           .append(UTF8ToWide(base::IntToString(kNoSuchElement)))
+           .append(L";throw e;")
+           .append(L"} else { return result; }");
+  } else {
+    jscript = build_atom(FIND_ELEMENTS, sizeof FIND_ELEMENT);
+    jscript.append(L"return findElements(arguments[0], arguments[1]);");
+  }
+
+  // The element search needs to loop until at least one element is found or the
+  // session's implicit wait timeout expires, whichever occurs first.
+  base::Time start_time = base::Time::Now();
+
+  while (!done) {
+    if (result) {
+      delete result;
+      result = NULL;
+    }
+
+    error = session_->ExecuteScript(jscript, args.get(), &result);
+    if (error == kSuccess) {
+      // If searching for many elements, make sure we found at least one before
+      // stopping.
+      done = find_one_element_ ||
+             (result->GetType() == Value::TYPE_LIST &&
+             static_cast<ListValue*>(result)->GetSize() > 0);
+    } else if (error != kNoSuchElement) {
+      SET_WEBDRIVER_ERROR(response, "Internal error in find_element atom",
+                          kInternalServerError);
+      return;
+    }
+
+    int64 elapsed_time = (base::Time::Now() - start_time).InMilliseconds();
+    done = done || elapsed_time > session_->implicit_wait();
+    PlatformThread::Sleep(50);  // Prevent a busy loop that eats the cpu.
+  }
+
+  response->set_value(result);
+  response->set_status(error);
+}
+
+}  // namespace webdriver
+
diff --git a/chrome/test/webdriver/commands/find_element_commands.h b/chrome/test/webdriver/commands/find_element_commands.h
new file mode 100644
index 0000000000000..cc45d107d6ee2
--- /dev/null
+++ b/chrome/test/webdriver/commands/find_element_commands.h
@@ -0,0 +1,74 @@
+// Copyright (c) 2010 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 CHROME_TEST_WEBDRIVER_COMMANDS_FIND_ELEMENT_COMMANDS_H_
+#define CHROME_TEST_WEBDRIVER_COMMANDS_FIND_ELEMENT_COMMANDS_H_
+
+#include <string>
+#include <vector>
+
+#include "chrome/test/webdriver/commands/command.h"
+#include "chrome/test/webdriver/commands/webdriver_command.h"
+
+namespace webdriver {
+
+// Base class for searching a page, this class can find either a single
+// webelement or return multiple matches.
+class FindElementCommand : public WebDriverCommand {
+ public:
+   FindElementCommand(const std::vector<std::string>& path_segments,
+                      const DictionaryValue* const parameters,
+                      const bool find_one_element)
+      : WebDriverCommand(path_segments, parameters),
+        find_one_element_(find_one_element) {}
+  virtual ~FindElementCommand() {}
+
+  virtual bool Init(Response* const response);
+
+  virtual bool DoesPost() { return true; }
+  virtual void ExecutePost(Response* const response);
+
+ private:
+  virtual bool RequiresValidTab() { return false; }
+  const bool find_one_element_;
+  std::string root_element_id_;
+  std::string use_;
+  std::string value_;
+
+  DISALLOW_COPY_AND_ASSIGN(FindElementCommand);
+};
+
+// Search for an element on the page, starting from the document root.
+// The located element will be returned as a WebElement JSON object. See:
+// http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/element
+class FindOneElementCommand : public FindElementCommand {
+ public:
+  FindOneElementCommand(const std::vector<std::string>& path_segments,
+                        const DictionaryValue* const parameters)
+      : FindElementCommand(path_segments, parameters, true) {}
+  virtual ~FindOneElementCommand() {}
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(FindOneElementCommand);
+};
+
+// Search for multiple elements on the page, starting from the identified
+// element. The located elements will be returned as a WebElement JSON
+// objects. See:
+// http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/elements
+class FindManyElementsCommand : public FindElementCommand {
+ public:
+  FindManyElementsCommand(const std::vector<std::string>& path_segments,
+                          const DictionaryValue* const parameters)
+      : FindElementCommand(path_segments, parameters, false) {}
+  virtual ~FindManyElementsCommand() {}
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(FindManyElementsCommand);
+};
+
+}  // namespace webdriver
+
+#endif  // CHROME_TEST_WEBDRIVER_COMMANDS_FIND_ELEMENT_COMMANDS_H_
+
diff --git a/chrome/test/webdriver/commands/implicit_wait_command.cc b/chrome/test/webdriver/commands/implicit_wait_command.cc
new file mode 100644
index 0000000000000..b15fd41ef05b9
--- /dev/null
+++ b/chrome/test/webdriver/commands/implicit_wait_command.cc
@@ -0,0 +1,45 @@
+// Copyright (c) 2010 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 <string>
+
+#include "base/utf_string_conversions.h"
+#include "chrome/test/webdriver/commands/implicit_wait_command.h"
+
+namespace webdriver {
+
+bool ImplicitWaitCommand::Init(Response* const response) {
+  if (!(WebDriverCommand::Init(response))) {
+    SET_WEBDRIVER_ERROR(response, "Failure on Init for find element",
+                        kInternalServerError);
+    return false;
+  }
+
+  // Record the requested wait time.
+  if (!GetIntegerParameter("ms", &ms_to_wait_)) {
+    SET_WEBDRIVER_ERROR(response, "Request missing ms parameter",
+                        kBadRequest);
+    return false;
+  }
+
+  return true;
+}
+
+void ImplicitWaitCommand::ExecutePost(Response* const response) {
+  // Validate the wait time before setting it to the session.
+  if (ms_to_wait_ < 0) {
+    SET_WEBDRIVER_ERROR(response, "Wait must be non-negative",
+                        kBadRequest);
+    return;
+  }
+
+  session_->set_implicit_wait(ms_to_wait_);
+  LOG(INFO) << "Implicit wait set to: " << ms_to_wait_ << " ms";
+
+  response->set_value(new StringValue("success"));
+  response->set_status(kSuccess);
+}
+
+}  // namespace webdriver
+
diff --git a/chrome/test/webdriver/commands/implicit_wait_command.h b/chrome/test/webdriver/commands/implicit_wait_command.h
new file mode 100644
index 0000000000000..0475c4b34f8d3
--- /dev/null
+++ b/chrome/test/webdriver/commands/implicit_wait_command.h
@@ -0,0 +1,42 @@
+// Copyright (c) 2010 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 CHROME_TEST_WEBDRIVER_COMMANDS_IMPLICIT_WAIT_COMMAND_H_
+#define CHROME_TEST_WEBDRIVER_COMMANDS_IMPLICIT_WAIT_COMMAND_H_
+
+#include <string>
+#include <vector>
+
+#include "chrome/test/webdriver/commands/webdriver_command.h"
+
+namespace webdriver {
+
+// Set the amount of time the driver should wait when searching for elements.
+// If this command is never sent, the driver will default to an implicit wait
+// of 0 ms. Until the webelement commands are checked in we do no use this
+// variable.  For more information see:
+// http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/timeouts/implicit_wait
+class ImplicitWaitCommand : public WebDriverCommand {
+ public:
+  inline ImplicitWaitCommand(const std::vector<std::string>& path_segments,
+                             const DictionaryValue* const parameters)
+      : WebDriverCommand(path_segments, parameters), ms_to_wait_(0) {}
+  virtual ~ImplicitWaitCommand() {}
+
+  virtual bool Init(Response* const response);
+
+  virtual bool DoesPost() { return true; }
+  virtual void ExecutePost(Response* const response);
+
+ private:
+  int ms_to_wait_;
+  virtual bool RequiresValidTab() { return true; }
+
+  DISALLOW_COPY_AND_ASSIGN(ImplicitWaitCommand);
+};
+
+}  // namespace webdriver
+
+#endif  // CHROME_TEST_WEBDRIVER_COMMANDS_IMPLICIT_WAIT_COMMAND_H_
+
diff --git a/chrome/test/webdriver/server.cc b/chrome/test/webdriver/server.cc
index 8465f14ad8c57..4760928cae84c 100644
--- a/chrome/test/webdriver/server.cc
+++ b/chrome/test/webdriver/server.cc
@@ -26,6 +26,8 @@
 #include "chrome/test/webdriver/utility_functions.h"
 #include "chrome/test/webdriver/commands/create_session.h"
 #include "chrome/test/webdriver/commands/execute_command.h"
+#include "chrome/test/webdriver/commands/find_element_commands.h"
+#include "chrome/test/webdriver/commands/implicit_wait_command.h"
 #include "chrome/test/webdriver/commands/navigate_commands.h"
 #include "chrome/test/webdriver/commands/session_with_id.h"
 #include "chrome/test/webdriver/commands/source_command.h"
@@ -68,6 +70,13 @@ void InitCallbacks(struct mg_context* ctx) {
   SetCallback<URLCommand>(ctx,      "/session/*/url");
   SetCallback<SpeedCommand>(ctx,    "/session/*/speed");
 
+  // WebElement commands
+  SetCallback<ImplicitWaitCommand>(ctx, "/session/*/timeouts/implicit_wait");
+  SetCallback<FindOneElementCommand>(ctx,   "/session/*/element");
+  SetCallback<FindManyElementsCommand>(ctx, "/session/*/elements");
+  SetCallback<FindOneElementCommand>(ctx,   "/session/*/element/*/element");
+  SetCallback<FindManyElementsCommand>(ctx, "/session/*/elements/*/elements");
+
   // Since the /session/* is a wild card that would match the above URIs, this
   // line MUST be the last registered URI with the server.
   SetCallback<SessionWithID>(ctx, "/session/*");
@@ -80,7 +89,7 @@ void InitCallbacks(struct mg_context* ctx) {
 int main(int argc, char *argv[]) {
   struct mg_context *ctx;
   base::AtExitManager exit;
-  std::string port = "8080";
+  std::string port = "9515";
 #ifdef OS_POSIX
   CommandLine cmd_line = CommandLine(argc, argv);
 #elif OS_WIN
@@ -110,14 +119,14 @@ int main(int argc, char *argv[]) {
   session->SetIPAddress(port);
 
   // Initialize SHTTPD context.
-  // Listen on port 8080 or port specified on command line.
-  // TODO(jmikhail) Maybe add port 8081 as a secure connection.
+  // Listen on port 9515 or port specified on command line.
+  // TODO(jmikhail) Maybe add port 9516 as a secure connection.
   ctx = mg_start();
   mg_set_option(ctx, "ports", port.c_str());
 
   webdriver::InitCallbacks(ctx);
 
-  std::cout << "Starting server" << std::endl;
+  std::cout << "Starting server on port: " << port << std::endl;
   // The default behavior is to run this service forever.
   while (true)
     PlatformThread::Sleep(3600);
diff --git a/chrome/test/webdriver/webdriver_remote_tests.py b/chrome/test/webdriver/webdriver_remote_tests.py
index 43371c7ad0219..1ff2b4461b7d0 100644
--- a/chrome/test/webdriver/webdriver_remote_tests.py
+++ b/chrome/test/webdriver/webdriver_remote_tests.py
@@ -18,8 +18,10 @@ import time
 import types
 import unittest
 import urllib2
-from selenium.remote.webdriver import WebDriver
 from selenium.common.exceptions import ErrorInResponseException
+from selenium.common.exceptions import NoSuchElementException
+from selenium.remote.webdriver import WebDriver
+from selenium.remote.webdriver.webelement import WebElement
 from urlparse import urlparse
 
 
@@ -42,6 +44,19 @@ if not WEBDRIVER_SERVER_URL:
   WEBDRIVER_SERVER_URL = 'http://localhost:%d' % WEBDRIVER_PORT
 
 class RemoteWebDriverTest(unittest.TestCase):
+  SEARCH = "http://www.google.com/webhp?hl=en"
+  NEWS = "http://www.google.com/news?hl=en"
+
+  """Verifies that navigation to a specific page is correct by asserting on the
+  the reported URL.  The function will not work with pages that redirect."""
+  def navigate(self, url):
+    self.driver.get(url)
+    self.assertURL(url)
+
+  def assertURL(self, url):
+    u = self.driver.get_current_url()
+    self.assertEqual(u, url)
+
   """A new instance of chrome driver is started for every test case"""
   def setUp(self):
     global WEBDRIVER_SERVER_URL
@@ -81,6 +96,50 @@ class RemoteWebDriverTest(unittest.TestCase):
     search = string.lower(search)
     self.assertNotEqual(-1, string.find(text, search))
 
+class TestFindElement(RemoteWebDriverTest):
+  def testFindByName(self):
+    navigate(SEARCH)
+    # Find the Google search button.
+    q = self.driver.find_element_by_name("q")
+    self.assertTrue(isinstance(q, WebElement))
+    # Trying looking for an element not on the page.
+    self.assertRaises(NoSuchElementException,
+                      self.driver.find_elment_by_name, "q2")
+    # Try to find the Google search button using the multiple find method.
+    q = self.driver.find_elements_by_name("q")
+    self.assertTrue(isinstance(q, list))
+    self.assertTrue(len(q), 1)
+    self.assertTrue(isinstance(q[0], WebElement))
+    # Try finding something not on page, with multiple find an empty array
+    # should return and no exception thrown.
+    q = self.driver.find_elements_by_name("q2")
+    assertTrue(q == [])
+    # Find a hidden element on the page
+    q = self.driver.find_element_by_name("oq")
+    self.assertTrue(isinstance(q, WebElement))
+
+ def testFindElementById(self):
+    navigate(SEARCH)
+    # Find the padding for the logo near the search bar.
+    elem = self.driver.find_element_by_id("logocont")
+    self.assertTrue(isinstance(elem, WebElement))
+    # Look for an ID not there.
+    self.assertRaises(NoSuchElementException,
+                      self.driver.find_element_by_id, "logocont")
+
+  def testFindElementById0WithTimeout(self):
+    self.set_implicit_wait(0)
+    navigate(SEARCH)
+    # Find the padding for the logo near the search bar.
+    elem = self.driver.find_element_by_id("logocont")
+    self.assertTrue(isinstance(elem, WebElement))
+    self.set_implicit_wait(5000)
+    navigate(SEARCH)
+    # Look for an ID not there.
+    self.assertRaises(NoSuchElementException,
+                      self.driver.find_element_by_id, "logocont")
+
+
 class TestJavaScriptExecution(RemoteWebDriverTest):
   """ Test the execute javascript ability of the remote driver"""
   def testNoModification(self):
@@ -120,19 +179,6 @@ class TestJavaScriptExecution(RemoteWebDriverTest):
 
 
 class TestNavigation(RemoteWebDriverTest):
-  SEARCH = "http://www.google.com/webhp?hl=en"
-  NEWS = "http://www.google.com/news?hl=en"
-
-  """Verifies that navigation to a specific page is correct by asserting on the
-  the reported URL.  The function will not work with pages that redirect."""
-  def navigate(self, url):
-    self.driver.get(url)
-    self.assertURL(url)
-
-  def assertURL(self, url):
-    u = self.driver.get_current_url()
-    self.assertEqual(u, url)
-
   def testNavigateToURL(self):
     # No redirects are allowed on the google home page.
     self.navigate(self.SEARCH)
diff --git a/third_party/webdriver/py/selenium/remote/webdriver/WebDriver.py b/third_party/webdriver/py/selenium/remote/webdriver/WebDriver.py
index b7246a00cada1..35bff2ed2374f 100644
--- a/third_party/webdriver/py/selenium/remote/webdriver/WebDriver.py
+++ b/third_party/webdriver/py/selenium/remote/webdriver/WebDriver.py
@@ -173,6 +173,17 @@ class WebDriver(object):
         resp = self._execute(Command.GET_TITLE)
         return resp['value']
 
+    def set_implicit_wait(self, wait):
+        """Set the amount of time the driver should wait when searching for
+        elements. When searching for a single element, the driver should poll
+        the page until an element is found or the timeout expires, whichever
+        occurs first. When searching for multiple elements, the driver should
+        poll the page until at least one element is found or the timeout
+        expires, at which point it should return an empty list. If this
+        command is never sent, the driver should default to an implicit wait
+        of 0ms."""
+        self._execute(Command.IMPLICIT_WAIT, {'ms': wait})
+
     def find_element_by_id(self, id_):
         """Finds element by id."""
         return self._find_element_by("id", id_)
diff --git a/third_party/webdriver/py/selenium/remote/webdriver/command.py b/third_party/webdriver/py/selenium/remote/webdriver/command.py
index 3eae3b1928664..b094c5a7d5a20 100644
--- a/third_party/webdriver/py/selenium/remote/webdriver/command.py
+++ b/third_party/webdriver/py/selenium/remote/webdriver/command.py
@@ -76,4 +76,5 @@ class Command(object):
     GET_ELEMENT_ATTRIBUTE = "getElementAttribute"
     GET_ELEMENT_VALUE_OF_CSS_PROPERTY = "getElementValueOfCssProperty"
     ELEMENT_EQUALS = "elementEquals"
+    IMPLICIT_WAIT = "setImplicitWait"
     SCREENSHOT = "screenshot"
diff --git a/third_party/webdriver/py/selenium/remote/webdriver/remote_connection.py b/third_party/webdriver/py/selenium/remote/webdriver/remote_connection.py
index 6c23684b0bb65..5556fc9db41f6 100644
--- a/third_party/webdriver/py/selenium/remote/webdriver/remote_connection.py
+++ b/third_party/webdriver/py/selenium/remote/webdriver/remote_connection.py
@@ -128,6 +128,7 @@ class RemoteConnection(object):
             Command.GET_CURRENT_URL: ('GET', '/session/$sessionId/url'),
             Command.GET_TITLE: ('GET', '/session/$sessionId/title'),
             Command.GET_PAGE_SOURCE: ('GET', '/session/$sessionId/source'),
+            Command.IMPLICIT_WAIT: ('POST', '/session/$sessionId/timeouts/implicit_wait'),
             Command.SCREENSHOT: ('GET', '/session/$sessionId/screenshot'),
             Command.SET_BROWSER_VISIBLE:
                 ('POST', '/session/$sessionId/visible'),