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. 4 5import types 6 7import selenium.common.exceptions 8from selenium.webdriver.common.action_chains import ActionChains 9from selenium.webdriver.support.ui import WebDriverWait 10 11 12def _FocusField(driver, list_elem, field_elem): 13 """Focuses a field in a dynamic list. 14 15 Note, the item containing the field should not be focused already. 16 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. 21 See crbug.com/97369. 22 23 Args: 24 list_elem: An element in the HTML list. 25 field_elem: An element in the HTML text field. 26 27 Raises: 28 RuntimeError: If a timeout occurs when waiting for the focus event. 29 """ 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 34 # already has focus. 35 # Ideally this should be fixed in the page. 36 37 correct_focus_script = """ 38 (function(listElem, itemElem, callback) { 39 if (document.activeElement == itemElem) { 40 callback(); 41 return; 42 } 43 itemElem.focus(); 44 listElem.focus(); 45 itemElem.addEventListener("focus", callback); 46 }).apply(null, arguments); 47 """ 48 driver.set_script_timeout(5) 49 try: 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) 53 54 55class Item(object): 56 """A list item web element.""" 57 def __init__(self, elem): 58 self._elem = elem 59 60 def Remove(self, driver): 61 button = self._elem.find_element_by_xpath('./button') 62 ActionChains(driver).move_to_element(button).click().perform() 63 64 65class TextFieldsItem(Item): 66 """An item consisting only of text fields.""" 67 def _GetFields(self): 68 """Returns the text fields list.""" 69 return self._elem.find_elements_by_tag_name('input') 70 71 def Set(self, values): 72 """Sets the value(s) of the item's text field(s). 73 74 Args: 75 values: The new value or the list of the new values of the fields. 76 """ 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 82 value_list = values 83 else: 84 value_list = [values] 85 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): 91 field.clear() 92 field.send_keys(value) 93 field_list[-1].send_keys('\n') # press enter on the last field. 94 95 def Get(self): 96 """Returns the list of the text field values.""" 97 return map(lambda f: f.get_attribute('value'), self._GetFields()) 98 99 100class TextField(object): 101 """A text field web element.""" 102 def __init__(self, elem): 103 self._elem = elem 104 105 def Set(self, value): 106 """Sets the value of the text field. 107 108 Args: 109 value: The new value of the field. 110 """ 111 self._elem.clear() 112 self._elem.send_keys(value) 113 114 def Get(self): 115 """Returns the value of the text field.""" 116 return self._elem.get_attribute('value') 117 118 119class List(object): 120 """A web element that holds a list of items.""" 121 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 126 self._elem = elem 127 self._item_class = item_class 128 129 def RemoveAll(self): 130 """Removes all items from the list. 131 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. 134 """ 135 for i in range(len(self.GetItems())): 136 self.GetItems()[0].Remove(self._driver) 137 138 def GetItems(self): 139 """Returns all the items that are in the list.""" 140 items = self._GetItemElems() 141 return map(lambda x: self._item_class(x), items) 142 143 def GetSize(self): 144 """Returns the number of items in the list.""" 145 return len(self._GetItemElems()) 146 147 def _GetItemElems(self): 148 return self._elem.find_elements_by_xpath('.//*[@role="listitem"]') 149 150 151class DynamicList(List): 152 """A web element that holds a dynamic list of items of text fields. 153 154 Terminology: 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 158 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 162 more text fields. 163 """ 164 165 def __init__(self, driver, elem, item_class=TextFieldsItem): 166 return super(DynamicList, self).__init__( 167 driver, elem, item_class=item_class) 168 169 def GetPlaceholderItem(self): 170 return self.GetItems()[-1] 171 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()) 175 176 def GetSize(self): 177 """Returns the number of items in the list, excluding the placeholder.""" 178 return len(self._GetCommittedItemElems()) 179 180 def _GetCommittedItemElems(self): 181 return self._GetItemElems()[:-1] 182 183 def _GetPlaceholderElem(self): 184 return self._GetItemElems()[-1] 185 186 187class AutofillEditAddressDialog(object): 188 """The overlay for editing an autofill address.""" 189 190 _URL = 'chrome://settings-frame/autofillEditAddress' 191 192 @staticmethod 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) 197 198 def __init__(self, driver): 199 self.driver = driver 200 assert self._URL == driver.current_url 201 self.dialog_elem = driver.find_element_by_id( 202 'autofill-edit-address-overlay') 203 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. 207 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 210 deleted. 211 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']] 215 216 Args: 217 names: List of names; each name should be [first, middle, last]. 218 addr_line_1: First line in the address. 219 city: City. 220 state: State. 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. 224 """ 225 id_dict = {'addr-line-1': addr_line_1, 226 'city': city, 227 'state': state, 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) 232 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)) 239 list.RemoveAll() 240 for value in values: 241 list.GetPlaceholderItem().Set(value) 242 243 if country_code is not None: 244 self.dialog_elem.find_element_by_xpath( 245 './/*[@id="country"]/*[@value="%s"]' % country_code).click() 246 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 250 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 254 255 def GetPhones(self): 256 """Returns a list of the phone numbers in the phones list.""" 257 list = DynamicList( 258 self.driver, self.dialog_elem.find_element_by_id('phone-list')) 259 return [item.Get()[0] for item in list.GetCommittedItems()] 260 261 262class ContentTypes(object): 263 COOKIES = 'cookies' 264 IMAGES = 'images' 265 JAVASCRIPT = 'javascript' 266 HANDLERS = 'handlers' 267 PLUGINS = 'plugins' 268 POPUPS = 'popups' 269 GEOLOCATION = 'location' 270 NOTIFICATIONS = 'notifications' 271 PASSWORDS = 'passwords' 272 273 274class Behaviors(object): 275 ALLOW = 'allow' 276 SESSION_ONLY = 'session_only' 277 ASK = 'ask' 278 BLOCK = 'block' 279 280 281class ContentSettingsPage(object): 282 """The overlay for managing exceptions on the Content Settings page.""" 283 284 _URL = 'chrome://settings-frame/content' 285 286 @staticmethod 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) 291 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') 296 297 def SetContentTypeOption(self, content_type, option): 298 """Set the option for the specified content type. 299 300 Args: 301 content_type: The content type to manage. 302 option: The option to allow, deny or ask. 303 """ 304 self.page_elem.find_element_by_xpath( 305 './/*[@name="%s"][@value="%s"]' % (content_type, option)).click() 306 307 308class ManageExceptionsPage(object): 309 """The overlay for the content exceptions page.""" 310 311 @staticmethod 312 def FromNavigation(driver, content_type): 313 """Creates an instance of the dialog by navigating directly to it. 314 315 Args: 316 driver: The remote WebDriver instance to manage some content type. 317 content_type: The content type to manage. 318 """ 319 content_url = 'chrome://settings-frame/contentExceptions#%s' % content_type 320 driver.get(content_url) 321 return ManageExceptionsPage(driver, content_type) 322 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 330 try: 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 338 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) 344 345 def _GetExceptionList(self, incognito): 346 if not incognito: 347 list_elem = self._list_elem 348 else: 349 list_elem = self._incognito_list_elem 350 return DynamicList(self._driver, list_elem) 351 352 def _GetPatternList(self, incognito): 353 if not incognito: 354 list_elem = self._list_elem 355 else: 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"]')] 361 return pattern_list 362 363 def AddNewException(self, pattern, behavior, incognito=False): 364 """Add a new pattern and behavior to the Exceptions page. 365 366 Args: 367 pattern: Hostname pattern string. 368 behavior: Setting for the hostname pattern (Allow, Block, Session Only). 369 incognito: Incognito list box. Display to false. 370 371 Raises: 372 AssertionError when an exception cannot be added on the content page. 373 """ 374 if incognito: 375 self._AssertIncognitoAvailable() 376 list_elem = self._incognito_list_elem 377 else: 378 list_elem = self._list_elem 379 # Select behavior first. 380 try: 381 list_elem.find_element_by_xpath( 382 './/*[@class="exception-setting"]' 383 '[not(@displaymode)]//option[@value="%s"]' 384 % behavior).click() 385 except selenium.common.exceptions.NoSuchElementException: 386 raise AssertionError( 387 'Adding new exception not allowed in "%s" content page' 388 % self._content_type) 389 # Set pattern now. 390 self._GetExceptionList(incognito).GetPlaceholderItem().Set(pattern) 391 392 def DeleteException(self, pattern, incognito=False): 393 """Delete the exception for the selected hostname pattern. 394 395 Args: 396 pattern: Hostname pattern string. 397 incognito: Incognito list box. Default to false. 398 """ 399 if incognito: 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) 405 406 def GetExceptions(self, incognito=False): 407 """Returns a dictionary of {pattern: behavior}. 408 409 Example: {'file:///*': 'block'} 410 411 Args: 412 incognito: Incognito list box. Default to false. 413 """ 414 if incognito: 415 self._AssertIncognitoAvailable() 416 list_elem = self._incognito_list_elem 417 else: 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])) 426 427 def GetBehaviorForPattern(self, pattern, incognito=False): 428 """Returns the behavior for a given pattern on the Exceptions page. 429 430 Args: 431 pattern: Hostname pattern string. 432 incognito: Incognito list box. Default to false. 433 """ 434 if incognito: 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] 439 440 def SetBehaviorForPattern(self, pattern, behavior, incognito=False): 441 """Set the behavior for the selected pattern on the Exceptions page. 442 443 Args: 444 pattern: Hostname pattern string. 445 behavior: Setting for the hostname pattern (Allow, Block, Session Only). 446 incognito: Incognito list box. Default to false. 447 448 Raises: 449 AssertionError when the behavior cannot be changed on the content page. 450 """ 451 if incognito: 452 self._AssertIncognitoAvailable() 453 list_elem = self._incognito_list_elem 454 else: 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() 463 # Set behavior. 464 try: 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)) 471 # Send enter key. 472 pattern_elem = listitem_elem.find_element_by_tag_name('input') 473 pattern_elem.send_keys('\n') 474 475 476class RestoreOnStartupType(object): 477 NEW_TAB_PAGE = 5 478 RESTORE_SESSION = 1 479 RESTORE_URLS = 4 480 481 482class BasicSettingsPage(object): 483 """The basic settings page.""" 484 _URL = 'chrome://settings-frame/settings' 485 486 @staticmethod 487 def FromNavigation(driver): 488 """Creates an instance of BasicSetting page by navigating to it.""" 489 driver.get(BasicSettingsPage._URL) 490 return BasicSettingsPage(driver) 491 492 def __init__(self, driver): 493 self._driver = driver 494 assert self._URL == driver.current_url 495 496 def SetOnStartupOptions(self, on_startup_option): 497 """Set on-startup options. 498 499 Args: 500 on_startup_option: option types for on start up settings. 501 502 Raises: 503 AssertionError when invalid startup option type is provided. 504 """ 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') 513 else: 514 raise AssertionError('Invalid value for restore start up option!') 515 startup_option_elem.click() 516 517 def _GoToStartupSetPages(self): 518 self._driver.find_element_by_id('startup-set-pages').click() 519 520 def _FillStartupURL(self, url): 521 list = DynamicList(self._driver, self._driver.find_element_by_id( 522 'startupPagesList')) 523 list.GetPlaceholderItem().Set(url + '\n') 524 525 def AddStartupPage(self, url): 526 """Add a startup URL. 527 528 Args: 529 url: A startup url. 530 """ 531 self._GoToStartupSetPages() 532 self._FillStartupURL(url) 533 self._driver.find_element_by_id('startup-overlay-confirm').click() 534 self._driver.get(self._URL) 535 536 def UseCurrentPageForStartup(self, title_list): 537 """Use current pages and verify page url show up in settings. 538 539 Args: 540 title_list: startup web page title list. 541 """ 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: 549 return False 550 return True 551 WebDriverWait(self._driver, 10).until(is_current_page_visible) 552 self._driver.get(self._URL) 553 554 def VerifyStartupURLs(self, title_list): 555 """Verify saved startup URLs appear in set page UI. 556 557 Args: 558 title_list: A list of startup page title. 559 560 Raises: 561 AssertionError when start up URLs do not appear in set page UI. 562 """ 563 self._GoToStartupSetPages() 564 for i in range(len(title_list)): 565 try: 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." 570 % title_list[i]) 571 self._driver.find_element_by_id('startup-overlay-cancel').click() 572 573 def CancelStartupURLSetting(self, url): 574 """Cancel start up URL settings. 575 576 Args: 577 url: A startup url. 578 """ 579 self._GoToStartupSetPages() 580 self._FillStartupURL(url) 581 self._driver.find_element_by_id('startup-overlay-cancel').click() 582 self._driver.get(self._URL) 583 584 585class PasswordsSettings(object): 586 """The overlay for managing passwords on the Content Settings page.""" 587 588 _URL = 'chrome://settings-frame/passwords' 589 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/*') 595 596 def GetSite(self): 597 """Returns the site field value.""" 598 return self._GetFields()[0].text 599 600 def GetUsername(self): 601 """Returns the username field value.""" 602 return self._GetFields()[1].text 603 604 605 @staticmethod 606 def FromNavigation(driver): 607 """Creates an instance of the dialog by navigating directly to it. 608 609 Args: 610 driver: The remote WebDriver instance to manage some content type. 611 """ 612 driver.get(PasswordsSettings._URL) 613 return PasswordsSettings(driver) 614 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) 620 621 def DeleteItem(self, url, username): 622 """Deletes a line entry in Passwords Content Settings. 623 624 Args: 625 url: The URL string as it appears in the UI. 626 username: The username string as it appears in the second column. 627 """ 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) 632 633 634class CookiesAndSiteDataSettings(object): 635 """The overlay for managing cookies on the Content Settings page.""" 636 637 _URL = 'chrome://settings-frame/cookies' 638 639 @staticmethod 640 def FromNavigation(driver): 641 """Creates an instance of the dialog by navigating directly to it. 642 643 Args: 644 driver: The remote WebDriver instance for managing content type. 645 """ 646 driver.get(CookiesAndSiteDataSettings._URL) 647 return CookiesAndSiteDataSettings(driver) 648 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') 653 654 def GetSiteNameList(self): 655 """Returns a list of the site names. 656 657 This is a public function since the test needs to check if the site is 658 deleted. 659 """ 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"]')] 664 return site_list 665 666 def _GetCookieNameList(self): 667 """Returns a list where each item is the list of cookie names of each site. 668 669 Example: site1 | cookie1 cookie2 670 site2 | cookieA 671 site3 | cookieA cookie1 cookieB 672 673 Returns: 674 A cookie names list such as: 675 [ ['cookie1', 'cookie2'], ['cookieA'], ['cookieA', 'cookie1', 'cookieB'] ] 676 """ 677 cookie_name_list = [] 678 for elem in self._list_elem.find_elements_by_xpath( 679 './/*[@role="listitem"]'): 680 elem.click() 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 684 685 def DeleteSiteData(self, site): 686 """Delete a site entry with its cookies in cookies content settings. 687 688 Args: 689 site: The site string as it appears in the UI. 690 """ 691 delete_button_list = self._list_elem.find_elements_by_class_name( 692 'row-delete-button') 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() 700