| OLD | NEW |
| (Empty) |
| 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. | |
| 4 | |
| 5 """The testing Environment class.""" | |
| 6 | |
| 7 import logging | |
| 8 import shutil | |
| 9 import sys | |
| 10 import time | |
| 11 import traceback | |
| 12 from xml.etree import ElementTree | |
| 13 from xml.sax.saxutils import escape | |
| 14 | |
| 15 sys.path.insert(0, '../../../../third_party/webdriver/pylib/') | |
| 16 | |
| 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 | |
| 21 | |
| 22 | |
| 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" | |
| 26 | |
| 27 | |
| 28 class TestResult: | |
| 29 """Stores the information related to a test result. """ | |
| 30 def __init__(self, name, test_type, successful, message): | |
| 31 """Creates a new TestResult. | |
| 32 | |
| 33 Args: | |
| 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. | |
| 38 """ | |
| 39 self.name = name | |
| 40 self.test_type = test_type | |
| 41 self.successful = successful | |
| 42 self.message = message | |
| 43 | |
| 44 | |
| 45 class Environment: | |
| 46 """Sets up the testing Environment. """ | |
| 47 | |
| 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. | |
| 52 | |
| 53 Args: | |
| 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 | |
| 63 not be stored. | |
| 64 | |
| 65 Raises: | |
| 66 Exception: An exception is raised if |profile_path| folder could not be | |
| 67 removed. | |
| 68 """ | |
| 69 # Setting up the login. | |
| 70 if numeric_level is not None: | |
| 71 if log_file: | |
| 72 # Set up logging to file. | |
| 73 logging.basicConfig(level=numeric_level, | |
| 74 filename=log_file, | |
| 75 filemode='w') | |
| 76 | |
| 77 if log_to_console: | |
| 78 console = logging.StreamHandler() | |
| 79 console.setLevel(numeric_level) | |
| 80 # Add the handler to the root logger. | |
| 81 logging.getLogger('').addHandler(console) | |
| 82 | |
| 83 elif log_to_console: | |
| 84 logging.basicConfig(level=numeric_level) | |
| 85 | |
| 86 # Cleaning the chrome testing profile folder. | |
| 87 try: | |
| 88 shutil.rmtree(profile_path) | |
| 89 except Exception, e: | |
| 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." | |
| 93 % e) | |
| 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. | |
| 97 if chrome_path: | |
| 98 options = Options() | |
| 99 self.enable_automatic_password_saving = enable_automatic_password_saving | |
| 100 if enable_automatic_password_saving: | |
| 101 options.add_argument("enable-automatic-password-saving") | |
| 102 # Chrome path. | |
| 103 options.binary_location = chrome_path | |
| 104 # Chrome testing profile path. | |
| 105 options.add_argument("user-data-dir=%s" % profile_path) | |
| 106 | |
| 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 | |
| 112 if passwords_path: | |
| 113 # An xml tree filled with logins and passwords. | |
| 114 self.passwords_tree = ElementTree.parse(passwords_path).getroot() | |
| 115 else: | |
| 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 | |
| 134 # execute GoTo. | |
| 135 self.first_go_to = True | |
| 136 # List of all tests results. | |
| 137 self.tests_results = [] | |
| 138 | |
| 139 def AddWebsiteTest(self, websitetest, disabled=False): | |
| 140 """Adds a WebsiteTest to the testing Environment. | |
| 141 | |
| 142 Args: | |
| 143 websitetest: The WebsiteTest instance to be added. | |
| 144 disabled: Whether test is disabled. | |
| 145 """ | |
| 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: | |
| 151 username_tag = ( | |
| 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: | |
| 157 password_tag = ( | |
| 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) | |
| 163 if disabled: | |
| 164 self.disabled_tests.append(websitetest.name) | |
| 165 else: | |
| 166 self.working_tests.append(websitetest.name) | |
| 167 | |
| 168 def ClearCache(self, clear_passwords): | |
| 169 """Clear the browser cookies. If |clear_passwords| is true, clear all the | |
| 170 saved passwords too. | |
| 171 | |
| 172 Args: | |
| 173 clear_passwords : Clear all the passwords if the bool value is true. | |
| 174 """ | |
| 175 logging.info("\nClearCache\n") | |
| 176 self.driver.get("chrome://settings/clearBrowserData") | |
| 177 self.driver.switch_to_frame("settings") | |
| 178 script = ( | |
| 179 "if (!document.querySelector('#delete-cookies-checkbox').checked)" | |
| 180 " document.querySelector('#delete-cookies-checkbox').click();" | |
| 181 ) | |
| 182 negation = "" | |
| 183 if clear_passwords: | |
| 184 negation = "!" | |
| 185 script += ( | |
| 186 "if (%sdocument.querySelector('#delete-passwords-checkbox').checked)" | |
| 187 " document.querySelector('#delete-passwords-checkbox').click();" | |
| 188 % negation) | |
| 189 script += "document.querySelector('#clear-browser-data-commit').click();" | |
| 190 self.driver.execute_script(script) | |
| 191 time.sleep(2) | |
| 192 | |
| 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. | |
| 197 | |
| 198 Args: | |
| 199 url: Url to go to in the new tab. | |
| 200 | |
| 201 Raises: | |
| 202 Exception: An exception is raised if |self.website_window| already | |
| 203 exists. | |
| 204 """ | |
| 205 if self.website_window: | |
| 206 raise Exception("Error: The window was already opened.") | |
| 207 | |
| 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];" | |
| 216 "a.innerHTML = '.';" | |
| 217 "document.body.appendChild(a);" | |
| 218 "return a;", | |
| 219 url) | |
| 220 | |
| 221 a.click() | |
| 222 time.sleep(1) | |
| 223 | |
| 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) | |
| 227 | |
| 228 def SwitchToInternals(self): | |
| 229 """Switches from the Website window to internals tab.""" | |
| 230 self.driver.switch_to_window(self.internals_window) | |
| 231 | |
| 232 def SwitchFromInternals(self): | |
| 233 """Switches from internals tab to the Website window.""" | |
| 234 self.driver.switch_to_window(self.website_window) | |
| 235 | |
| 236 def _DidMessageAppearUntilTimeout(self, log_message, timeout): | |
| 237 """Checks whether the save password prompt is shown. | |
| 238 | |
| 239 Args: | |
| 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. | |
| 246 | |
| 247 Returns: | |
| 248 True if the save password prompt is shown. | |
| 249 False otherwise. | |
| 250 """ | |
| 251 log = self.driver.find_element_by_css_selector("#log-entries") | |
| 252 count = log.text.count(log_message) | |
| 253 | |
| 254 if count > self.message_count[log_message]: | |
| 255 self.message_count[log_message] = count | |
| 256 return True | |
| 257 elif timeout > 0: | |
| 258 time.sleep(1) | |
| 259 return self._DidMessageAppearUntilTimeout(log_message, timeout - 1) | |
| 260 else: | |
| 261 return False | |
| 262 | |
| 263 def CheckForNewMessage(self, log_message, message_should_show_up, | |
| 264 error_message, timeout=3): | |
| 265 """Detects whether the save password prompt is shown. | |
| 266 | |
| 267 Args: | |
| 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 | |
| 272 shown. | |
| 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. | |
| 279 | |
| 280 Raises: | |
| 281 Exception: An exception is raised in case the result does not match the | |
| 282 expectation | |
| 283 """ | |
| 284 if (self._DidMessageAppearUntilTimeout(log_message, timeout) != | |
| 285 message_should_show_up): | |
| 286 raise Exception(error_message) | |
| 287 | |
| 288 def AllTests(self, prompt_test): | |
| 289 """Runs the tests on all the WebsiteTests. | |
| 290 | |
| 291 Args: | |
| 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. | |
| 295 | |
| 296 Raises: | |
| 297 Exception: An exception is raised if the tests fail. | |
| 298 """ | |
| 299 if prompt_test: | |
| 300 self.PromptTestList(self.websitetests) | |
| 301 else: | |
| 302 self.TestList(self.websitetests) | |
| 303 | |
| 304 def DisabledTests(self, prompt_test): | |
| 305 """Runs the tests on all the disabled WebsiteTests. | |
| 306 | |
| 307 Args: | |
| 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. | |
| 311 | |
| 312 Raises: | |
| 313 Exception: An exception is raised if the tests fail. | |
| 314 """ | |
| 315 self.Test(self.disabled_tests, prompt_test) | |
| 316 | |
| 317 def WorkingTests(self, prompt_test): | |
| 318 """Runs the tests on all the enabled WebsiteTests. | |
| 319 | |
| 320 Args: | |
| 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. | |
| 324 | |
| 325 Raises: | |
| 326 Exception: An exception is raised if the tests fail. | |
| 327 """ | |
| 328 self.Test(self.working_tests, prompt_test) | |
| 329 | |
| 330 def Test(self, tests, prompt_test): | |
| 331 """Runs the tests on websites named in |tests|. | |
| 332 | |
| 333 Args: | |
| 334 tests: A list of the names of the WebsiteTests that are going to be | |
| 335 tested. | |
| 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. | |
| 339 | |
| 340 Raises: | |
| 341 Exception: An exception is raised if the tests fail. | |
| 342 """ | |
| 343 websitetests = [] | |
| 344 for websitetest in self.websitetests: | |
| 345 if websitetest.name in tests: | |
| 346 websitetests.append(websitetest) | |
| 347 | |
| 348 if prompt_test: | |
| 349 self.PromptTestList(websitetests) | |
| 350 else: | |
| 351 self.TestList(websitetests) | |
| 352 | |
| 353 def TestList(self, websitetests): | |
| 354 """Runs the tests on the websites in |websitetests|. | |
| 355 | |
| 356 Args: | |
| 357 websitetests: A list of WebsiteTests that are going to be tested. | |
| 358 | |
| 359 Raises: | |
| 360 Exception: An exception is raised if the tests fail. | |
| 361 """ | |
| 362 self.ClearCache(True) | |
| 363 | |
| 364 for websitetest in websitetests: | |
| 365 successful = True | |
| 366 error = "" | |
| 367 try: | |
| 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) | |
| 376 except Exception: | |
| 377 successful = False | |
| 378 error = traceback.format_exc() | |
| 379 self.tests_results.append(TestResult(websitetest.name, "normal", | |
| 380 successful, escape(error))) | |
| 381 | |
| 382 | |
| 383 def PromptTestList(self, websitetests): | |
| 384 """Runs the prompt tests on the websites in |websitetests|. | |
| 385 | |
| 386 Args: | |
| 387 websitetests: A list of WebsiteTests that are going to be tested. | |
| 388 | |
| 389 Raises: | |
| 390 Exception: An exception is raised if the tests fail. | |
| 391 """ | |
| 392 self.ClearCache(True) | |
| 393 | |
| 394 for websitetest in websitetests: | |
| 395 successful = True | |
| 396 error = "" | |
| 397 try: | |
| 398 websitetest.was_run = True | |
| 399 websitetest.PromptTest() | |
| 400 except Exception: | |
| 401 successful = False | |
| 402 error = traceback.format_exc() | |
| 403 self.tests_results.append(TestResult(websitetest.name, "prompt", | |
| 404 successful, escape(error))) | |
| 405 | |
| 406 def Quit(self): | |
| 407 """Closes the tests.""" | |
| 408 # Close the webdriver. | |
| 409 self.driver.quit() | |
| OLD | NEW |