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.
7 It holds the WebsiteTest instances, provides them with credentials,
8 provides clean browser environment, runs the tests, and gathers the
15 from xml
.etree
import ElementTree
17 from selenium
import webdriver
18 from selenium
.webdriver
.chrome
.options
import Options
21 # Message strings to look for in chrome://password-manager-internals.
22 MESSAGE_ASK
= "Message: Decision: ASK the user"
23 MESSAGE_SAVE
= "Message: Decision: SAVE the password"
25 INTERNALS_PAGE_URL
= "chrome://password-manager-internals/"
28 """Sets up the testing Environment. """
30 def __init__(self
, chrome_path
, chromedriver_path
, profile_path
,
31 passwords_path
, enable_automatic_password_saving
):
32 """Creates a new testing Environment, starts Chromedriver.
35 chrome_path: The chrome binary file.
36 chromedriver_path: The chromedriver binary file.
37 profile_path: The chrome testing profile folder.
38 passwords_path: The usernames and passwords file.
39 enable_automatic_password_saving: If True, the passwords are going to be
40 saved without showing the prompt.
43 IOError: When the passwords file cannot be accessed.
44 ParseError: When the passwords file cannot be parsed.
45 Exception: An exception is raised if |profile_path| folder could not be
49 # Cleaning the chrome testing profile folder.
50 if os
.path
.exists(profile_path
):
51 shutil
.rmtree(profile_path
)
54 if enable_automatic_password_saving
:
55 options
.add_argument("enable-automatic-password-saving")
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
60 options
.binary_location
= chrome_path
61 options
.add_argument("user-data-dir=%s" % profile_path
)
63 # The webdriver. It's possible to choose the port the service is going to
64 # run on. If it's left to 0, a free port will be found.
65 self
.driver
= webdriver
.Chrome(chromedriver_path
, 0, options
)
67 # Password internals page tab/window handle.
68 self
.internals_window
= self
.driver
.current_window_handle
70 # An xml tree filled with logins and passwords.
71 self
.passwords_tree
= ElementTree
.parse(passwords_path
).getroot()
73 self
.website_window
= self
._OpenNewTab
()
75 self
.websitetests
= []
77 # Map messages to the number of their appearance in the log.
78 self
.message_count
= { MESSAGE_ASK
: 0, MESSAGE_SAVE
: 0 }
80 # A list of (test_name, test_type, test_success, failure_log).
81 self
.tests_results
= []
83 def AddWebsiteTest(self
, websitetest
):
84 """Adds a WebsiteTest to the testing Environment.
86 TODO(vabr): Currently, this is only called at most once for each
87 Environment instance. That is because to run all tests efficiently in
88 parallel, each test gets its own process spawned (outside of Python).
89 That makes sense, but then we should flatten the hierarchy of calls
90 and consider making the 1:1 relation of environment to tests more
94 websitetest: The WebsiteTest instance to be added.
96 websitetest
.environment
= self
97 # TODO(vabr): Make driver a property of WebsiteTest.
98 websitetest
.driver
= self
.driver
99 if not websitetest
.username
:
100 username_tag
= (self
.passwords_tree
.find(
101 ".//*[@name='%s']/username" % websitetest
.name
))
102 websitetest
.username
= username_tag
.text
103 if not websitetest
.password
:
104 password_tag
= (self
.passwords_tree
.find(
105 ".//*[@name='%s']/password" % websitetest
.name
))
106 websitetest
.password
= password_tag
.text
107 self
.websitetests
.append(websitetest
)
109 def _ClearBrowserDataInit(self
):
110 """Opens and resets the chrome://settings/clearBrowserData dialog.
112 It unchecks all checkboxes, and sets the time range to the "beginning of
116 self
.driver
.get("chrome://settings-frame/clearBrowserData")
118 time_range_selector
= "#clear-browser-data-time-period"
119 # TODO(vabr): Wait until time_range_selector is displayed instead.
122 "var range = document.querySelector('{0}');".format(
123 time_range_selector
) +
124 "range.value = 4" # 4 == the beginning of time
126 self
.driver
.execute_script(set_time_range
)
128 all_cboxes_selector
= (
129 "#clear-data-checkboxes [type=\"checkbox\"]")
131 "var checkboxes = document.querySelectorAll('{0}');".format(
132 all_cboxes_selector
) +
133 "for (var i = 0; i < checkboxes.length; ++i) {"
134 " checkboxes[i].checked = false;"
137 self
.driver
.execute_script(uncheck_all
)
139 def _ClearDataForCheckbox(self
, selector
):
140 """Causes the data associated with |selector| to be cleared.
142 Opens chrome://settings/clearBrowserData, unchecks all checkboxes, then
143 checks the one described by |selector|, then clears the corresponding
144 browsing data for the full time range.
147 selector: describes the checkbox through which to delete the data.
150 self
._ClearBrowserDataInit
()
151 check_cookies_and_submit
= (
152 "document.querySelector('{0}').checked = true;".format(selector
) +
153 "document.querySelector('#clear-browser-data-commit').click();"
155 self
.driver
.execute_script(check_cookies_and_submit
)
157 def _EnablePasswordSaving(self
):
158 """Make sure that password manager is enabled."""
160 # TODO(melandory): We should check why it's off in a first place.
161 # TODO(melandory): Investigate, maybe there is no need to enable it that
163 self
.driver
.get("chrome://settings-frame")
164 script
= "document.getElementById('advanced-settings-expander').click();"
165 self
.driver
.execute_script(script
)
166 # TODO(vabr): Wait until element is displayed instead.
169 "if (!document.querySelector('#password-manager-enabled').checked) {"
170 " document.querySelector('#password-manager-enabled').click();"
172 self
.driver
.execute_script(script
)
175 def _OpenNewTab(self
):
176 """Open a new tab, and loads the internals page in the old tab.
179 A handle to the new tab.
182 number_old_tabs
= len(self
.driver
.window_handles
)
183 # There is no straightforward way to open a new tab with chromedriver.
184 # One work-around is to go to a website, insert a link that is going
185 # to be opened in a new tab, and click on it.
186 self
.driver
.get("about:blank")
187 a
= self
.driver
.execute_script(
188 "var a = document.createElement('a');"
189 "a.target = '_blank';"
190 "a.href = 'about:blank';"
192 "document.body.appendChild(a);"
195 while number_old_tabs
== len(self
.driver
.window_handles
):
196 time
.sleep(1) # Wait until the new tab is opened.
198 new_tab
= self
.driver
.window_handles
[-1]
199 self
.driver
.get(INTERNALS_PAGE_URL
)
200 self
.driver
.switch_to_window(new_tab
)
203 def _DidStringAppearUntilTimeout(self
, strings
, timeout
):
204 """Checks whether some of |strings| appeared in the current page.
206 Waits for up to |timeout| seconds until at least one of |strings| is
207 shown in the current page. Updates self.message_count with the current
208 number of occurrences of the shown string. Assumes that at most
209 one of |strings| is newly shown.
212 strings: A list of strings to look for.
213 timeout: If any such string does not appear within the first |timeout|
214 seconds, it is considered a no-show.
217 True if one of |strings| is observed until |timeout|, False otherwise.
220 log
= self
.driver
.find_element_by_css_selector("#log-entries")
222 for string
in strings
:
223 count
= log
.text
.count(string
)
224 if count
> self
.message_count
[string
]:
225 self
.message_count
[string
] = count
231 def CheckForNewString(self
, strings
, string_should_show_up
, error
):
232 """Checks that |strings| show up on the internals page as it should.
234 Switches to the internals page and looks for a new instances of |strings|
235 being shown up there. It checks that |string_should_show_up| is true if
236 and only if at leas one string from |strings| shows up, and throws an
237 Exception if that check fails.
240 strings: A list of strings to look for in the internals page.
241 string_should_show_up: Whether or not at least one string from |strings|
242 is expected to be shown.
243 error: Error message for the exception.
246 Exception: (See above.)
249 self
.driver
.switch_to_window(self
.internals_window
)
251 if (self
._DidStringAppearUntilTimeout
(strings
, 15) !=
252 string_should_show_up
):
253 raise Exception(error
)
255 self
.driver
.switch_to_window(self
.website_window
)
257 def DeleteCookies(self
):
258 """Deletes cookies via the settings page."""
260 self
._ClearDataForCheckbox
("#delete-cookies-checkbox")
262 def RunTestsOnSites(self
, test_case_name
):
263 """Runs the specified test on the known websites.
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|
272 test_case_name: A test name which is a method of WebsiteTest.
276 self
._ClearDataForCheckbox
("#delete-passwords-checkbox")
277 self
._EnablePasswordSaving
()
279 for websitetest
in self
.websitetests
:
283 # TODO(melandory): Implement a decorator for WesiteTest methods
284 # which allows to mark them as test cases. And then add a check if
285 # test_case_name is a valid test case.
286 getattr(websitetest
, test_case_name
)()
287 except Exception as e
:
289 # httplib.CannotSendRequest doesn't define a message,
290 # so type(e).__name__ will at least log exception name as a reason.
291 # TODO(melandory): logging.exception(e) produces meaningful result
292 # for httplib.CannotSendRequest, so we can try to propagate information
293 # that reason is an exception to the logging phase.
294 error
= "Exception %s %s" % (type(e
).__name
__, e
)
295 self
.tests_results
.append(
296 (websitetest
.name
, test_case_name
, successful
, error
))
299 """Shuts down the driver."""