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 |