1 # Copyright (c) 2012 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.
7 import selenium.common.exceptions
8 from selenium.webdriver.common.action_chains import ActionChains
9 from selenium.webdriver.support.ui import WebDriverWait
12 def _FocusField(driver, list_elem, field_elem):
13 """Focuses a field in a dynamic list.
15 Note, the item containing the field should not be focused already.
17 Typing into a field is tricky because the js automatically focuses and
18 selects the text field after 50ms after it first receives focus. This
19 method focuses the field and waits for the timeout to occur.
20 For more info, see inline_editable_list.js and search for setTimeout.
24 list_elem: An element in the HTML list.
25 field_elem: An element in the HTML text field.
28 RuntimeError: If a timeout occurs when waiting for the focus event.
30 # To wait properly for the focus, we focus the last text field, and then
31 # add a focus listener to it, so that we return when the element is focused
32 # again after the timeout. We have to focus a different element in between
33 # these steps, otherwise the focus event will not fire since the element
35 # Ideally this should be fixed in the page.
37 correct_focus_script = """
38 (function(listElem, itemElem, callback) {
39 if (document.activeElement == itemElem) {
45 itemElem.addEventListener("focus", callback);
46 }).apply(null, arguments);
48 driver.set_script_timeout(5)
50 driver.execute_async_script(correct_focus_script, list_elem, field_elem)
51 except selenium.common.exceptions.TimeoutException:
52 raise RuntimeError('Unable to focus list item ' + field_elem.tag_name)
56 """A list item web element."""
57 def __init__(self, elem):
60 def Remove(self, driver):
61 button = self._elem.find_element_by_xpath('./button')
62 ActionChains(driver).move_to_element(button).click().perform()
65 class TextFieldsItem(Item):
66 """An item consisting only of text fields."""
68 """Returns the text fields list."""
69 return self._elem.find_elements_by_tag_name('input')
71 def Set(self, values):
72 """Sets the value(s) of the item's text field(s).
75 values: The new value or the list of the new values of the fields.
77 field_list = self._GetFields()
78 if len(field_list) > 1:
79 assert type(values) == types.ListType, \
80 """The values must be a list for a HTML list that has multi-field
81 items. '%s' should be in a list.""" % values
86 assert len(field_list) == len(value_list), \
87 """The item to be added must have the same number of fields as an item
88 in the HTML list. Given item '%s' should have %s fields.""" % (
89 value_list, len(field_list))
90 for field, value in zip(field_list, value_list):
92 field.send_keys(value)
93 field_list[-1].send_keys('\n') # press enter on the last field.
96 """Returns the list of the text field values."""
97 return map(lambda f: f.get_attribute('value'), self._GetFields())
100 class TextField(object):
101 """A text field web element."""
102 def __init__(self, elem):
105 def Set(self, value):
106 """Sets the value of the text field.
109 value: The new value of the field.
112 self._elem.send_keys(value)
115 """Returns the value of the text field."""
116 return self._elem.get_attribute('value')
120 """A web element that holds a list of items."""
122 def __init__(self, driver, elem, item_class=Item):
123 """item element is an element in the HTML list.
124 item class is the class of item the list holds."""
125 self._driver = driver
127 self._item_class = item_class
130 """Removes all items from the list.
132 In the loop the removal of an elem renders the remaining elems of the list
133 invalid. After each item is removed, GetItems() is called.
135 for i in range(len(self.GetItems())):
136 self.GetItems()[0].Remove(self._driver)
139 """Returns all the items that are in the list."""
140 items = self._GetItemElems()
141 return map(lambda x: self._item_class(x), items)
144 """Returns the number of items in the list."""
145 return len(self._GetItemElems())
147 def _GetItemElems(self):
148 return self._elem.find_elements_by_xpath('.//*[@role="listitem"]')
151 class DynamicList(List):
152 """A web element that holds a dynamic list of items of text fields.
155 item element: an element in the HTML list item.
156 item_class: the class of item the list holds
157 placeholder: the last item element in the list, which is not committed yet
159 The user can add new items to the list by typing in the placeholder item.
160 When a user presses enter or focuses something else, the placeholder item
161 is committed and a new placeholder is created. An item may contain 1 or
165 def __init__(self, driver, elem, item_class=TextFieldsItem):
166 return super(DynamicList, self).__init__(
167 driver, elem, item_class=item_class)
169 def GetPlaceholderItem(self):
170 return self.GetItems()[-1]
172 def GetCommittedItems(self):
173 """Returns all the items that are in the list, except the placeholder."""
174 return map(lambda x: self._item_class(x), self._GetCommittedItemElems())
177 """Returns the number of items in the list, excluding the placeholder."""
178 return len(self._GetCommittedItemElems())
180 def _GetCommittedItemElems(self):
181 return self._GetItemElems()[:-1]
183 def _GetPlaceholderElem(self):
184 return self._GetItemElems()[-1]
187 class AutofillEditAddressDialog(object):
188 """The overlay for editing an autofill address."""
190 _URL = 'chrome://settings-frame/autofillEditAddress'
193 def FromNavigation(driver):
194 """Creates an instance of the dialog by navigating directly to it."""
195 driver.get(AutofillEditAddressDialog._URL)
196 return AutofillEditAddressDialog(driver)
198 def __init__(self, driver):
200 assert self._URL == driver.current_url
201 self.dialog_elem = driver.find_element_by_id(
202 'autofill-edit-address-overlay')
204 def Fill(self, names=None, addr_line_1=None, city=None, state=None,
205 postal_code=None, country_code=None, phones=None):
206 """Fills in the given data into the appropriate fields.
208 If filling into a text field, the given value will replace the current one.
209 If filling into a list, the values will be added after all items are
212 Note: 'names', in the new autofill UI, is an array of full names. A full
213 name is an array of first, middle, last names. Example:
214 names=[['Joe', '', 'King'], ['Fred', 'W', 'Michno']]
217 names: List of names; each name should be [first, middle, last].
218 addr_line_1: First line in the address.
221 postal_code: Postal code (zip code for US).
222 country_code: Country code (e.g., US or FR).
223 phones: List of phone numbers.
225 id_dict = {'addr-line-1': addr_line_1,
228 'postal-code': postal_code}
229 for id, value in id_dict.items():
230 if value is not None:
231 TextField(self.dialog_elem.find_element_by_id(id)).Set(value)
233 list_id_dict = {'full-name-list': names,
234 'phone-list': phones}
235 for list_id, values in list_id_dict.items():
236 if values is not None:
237 list = DynamicList(self.driver,
238 self.dialog_elem.find_element_by_id(list_id))
241 list.GetPlaceholderItem().Set(value)
243 if country_code is not None:
244 self.dialog_elem.find_element_by_xpath(
245 './/*[@id="country"]/*[@value="%s"]' % country_code).click()
247 def GetStateLabel(self):
248 """Returns the label used for the state text field."""
249 return self.dialog_elem.find_element_by_id('state-label').text
251 def GetPostalCodeLabel(self):
252 """Returns the label used for the postal code text field."""
253 return self.dialog_elem.find_element_by_id('postal-code-label').text
256 """Returns a list of the phone numbers in the phones list."""
258 self.driver, self.dialog_elem.find_element_by_id('phone-list'))
259 return [item.Get()[0] for item in list.GetCommittedItems()]
262 class ContentTypes(object):
265 JAVASCRIPT = 'javascript'
266 HANDLERS = 'handlers'
269 GEOLOCATION = 'location'
270 NOTIFICATIONS = 'notifications'
271 PASSWORDS = 'passwords'
274 class Behaviors(object):
276 SESSION_ONLY = 'session_only'
281 class ContentSettingsPage(object):
282 """The overlay for managing exceptions on the Content Settings page."""
284 _URL = 'chrome://settings-frame/content'
287 def FromNavigation(driver):
288 """Creates an instance of the dialog by navigating directly to it."""
289 driver.get(ContentSettingsPage._URL)
290 return ContentSettingsPage(driver)
292 def __init__(self, driver):
293 assert self._URL == driver.current_url
294 self.page_elem = driver.find_element_by_id(
295 'content-settings-page')
297 def SetContentTypeOption(self, content_type, option):
298 """Set the option for the specified content type.
301 content_type: The content type to manage.
302 option: The option to allow, deny or ask.
304 self.page_elem.find_element_by_xpath(
305 './/*[@name="%s"][@value="%s"]' % (content_type, option)).click()
308 class ManageExceptionsPage(object):
309 """The overlay for the content exceptions page."""
312 def FromNavigation(driver, content_type):
313 """Creates an instance of the dialog by navigating directly to it.
316 driver: The remote WebDriver instance to manage some content type.
317 content_type: The content type to manage.
319 content_url = 'chrome://settings-frame/contentExceptions#%s' % content_type
320 driver.get(content_url)
321 return ManageExceptionsPage(driver, content_type)
323 def __init__(self, driver, content_type):
324 self._list_elem = driver.find_element_by_xpath(
325 './/*[@id="content-settings-exceptions-area"]'
326 '//*[@contenttype="%s"]//list[@role="list"]'
327 '[@class="settings-list"]' % content_type)
328 self._driver = driver
329 self._content_type = content_type
331 self._incognito_list_elem = driver.find_element_by_xpath(
332 './/*[@id="content-settings-exceptions-area"]'
333 '//*[@contenttype="%s"]//div[not(@hidden)]'
334 '//list[@mode="otr"][@role="list"]'
335 '[@class="settings-list"]' % content_type)
336 except selenium.common.exceptions.NoSuchElementException:
337 self._incognito_list_elem = None
339 def _AssertIncognitoAvailable(self):
340 if not self._incognito_list_elem:
341 raise AssertionError(
342 'Incognito settings in "%s" content page not available'
343 % self._content_type)
345 def _GetExceptionList(self, incognito):
347 list_elem = self._list_elem
349 list_elem = self._incognito_list_elem
350 return DynamicList(self._driver, list_elem)
352 def _GetPatternList(self, incognito):
354 list_elem = self._list_elem
356 list_elem = self._incognito_list_elem
357 pattern_list = [p.text for p in
358 list_elem.find_elements_by_xpath(
359 './/*[contains(@class, "exception-pattern")]'
360 '//*[@class="static-text"]')]
363 def AddNewException(self, pattern, behavior, incognito=False):
364 """Add a new pattern and behavior to the Exceptions page.
367 pattern: Hostname pattern string.
368 behavior: Setting for the hostname pattern (Allow, Block, Session Only).
369 incognito: Incognito list box. Display to false.
372 AssertionError when an exception cannot be added on the content page.
375 self._AssertIncognitoAvailable()
376 list_elem = self._incognito_list_elem
378 list_elem = self._list_elem
379 # Select behavior first.
381 list_elem.find_element_by_xpath(
382 './/*[@class="exception-setting"]'
383 '[not(@displaymode)]//option[@value="%s"]'
385 except selenium.common.exceptions.NoSuchElementException:
386 raise AssertionError(
387 'Adding new exception not allowed in "%s" content page'
388 % self._content_type)
390 self._GetExceptionList(incognito).GetPlaceholderItem().Set(pattern)
392 def DeleteException(self, pattern, incognito=False):
393 """Delete the exception for the selected hostname pattern.
396 pattern: Hostname pattern string.
397 incognito: Incognito list box. Default to false.
400 self._AssertIncognitoAvailable()
401 list = self._GetExceptionList(incognito)
402 items = filter(lambda item: item.Get()[0] == pattern,
403 list.GetComittedItems())
404 map(lambda item: item.Remove(self._driver), items)
406 def GetExceptions(self, incognito=False):
407 """Returns a dictionary of {pattern: behavior}.
409 Example: {'file:///*': 'block'}
412 incognito: Incognito list box. Default to false.
415 self._AssertIncognitoAvailable()
416 list_elem = self._incognito_list_elem
418 list_elem = self._list_elem
419 pattern_list = self._GetPatternList(incognito)
420 behavior_list = list_elem.find_elements_by_xpath(
421 './/*[@role="listitem"][@class="deletable-item"]'
422 '//*[@class="exception-setting"][@displaymode="static"]')
423 assert len(pattern_list) == len(behavior_list), \
424 'Number of patterns does not match the behaviors.'
425 return dict(zip(pattern_list, [b.text.lower() for b in behavior_list]))
427 def GetBehaviorForPattern(self, pattern, incognito=False):
428 """Returns the behavior for a given pattern on the Exceptions page.
431 pattern: Hostname pattern string.
432 incognito: Incognito list box. Default to false.
435 self._AssertIncognitoAvailable()
436 assert self.GetExceptions(incognito).has_key(pattern), \
437 'No displayed host name matches pattern "%s"' % pattern
438 return self.GetExceptions(incognito)[pattern]
440 def SetBehaviorForPattern(self, pattern, behavior, incognito=False):
441 """Set the behavior for the selected pattern on the Exceptions page.
444 pattern: Hostname pattern string.
445 behavior: Setting for the hostname pattern (Allow, Block, Session Only).
446 incognito: Incognito list box. Default to false.
449 AssertionError when the behavior cannot be changed on the content page.
452 self._AssertIncognitoAvailable()
453 list_elem = self._incognito_list_elem
455 list_elem = self._list_elem
456 pattern_list = self._GetPatternList(incognito)
457 listitem_list = list_elem.find_elements_by_xpath(
458 './/*[@role="listitem"][@class="deletable-item"]')
459 pattern_listitem_dict = dict(zip(pattern_list, listitem_list))
460 # Set focus to appropriate listitem.
461 listitem_elem = pattern_listitem_dict[pattern]
462 listitem_elem.click()
465 listitem_elem.find_element_by_xpath(
466 './/option[@value="%s"]' % behavior).click()
467 except selenium.common.exceptions.ElementNotVisibleException:
468 raise AssertionError(
469 'Changing the behavior is invalid for pattern '
470 '"%s" in "%s" content page' % (behavior, self._content_type))
472 pattern_elem = listitem_elem.find_element_by_tag_name('input')
473 pattern_elem.send_keys('\n')
476 class RestoreOnStartupType(object):
482 class BasicSettingsPage(object):
483 """The basic settings page."""
484 _URL = 'chrome://settings-frame/settings'
487 def FromNavigation(driver):
488 """Creates an instance of BasicSetting page by navigating to it."""
489 driver.get(BasicSettingsPage._URL)
490 return BasicSettingsPage(driver)
492 def __init__(self, driver):
493 self._driver = driver
494 assert self._URL == driver.current_url
496 def SetOnStartupOptions(self, on_startup_option):
497 """Set on-startup options.
500 on_startup_option: option types for on start up settings.
503 AssertionError when invalid startup option type is provided.
505 if on_startup_option == RestoreOnStartupType.NEW_TAB_PAGE:
506 startup_option_elem = self._driver.find_element_by_id('startup-newtab')
507 elif on_startup_option == RestoreOnStartupType.RESTORE_SESSION:
508 startup_option_elem = self._driver.find_element_by_id(
509 'startup-restore-session')
510 elif on_startup_option == RestoreOnStartupType.RESTORE_URLS:
511 startup_option_elem = self._driver.find_element_by_id(
512 'startup-show-pages')
514 raise AssertionError('Invalid value for restore start up option!')
515 startup_option_elem.click()
517 def _GoToStartupSetPages(self):
518 self._driver.find_element_by_id('startup-set-pages').click()
520 def _FillStartupURL(self, url):
521 list = DynamicList(self._driver, self._driver.find_element_by_id(
523 list.GetPlaceholderItem().Set(url + '\n')
525 def AddStartupPage(self, url):
526 """Add a startup URL.
531 self._GoToStartupSetPages()
532 self._FillStartupURL(url)
533 self._driver.find_element_by_id('startup-overlay-confirm').click()
534 self._driver.get(self._URL)
536 def UseCurrentPageForStartup(self, title_list):
537 """Use current pages and verify page url show up in settings.
540 title_list: startup web page title list.
542 self._GoToStartupSetPages()
543 self._driver.find_element_by_id('startupUseCurrentButton').click()
544 self._driver.find_element_by_id('startup-overlay-confirm').click()
545 def is_current_page_visible(driver):
546 title_elem_list = driver.find_elements_by_xpath(
547 '//*[contains(@class, "title")][text()="%s"]' % title_list[0])
548 if len(title_elem_list) == 0:
551 WebDriverWait(self._driver, 10).until(is_current_page_visible)
552 self._driver.get(self._URL)
554 def VerifyStartupURLs(self, title_list):
555 """Verify saved startup URLs appear in set page UI.
558 title_list: A list of startup page title.
561 AssertionError when start up URLs do not appear in set page UI.
563 self._GoToStartupSetPages()
564 for i in range(len(title_list)):
566 self._driver.find_element_by_xpath(
567 '//*[contains(@class, "title")][text()="%s"]' % title_list[i])
568 except selenium.common.exceptions.NoSuchElementException:
569 raise AssertionError("Current page %s did not appear as startup page."
571 self._driver.find_element_by_id('startup-overlay-cancel').click()
573 def CancelStartupURLSetting(self, url):
574 """Cancel start up URL settings.
579 self._GoToStartupSetPages()
580 self._FillStartupURL(url)
581 self._driver.find_element_by_id('startup-overlay-cancel').click()
582 self._driver.get(self._URL)
585 class PasswordsSettings(object):
586 """The overlay for managing passwords on the Content Settings page."""
588 _URL = 'chrome://settings-frame/passwords'
590 class PasswordsItem(Item):
591 """A list of passwords item web element."""
592 def _GetFields(self):
593 """Returns the field list element."""
594 return self._elem.find_elements_by_xpath('./div/*')
597 """Returns the site field value."""
598 return self._GetFields()[0].text
600 def GetUsername(self):
601 """Returns the username field value."""
602 return self._GetFields()[1].text
606 def FromNavigation(driver):
607 """Creates an instance of the dialog by navigating directly to it.
610 driver: The remote WebDriver instance to manage some content type.
612 driver.get(PasswordsSettings._URL)
613 return PasswordsSettings(driver)
615 def __init__(self, driver):
616 self._driver = driver
617 assert self._URL == driver.current_url
618 list_elem = driver.find_element_by_id('saved-passwords-list')
619 self._items_list = List(self._driver, list_elem, self.PasswordsItem)
621 def DeleteItem(self, url, username):
622 """Deletes a line entry in Passwords Content Settings.
625 url: The URL string as it appears in the UI.
626 username: The username string as it appears in the second column.
628 for password_item in self._items_list.GetItems():
629 if (password_item.GetSite() == url and
630 password_item.GetUsername() == username):
631 password_item.Remove(self._driver)
634 class CookiesAndSiteDataSettings(object):
635 """The overlay for managing cookies on the Content Settings page."""
637 _URL = 'chrome://settings-frame/cookies'
640 def FromNavigation(driver):
641 """Creates an instance of the dialog by navigating directly to it.
644 driver: The remote WebDriver instance for managing content type.
646 driver.get(CookiesAndSiteDataSettings._URL)
647 return CookiesAndSiteDataSettings(driver)
649 def __init__(self, driver):
650 self._driver = driver
651 assert self._URL == driver.current_url
652 self._list_elem = driver.find_element_by_id('cookies-list')
654 def GetSiteNameList(self):
655 """Returns a list of the site names.
657 This is a public function since the test needs to check if the site is
660 site_list = [p.text for p in
661 self._list_elem.find_elements_by_xpath(
662 './/*[contains(@class, "deletable-item")]'
663 '//div[@class="cookie-site"]')]
666 def _GetCookieNameList(self):
667 """Returns a list where each item is the list of cookie names of each site.
669 Example: site1 | cookie1 cookie2
671 site3 | cookieA cookie1 cookieB
674 A cookie names list such as:
675 [ ['cookie1', 'cookie2'], ['cookieA'], ['cookieA', 'cookie1', 'cookieB'] ]
677 cookie_name_list = []
678 for elem in self._list_elem.find_elements_by_xpath(
679 './/*[@role="listitem"]'):
681 cookie_name_list.append([c.text for c in
682 elem.find_elements_by_xpath('.//div[@class="cookie-item"]')])
683 return cookie_name_list
685 def DeleteSiteData(self, site):
686 """Delete a site entry with its cookies in cookies content settings.
689 site: The site string as it appears in the UI.
691 delete_button_list = self._list_elem.find_elements_by_class_name(
693 site_list = self.GetSiteNameList()
694 for i in range(len(site_list)):
695 if site_list[i] == site:
696 # Highlight the item so the close button shows up, then delete button
697 # shows up, then click on the delete button.
698 ActionChains(self._driver).move_to_element(
699 delete_button_list[i]).click().perform()