1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """The testing Environment class."""
12 from xml.etree import ElementTree
13 from xml.sax.saxutils import escape
15 sys.path.insert(0, '../../../../third_party/webdriver/pylib/')
17 from selenium import webdriver
18 from selenium.common.exceptions import NoSuchElementException
19 from selenium.common.exceptions import WebDriverException
20 from selenium.webdriver.chrome.options import Options
23 # Message strings to look for in chrome://password-manager-internals
24 MESSAGE_ASK = "Message: Decision: ASK the user"
25 MESSAGE_SAVE = "Message: Decision: SAVE the password"
29 """Stores the information related to a test result. """
30 def __init__(self, name, test_type, successful, message):
31 """Creates a new TestResult.
34 name: The tested website name.
35 test_type: The test type.
36 successful: Whether or not the test was successful.
37 message: The error message of the test.
40 self.test_type = test_type
41 self.successful = successful
42 self.message = message
46 """Sets up the testing Environment. """
48 def __init__(self, chrome_path, chromedriver_path, profile_path,
49 passwords_path, enable_automatic_password_saving,
50 numeric_level=None, log_to_console=False, log_file=""):
51 """Creates a new testing Environment.
54 chrome_path: The chrome binary file.
55 chromedriver_path: The chromedriver binary file.
56 profile_path: The chrome testing profile folder.
57 passwords_path: The usernames and passwords file.
58 enable_automatic_password_saving: If True, the passwords are going to be
59 saved without showing the prompt.
60 numeric_level: The log verbosity.
61 log_to_console: If True, the debug logs will be shown on the console.
62 log_file: The file where to store the log. If it's empty, the log will
66 Exception: An exception is raised if |profile_path| folder could not be
69 # Setting up the login.
70 if numeric_level is not None:
72 # Set up logging to file.
73 logging.basicConfig(level=numeric_level,
78 console = logging.StreamHandler()
79 console.setLevel(numeric_level)
80 # Add the handler to the root logger.
81 logging.getLogger('').addHandler(console)
84 logging.basicConfig(level=numeric_level)
86 # Cleaning the chrome testing profile folder.
88 shutil.rmtree(profile_path)
90 # The tests execution can continue, but this make them less stable.
91 logging.error("Error: Could not wipe the chrome profile directory (%s). \
92 This affects the stability of the tests. Continuing to run tests."
94 # If |chrome_path| is not defined, this means that we are in the dashboard
95 # website, and we just need to get the list of all websites. In this case,
96 # we don't need to initilize the webdriver.
99 self.enable_automatic_password_saving = enable_automatic_password_saving
100 if enable_automatic_password_saving:
101 options.add_argument("enable-automatic-password-saving")
103 options.binary_location = chrome_path
104 # Chrome testing profile path.
105 options.add_argument("user-data-dir=%s" % profile_path)
107 # The webdriver. It's possible to choose the port the service is going to
108 # run on. If it's left to 0, a free port will be found.
109 self.driver = webdriver.Chrome(chromedriver_path, 0, options)
110 # The password internals window.
111 self.internals_window = self.driver.current_window_handle
113 # An xml tree filled with logins and passwords.
114 self.passwords_tree = ElementTree.parse(passwords_path).getroot()
116 raise Exception("Error: |passwords_path| needs to be provided if"
117 "|chrome_path| is provided, otherwise the tests could not be run")
118 # Password internals page.
119 self.internals_page = "chrome://password-manager-internals/"
120 # The Website window.
121 self.website_window = None
122 # The WebsiteTests list.
123 self.websitetests = []
124 # The enabled WebsiteTests list.
125 self.working_tests = []
126 # The disabled WebsiteTests list.
127 self.disabled_tests = []
128 # Map messages to the number of their appearance in the log.
129 self.message_count = dict()
130 self.message_count[MESSAGE_ASK] = 0
131 self.message_count[MESSAGE_SAVE] = 0
132 # The tests needs two tabs to work. A new tab is opened with the first
133 # GoTo. This is why we store here whether or not it's the first time to
135 self.first_go_to = True
136 # List of all tests results.
137 self.tests_results = []
139 def AddWebsiteTest(self, websitetest, disabled=False):
140 """Adds a WebsiteTest to the testing Environment.
143 websitetest: The WebsiteTest instance to be added.
144 disabled: Whether test is disabled.
146 websitetest.environment = self
147 if hasattr(self, "driver"):
148 websitetest.driver = self.driver
149 if hasattr(self, "passwords_tree") and self.passwords_tree is not None:
150 if not websitetest.username:
152 self.passwords_tree.find(
153 ".//*[@name='%s']/username" % websitetest.name))
154 if username_tag.text:
155 websitetest.username = username_tag.text
156 if not websitetest.password:
158 self.passwords_tree.find(
159 ".//*[@name='%s']/password" % websitetest.name))
160 if password_tag.text:
161 websitetest.password = password_tag.text
162 self.websitetests.append(websitetest)
164 self.disabled_tests.append(websitetest.name)
166 self.working_tests.append(websitetest.name)
168 def ClearCache(self, clear_passwords):
169 """Clear the browser cookies. If |clear_passwords| is true, clear all the
173 clear_passwords : Clear all the passwords if the bool value is true.
175 logging.info("\nClearCache\n")
176 self.driver.get("chrome://settings/clearBrowserData")
177 self.driver.switch_to_frame("settings")
179 "if (!document.querySelector('#delete-cookies-checkbox').checked)"
180 " document.querySelector('#delete-cookies-checkbox').click();"
186 "if (%sdocument.querySelector('#delete-passwords-checkbox').checked)"
187 " document.querySelector('#delete-passwords-checkbox').click();"
189 script += "document.querySelector('#clear-browser-data-commit').click();"
190 self.driver.execute_script(script)
193 def OpenTabAndGoToInternals(self, url):
194 """If there is no |self.website_window|, opens a new tab and navigates to
195 |url| in the new tab. Navigates to the passwords internals page in the
196 first tab. Raises an exception otherwise.
199 url: Url to go to in the new tab.
202 Exception: An exception is raised if |self.website_window| already
205 if self.website_window:
206 raise Exception("Error: The window was already opened.")
208 self.driver.get("chrome://newtab")
209 # There is no straightforward way to open a new tab with chromedriver.
210 # One work-around is to go to a website, insert a link that is going
211 # to be opened in a new tab, click on it.
212 a = self.driver.execute_script(
213 "var a = document.createElement('a');"
214 "a.target = '_blank';"
215 "a.href = arguments[0];"
217 "document.body.appendChild(a);"
224 self.website_window = self.driver.window_handles[-1]
225 self.driver.get(self.internals_page)
226 self.driver.switch_to_window(self.website_window)
228 def SwitchToInternals(self):
229 """Switches from the Website window to internals tab."""
230 self.driver.switch_to_window(self.internals_window)
232 def SwitchFromInternals(self):
233 """Switches from internals tab to the Website window."""
234 self.driver.switch_to_window(self.website_window)
236 def _DidMessageAppearUntilTimeout(self, log_message, timeout):
237 """Checks whether the save password prompt is shown.
240 log_message: Log message to look for in the password internals.
241 timeout: There is some delay between the login and the password
242 internals update. The method checks periodically during the first
243 |timeout| seconds if the internals page reports the prompt being
244 shown. If the prompt is not reported shown within the first
245 |timeout| seconds, it is considered not shown at all.
248 True if the save password prompt is shown.
251 log = self.driver.find_element_by_css_selector("#log-entries")
252 count = log.text.count(log_message)
254 if count > self.message_count[log_message]:
255 self.message_count[log_message] = count
259 return self._DidMessageAppearUntilTimeout(log_message, timeout - 1)
263 def CheckForNewMessage(self, log_message, message_should_show_up,
264 error_message, timeout=3):
265 """Detects whether the save password prompt is shown.
268 log_message: Log message to look for in the password internals. The
269 only valid values are the constants MESSAGE_* defined at the
270 beginning of this file.
271 message_should_show_up: Whether or not the message is expected to be
273 error_message: Error message for the exception.
274 timeout: There is some delay between the login and the password
275 internals update. The method checks periodically during the first
276 |timeout| seconds if the internals page reports the prompt being
277 shown. If the prompt is not reported shown within the first
278 |timeout| seconds, it is considered not shown at all.
281 Exception: An exception is raised in case the result does not match the
284 if (self._DidMessageAppearUntilTimeout(log_message, timeout) !=
285 message_should_show_up):
286 raise Exception(error_message)
288 def AllTests(self, prompt_test):
289 """Runs the tests on all the WebsiteTests.
292 prompt_test: If True, tests caring about showing the save-password
293 prompt are going to be run, otherwise tests which don't care about
294 the prompt are going to be run.
297 Exception: An exception is raised if the tests fail.
300 self.PromptTestList(self.websitetests)
302 self.TestList(self.websitetests)
304 def DisabledTests(self, prompt_test):
305 """Runs the tests on all the disabled WebsiteTests.
308 prompt_test: If True, tests caring about showing the save-password
309 prompt are going to be run, otherwise tests which don't care about
310 the prompt are going to be executed.
313 Exception: An exception is raised if the tests fail.
315 self.Test(self.disabled_tests, prompt_test)
317 def WorkingTests(self, prompt_test):
318 """Runs the tests on all the enabled WebsiteTests.
321 prompt_test: If True, tests caring about showing the save-password
322 prompt are going to be run, otherwise tests which don't care about
323 the prompt are going to be executed.
326 Exception: An exception is raised if the tests fail.
328 self.Test(self.working_tests, prompt_test)
330 def Test(self, tests, prompt_test):
331 """Runs the tests on websites named in |tests|.
334 tests: A list of the names of the WebsiteTests that are going to be
336 prompt_test: If True, tests caring about showing the save-password
337 prompt are going to be run, otherwise tests which don't care about
338 the prompt are going to be executed.
341 Exception: An exception is raised if the tests fail.
344 for websitetest in self.websitetests:
345 if websitetest.name in tests:
346 websitetests.append(websitetest)
349 self.PromptTestList(websitetests)
351 self.TestList(websitetests)
353 def TestList(self, websitetests):
354 """Runs the tests on the websites in |websitetests|.
357 websitetests: A list of WebsiteTests that are going to be tested.
360 Exception: An exception is raised if the tests fail.
362 self.ClearCache(True)
364 for websitetest in websitetests:
368 websitetest.was_run = True
369 websitetest.WrongLoginTest()
370 websitetest.SuccessfulLoginTest()
371 self.ClearCache(False)
372 websitetest.SuccessfulLoginWithAutofilledPasswordTest()
373 self.ClearCache(True)
374 websitetest.SuccessfulLoginTest()
375 self.ClearCache(True)
378 error = traceback.format_exc()
379 self.tests_results.append(TestResult(websitetest.name, "normal",
380 successful, escape(error)))
383 def PromptTestList(self, websitetests):
384 """Runs the prompt tests on the websites in |websitetests|.
387 websitetests: A list of WebsiteTests that are going to be tested.
390 Exception: An exception is raised if the tests fail.
392 self.ClearCache(True)
394 for websitetest in websitetests:
398 websitetest.was_run = True
399 websitetest.PromptTest()
402 error = traceback.format_exc()
403 self.tests_results.append(TestResult(websitetest.name, "prompt",
404 successful, escape(error)))
407 """Closes the tests."""
408 # Close the webdriver.