OLD | NEW |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """The testing Environment class.""" | 5 """The testing Environment class. |
| 6 |
| 7 It holds the WebsiteTest instances, provides them with credentials, |
| 8 provides clean browser environment, runs the tests, and gathers the |
| 9 results. |
| 10 """ |
6 | 11 |
7 import os | 12 import os |
8 import shutil | 13 import shutil |
9 import time | 14 import time |
10 from xml.etree import ElementTree | 15 from xml.etree import ElementTree |
11 | 16 |
12 from selenium import webdriver | 17 from selenium import webdriver |
13 from selenium.webdriver.chrome.options import Options | 18 from selenium.webdriver.chrome.options import Options |
14 | 19 |
15 | 20 |
16 # Message strings to look for in chrome://password-manager-internals | 21 # Message strings to look for in chrome://password-manager-internals. |
17 MESSAGE_ASK = "Message: Decision: ASK the user" | 22 MESSAGE_ASK = "Message: Decision: ASK the user" |
18 MESSAGE_SAVE = "Message: Decision: SAVE the password" | 23 MESSAGE_SAVE = "Message: Decision: SAVE the password" |
19 | 24 |
20 | 25 INTERNALS_PAGE_URL = "chrome://password-manager-internals/" |
21 class TestResult: | |
22 """Stores the information related to a test result. """ | |
23 def __init__(self, name, test_type, successful, message): | |
24 """Creates a new TestResult. | |
25 | |
26 Args: | |
27 name: The tested website name. | |
28 test_type: The test type. | |
29 successful: Whether or not the test was successful. | |
30 message: The error message of the test. | |
31 """ | |
32 self.name = name | |
33 self.test_type = test_type | |
34 self.successful = successful | |
35 self.message = message | |
36 | |
37 | 26 |
38 class Environment: | 27 class Environment: |
39 """Sets up the testing Environment. """ | 28 """Sets up the testing Environment. """ |
40 | 29 |
41 def __init__(self, chrome_path, chromedriver_path, profile_path, | 30 def __init__(self, chrome_path, chromedriver_path, profile_path, |
42 passwords_path, enable_automatic_password_saving): | 31 passwords_path, enable_automatic_password_saving): |
43 """Creates a new testing Environment. | 32 """Creates a new testing Environment, starts Chromedriver. |
44 | 33 |
45 Args: | 34 Args: |
46 chrome_path: The chrome binary file. | 35 chrome_path: The chrome binary file. |
47 chromedriver_path: The chromedriver binary file. | 36 chromedriver_path: The chromedriver binary file. |
48 profile_path: The chrome testing profile folder. | 37 profile_path: The chrome testing profile folder. |
49 passwords_path: The usernames and passwords file. | 38 passwords_path: The usernames and passwords file. |
50 enable_automatic_password_saving: If True, the passwords are going to be | 39 enable_automatic_password_saving: If True, the passwords are going to be |
51 saved without showing the prompt. | 40 saved without showing the prompt. |
52 | 41 |
53 Raises: | 42 Raises: |
| 43 IOError: When the passwords file cannot be accessed. |
| 44 ParseError: When the passwords file cannot be parsed. |
54 Exception: An exception is raised if |profile_path| folder could not be | 45 Exception: An exception is raised if |profile_path| folder could not be |
55 removed. | 46 removed. |
56 """ | 47 """ |
57 | 48 |
58 # Cleaning the chrome testing profile folder. | 49 # Cleaning the chrome testing profile folder. |
59 if os.path.exists(profile_path): | 50 if os.path.exists(profile_path): |
60 shutil.rmtree(profile_path) | 51 shutil.rmtree(profile_path) |
| 52 |
61 options = Options() | 53 options = Options() |
62 self.enable_automatic_password_saving = enable_automatic_password_saving | |
63 if enable_automatic_password_saving: | 54 if enable_automatic_password_saving: |
64 options.add_argument("enable-automatic-password-saving") | 55 options.add_argument("enable-automatic-password-saving") |
65 # Chrome path. | 56 # TODO(vabr): show_prompt is used in WebsiteTest for asserting that |
| 57 # Chrome set-up corresponds to the test type. Remove that knowledge |
| 58 # about Environment from the WebsiteTest. |
| 59 self.show_prompt = not enable_automatic_password_saving |
66 options.binary_location = chrome_path | 60 options.binary_location = chrome_path |
67 # Chrome testing profile path. | |
68 options.add_argument("user-data-dir=%s" % profile_path) | 61 options.add_argument("user-data-dir=%s" % profile_path) |
69 | 62 |
70 # The webdriver. It's possible to choose the port the service is going to | 63 # The webdriver. It's possible to choose the port the service is going to |
71 # run on. If it's left to 0, a free port will be found. | 64 # run on. If it's left to 0, a free port will be found. |
72 self.driver = webdriver.Chrome(chromedriver_path, 0, options) | 65 self.driver = webdriver.Chrome(chromedriver_path, 0, options) |
73 # The password internals window. | 66 |
| 67 # Password internals page tab/window handle. |
74 self.internals_window = self.driver.current_window_handle | 68 self.internals_window = self.driver.current_window_handle |
75 if passwords_path: | 69 |
76 # An xml tree filled with logins and passwords. | 70 # An xml tree filled with logins and passwords. |
77 self.passwords_tree = ElementTree.parse(passwords_path).getroot() | 71 self.passwords_tree = ElementTree.parse(passwords_path).getroot() |
78 else: | 72 |
79 raise Exception("Error: |passwords_path| needs to be provided if" | 73 self.website_window = self._OpenNewTab() |
80 "|chrome_path| is provided, otherwise the tests could not be run") | 74 |
81 # Password internals page. | |
82 self.internals_page = "chrome://password-manager-internals/" | |
83 # The Website window. | |
84 self.website_window = None | |
85 # The WebsiteTests list. | |
86 self.websitetests = [] | 75 self.websitetests = [] |
| 76 |
87 # Map messages to the number of their appearance in the log. | 77 # Map messages to the number of their appearance in the log. |
88 self.message_count = { MESSAGE_ASK: 0, MESSAGE_SAVE: 0 } | 78 self.message_count = { MESSAGE_ASK: 0, MESSAGE_SAVE: 0 } |
89 # The tests needs two tabs to work. A new tab is opened with the first | 79 |
90 # GoTo. This is why we store here whether or not it's the first time to | 80 # A list of (test_name, test_type, test_success, failure_log). |
91 # execute GoTo. | |
92 self.first_go_to = True | |
93 # List of all tests results. | |
94 self.tests_results = [] | 81 self.tests_results = [] |
95 | 82 |
96 def AddWebsiteTest(self, websitetest): | 83 def AddWebsiteTest(self, websitetest): |
97 """Adds a WebsiteTest to the testing Environment. | 84 """Adds a WebsiteTest to the testing Environment. |
98 | 85 |
99 TODO(vabr): Currently, this is only called at most once for each | 86 TODO(vabr): Currently, this is only called at most once for each |
100 Environment instance. That is because to run all tests efficiently in | 87 Environment instance. That is because to run all tests efficiently in |
101 parallel, each test gets its own process spawned (outside of Python). | 88 parallel, each test gets its own process spawned (outside of Python). |
102 That makes sense, but then we should flatten the hierarchy of calls | 89 That makes sense, but then we should flatten the hierarchy of calls |
103 and consider making the 1:1 relation of environment to tests more | 90 and consider making the 1:1 relation of environment to tests more |
104 explicit. | 91 explicit. |
105 | 92 |
106 Args: | 93 Args: |
107 websitetest: The WebsiteTest instance to be added. | 94 websitetest: The WebsiteTest instance to be added. |
108 """ | 95 """ |
109 websitetest.environment = self | 96 websitetest.environment = self |
110 # TODO(vabr): Make driver a property of WebsiteTest. | 97 # TODO(vabr): Make driver a property of WebsiteTest. |
111 websitetest.driver = self.driver | 98 websitetest.driver = self.driver |
112 if not websitetest.username: | 99 if not websitetest.username: |
113 username_tag = ( | 100 username_tag = (self.passwords_tree.find( |
114 self.passwords_tree.find( | 101 ".//*[@name='%s']/username" % websitetest.name)) |
115 ".//*[@name='%s']/username" % websitetest.name)) | |
116 websitetest.username = username_tag.text | 102 websitetest.username = username_tag.text |
117 if not websitetest.password: | 103 if not websitetest.password: |
118 password_tag = ( | 104 password_tag = (self.passwords_tree.find( |
119 self.passwords_tree.find( | 105 ".//*[@name='%s']/password" % websitetest.name)) |
120 ".//*[@name='%s']/password" % websitetest.name)) | |
121 websitetest.password = password_tag.text | 106 websitetest.password = password_tag.text |
122 self.websitetests.append(websitetest) | 107 self.websitetests.append(websitetest) |
123 | 108 |
124 def ClearCache(self, clear_passwords): | 109 def _ClearBrowserDataInit(self): |
125 """Clear the browser cookies. If |clear_passwords| is true, clear all the | 110 """Opens and resets the chrome://settings/clearBrowserData dialog. |
126 saved passwords too. | 111 |
| 112 It unchecks all checkboxes, and sets the time range to the "beginning of |
| 113 time". |
| 114 """ |
| 115 |
| 116 self.driver.get("chrome://settings/clearBrowserData") |
| 117 self.driver.switch_to_frame("settings") |
| 118 |
| 119 time_range_selector = "#clear-browser-data-time-period" |
| 120 # TODO(vabr): Wait until time_range_selector is displayed instead. |
| 121 time.sleep(2) |
| 122 set_time_range = ( |
| 123 "var range = document.querySelector('{0}');".format( |
| 124 time_range_selector) + |
| 125 "range.value = 4" # 4 == the beginning of time |
| 126 ) |
| 127 self.driver.execute_script(set_time_range) |
| 128 |
| 129 all_cboxes_selector = ( |
| 130 "#clear-data-checkboxes [type=\"checkbox\"]") |
| 131 uncheck_all = ( |
| 132 "var checkboxes = document.querySelectorAll('{0}');".format( |
| 133 all_cboxes_selector ) + |
| 134 "for (var i = 0; i < checkboxes.length; ++i) {" |
| 135 " checkboxes[i].checked = false;" |
| 136 "}" |
| 137 ) |
| 138 self.driver.execute_script(uncheck_all) |
| 139 |
| 140 def _ClearDataForCheckbox(self, selector): |
| 141 """Causes the data associated with |selector| to be cleared. |
| 142 |
| 143 Opens chrome://settings/clearBrowserData, unchecks all checkboxes, then |
| 144 checks the one described by |selector|, then clears the corresponding |
| 145 browsing data for the full time range. |
127 | 146 |
128 Args: | 147 Args: |
129 clear_passwords : Clear all the passwords if the bool value is true. | 148 selector: describes the checkbox through which to delete the data. |
130 """ | 149 """ |
131 self.driver.get("chrome://settings/clearBrowserData") | 150 |
132 self.driver.switch_to_frame("settings") | 151 self._ClearBrowserDataInit() |
133 script = ( | 152 check_cookies_and_submit = ( |
134 "if (!document.querySelector('#delete-cookies-checkbox').checked)" | 153 "document.querySelector('{0}').checked = true;".format(selector) + |
135 " document.querySelector('#delete-cookies-checkbox').click();" | 154 "document.querySelector('#clear-browser-data-commit').click();" |
136 ) | 155 ) |
137 negation = "" | 156 self.driver.execute_script(check_cookies_and_submit) |
138 if clear_passwords: | 157 |
139 negation = "!" | 158 def _EnablePasswordSaving(self): |
140 script += ( | 159 """Make sure that password manager is enabled.""" |
141 "if (%sdocument.querySelector('#delete-passwords-checkbox').checked)" | 160 |
142 " document.querySelector('#delete-passwords-checkbox').click();" | |
143 % negation) | |
144 script += "document.querySelector('#clear-browser-data-commit').click();" | |
145 self.driver.execute_script(script) | |
146 time.sleep(2) | |
147 # Every time we do something to the cache let's enable password saving. | |
148 # TODO(melandory): We should check why it's off in a first place. | 161 # TODO(melandory): We should check why it's off in a first place. |
149 # TODO(melandory): Investigate, maybe there is no need to enable it that | 162 # TODO(melandory): Investigate, maybe there is no need to enable it that |
150 # often. | 163 # often. |
151 self.EnablePasswordsSaving() | |
152 | |
153 def EnablePasswordsSaving(self): | |
154 self.driver.get("chrome://settings") | 164 self.driver.get("chrome://settings") |
155 self.driver.switch_to_frame("settings") | 165 self.driver.switch_to_frame("settings") |
156 script = "document.getElementById('advanced-settings-expander').click();" | 166 script = "document.getElementById('advanced-settings-expander').click();" |
157 self.driver.execute_script(script) | 167 self.driver.execute_script(script) |
| 168 # TODO(vabr): Wait until element is displayed instead. |
158 time.sleep(2) | 169 time.sleep(2) |
159 script = ( | 170 script = ( |
160 "if (!document.querySelector('#password-manager-enabled').checked)" | 171 "document.querySelector('#password-manager-enabled').checked = true;") |
161 "{ document.querySelector('#password-manager-enabled').click();}") | |
162 self.driver.execute_script(script) | 172 self.driver.execute_script(script) |
163 time.sleep(2) | 173 time.sleep(2) |
164 | 174 |
165 def OpenTabAndGoToInternals(self, url): | 175 def _OpenNewTab(self): |
166 """If there is no |self.website_window|, opens a new tab and navigates to | 176 """Open a new tab, and loads the internals page in the old tab. |
167 |url| in the new tab. Navigates to the passwords internals page in the | |
168 first tab. Raises an exception otherwise. | |
169 | 177 |
170 Args: | 178 Returns: |
171 url: Url to go to in the new tab. | 179 A handle to the new tab. |
| 180 """ |
172 | 181 |
173 Raises: | 182 number_old_tabs = len(self.driver.window_handles) |
174 Exception: An exception is raised if |self.website_window| already | |
175 exists. | |
176 """ | |
177 if self.website_window: | |
178 raise Exception("Error: The window was already opened.") | |
179 | |
180 self.driver.get("chrome://newtab") | |
181 # There is no straightforward way to open a new tab with chromedriver. | 183 # There is no straightforward way to open a new tab with chromedriver. |
182 # One work-around is to go to a website, insert a link that is going | 184 # One work-around is to go to a website, insert a link that is going |
183 # to be opened in a new tab, click on it. | 185 # to be opened in a new tab, and click on it. |
| 186 self.driver.get("about:blank") |
184 a = self.driver.execute_script( | 187 a = self.driver.execute_script( |
185 "var a = document.createElement('a');" | 188 "var a = document.createElement('a');" |
186 "a.target = '_blank';" | 189 "a.target = '_blank';" |
187 "a.href = arguments[0];" | 190 "a.href = 'about:blank';" |
188 "a.innerHTML = '.';" | 191 "a.innerHTML = '.';" |
189 "document.body.appendChild(a);" | 192 "document.body.appendChild(a);" |
190 "return a;", | 193 "return a;") |
191 url) | 194 a.click() |
| 195 while number_old_tabs == len(self.driver.window_handles): |
| 196 time.sleep(1) # Wait until the new tab is opened. |
192 | 197 |
193 a.click() | 198 new_tab = self.driver.window_handles[-1] |
194 time.sleep(1) | 199 self.driver.get(INTERNALS_PAGE_URL) |
| 200 self.driver.switch_to_window(new_tab) |
| 201 return new_tab |
195 | 202 |
196 self.website_window = self.driver.window_handles[-1] | 203 def _DidStringAppearUntilTimeout(self, strings, timeout): |
197 self.driver.get(self.internals_page) | 204 """Checks whether some of |strings| appeared in the current page. |
198 self.driver.switch_to_window(self.website_window) | |
199 | 205 |
200 def SwitchToInternals(self): | 206 Waits for up to |timeout| seconds until at least one of |strings| is |
201 """Switches from the Website window to internals tab.""" | 207 shown in the current page. Updates self.message_count with the current |
202 self.driver.switch_to_window(self.internals_window) | 208 number of occurrences of the shown string. Assumes that at most |
203 | 209 one of |strings| is newly shown. |
204 def SwitchFromInternals(self): | |
205 """Switches from internals tab to the Website window.""" | |
206 self.driver.switch_to_window(self.website_window) | |
207 | |
208 def _DidMessageAppearUntilTimeout(self, log_message, timeout): | |
209 """Checks whether the save password prompt is shown. | |
210 | 210 |
211 Args: | 211 Args: |
212 log_message: Log message to look for in the password internals. | 212 strings: A list of strings to look for. |
213 timeout: There is some delay between the login and the password | 213 timeout: If any such string does not appear within the first |timeout| |
214 internals update. The method checks periodically during the first | 214 seconds, it is considered a no-show. |
215 |timeout| seconds if the internals page reports the prompt being | |
216 shown. If the prompt is not reported shown within the first | |
217 |timeout| seconds, it is considered not shown at all. | |
218 | 215 |
219 Returns: | 216 Returns: |
220 True if the save password prompt is shown. | 217 True if one of |strings| is observed until |timeout|, False otherwise. |
221 False otherwise. | |
222 """ | 218 """ |
| 219 |
223 log = self.driver.find_element_by_css_selector("#log-entries") | 220 log = self.driver.find_element_by_css_selector("#log-entries") |
224 count = log.text.count(log_message) | 221 while timeout: |
| 222 for string in strings: |
| 223 count = log.text.count(string) |
| 224 if count > self.message_count[string]: |
| 225 self.message_count[string] = count |
| 226 return True |
| 227 time.sleep(1) |
| 228 timeout -= 1 |
| 229 return False |
225 | 230 |
226 if count > self.message_count[log_message]: | 231 def CheckForNewString(self, strings, string_should_show_up, error): |
227 self.message_count[log_message] = count | 232 """Checks that |strings| show up on the internals page as it should. |
228 return True | |
229 elif timeout > 0: | |
230 time.sleep(1) | |
231 return self._DidMessageAppearUntilTimeout(log_message, timeout - 1) | |
232 else: | |
233 return False | |
234 | 233 |
235 def CheckForNewMessage(self, log_message, message_should_show_up, | 234 Switches to the internals page and looks for a new instances of |strings| |
236 error_message, timeout=15): | 235 being shown up there. It checks that |string_should_show_up| is true if |
237 """Detects whether the save password prompt is shown. | 236 and only if at leas one string from |strings| shows up, and throws an |
| 237 Exception if that check fails. |
238 | 238 |
239 Args: | 239 Args: |
240 log_message: Log message to look for in the password internals. The | 240 strings: A list of strings to look for in the internals page. |
241 only valid values are the constants MESSAGE_* defined at the | 241 string_should_show_up: Whether or not at least one string from |strings| |
242 beginning of this file. | 242 is expected to be shown. |
243 message_should_show_up: Whether or not the message is expected to be | 243 error: Error message for the exception. |
244 shown. | |
245 error_message: Error message for the exception. | |
246 timeout: There is some delay between the login and the password | |
247 internals update. The method checks periodically during the first | |
248 |timeout| seconds if the internals page reports the prompt being | |
249 shown. If the prompt is not reported shown within the first | |
250 |timeout| seconds, it is considered not shown at all. | |
251 | 244 |
252 Raises: | 245 Raises: |
253 Exception: An exception is raised in case the result does not match the | 246 Exception: (See above.) |
254 expectation | |
255 """ | 247 """ |
256 if (self._DidMessageAppearUntilTimeout(log_message, timeout) != | |
257 message_should_show_up): | |
258 raise Exception(error_message) | |
259 | 248 |
260 def AllTests(self, prompt_test): | 249 self.driver.switch_to_window(self.internals_window) |
261 """Runs the tests on all the WebsiteTests. | 250 try: |
| 251 if (self._DidStringAppearUntilTimeout(strings, 15) != |
| 252 string_should_show_up): |
| 253 raise Exception(error) |
| 254 finally: |
| 255 self.driver.switch_to_window(self.website_window) |
262 | 256 |
263 TODO(vabr): Currently, "all tests" always means one. | 257 def DeleteCookies(self): |
| 258 """Deletes cookies via the settings page.""" |
| 259 |
| 260 self._ClearDataForCheckbox("#delete-cookies-checkbox") |
| 261 |
| 262 def RunTestsOnSites(self, test_type): |
| 263 """Runs the specified test on the known websites. |
| 264 |
| 265 Also saves the test results in the environment. Note that test types |
| 266 differ in their requirements on whether the save password prompt |
| 267 should be displayed. Make sure that such requirements are consistent |
| 268 with the enable_automatic_password_saving argument passed to |self| |
| 269 on construction. |
264 | 270 |
265 Args: | 271 Args: |
266 prompt_test: If True, tests caring about showing the save-password | 272 test_type: A test identifier understood by WebsiteTest.run_test(). |
267 prompt are going to be run, otherwise tests which don't care about | 273 """ |
268 the prompt are going to be run. | |
269 | 274 |
270 Raises: | 275 self.DeleteCookies() |
271 Exception: An exception is raised if the tests fail. | 276 self._ClearDataForCheckbox("#delete-passwords-checkbox") |
272 """ | 277 self._EnablePasswordSaving() |
273 if prompt_test: | |
274 self.PromptTestList(self.websitetests) | |
275 else: | |
276 self.TestList(self.websitetests) | |
277 | 278 |
278 def Test(self, tests, prompt_test): | |
279 """Runs the tests on websites named in |tests|. | |
280 | |
281 Args: | |
282 tests: A list of the names of the WebsiteTests that are going to be | |
283 tested. | |
284 prompt_test: If True, tests caring about showing the save-password | |
285 prompt are going to be run, otherwise tests which don't care about | |
286 the prompt are going to be executed. | |
287 | |
288 Raises: | |
289 Exception: An exception is raised if the tests fail. | |
290 """ | |
291 websitetests = [] | |
292 for websitetest in self.websitetests: | 279 for websitetest in self.websitetests: |
293 if websitetest.name in tests: | |
294 websitetests.append(websitetest) | |
295 | |
296 if prompt_test: | |
297 self.PromptTestList(websitetests) | |
298 else: | |
299 self.TestList(websitetests) | |
300 | |
301 def TestList(self, websitetests): | |
302 """Runs the tests on the websites in |websitetests|. | |
303 | |
304 Args: | |
305 websitetests: A list of WebsiteTests that are going to be tested. | |
306 | |
307 Raises: | |
308 Exception: An exception is raised if the tests fail. | |
309 """ | |
310 self.ClearCache(True) | |
311 | |
312 for websitetest in websitetests: | |
313 successful = True | 280 successful = True |
314 error = "" | 281 error = "" |
315 try: | 282 try: |
316 websitetest.was_run = True | 283 websitetest.RunTest(test_type) |
317 websitetest.WrongLoginTest() | |
318 websitetest.SuccessfulLoginTest() | |
319 self.ClearCache(False) | |
320 websitetest.SuccessfulLoginWithAutofilledPasswordTest() | |
321 self.ClearCache(True) | |
322 websitetest.SuccessfulLoginTest() | |
323 self.ClearCache(True) | |
324 except Exception as e: | 284 except Exception as e: |
325 successful = False | 285 successful = False |
326 error = e.message | 286 error = e.message |
327 self.tests_results.append(TestResult(websitetest.name, "normal", | 287 self.tests_results.append( |
328 successful, error)) | 288 (websitetest.name, test_type, successful, error)) |
329 | |
330 | |
331 def PromptTestList(self, websitetests): | |
332 """Runs the prompt tests on the websites in |websitetests|. | |
333 | |
334 Args: | |
335 websitetests: A list of WebsiteTests that are going to be tested. | |
336 | |
337 Raises: | |
338 Exception: An exception is raised if the tests fail. | |
339 """ | |
340 self.ClearCache(True) | |
341 | |
342 for websitetest in websitetests: | |
343 successful = True | |
344 error = "" | |
345 try: | |
346 websitetest.was_run = True | |
347 websitetest.PromptTest() | |
348 except Exception as e: | |
349 successful = False | |
350 error = e.message | |
351 self.tests_results.append(TestResult(websitetest.name, "prompt", | |
352 successful, error)) | |
353 | 289 |
354 def Quit(self): | 290 def Quit(self): |
355 """Closes the tests.""" | 291 """Shuts down the driver.""" |
356 # Close the webdriver. | 292 |
357 self.driver.quit() | 293 self.driver.quit() |
OLD | NEW |