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 7import logging 8import shutil 9import sys 10import time 11import traceback 12from xml.etree import ElementTree 13from xml.sax.saxutils import escape 14 15sys.path.insert(0, '../../../../third_party/webdriver/pylib/') 16 17from selenium import webdriver 18from selenium.common.exceptions import NoSuchElementException 19from selenium.common.exceptions import WebDriverException 20from selenium.webdriver.chrome.options import Options 21 22 23# Message strings to look for in chrome://password-manager-internals 24MESSAGE_ASK = "Message: Decision: ASK the user" 25MESSAGE_SAVE = "Message: Decision: SAVE the password" 26 27 28class 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 45class 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() 410