• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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