Home | History | Annotate | Download | only in pyautolib
      1 #!/usr/bin/env python
      2 # Copyright 2013 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """PyAuto: Python Interface to Chromium's Automation Proxy.
      7 
      8 PyAuto uses swig to expose Automation Proxy interfaces to Python.
      9 For complete documentation on the functionality available,
     10 run pydoc on this file.
     11 
     12 Ref: http://dev.chromium.org/developers/testing/pyauto
     13 
     14 
     15 Include the following in your PyAuto test script to make it run standalone.
     16 
     17 from pyauto import Main
     18 
     19 if __name__ == '__main__':
     20   Main()
     21 
     22 This script can be used as an executable to fire off other scripts, similar
     23 to unittest.py
     24   python pyauto.py test_script
     25 """
     26 
     27 import cStringIO
     28 import copy
     29 import functools
     30 import hashlib
     31 import inspect
     32 import logging
     33 import optparse
     34 import os
     35 import pickle
     36 import pprint
     37 import re
     38 import shutil
     39 import signal
     40 import socket
     41 import stat
     42 import string
     43 import subprocess
     44 import sys
     45 import tempfile
     46 import time
     47 import types
     48 import unittest
     49 import urllib
     50 
     51 import pyauto_paths
     52 
     53 
     54 def _LocateBinDirs():
     55   """Setup a few dirs where we expect to find dependency libraries."""
     56   deps_dirs = [
     57       os.path.dirname(__file__),
     58       pyauto_paths.GetThirdPartyDir(),
     59       os.path.join(pyauto_paths.GetThirdPartyDir(), 'webdriver', 'pylib'),
     60   ]
     61   sys.path += map(os.path.normpath, pyauto_paths.GetBuildDirs() + deps_dirs)
     62 
     63 _LocateBinDirs()
     64 
     65 _PYAUTO_DOC_URL = 'http://dev.chromium.org/developers/testing/pyauto'
     66 
     67 try:
     68   import pyautolib
     69   # Needed so that all additional classes (like: FilePath, GURL) exposed by
     70   # swig interface get available in this module.
     71   from pyautolib import *
     72 except ImportError:
     73   print >>sys.stderr, 'Could not locate pyautolib shared libraries.  ' \
     74                       'Did you build?\n  Documentation: %s' % _PYAUTO_DOC_URL
     75   # Mac requires python2.5 even when not the default 'python' (e.g. 10.6)
     76   if 'darwin' == sys.platform and sys.version_info[:2] != (2,5):
     77     print  >>sys.stderr, '*\n* Perhaps use "python2.5", not "python" ?\n*'
     78   raise
     79 
     80 # Should go after sys.path is set appropriately
     81 import bookmark_model
     82 import download_info
     83 import history_info
     84 import omnibox_info
     85 import plugins_info
     86 import prefs_info
     87 from pyauto_errors import AutomationCommandFail
     88 from pyauto_errors import AutomationCommandTimeout
     89 from pyauto_errors import JavascriptRuntimeError
     90 from pyauto_errors import JSONInterfaceError
     91 from pyauto_errors import NTPThumbnailNotShownError
     92 import pyauto_utils
     93 import simplejson as json  # found in third_party
     94 
     95 _CHROME_DRIVER_FACTORY = None
     96 _DEFAULT_AUTOMATION_TIMEOUT = 45
     97 _HTTP_SERVER = None
     98 _REMOTE_PROXY = None
     99 _OPTIONS = None
    100 _BROWSER_PID = None
    101 
    102 class PyUITest(pyautolib.PyUITestBase, unittest.TestCase):
    103   """Base class for UI Test Cases in Python.
    104 
    105   A browser is created before executing each test, and is destroyed after
    106   each test irrespective of whether the test passed or failed.
    107 
    108   You should derive from this class and create methods with 'test' prefix,
    109   and use methods inherited from PyUITestBase (the C++ side).
    110 
    111   Example:
    112 
    113     class MyTest(PyUITest):
    114 
    115       def testNavigation(self):
    116         self.NavigateToURL("http://www.google.com")
    117         self.assertEqual("Google", self.GetActiveTabTitle())
    118   """
    119 
    120   def __init__(self, methodName='runTest', **kwargs):
    121     """Initialize PyUITest.
    122 
    123     When redefining __init__ in a derived class, make sure that:
    124       o you make a call this __init__
    125       o __init__ takes methodName as an arg. this is mandated by unittest module
    126 
    127     Args:
    128       methodName: the default method name. Internal use by unittest module
    129 
    130       (The rest of the args can be in any order. They can even be skipped in
    131        which case the defaults will be used.)
    132 
    133       clear_profile: If True, clean the profile dir before use. Defaults to True
    134       homepage: the home page. Defaults to "about:blank"
    135     """
    136     # Fetch provided keyword args, or fill in defaults.
    137     clear_profile = kwargs.get('clear_profile', True)
    138     homepage = kwargs.get('homepage', 'about:blank')
    139     self._automation_timeout = _DEFAULT_AUTOMATION_TIMEOUT * 1000
    140 
    141     pyautolib.PyUITestBase.__init__(self, clear_profile, homepage)
    142     self.Initialize(pyautolib.FilePath(self.BrowserPath()))
    143     unittest.TestCase.__init__(self, methodName)
    144 
    145     # Give all pyauto tests easy access to pprint.PrettyPrinter functions.
    146     self.pprint = pprint.pprint
    147     self.pformat = pprint.pformat
    148 
    149     # Set up remote proxies, if they were requested.
    150     self.remotes = []
    151     self.remote = None
    152     global _REMOTE_PROXY
    153     if _REMOTE_PROXY:
    154       self.remotes = _REMOTE_PROXY
    155       self.remote = _REMOTE_PROXY[0]
    156 
    157   def __del__(self):
    158     pyautolib.PyUITestBase.__del__(self)
    159 
    160   def _SetExtraChromeFlags(self):
    161     """Prepares the browser to launch with the specified extra Chrome flags.
    162 
    163     This function is called right before the browser is launched for the first
    164     time.
    165     """
    166     for flag in self.ExtraChromeFlags():
    167       if flag.startswith('--'):
    168         flag = flag[2:]
    169       split_pos = flag.find('=')
    170       if split_pos >= 0:
    171         flag_name = flag[:split_pos]
    172         flag_val = flag[split_pos + 1:]
    173         self.AppendBrowserLaunchSwitch(flag_name, flag_val)
    174       else:
    175         self.AppendBrowserLaunchSwitch(flag)
    176 
    177   def __SetUp(self):
    178     named_channel_id = None
    179     if _OPTIONS:
    180       named_channel_id = _OPTIONS.channel_id
    181     if self.IsChromeOS():  # Enable testing interface on ChromeOS.
    182       if self.get_clear_profile():
    183         self.CleanupBrowserProfileOnChromeOS()
    184       self.EnableCrashReportingOnChromeOS()
    185       if not named_channel_id:
    186         named_channel_id = self.EnableChromeTestingOnChromeOS()
    187     else:
    188       self._SetExtraChromeFlags()  # Flags already previously set for ChromeOS.
    189     if named_channel_id:
    190       self._named_channel_id = named_channel_id
    191       self.UseNamedChannelID(named_channel_id)
    192     # Initialize automation and fire the browser (does not fire the browser
    193     # on ChromeOS).
    194     self.SetUp()
    195 
    196     global _BROWSER_PID
    197     try:
    198       _BROWSER_PID = self.GetBrowserInfo()['browser_pid']
    199     except JSONInterfaceError:
    200       raise JSONInterfaceError('Unable to get browser_pid over automation '
    201                                'channel on first attempt.  Something went very '
    202                                'wrong.  Chrome probably did not launch.')
    203 
    204     # Forcibly trigger all plugins to get registered.  crbug.com/94123
    205     # Sometimes flash files loaded too quickly after firing browser
    206     # ends up getting downloaded, which seems to indicate that the plugin
    207     # hasn't been registered yet.
    208     if not self.IsChromeOS():
    209       self.GetPluginsInfo()
    210 
    211     # TODO(dtu): Remove this after crosbug.com/4558 is fixed.
    212     if self.IsChromeOS():
    213       self.WaitUntil(lambda: not self.GetNetworkInfo()['offline_mode'])
    214 
    215     if (self.IsChromeOS() and not self.GetLoginInfo()['is_logged_in'] and
    216         self.ShouldOOBESkipToLogin()):
    217       if self.GetOOBEScreenInfo()['screen_name'] != 'login':
    218         self.SkipToLogin()
    219       if self.ShouldAutoLogin():
    220         # Login with default creds.
    221         sys.path.append('/usr/local')  # to import autotest libs
    222         from autotest.cros import constants
    223         creds = constants.CREDENTIALS['$default']
    224         self.Login(creds[0], creds[1])
    225         assert self.GetLoginInfo()['is_logged_in']
    226         logging.info('Logged in as %s.' % creds[0])
    227 
    228     # If we are connected to any RemoteHosts, create PyAuto
    229     # instances on the remote sides and set them up too.
    230     for remote in self.remotes:
    231       remote.CreateTarget(self)
    232       remote.setUp()
    233 
    234   def setUp(self):
    235     """Override this method to launch browser differently.
    236 
    237     Can be used to prevent launching the browser window by default in case a
    238     test wants to do some additional setup before firing browser.
    239 
    240     When using the named interface, it connects to an existing browser
    241     instance.
    242 
    243     On ChromeOS, a browser showing the login window is started. Tests can
    244     initiate a user session by calling Login() or LoginAsGuest(). Cryptohome
    245     vaults or flimflam profiles left over by previous tests can be cleared by
    246     calling RemoveAllCryptohomeVaults() respectively CleanFlimflamDirs() before
    247     logging in to improve isolation. Note that clearing flimflam profiles
    248     requires a flimflam restart, briefly taking down network connectivity and
    249     slowing down the test. This should be done for tests that use flimflam only.
    250     """
    251     self.__SetUp()
    252 
    253   def tearDown(self):
    254     for remote in self.remotes:
    255       remote.tearDown()
    256 
    257     self.TearDown()  # Destroy browser
    258 
    259   # Method required by the Python standard library unittest.TestCase.
    260   def runTest(self):
    261     pass
    262 
    263   @staticmethod
    264   def BrowserPath():
    265     """Returns the path to Chromium binaries.
    266 
    267     Expects the browser binaries to be in the
    268     same location as the pyautolib binaries.
    269     """
    270     return os.path.normpath(os.path.dirname(pyautolib.__file__))
    271 
    272   def ExtraChromeFlags(self):
    273     """Return a list of extra chrome flags to use with Chrome for testing.
    274 
    275     These are flags needed to facilitate testing.  Override this function to
    276     use a custom set of Chrome flags.
    277     """
    278     auth_ext_path = ('/usr/local/autotest/deps/pyauto_dep/' +
    279         'test_src/chrome/browser/resources/gaia_auth')
    280     if self.IsChromeOS():
    281       return [
    282         '--homepage=about:blank',
    283         '--allow-file-access',
    284         '--allow-file-access-from-files',
    285         '--enable-file-cookies',
    286         '--disable-default-apps',
    287         '--dom-automation',
    288         '--skip-oauth-login',
    289         # Enables injection of test content script for webui login automation
    290         '--auth-ext-path=%s' % auth_ext_path,
    291         # Enable automation provider, chromeos net and chromeos login logs
    292         '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2,' +
    293             '*/chromeos/login/*=2',
    294       ]
    295     else:
    296       return []
    297 
    298   def ShouldOOBESkipToLogin(self):
    299     """Determine if we should skip the OOBE flow on ChromeOS.
    300 
    301     This makes automation skip the OOBE flow during setUp() and land directly
    302     to the login screen. Applies only if not logged in already.
    303 
    304     Override and return False if OOBE flow is required, for OOBE tests, for
    305     example. Calling this function directly will have no effect.
    306 
    307     Returns:
    308       True, if the OOBE should be skipped and automation should
    309             go to the 'Add user' login screen directly
    310       False, if the OOBE should not be skipped.
    311     """
    312     assert self.IsChromeOS()
    313     return True
    314 
    315   def ShouldAutoLogin(self):
    316     """Determine if we should auto-login on ChromeOS at browser startup.
    317 
    318     To be used for tests that expect user to be logged in before running test,
    319     without caring which user. ShouldOOBESkipToLogin() should return True
    320     for this to take effect.
    321 
    322     Override and return False to not auto login, for tests where login is part
    323     of the use case.
    324 
    325     Returns:
    326       True, if chrome should auto login after startup.
    327       False, otherwise.
    328     """
    329     assert self.IsChromeOS()
    330     return True
    331 
    332   def CloseChromeOnChromeOS(self):
    333     """Gracefully exit chrome on ChromeOS."""
    334 
    335     def _GetListOfChromePids():
    336       """Retrieves the list of currently-running Chrome process IDs.
    337 
    338       Returns:
    339         A list of strings, where each string represents a currently-running
    340         'chrome' process ID.
    341       """
    342       proc = subprocess.Popen(['pgrep', '^chrome$'], stdout=subprocess.PIPE)
    343       proc.wait()
    344       return [x.strip() for x in proc.stdout.readlines()]
    345 
    346     orig_pids = _GetListOfChromePids()
    347     subprocess.call(['pkill', '^chrome$'])
    348 
    349     def _AreOrigPidsDead(orig_pids):
    350       """Determines whether all originally-running 'chrome' processes are dead.
    351 
    352       Args:
    353         orig_pids: A list of strings, where each string represents the PID for
    354                    an originally-running 'chrome' process.
    355 
    356       Returns:
    357         True, if all originally-running 'chrome' processes have been killed, or
    358         False otherwise.
    359       """
    360       for new_pid in _GetListOfChromePids():
    361         if new_pid in orig_pids:
    362           return False
    363       return True
    364 
    365     self.WaitUntil(lambda: _AreOrigPidsDead(orig_pids))
    366 
    367   @staticmethod
    368   def _IsRootSuid(path):
    369     """Determine if |path| is a suid-root file."""
    370     return os.path.isfile(path) and (os.stat(path).st_mode & stat.S_ISUID)
    371 
    372   @staticmethod
    373   def SuidPythonPath():
    374     """Path to suid_python binary on ChromeOS.
    375 
    376     This is typically in the same directory as pyautolib.py
    377     """
    378     return os.path.join(PyUITest.BrowserPath(), 'suid-python')
    379 
    380   @staticmethod
    381   def RunSuperuserActionOnChromeOS(action):
    382     """Run the given action with superuser privs (on ChromeOS).
    383 
    384     Uses the suid_actions.py script.
    385 
    386     Args:
    387       action: An action to perform.
    388               See suid_actions.py for available options.
    389 
    390     Returns:
    391       (stdout, stderr)
    392     """
    393     assert PyUITest._IsRootSuid(PyUITest.SuidPythonPath()), \
    394         'Did not find suid-root python at %s' % PyUITest.SuidPythonPath()
    395     file_path = os.path.join(os.path.dirname(__file__), 'chromeos',
    396                              'suid_actions.py')
    397     args = [PyUITest.SuidPythonPath(), file_path, '--action=%s' % action]
    398     proc = subprocess.Popen(
    399         args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    400     stdout, stderr = proc.communicate()
    401     return (stdout, stderr)
    402 
    403   def EnableChromeTestingOnChromeOS(self):
    404     """Enables the named automation interface on chromeos.
    405 
    406     Restarts chrome so that you get a fresh instance.
    407     Also sets some testing-friendly flags for chrome.
    408 
    409     Expects suid python to be present in the same dir as pyautolib.py
    410     """
    411     assert PyUITest._IsRootSuid(self.SuidPythonPath()), \
    412         'Did not find suid-root python at %s' % self.SuidPythonPath()
    413     file_path = os.path.join(os.path.dirname(__file__), 'chromeos',
    414                              'enable_testing.py')
    415     args = [self.SuidPythonPath(), file_path]
    416     # Pass extra chrome flags for testing
    417     for flag in self.ExtraChromeFlags():
    418       args.append('--extra-chrome-flags=%s' % flag)
    419     assert self.WaitUntil(lambda: self._IsSessionManagerReady(0))
    420     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
    421     automation_channel_path = proc.communicate()[0].strip()
    422     assert len(automation_channel_path), 'Could not enable testing interface'
    423     return automation_channel_path
    424 
    425   @staticmethod
    426   def EnableCrashReportingOnChromeOS():
    427     """Enables crash reporting on ChromeOS.
    428 
    429     Writes the "/home/chronos/Consent To Send Stats" file with a 32-char
    430     readable string.  See comment in session_manager_setup.sh which does this
    431     too.
    432 
    433     Note that crash reporting will work only if breakpad is built in, ie in a
    434     'Google Chrome' build (not Chromium).
    435     """
    436     consent_file = '/home/chronos/Consent To Send Stats'
    437     def _HasValidConsentFile():
    438       if not os.path.isfile(consent_file):
    439         return False
    440       stat = os.stat(consent_file)
    441       return (len(open(consent_file).read()) and
    442               (1000, 1000) == (stat.st_uid, stat.st_gid))
    443     if not _HasValidConsentFile():
    444       client_id = hashlib.md5('abcdefgh').hexdigest()
    445       # Consent file creation and chown to chronos needs to be atomic
    446       # to avoid races with the session_manager.  crosbug.com/18413
    447       # Therefore, create a temp file, chown, then rename it as consent file.
    448       temp_file = consent_file + '.tmp'
    449       open(temp_file, 'w').write(client_id)
    450       # This file must be owned by chronos:chronos!
    451       os.chown(temp_file, 1000, 1000);
    452       shutil.move(temp_file, consent_file)
    453     assert _HasValidConsentFile(), 'Could not create %s' % consent_file
    454 
    455   @staticmethod
    456   def _IsSessionManagerReady(old_pid):
    457     """Is the ChromeOS session_manager running and ready to accept DBus calls?
    458 
    459     Called after session_manager is killed to know when it has restarted.
    460 
    461     Args:
    462       old_pid: The pid that session_manager had before it was killed,
    463                to ensure that we don't look at the DBus interface
    464                of an old session_manager process.
    465     """
    466     pgrep_process = subprocess.Popen(['pgrep', 'session_manager'],
    467                                      stdout=subprocess.PIPE)
    468     new_pid = pgrep_process.communicate()[0].strip()
    469     if not new_pid or old_pid == new_pid:
    470       return False
    471 
    472     import dbus
    473     try:
    474       bus = dbus.SystemBus()
    475       proxy = bus.get_object('org.chromium.SessionManager',
    476                              '/org/chromium/SessionManager')
    477       dbus.Interface(proxy, 'org.chromium.SessionManagerInterface')
    478     except dbus.DBusException:
    479       return False
    480     return True
    481 
    482   @staticmethod
    483   def CleanupBrowserProfileOnChromeOS():
    484     """Cleanup browser profile dir on ChromeOS.
    485 
    486     This does not clear cryptohome.
    487 
    488     Browser should not be running, or else there will be locked files.
    489     """
    490     profile_dir = '/home/chronos/user'
    491     for item in os.listdir(profile_dir):
    492       # Deleting .pki causes stateful partition to get erased.
    493       if item not in ['log', 'flimflam'] and not item.startswith('.'):
    494          pyauto_utils.RemovePath(os.path.join(profile_dir, item))
    495 
    496     chronos_dir = '/home/chronos'
    497     for item in os.listdir(chronos_dir):
    498       if item != 'user' and not item.startswith('.'):
    499         pyauto_utils.RemovePath(os.path.join(chronos_dir, item))
    500 
    501   @staticmethod
    502   def CleanupFlimflamDirsOnChromeOS():
    503     """Clean the contents of flimflam profiles and restart flimflam."""
    504     PyUITest.RunSuperuserActionOnChromeOS('CleanFlimflamDirs')
    505 
    506   @staticmethod
    507   def RemoveAllCryptohomeVaultsOnChromeOS():
    508     """Remove any existing cryptohome vaults."""
    509     PyUITest.RunSuperuserActionOnChromeOS('RemoveAllCryptohomeVaults')
    510 
    511   @staticmethod
    512   def _IsInodeNew(path, old_inode):
    513     """Determine whether an inode has changed. POSIX only.
    514 
    515     Args:
    516       path: The file path to check for changes.
    517       old_inode: The old inode number.
    518 
    519     Returns:
    520       True if the path exists and its inode number is different from old_inode.
    521       False otherwise.
    522     """
    523     try:
    524       stat_result = os.stat(path)
    525     except OSError:
    526       return False
    527     if not stat_result:
    528       return False
    529     return stat_result.st_ino != old_inode
    530 
    531   def RestartBrowser(self, clear_profile=True, pre_launch_hook=None):
    532     """Restart the browser.
    533 
    534     For use with tests that require to restart the browser.
    535 
    536     Args:
    537       clear_profile: If True, the browser profile is cleared before restart.
    538                      Defaults to True, that is restarts browser with a clean
    539                      profile.
    540       pre_launch_hook: If specified, must be a callable that is invoked before
    541                        the browser is started again. Not supported in ChromeOS.
    542     """
    543     if self.IsChromeOS():
    544       assert pre_launch_hook is None, 'Not supported in ChromeOS'
    545       self.TearDown()
    546       if clear_profile:
    547         self.CleanupBrowserProfileOnChromeOS()
    548       self.CloseChromeOnChromeOS()
    549       self.EnableChromeTestingOnChromeOS()
    550       self.SetUp()
    551       return
    552     # Not chromeos
    553     orig_clear_state = self.get_clear_profile()
    554     self.CloseBrowserAndServer()
    555     self.set_clear_profile(clear_profile)
    556     if pre_launch_hook:
    557       pre_launch_hook()
    558     logging.debug('Restarting browser with clear_profile=%s',
    559                   self.get_clear_profile())
    560     self.LaunchBrowserAndServer()
    561     self.set_clear_profile(orig_clear_state)  # Reset to original state.
    562 
    563   @staticmethod
    564   def DataDir():
    565     """Returns the path to the data dir chrome/test/data."""
    566     return os.path.normpath(
    567         os.path.join(os.path.dirname(__file__), os.pardir, "data"))
    568 
    569   @staticmethod
    570   def ChromeOSDataDir():
    571     """Returns the path to the data dir chromeos/test/data."""
    572     return os.path.normpath(
    573         os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
    574                      "chromeos", "test", "data"))
    575 
    576   @staticmethod
    577   def GetFileURLForPath(*path):
    578     """Get file:// url for the given path.
    579 
    580     Also quotes the url using urllib.quote().
    581 
    582     Args:
    583       path: Variable number of strings that can be joined.
    584     """
    585     path_str = os.path.join(*path)
    586     abs_path = os.path.abspath(path_str)
    587     if sys.platform == 'win32':
    588       # Don't quote the ':' in drive letter ( say, C: ) on win.
    589       # Also, replace '\' with '/' as expected in a file:/// url.
    590       drive, rest = os.path.splitdrive(abs_path)
    591       quoted_path = drive.upper() + urllib.quote((rest.replace('\\', '/')))
    592       return 'file:///' + quoted_path
    593     else:
    594       quoted_path = urllib.quote(abs_path)
    595       return 'file://' + quoted_path
    596 
    597   @staticmethod
    598   def GetFileURLForDataPath(*relative_path):
    599     """Get file:// url for the given path relative to the chrome test data dir.
    600 
    601     Also quotes the url using urllib.quote().
    602 
    603     Args:
    604       relative_path: Variable number of strings that can be joined.
    605     """
    606     return PyUITest.GetFileURLForPath(PyUITest.DataDir(), *relative_path)
    607 
    608   @staticmethod
    609   def GetHttpURLForDataPath(*relative_path):
    610     """Get http:// url for the given path in the data dir.
    611 
    612     The URL will be usable only after starting the http server.
    613     """
    614     global _HTTP_SERVER
    615     assert _HTTP_SERVER, 'HTTP Server not yet started'
    616     return _HTTP_SERVER.GetURL(os.path.join('files', *relative_path)).spec()
    617 
    618   @staticmethod
    619   def ContentDataDir():
    620     """Get path to content/test/data."""
    621     return os.path.join(PyUITest.DataDir(), os.pardir, os.pardir, os.pardir,
    622         'content', 'test', 'data')
    623 
    624   @staticmethod
    625   def GetFileURLForContentDataPath(*relative_path):
    626     """Get file:// url for the given path relative to content test data dir.
    627 
    628     Also quotes the url using urllib.quote().
    629 
    630     Args:
    631       relative_path: Variable number of strings that can be joined.
    632     """
    633     return PyUITest.GetFileURLForPath(PyUITest.ContentDataDir(), *relative_path)
    634 
    635   @staticmethod
    636   def GetFtpURLForDataPath(ftp_server, *relative_path):
    637     """Get ftp:// url for the given path in the data dir.
    638 
    639     Args:
    640       ftp_server: handle to ftp server, an instance of SpawnedTestServer
    641       relative_path: any number of path elements
    642 
    643     The URL will be usable only after starting the ftp server.
    644     """
    645     assert ftp_server, 'FTP Server not yet started'
    646     return ftp_server.GetURL(os.path.join(*relative_path)).spec()
    647 
    648   @staticmethod
    649   def IsMac():
    650     """Are we on Mac?"""
    651     return 'darwin' == sys.platform
    652 
    653   @staticmethod
    654   def IsLinux():
    655     """Are we on Linux? ChromeOS is linux too."""
    656     return sys.platform.startswith('linux')
    657 
    658   @staticmethod
    659   def IsWin():
    660     """Are we on Win?"""
    661     return 'win32' == sys.platform
    662 
    663   @staticmethod
    664   def IsWin7():
    665     """Are we on Windows 7?"""
    666     if not PyUITest.IsWin():
    667       return False
    668     ver = sys.getwindowsversion()
    669     return (ver[3], ver[0], ver[1]) == (2, 6, 1)
    670 
    671   @staticmethod
    672   def IsWinVista():
    673     """Are we on Windows Vista?"""
    674     if not PyUITest.IsWin():
    675       return False
    676     ver = sys.getwindowsversion()
    677     return (ver[3], ver[0], ver[1]) == (2, 6, 0)
    678 
    679   @staticmethod
    680   def IsWinXP():
    681     """Are we on Windows XP?"""
    682     if not PyUITest.IsWin():
    683       return False
    684     ver = sys.getwindowsversion()
    685     return (ver[3], ver[0], ver[1]) == (2, 5, 1)
    686 
    687   @staticmethod
    688   def IsChromeOS():
    689     """Are we on ChromeOS (or Chromium OS)?
    690 
    691     Checks for "CHROMEOS_RELEASE_NAME=" in /etc/lsb-release.
    692     """
    693     lsb_release = '/etc/lsb-release'
    694     if not PyUITest.IsLinux() or not os.path.isfile(lsb_release):
    695       return False
    696     for line in open(lsb_release).readlines():
    697       if line.startswith('CHROMEOS_RELEASE_NAME='):
    698         return True
    699     return False
    700 
    701   @staticmethod
    702   def IsPosix():
    703     """Are we on Mac/Linux?"""
    704     return PyUITest.IsMac() or PyUITest.IsLinux()
    705 
    706   @staticmethod
    707   def IsEnUS():
    708     """Are we en-US?"""
    709     # TODO: figure out the machine's langugage.
    710     return True
    711 
    712   @staticmethod
    713   def GetPlatform():
    714     """Return the platform name."""
    715     # Since ChromeOS is also Linux, we check for it first.
    716     if PyUITest.IsChromeOS():
    717       return 'chromeos'
    718     elif PyUITest.IsLinux():
    719       return 'linux'
    720     elif PyUITest.IsMac():
    721       return 'mac'
    722     elif PyUITest.IsWin():
    723       return 'win'
    724     else:
    725       return 'unknown'
    726 
    727   @staticmethod
    728   def EvalDataFrom(filename):
    729     """Return eval of python code from given file.
    730 
    731     The datastructure used in the file will be preserved.
    732     """
    733     data_file = os.path.join(filename)
    734     contents = open(data_file).read()
    735     try:
    736       ret = eval(contents)
    737     except:
    738       print >>sys.stderr, '%s is an invalid data file.' % data_file
    739       raise
    740     return ret
    741 
    742   @staticmethod
    743   def ChromeOSBoard():
    744     """What is the ChromeOS board name"""
    745     if PyUITest.IsChromeOS():
    746       for line in open('/etc/lsb-release'):
    747         line = line.strip()
    748         if line.startswith('CHROMEOS_RELEASE_BOARD='):
    749           return line.split('=')[1]
    750     return None
    751 
    752   @staticmethod
    753   def Kill(pid):
    754     """Terminate the given pid.
    755 
    756     If the pid refers to a renderer, use KillRendererProcess instead.
    757     """
    758     if PyUITest.IsWin():
    759       subprocess.call(['taskkill.exe', '/T', '/F', '/PID', str(pid)])
    760     else:
    761       os.kill(pid, signal.SIGTERM)
    762 
    763   @staticmethod
    764   def GetPrivateInfo():
    765     """Fetch info from private_tests_info.txt in private dir.
    766 
    767     Returns:
    768       a dictionary of items from private_tests_info.txt
    769     """
    770     private_file = os.path.join(
    771         PyUITest.DataDir(), 'pyauto_private', 'private_tests_info.txt')
    772     assert os.path.exists(private_file), '%s missing' % private_file
    773     return PyUITest.EvalDataFrom(private_file)
    774 
    775   def WaitUntil(self, function, timeout=-1, retry_sleep=0.25, args=[],
    776                 expect_retval=None, return_retval=False, debug=True):
    777     """Poll on a condition until timeout.
    778 
    779     Waits until the |function| evalues to |expect_retval| or until |timeout|
    780     secs, whichever occurs earlier.
    781 
    782     This is better than using a sleep, since it waits (almost) only as much
    783     as needed.
    784 
    785     WARNING: This method call should be avoided as far as possible in favor
    786     of a real wait from chromium (like wait-until-page-loaded).
    787     Only use in case there's really no better option.
    788 
    789     EXAMPLES:-
    790     Wait for "file.txt" to get created:
    791       WaitUntil(os.path.exists, args=["file.txt"])
    792 
    793     Same as above, but using lambda:
    794       WaitUntil(lambda: os.path.exists("file.txt"))
    795 
    796     Args:
    797       function: the function whose truth value is to be evaluated
    798       timeout: the max timeout (in secs) for which to wait. The default
    799                action is to wait for kWaitForActionMaxMsec, as set in
    800                ui_test.cc
    801                Use None to wait indefinitely.
    802       retry_sleep: the sleep interval (in secs) before retrying |function|.
    803                    Defaults to 0.25 secs.
    804       args: the args to pass to |function|
    805       expect_retval: the expected return value for |function|. This forms the
    806                      exit criteria. In case this is None (the default),
    807                      |function|'s return value is checked for truth,
    808                      so 'non-empty-string' should match with True
    809       return_retval: If True, return the value returned by the last call to
    810                      |function()|
    811       debug: if True, displays debug info at each retry.
    812 
    813     Returns:
    814       The return value of the |function| (when return_retval == True)
    815       True, if returning when |function| evaluated to True (when
    816           return_retval == False)
    817       False, when returning due to timeout
    818     """
    819     if timeout == -1:  # Default
    820       timeout = self._automation_timeout / 1000.0
    821     assert callable(function), "function should be a callable"
    822     begin = time.time()
    823     debug_begin = begin
    824     retval = None
    825     while timeout is None or time.time() - begin <= timeout:
    826       retval = function(*args)
    827       if (expect_retval is None and retval) or \
    828          (expect_retval is not None and expect_retval == retval):
    829         return retval if return_retval else True
    830       if debug and time.time() - debug_begin > 5:
    831         debug_begin += 5
    832         if function.func_name == (lambda: True).func_name:
    833           function_info = inspect.getsource(function).strip()
    834         else:
    835           function_info = '%s()' % function.func_name
    836         logging.debug('WaitUntil(%s:%d %s) still waiting. '
    837                       'Expecting %s. Last returned %s.',
    838                       os.path.basename(inspect.getsourcefile(function)),
    839                       inspect.getsourcelines(function)[1],
    840                       function_info,
    841                       True if expect_retval is None else expect_retval,
    842                       retval)
    843       time.sleep(retry_sleep)
    844     return retval if return_retval else False
    845 
    846   def StartFTPServer(self, data_dir):
    847     """Start a local file server hosting data files over ftp://
    848 
    849     Args:
    850       data_dir: path where ftp files should be served
    851 
    852     Returns:
    853       handle to FTP Server, an instance of SpawnedTestServer
    854     """
    855     ftp_server = pyautolib.SpawnedTestServer(
    856         pyautolib.SpawnedTestServer.TYPE_FTP,
    857         '127.0.0.1',
    858         pyautolib.FilePath(data_dir))
    859     assert ftp_server.Start(), 'Could not start ftp server'
    860     logging.debug('Started ftp server at "%s".', data_dir)
    861     return ftp_server
    862 
    863   def StopFTPServer(self, ftp_server):
    864     """Stop the local ftp server."""
    865     assert ftp_server, 'FTP Server not yet started'
    866     assert ftp_server.Stop(), 'Could not stop ftp server'
    867     logging.debug('Stopped ftp server.')
    868 
    869   def StartHTTPServer(self, data_dir):
    870     """Starts a local HTTP SpawnedTestServer serving files from |data_dir|.
    871 
    872     Args:
    873       data_dir: path where the SpawnedTestServer should serve files from.
    874       This will be appended to the source dir to get the final document root.
    875 
    876     Returns:
    877       handle to the HTTP SpawnedTestServer
    878     """
    879     http_server = pyautolib.SpawnedTestServer(
    880         pyautolib.SpawnedTestServer.TYPE_HTTP,
    881         '127.0.0.1',
    882         pyautolib.FilePath(data_dir))
    883     assert http_server.Start(), 'Could not start HTTP server'
    884     logging.debug('Started HTTP server at "%s".', data_dir)
    885     return http_server
    886 
    887   def StopHTTPServer(self, http_server):
    888     assert http_server, 'HTTP server not yet started'
    889     assert http_server.Stop(), 'Cloud not stop the HTTP server'
    890     logging.debug('Stopped HTTP server.')
    891 
    892   def StartHttpsServer(self, cert_type, data_dir):
    893     """Starts a local HTTPS SpawnedTestServer serving files from |data_dir|.
    894 
    895     Args:
    896       cert_type: An instance of SSLOptions.ServerCertificate for three
    897                  certificate types: ok, expired, or mismatch.
    898       data_dir: The path where SpawnedTestServer should serve files from.
    899                 This is appended to the source dir to get the final
    900                 document root.
    901 
    902     Returns:
    903       Handle to the HTTPS SpawnedTestServer
    904     """
    905     https_server = pyautolib.SpawnedTestServer(
    906         pyautolib.SpawnedTestServer.TYPE_HTTPS,
    907         pyautolib.SSLOptions(cert_type),
    908         pyautolib.FilePath(data_dir))
    909     assert https_server.Start(), 'Could not start HTTPS server.'
    910     logging.debug('Start HTTPS server at "%s".' % data_dir)
    911     return https_server
    912 
    913   def StopHttpsServer(self, https_server):
    914     assert https_server, 'HTTPS server not yet started.'
    915     assert https_server.Stop(), 'Could not stop the HTTPS server.'
    916     logging.debug('Stopped HTTPS server.')
    917 
    918   class ActionTimeoutChanger(object):
    919     """Facilitate temporary changes to PyAuto command timeout.
    920 
    921     Automatically resets to original timeout when object is destroyed.
    922     """
    923     _saved_timeout = -1  # Saved timeout value
    924 
    925     def __init__(self, ui_test, new_timeout):
    926       """Initialize.
    927 
    928       Args:
    929         ui_test: a PyUITest object
    930         new_timeout: new timeout to use (in milli secs)
    931       """
    932       self._saved_timeout = ui_test._automation_timeout
    933       ui_test._automation_timeout = new_timeout
    934       self._ui_test = ui_test
    935 
    936     def __del__(self):
    937       """Reset command_execution_timeout_ms to original value."""
    938       self._ui_test._automation_timeout = self._saved_timeout
    939 
    940   class JavascriptExecutor(object):
    941     """Abstract base class for JavaScript injection.
    942 
    943     Derived classes should override Execute method."""
    944     def Execute(self, script):
    945       pass
    946 
    947   class JavascriptExecutorInTab(JavascriptExecutor):
    948     """Wrapper for injecting JavaScript in a tab."""
    949     def __init__(self, ui_test, tab_index=0, windex=0, frame_xpath=''):
    950       """Initialize.
    951 
    952         Refer to ExecuteJavascript() for the complete argument list
    953         description.
    954 
    955       Args:
    956         ui_test: a PyUITest object
    957       """
    958       self._ui_test = ui_test
    959       self.windex = windex
    960       self.tab_index = tab_index
    961       self.frame_xpath = frame_xpath
    962 
    963     def Execute(self, script):
    964       """Execute script in the tab."""
    965       return self._ui_test.ExecuteJavascript(script,
    966                                              self.tab_index,
    967                                              self.windex,
    968                                              self.frame_xpath)
    969 
    970   class JavascriptExecutorInRenderView(JavascriptExecutor):
    971     """Wrapper for injecting JavaScript in an extension view."""
    972     def __init__(self, ui_test, view, frame_xpath=''):
    973       """Initialize.
    974 
    975         Refer to ExecuteJavascriptInRenderView() for the complete argument list
    976         description.
    977 
    978       Args:
    979         ui_test: a PyUITest object
    980       """
    981       self._ui_test = ui_test
    982       self.view = view
    983       self.frame_xpath = frame_xpath
    984 
    985     def Execute(self, script):
    986       """Execute script in the render view."""
    987       return self._ui_test.ExecuteJavascriptInRenderView(script,
    988                                                          self.view,
    989                                                          self.frame_xpath)
    990 
    991   def _GetResultFromJSONRequestDiagnostics(self):
    992     """Same as _GetResultFromJSONRequest without throwing a timeout exception.
    993 
    994     This method is used to diagnose if a command returns without causing a
    995     timout exception to be thrown.  This should be used for debugging purposes
    996     only.
    997 
    998     Returns:
    999       True if the request returned; False if it timed out.
   1000     """
   1001     result = self._SendJSONRequest(-1,
   1002              json.dumps({'command': 'GetBrowserInfo',}),
   1003              self._automation_timeout)
   1004     if not result:
   1005       # The diagnostic command did not complete, Chrome is probably in a bad
   1006       # state
   1007       return False
   1008     return True
   1009 
   1010   def _GetResultFromJSONRequest(self, cmd_dict, windex=0, timeout=-1):
   1011     """Issue call over the JSON automation channel and fetch output.
   1012 
   1013     This method packages the given dictionary into a json string, sends it
   1014     over the JSON automation channel, loads the json output string returned,
   1015     and returns it back as a dictionary.
   1016 
   1017     Args:
   1018       cmd_dict: the command dictionary. It must have a 'command' key
   1019                 Sample:
   1020                   {
   1021                     'command': 'SetOmniboxText',
   1022                     'text': text,
   1023                   }
   1024       windex: 0-based window index on which to work. Default: 0 (first window)
   1025               Use -ve windex or None if the automation command does not apply
   1026               to a browser window. Example: for chromeos login
   1027 
   1028       timeout: request timeout (in milliseconds)
   1029 
   1030     Returns:
   1031       a dictionary for the output returned by the automation channel.
   1032 
   1033     Raises:
   1034       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1035     """
   1036     if timeout == -1:  # Default
   1037       timeout = self._automation_timeout
   1038     if windex is None:  # Do not target any window
   1039       windex = -1
   1040     result = self._SendJSONRequest(windex, json.dumps(cmd_dict), timeout)
   1041     if not result:
   1042       additional_info = 'No information available.'
   1043       # Windows does not support os.kill until Python 2.7.
   1044       if not self.IsWin() and _BROWSER_PID:
   1045         browser_pid_exists = True
   1046         # Does the browser PID exist?
   1047         try:
   1048           # Does not actually kill the process
   1049           os.kill(int(_BROWSER_PID), 0)
   1050         except OSError:
   1051           browser_pid_exists = False
   1052         if browser_pid_exists:
   1053           if self._GetResultFromJSONRequestDiagnostics():
   1054             # Browser info, worked, that means this hook had a problem
   1055             additional_info = ('The browser process ID %d still exists. '
   1056                                'PyAuto was able to obtain browser info. It '
   1057                                'is possible this hook is broken.'
   1058                                % _BROWSER_PID)
   1059           else:
   1060             additional_info = ('The browser process ID %d still exists. '
   1061                                'PyAuto was not able to obtain browser info. '
   1062                                'It is possible the browser is hung.'
   1063                                % _BROWSER_PID)
   1064         else:
   1065           additional_info = ('The browser process ID %d no longer exists. '
   1066                              'Perhaps the browser crashed.' % _BROWSER_PID)
   1067       elif not _BROWSER_PID:
   1068         additional_info = ('The browser PID was not obtained. Does this test '
   1069                            'have a unique startup configuration?')
   1070       # Mask private data if it is in the JSON dictionary
   1071       cmd_dict_copy = copy.copy(cmd_dict)
   1072       if 'password' in cmd_dict_copy.keys():
   1073         cmd_dict_copy['password'] = '**********'
   1074       if 'username' in cmd_dict_copy.keys():
   1075         cmd_dict_copy['username'] = 'removed_username'
   1076       raise JSONInterfaceError('Automation call %s received empty response.  '
   1077                                'Additional information:\n%s' % (cmd_dict_copy,
   1078                                additional_info))
   1079     ret_dict = json.loads(result)
   1080     if ret_dict.has_key('error'):
   1081       if ret_dict.get('is_interface_timeout'):
   1082         raise AutomationCommandTimeout(ret_dict['error'])
   1083       elif ret_dict.get('is_interface_error'):
   1084         raise JSONInterfaceError(ret_dict['error'])
   1085       else:
   1086         raise AutomationCommandFail(ret_dict['error'])
   1087     return ret_dict
   1088 
   1089   def NavigateToURL(self, url, windex=0, tab_index=None, navigation_count=1):
   1090     """Navigate the given tab to the given URL.
   1091 
   1092     Note that this method also activates the corresponding tab/window if it's
   1093     not active already. Blocks until |navigation_count| navigations have
   1094     completed.
   1095 
   1096     Args:
   1097       url: The URL to which to navigate, can be a string or GURL object.
   1098       windex: The index of the browser window to work on. Defaults to the first
   1099           window.
   1100       tab_index: The index of the tab to work on. Defaults to the active tab.
   1101       navigation_count: the number of navigations to wait for. Defaults to 1.
   1102 
   1103     Raises:
   1104       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1105     """
   1106     if isinstance(url, GURL):
   1107       url = url.spec()
   1108     if tab_index is None:
   1109       tab_index = self.GetActiveTabIndex(windex)
   1110     cmd_dict = {
   1111         'command': 'NavigateToURL',
   1112         'url': url,
   1113         'windex': windex,
   1114         'tab_index': tab_index,
   1115         'navigation_count': navigation_count,
   1116     }
   1117     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1118 
   1119   def NavigateToURLAsync(self, url, windex=0, tab_index=None):
   1120     """Initiate a URL navigation.
   1121 
   1122     A wrapper for NavigateToURL with navigation_count set to 0.
   1123     """
   1124     self.NavigateToURL(url, windex, tab_index, 0)
   1125 
   1126   def ApplyAccelerator(self, accelerator, windex=0):
   1127     """Apply the accelerator with the given id.
   1128 
   1129     Note that this method schedules the accelerator, but does not wait for it to
   1130     actually finish doing anything.
   1131 
   1132     Args:
   1133       accelerator: The accelerator id, IDC_BACK, IDC_NEWTAB, etc. The list of
   1134           ids can be found at chrome/app/chrome_command_ids.h.
   1135       windex: The index of the browser window to work on. Defaults to the first
   1136           window.
   1137 
   1138     Raises:
   1139       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1140     """
   1141 
   1142     cmd_dict = {
   1143         'command': 'ApplyAccelerator',
   1144         'accelerator': accelerator,
   1145         'windex': windex,
   1146     }
   1147     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1148 
   1149   def RunCommand(self, accelerator, windex=0):
   1150     """Apply the accelerator with the given id and wait for it to finish.
   1151 
   1152     This is like ApplyAccelerator except that it waits for the command to finish
   1153     executing.
   1154 
   1155     Args:
   1156       accelerator: The accelerator id. The list of ids can be found at
   1157           chrome/app/chrome_command_ids.h.
   1158       windex: The index of the browser window to work on. Defaults to the first
   1159           window.
   1160 
   1161     Raises:
   1162       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1163     """
   1164     cmd_dict = {
   1165         'command': 'RunCommand',
   1166         'accelerator': accelerator,
   1167         'windex': windex,
   1168     }
   1169     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1170 
   1171   def IsMenuCommandEnabled(self, accelerator, windex=0):
   1172     """Check if a command is enabled for a window.
   1173 
   1174     Returns true if the command with the given accelerator id is enabled on the
   1175     given window.
   1176 
   1177     Args:
   1178       accelerator: The accelerator id. The list of ids can be found at
   1179           chrome/app/chrome_command_ids.h.
   1180       windex: The index of the browser window to work on. Defaults to the first
   1181           window.
   1182 
   1183     Returns:
   1184       True if the command is enabled for the given window.
   1185     """
   1186     cmd_dict = {
   1187         'command': 'IsMenuCommandEnabled',
   1188         'accelerator': accelerator,
   1189         'windex': windex,
   1190     }
   1191     return self._GetResultFromJSONRequest(cmd_dict, windex=None).get('enabled')
   1192 
   1193   def TabGoForward(self, tab_index=0, windex=0):
   1194     """Navigate a tab forward in history.
   1195 
   1196     Equivalent to clicking the Forward button in the UI. Activates the tab as a
   1197     side effect.
   1198 
   1199     Raises:
   1200       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1201     """
   1202     self.ActivateTab(tab_index, windex)
   1203     self.RunCommand(IDC_FORWARD, windex)
   1204 
   1205   def TabGoBack(self, tab_index=0, windex=0):
   1206     """Navigate a tab backwards in history.
   1207 
   1208     Equivalent to clicking the Back button in the UI. Activates the tab as a
   1209     side effect.
   1210 
   1211     Raises:
   1212       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1213     """
   1214     self.ActivateTab(tab_index, windex)
   1215     self.RunCommand(IDC_BACK, windex)
   1216 
   1217   def ReloadTab(self, tab_index=0, windex=0):
   1218     """Reload the given tab.
   1219 
   1220     Blocks until the page has reloaded.
   1221 
   1222     Args:
   1223       tab_index: The index of the tab to reload. Defaults to 0.
   1224       windex: The index of the browser window to work on. Defaults to the first
   1225           window.
   1226 
   1227     Raises:
   1228       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1229     """
   1230     self.ActivateTab(tab_index, windex)
   1231     self.RunCommand(IDC_RELOAD, windex)
   1232 
   1233   def CloseTab(self, tab_index=0, windex=0, wait_until_closed=True):
   1234     """Close the given tab.
   1235 
   1236     Note: Be careful closing the last tab in a window as it may close the
   1237         browser.
   1238 
   1239     Args:
   1240       tab_index: The index of the tab to reload. Defaults to 0.
   1241       windex: The index of the browser window to work on. Defaults to the first
   1242           window.
   1243       wait_until_closed: Whether to block until the tab finishes closing.
   1244 
   1245     Raises:
   1246       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1247     """
   1248     cmd_dict = {
   1249         'command': 'CloseTab',
   1250         'tab_index': tab_index,
   1251         'windex': windex,
   1252         'wait_until_closed': wait_until_closed,
   1253     }
   1254     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1255 
   1256   def WaitForTabToBeRestored(self, tab_index=0, windex=0, timeout=-1):
   1257     """Wait for the given tab to be restored.
   1258 
   1259     Args:
   1260       tab_index: The index of the tab to reload. Defaults to 0.
   1261       windex: The index of the browser window to work on. Defaults to the first
   1262           window.
   1263       timeout: Timeout in milliseconds.
   1264 
   1265     Raises:
   1266       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1267     """
   1268     cmd_dict = {
   1269         'command': 'CloseTab',
   1270         'tab_index': tab_index,
   1271         'windex': windex,
   1272     }
   1273     self._GetResultFromJSONRequest(cmd_dict, windex=None, timeout=timeout)
   1274 
   1275   def ReloadActiveTab(self, windex=0):
   1276     """Reload an active tab.
   1277 
   1278     Warning: Depending on the concept of an active tab is dangerous as it can
   1279     change during the test. Use ReloadTab and supply a tab_index explicitly.
   1280 
   1281     Args:
   1282       windex: The index of the browser window to work on. Defaults to the first
   1283           window.
   1284 
   1285     Raises:
   1286       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1287     """
   1288     self.ReloadTab(self.GetActiveTabIndex(windex), windex)
   1289 
   1290   def GetActiveTabIndex(self, windex=0):
   1291     """Get the index of the currently active tab in the given browser window.
   1292 
   1293     Warning: Depending on the concept of an active tab is dangerous as it can
   1294     change during the test. Supply the tab_index explicitly, if possible.
   1295 
   1296     Args:
   1297       windex: The index of the browser window to work on. Defaults to the first
   1298           window.
   1299 
   1300     Returns:
   1301       An integer index for the currently active tab.
   1302     """
   1303     cmd_dict = {
   1304         'command': 'GetActiveTabIndex',
   1305         'windex': windex,
   1306     }
   1307     return self._GetResultFromJSONRequest(cmd_dict,
   1308                                           windex=None).get('tab_index')
   1309 
   1310   def ActivateTab(self, tab_index=0, windex=0):
   1311     """Activates the given tab in the specified window.
   1312 
   1313     Warning: Depending on the concept of an active tab is dangerous as it can
   1314     change during the test. Instead use functions that accept a tab_index
   1315     explicitly.
   1316 
   1317     Args:
   1318       tab_index: Integer index of the tab to activate; defaults to 0.
   1319       windex: Integer index of the browser window to use; defaults to the first
   1320           window.
   1321 
   1322     Raises:
   1323       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1324     """
   1325     cmd_dict = {
   1326         'command': 'ActivateTab',
   1327         'tab_index': tab_index,
   1328         'windex': windex,
   1329     }
   1330     self.BringBrowserToFront(windex)
   1331     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1332 
   1333   def BringBrowserToFront(self, windex=0):
   1334     """Activate the browser's window and bring it to front.
   1335 
   1336     Args:
   1337       windex: Integer index of the browser window to use; defaults to the first
   1338           window.
   1339 
   1340     Raises:
   1341       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1342     """
   1343     cmd_dict = {
   1344         'command': 'BringBrowserToFront',
   1345         'windex': windex,
   1346     }
   1347     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1348 
   1349   def GetBrowserWindowCount(self):
   1350     """Get the browser window count.
   1351 
   1352     Args:
   1353       None.
   1354 
   1355     Returns:
   1356       Integer count of the number of browser windows. Includes popups.
   1357 
   1358     Raises:
   1359       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1360     """
   1361     cmd_dict = {'command': 'GetBrowserWindowCount'}
   1362     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['count']
   1363 
   1364   def OpenNewBrowserWindow(self, show):
   1365     """Create a new browser window.
   1366 
   1367     Args:
   1368       show: Boolean indicating whether to show the window.
   1369 
   1370     Raises:
   1371       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1372     """
   1373     cmd_dict = {
   1374         'command': 'OpenNewBrowserWindow',
   1375         'show': show,
   1376     }
   1377     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1378 
   1379   def CloseBrowserWindow(self, windex=0):
   1380     """Create a new browser window.
   1381 
   1382     Args:
   1383       windex: Index of the browser window to close; defaults to 0.
   1384 
   1385     Raises:
   1386       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1387     """
   1388     cmd_dict = {
   1389         'command': 'CloseBrowserWindow',
   1390         'windex': windex,
   1391     }
   1392     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1393 
   1394   def AppendTab(self, url, windex=0):
   1395     """Append a new tab.
   1396 
   1397     Creates a new tab at the end of given browser window and activates
   1398     it. Blocks until the specified |url| is loaded.
   1399 
   1400     Args:
   1401       url: The url to load, can be string or a GURL object.
   1402       windex: The index of the browser window to work on. Defaults to the first
   1403           window.
   1404 
   1405     Returns:
   1406       True if the url loads successfully in the new tab. False otherwise.
   1407 
   1408     Raises:
   1409       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1410     """
   1411     if isinstance(url, GURL):
   1412       url = url.spec()
   1413     cmd_dict = {
   1414         'command': 'AppendTab',
   1415         'url': url,
   1416         'windex': windex,
   1417     }
   1418     return self._GetResultFromJSONRequest(cmd_dict, windex=None).get('result')
   1419 
   1420   def GetTabCount(self, windex=0):
   1421     """Gets the number of tab in the given browser window.
   1422 
   1423     Args:
   1424       windex: Integer index of the browser window to use; defaults to the first
   1425           window.
   1426 
   1427     Returns:
   1428       The tab count.
   1429 
   1430     Raises:
   1431       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1432     """
   1433     cmd_dict = {
   1434         'command': 'GetTabCount',
   1435         'windex': windex,
   1436     }
   1437     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['tab_count']
   1438 
   1439   def GetTabInfo(self, tab_index=0, windex=0):
   1440     """Gets information about the specified tab.
   1441 
   1442     Args:
   1443       tab_index: Integer index of the tab to activate; defaults to 0.
   1444       windex: Integer index of the browser window to use; defaults to the first
   1445           window.
   1446 
   1447     Returns:
   1448       A dictionary containing information about the tab.
   1449       Example:
   1450         { u'title': "Hello World",
   1451           u'url': "http://foo.bar", }
   1452 
   1453     Raises:
   1454       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1455     """
   1456     cmd_dict = {
   1457         'command': 'GetTabInfo',
   1458         'tab_index': tab_index,
   1459         'windex': windex,
   1460     }
   1461     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1462 
   1463   def GetActiveTabTitle(self, windex=0):
   1464     """Gets the title of the active tab.
   1465 
   1466     Warning: Depending on the concept of an active tab is dangerous as it can
   1467     change during the test. Use GetTabInfo and supply a tab_index explicitly.
   1468 
   1469     Args:
   1470       windex: Integer index of the browser window to use; defaults to the first
   1471           window.
   1472 
   1473     Returns:
   1474       The tab title as a string.
   1475 
   1476     Raises:
   1477       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1478     """
   1479     return self.GetTabInfo(self.GetActiveTabIndex(windex), windex)['title']
   1480 
   1481   def GetActiveTabURL(self, windex=0):
   1482     """Gets the URL of the active tab.
   1483 
   1484     Warning: Depending on the concept of an active tab is dangerous as it can
   1485     change during the test. Use GetTabInfo and supply a tab_index explicitly.
   1486 
   1487     Args:
   1488       windex: Integer index of the browser window to use; defaults to the first
   1489           window.
   1490 
   1491     Returns:
   1492       The tab URL as a GURL object.
   1493 
   1494     Raises:
   1495       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1496     """
   1497     return GURL(str(self.GetTabInfo(self.GetActiveTabIndex(windex),
   1498                                     windex)['url']))
   1499 
   1500   def ActionOnSSLBlockingPage(self, tab_index=0, windex=0, proceed=True):
   1501     """Take action on an interstitial page.
   1502 
   1503     Calling this when an interstitial page is not showing is an error.
   1504 
   1505     Args:
   1506       tab_index: Integer index of the tab to activate; defaults to 0.
   1507       windex: Integer index of the browser window to use; defaults to the first
   1508           window.
   1509       proceed: Whether to proceed to the URL or not.
   1510 
   1511     Raises:
   1512       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1513     """
   1514     cmd_dict = {
   1515         'command': 'ActionOnSSLBlockingPage',
   1516         'tab_index': tab_index,
   1517         'windex': windex,
   1518         'proceed': proceed,
   1519     }
   1520     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1521 
   1522   def GetBookmarkModel(self, windex=0):
   1523     """Return the bookmark model as a BookmarkModel object.
   1524 
   1525     This is a snapshot of the bookmark model; it is not a proxy and
   1526     does not get updated as the bookmark model changes.
   1527     """
   1528     bookmarks_as_json = self._GetBookmarksAsJSON(windex)
   1529     if not bookmarks_as_json:
   1530       raise JSONInterfaceError('Could not resolve browser proxy.')
   1531     return bookmark_model.BookmarkModel(bookmarks_as_json)
   1532 
   1533   def _GetBookmarksAsJSON(self, windex=0):
   1534     """Get bookmarks as a JSON dictionary; used by GetBookmarkModel()."""
   1535     cmd_dict = {
   1536         'command': 'GetBookmarksAsJSON',
   1537         'windex': windex,
   1538     }
   1539     self.WaitForBookmarkModelToLoad(windex)
   1540     return self._GetResultFromJSONRequest(cmd_dict,
   1541                                           windex=None)['bookmarks_as_json']
   1542 
   1543   def WaitForBookmarkModelToLoad(self, windex=0):
   1544     """Gets the status of the bookmark bar as a dictionary.
   1545 
   1546     Args:
   1547       windex: Integer index of the browser window to use; defaults to the first
   1548           window.
   1549 
   1550     Raises:
   1551       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1552     """
   1553     cmd_dict = {
   1554         'command': 'WaitForBookmarkModelToLoad',
   1555         'windex': windex,
   1556     }
   1557     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1558 
   1559   def GetBookmarkBarStatus(self, windex=0):
   1560     """Gets the status of the bookmark bar as a dictionary.
   1561 
   1562     Args:
   1563       windex: Integer index of the browser window to use; defaults to the first
   1564           window.
   1565 
   1566     Returns:
   1567       A dictionary.
   1568       Example:
   1569         { u'visible': True,
   1570           u'animating': False,
   1571           u'detached': False, }
   1572 
   1573     Raises:
   1574       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1575     """
   1576     cmd_dict = {
   1577         'command': 'GetBookmarkBarStatus',
   1578         'windex': windex,
   1579     }
   1580     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1581 
   1582   def GetBookmarkBarStatus(self, windex=0):
   1583     """Gets the status of the bookmark bar as a dictionary.
   1584 
   1585     Args:
   1586       windex: Integer index of the browser window to use; defaults to the first
   1587           window.
   1588 
   1589     Returns:
   1590       A dictionary.
   1591       Example:
   1592         { u'visible': True,
   1593           u'animating': False,
   1594           u'detached': False, }
   1595 
   1596     Raises:
   1597       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1598     """
   1599     cmd_dict = {
   1600         'command': 'GetBookmarkBarStatus',
   1601         'windex': windex,
   1602     }
   1603     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1604 
   1605   def GetBookmarkBarStatus(self, windex=0):
   1606     """Gets the status of the bookmark bar as a dictionary.
   1607 
   1608     Args:
   1609       windex: Integer index of the browser window to use; defaults to the first
   1610           window.
   1611 
   1612     Returns:
   1613       A dictionary.
   1614       Example:
   1615         { u'visible': True,
   1616           u'animating': False,
   1617           u'detached': False, }
   1618 
   1619     Raises:
   1620       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1621     """
   1622     cmd_dict = {
   1623         'command': 'GetBookmarkBarStatus',
   1624         'windex': windex,
   1625     }
   1626     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1627 
   1628   def GetBookmarkBarVisibility(self, windex=0):
   1629     """Returns the visibility of the bookmark bar.
   1630 
   1631     Args:
   1632       windex: Integer index of the browser window to use; defaults to the first
   1633           window.
   1634 
   1635     Returns:
   1636       True if the bookmark bar is visible, false otherwise.
   1637 
   1638     Raises:
   1639       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1640     """
   1641     return self.GetBookmarkBarStatus(windex)['visible']
   1642 
   1643   def AddBookmarkGroup(self, parent_id, index, title, windex=0):
   1644     """Adds a bookmark folder.
   1645 
   1646     Args:
   1647       parent_id: The parent bookmark folder.
   1648       index: The location in the parent's list to insert this bookmark folder.
   1649       title: The name of the bookmark folder.
   1650       windex: Integer index of the browser window to use; defaults to the first
   1651           window.
   1652 
   1653     Returns:
   1654       True if the bookmark bar is detached, false otherwise.
   1655 
   1656     Raises:
   1657       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1658     """
   1659     if isinstance(parent_id, basestring):
   1660       parent_id = int(parent_id)
   1661     cmd_dict = {
   1662         'command': 'AddBookmark',
   1663         'parent_id': parent_id,
   1664         'index': index,
   1665         'title': title,
   1666         'is_folder': True,
   1667         'windex': windex,
   1668     }
   1669     self.WaitForBookmarkModelToLoad(windex)
   1670     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1671 
   1672   def AddBookmarkURL(self, parent_id, index, title, url, windex=0):
   1673     """Add a bookmark URL.
   1674 
   1675     Args:
   1676       parent_id: The parent bookmark folder.
   1677       index: The location in the parent's list to insert this bookmark.
   1678       title: The name of the bookmark.
   1679       url: The url of the bookmark.
   1680       windex: Integer index of the browser window to use; defaults to the first
   1681           window.
   1682 
   1683     Raises:
   1684       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1685     """
   1686     if isinstance(parent_id, basestring):
   1687       parent_id = int(parent_id)
   1688     cmd_dict = {
   1689         'command': 'AddBookmark',
   1690         'parent_id': parent_id,
   1691         'index': index,
   1692         'title': title,
   1693         'url': url,
   1694         'is_folder': False,
   1695         'windex': windex,
   1696     }
   1697     self.WaitForBookmarkModelToLoad(windex)
   1698     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1699 
   1700   def ReparentBookmark(self, id, new_parent_id, index, windex=0):
   1701     """Move a bookmark.
   1702 
   1703     Args:
   1704       id: The bookmark to move.
   1705       new_parent_id: The new parent bookmark folder.
   1706       index: The location in the parent's list to insert this bookmark.
   1707       windex: Integer index of the browser window to use; defaults to the first
   1708           window.
   1709 
   1710     Raises:
   1711       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1712     """
   1713     if isinstance(id, basestring):
   1714       id = int(id)
   1715     if isinstance(new_parent_id, basestring):
   1716       new_parent_id = int(new_parent_id)
   1717     cmd_dict = {
   1718         'command': 'ReparentBookmark',
   1719         'id': id,
   1720         'new_parent_id': new_parent_id,
   1721         'index': index,
   1722         'windex': windex,
   1723     }
   1724     self.WaitForBookmarkModelToLoad(windex)
   1725     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1726 
   1727   def SetBookmarkTitle(self, id, title, windex=0):
   1728     """Change the title of a bookmark.
   1729 
   1730     Args:
   1731       id: The bookmark to rename.
   1732       title: The new title for the bookmark.
   1733       windex: Integer index of the browser window to use; defaults to the first
   1734           window.
   1735 
   1736     Raises:
   1737       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1738     """
   1739     if isinstance(id, basestring):
   1740       id = int(id)
   1741     cmd_dict = {
   1742         'command': 'SetBookmarkTitle',
   1743         'id': id,
   1744         'title': title,
   1745         'windex': windex,
   1746     }
   1747     self.WaitForBookmarkModelToLoad(windex)
   1748     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1749 
   1750   def SetBookmarkURL(self, id, url, windex=0):
   1751     """Change the URL of a bookmark.
   1752 
   1753     Args:
   1754       id: The bookmark to change.
   1755       url: The new url for the bookmark.
   1756       windex: Integer index of the browser window to use; defaults to the first
   1757           window.
   1758 
   1759     Raises:
   1760       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1761     """
   1762     if isinstance(id, basestring):
   1763       id = int(id)
   1764     cmd_dict = {
   1765         'command': 'SetBookmarkURL',
   1766         'id': id,
   1767         'url': url,
   1768         'windex': windex,
   1769     }
   1770     self.WaitForBookmarkModelToLoad(windex)
   1771     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1772 
   1773   def RemoveBookmark(self, id, windex=0):
   1774     """Remove a bookmark.
   1775 
   1776     Args:
   1777       id: The bookmark to remove.
   1778       windex: Integer index of the browser window to use; defaults to the first
   1779           window.
   1780 
   1781     Raises:
   1782       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1783     """
   1784     if isinstance(id, basestring):
   1785       id = int(id)
   1786     cmd_dict = {
   1787         'command': 'RemoveBookmark',
   1788         'id': id,
   1789         'windex': windex,
   1790     }
   1791     self.WaitForBookmarkModelToLoad(windex)
   1792     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1793 
   1794   def GetDownloadsInfo(self, windex=0):
   1795     """Return info about downloads.
   1796 
   1797     This includes all the downloads recognized by the history system.
   1798 
   1799     Returns:
   1800       an instance of downloads_info.DownloadInfo
   1801     """
   1802     return download_info.DownloadInfo(
   1803         self._GetResultFromJSONRequest({'command': 'GetDownloadsInfo'},
   1804                                        windex=windex))
   1805 
   1806   def GetOmniboxInfo(self, windex=0):
   1807     """Return info about Omnibox.
   1808 
   1809     This represents a snapshot of the omnibox.  If you expect changes
   1810     you need to call this method again to get a fresh snapshot.
   1811     Note that this DOES NOT shift focus to the omnibox; you've to ensure that
   1812     the omnibox is in focus or else you won't get any interesting info.
   1813 
   1814     It's OK to call this even when the omnibox popup is not showing.  In this
   1815     case however, there won't be any matches, but other properties (like the
   1816     current text in the omnibox) will still be fetched.
   1817 
   1818     Due to the nature of the omnibox, this function is sensitive to mouse
   1819     focus.  DO NOT HOVER MOUSE OVER OMNIBOX OR CHANGE WINDOW FOCUS WHEN USING
   1820     THIS METHOD.
   1821 
   1822     Args:
   1823       windex: the index of the browser window to work on.
   1824               Default: 0 (first window)
   1825 
   1826     Returns:
   1827       an instance of omnibox_info.OmniboxInfo
   1828     """
   1829     return omnibox_info.OmniboxInfo(
   1830         self._GetResultFromJSONRequest({'command': 'GetOmniboxInfo'},
   1831                                        windex=windex))
   1832 
   1833   def SetOmniboxText(self, text, windex=0):
   1834     """Enter text into the omnibox. This shifts focus to the omnibox.
   1835 
   1836     Args:
   1837       text: the text to be set.
   1838       windex: the index of the browser window to work on.
   1839               Default: 0 (first window)
   1840     """
   1841     # Ensure that keyword data is loaded from the profile.
   1842     # This would normally be triggered by the user inputting this text.
   1843     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'})
   1844     cmd_dict = {
   1845         'command': 'SetOmniboxText',
   1846         'text': text,
   1847     }
   1848     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   1849 
   1850   # TODO(ace): Remove this hack, update bug 62783.
   1851   def WaitUntilOmniboxReadyHack(self, windex=0):
   1852     """Wait until the omnibox is ready for input.
   1853 
   1854     This is a hack workaround for linux platform, which returns from
   1855     synchronous window creation methods before the omnibox is fully functional.
   1856 
   1857     No-op on non-linux platforms.
   1858 
   1859     Args:
   1860       windex: the index of the browser to work on.
   1861     """
   1862     if self.IsLinux():
   1863       return self.WaitUntil(
   1864           lambda : self.GetOmniboxInfo(windex).Properties('has_focus'))
   1865 
   1866   def WaitUntilOmniboxQueryDone(self, windex=0):
   1867     """Wait until omnibox has finished populating results.
   1868 
   1869     Uses WaitUntil() so the wait duration is capped by the timeout values
   1870     used by automation, which WaitUntil() uses.
   1871 
   1872     Args:
   1873       windex: the index of the browser window to work on.
   1874               Default: 0 (first window)
   1875     """
   1876     return self.WaitUntil(
   1877         lambda : not self.GetOmniboxInfo(windex).IsQueryInProgress())
   1878 
   1879   def OmniboxMovePopupSelection(self, count, windex=0):
   1880     """Move omnibox popup selection up or down.
   1881 
   1882     Args:
   1883       count: number of rows by which to move.
   1884              -ve implies down, +ve implies up
   1885       windex: the index of the browser window to work on.
   1886               Default: 0 (first window)
   1887     """
   1888     cmd_dict = {
   1889         'command': 'OmniboxMovePopupSelection',
   1890         'count': count,
   1891     }
   1892     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   1893 
   1894   def OmniboxAcceptInput(self, windex=0):
   1895     """Accepts the current string of text in the omnibox.
   1896 
   1897     This is equivalent to clicking or hiting enter on a popup selection.
   1898     Blocks until the page loads.
   1899 
   1900     Args:
   1901       windex: the index of the browser window to work on.
   1902               Default: 0 (first window)
   1903     """
   1904     cmd_dict = {
   1905         'command': 'OmniboxAcceptInput',
   1906     }
   1907     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   1908 
   1909   def GetCookie(self, url, windex=0):
   1910     """Get the value of the cookie at url in context of the specified browser.
   1911 
   1912     Args:
   1913       url: Either a GURL object or url string specifing the cookie url.
   1914       windex: The index of the browser window to work on. Defaults to the first
   1915           window.
   1916 
   1917     Raises:
   1918       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1919     """
   1920     if isinstance(url, GURL):
   1921       url = url.spec()
   1922     cmd_dict = {
   1923         'command': 'GetCookiesInBrowserContext',
   1924         'url': url,
   1925         'windex': windex,
   1926     }
   1927     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['cookies']
   1928 
   1929   def DeleteCookie(self, url, cookie_name, windex=0):
   1930     """Delete the cookie at url with name cookie_name.
   1931 
   1932     Args:
   1933       url: Either a GURL object or url string specifing the cookie url.
   1934       cookie_name: The name of the cookie to delete as a string.
   1935       windex: The index of the browser window to work on. Defaults to the first
   1936           window.
   1937 
   1938     Raises:
   1939       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1940     """
   1941     if isinstance(url, GURL):
   1942       url = url.spec()
   1943     cmd_dict = {
   1944         'command': 'DeleteCookieInBrowserContext',
   1945         'url': url,
   1946         'cookie_name': cookie_name,
   1947         'windex': windex,
   1948     }
   1949     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1950 
   1951   def SetCookie(self, url, value, windex=0):
   1952     """Set the value of the cookie at url to value in the context of a browser.
   1953 
   1954     Args:
   1955       url: Either a GURL object or url string specifing the cookie url.
   1956       value: A string to set as the cookie's value.
   1957       windex: The index of the browser window to work on. Defaults to the first
   1958           window.
   1959 
   1960     Raises:
   1961       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   1962     """
   1963     if isinstance(url, GURL):
   1964       url = url.spec()
   1965     cmd_dict = {
   1966         'command': 'SetCookieInBrowserContext',
   1967         'url': url,
   1968         'value': value,
   1969         'windex': windex,
   1970     }
   1971     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   1972 
   1973   def GetSearchEngineInfo(self, windex=0):
   1974     """Return info about search engines.
   1975 
   1976     Args:
   1977       windex: The window index, default is 0.
   1978 
   1979     Returns:
   1980       An ordered list of dictionaries describing info about each search engine.
   1981 
   1982       Example:
   1983         [ { u'display_url': u'{google:baseURL}search?q=%s',
   1984             u'host': u'www.google.com',
   1985             u'in_default_list': True,
   1986             u'is_default': True,
   1987             u'is_valid': True,
   1988             u'keyword': u'google.com',
   1989             u'path': u'/search',
   1990             u'short_name': u'Google',
   1991             u'supports_replacement': True,
   1992             u'url': u'{google:baseURL}search?q={searchTerms}'},
   1993           { u'display_url': u'http://search.yahoo.com/search?p=%s',
   1994             u'host': u'search.yahoo.com',
   1995             u'in_default_list': True,
   1996             u'is_default': False,
   1997             u'is_valid': True,
   1998             u'keyword': u'yahoo.com',
   1999             u'path': u'/search',
   2000             u'short_name': u'Yahoo!',
   2001             u'supports_replacement': True,
   2002             u'url': u'http://search.yahoo.com/search?p={searchTerms}'},
   2003     """
   2004     # Ensure that the search engine profile is loaded into data model.
   2005     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
   2006                                    windex=windex)
   2007     cmd_dict = {'command': 'GetSearchEngineInfo'}
   2008     return self._GetResultFromJSONRequest(
   2009         cmd_dict, windex=windex)['search_engines']
   2010 
   2011   def AddSearchEngine(self, title, keyword, url, windex=0):
   2012     """Add a search engine, as done through the search engines UI.
   2013 
   2014     Args:
   2015       title: name for search engine.
   2016       keyword: keyword, used to initiate a custom search from omnibox.
   2017       url: url template for this search engine's query.
   2018            '%s' is replaced by search query string when used to search.
   2019       windex: The window index, default is 0.
   2020     """
   2021     # Ensure that the search engine profile is loaded into data model.
   2022     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
   2023                                    windex=windex)
   2024     cmd_dict = {'command': 'AddOrEditSearchEngine',
   2025                 'new_title': title,
   2026                 'new_keyword': keyword,
   2027                 'new_url': url}
   2028     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2029 
   2030   def EditSearchEngine(self, keyword, new_title, new_keyword, new_url,
   2031                        windex=0):
   2032     """Edit info for existing search engine.
   2033 
   2034     Args:
   2035       keyword: existing search engine keyword.
   2036       new_title: new name for this search engine.
   2037       new_keyword: new keyword for this search engine.
   2038       new_url: new url for this search engine.
   2039       windex: The window index, default is 0.
   2040     """
   2041     # Ensure that the search engine profile is loaded into data model.
   2042     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
   2043                                    windex=windex)
   2044     cmd_dict = {'command': 'AddOrEditSearchEngine',
   2045                 'keyword': keyword,
   2046                 'new_title': new_title,
   2047                 'new_keyword': new_keyword,
   2048                 'new_url': new_url}
   2049     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2050 
   2051   def DeleteSearchEngine(self, keyword, windex=0):
   2052     """Delete search engine with given keyword.
   2053 
   2054     Args:
   2055       keyword: the keyword string of the search engine to delete.
   2056       windex: The window index, default is 0.
   2057     """
   2058     # Ensure that the search engine profile is loaded into data model.
   2059     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
   2060                                    windex=windex)
   2061     cmd_dict = {'command': 'PerformActionOnSearchEngine', 'keyword': keyword,
   2062                 'action': 'delete'}
   2063     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2064 
   2065   def MakeSearchEngineDefault(self, keyword, windex=0):
   2066     """Make search engine with given keyword the default search.
   2067 
   2068     Args:
   2069       keyword: the keyword string of the search engine to make default.
   2070       windex: The window index, default is 0.
   2071     """
   2072     # Ensure that the search engine profile is loaded into data model.
   2073     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
   2074                                    windex=windex)
   2075     cmd_dict = {'command': 'PerformActionOnSearchEngine', 'keyword': keyword,
   2076                 'action': 'default'}
   2077     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2078 
   2079   def GetLocalStatePrefsInfo(self):
   2080     """Return info about preferences.
   2081 
   2082     This represents a snapshot of the local state preferences. If you expect
   2083     local state preferences to have changed, you need to call this method again
   2084     to get a fresh snapshot.
   2085 
   2086     Returns:
   2087       an instance of prefs_info.PrefsInfo
   2088     """
   2089     return prefs_info.PrefsInfo(
   2090         self._GetResultFromJSONRequest({'command': 'GetLocalStatePrefsInfo'},
   2091                                        windex=None))
   2092 
   2093   def SetLocalStatePrefs(self, path, value):
   2094     """Set local state preference for the given path.
   2095 
   2096     Preferences are stored by Chromium as a hierarchical dictionary.
   2097     dot-separated paths can be used to refer to a particular preference.
   2098     example: "session.restore_on_startup"
   2099 
   2100     Some preferences are managed, that is, they cannot be changed by the
   2101     user. It's up to the user to know which ones can be changed. Typically,
   2102     the options available via Chromium preferences can be changed.
   2103 
   2104     Args:
   2105       path: the path the preference key that needs to be changed
   2106             example: "session.restore_on_startup"
   2107             One of the equivalent names in chrome/common/pref_names.h could
   2108             also be used.
   2109       value: the value to be set. It could be plain values like int, bool,
   2110              string or complex ones like list.
   2111              The user has to ensure that the right value is specified for the
   2112              right key. It's useful to dump the preferences first to determine
   2113              what type is expected for a particular preference path.
   2114     """
   2115     cmd_dict = {
   2116       'command': 'SetLocalStatePrefs',
   2117       'windex': 0,
   2118       'path': path,
   2119       'value': value,
   2120     }
   2121     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2122 
   2123   def GetPrefsInfo(self, windex=0):
   2124     """Return info about preferences.
   2125 
   2126     This represents a snapshot of the preferences. If you expect preferences
   2127     to have changed, you need to call this method again to get a fresh
   2128     snapshot.
   2129 
   2130     Args:
   2131       windex: The window index, default is 0.
   2132     Returns:
   2133       an instance of prefs_info.PrefsInfo
   2134     """
   2135     cmd_dict = {
   2136       'command': 'GetPrefsInfo',
   2137       'windex': windex,
   2138     }
   2139     return prefs_info.PrefsInfo(
   2140         self._GetResultFromJSONRequest(cmd_dict, windex=None))
   2141 
   2142   def SetPrefs(self, path, value, windex=0):
   2143     """Set preference for the given path.
   2144 
   2145     Preferences are stored by Chromium as a hierarchical dictionary.
   2146     dot-separated paths can be used to refer to a particular preference.
   2147     example: "session.restore_on_startup"
   2148 
   2149     Some preferences are managed, that is, they cannot be changed by the
   2150     user. It's up to the user to know which ones can be changed. Typically,
   2151     the options available via Chromium preferences can be changed.
   2152 
   2153     Args:
   2154       path: the path the preference key that needs to be changed
   2155             example: "session.restore_on_startup"
   2156             One of the equivalent names in chrome/common/pref_names.h could
   2157             also be used.
   2158       value: the value to be set. It could be plain values like int, bool,
   2159              string or complex ones like list.
   2160              The user has to ensure that the right value is specified for the
   2161              right key. It's useful to dump the preferences first to determine
   2162              what type is expected for a particular preference path.
   2163       windex: window index to work on. Defaults to 0 (first window).
   2164     """
   2165     cmd_dict = {
   2166       'command': 'SetPrefs',
   2167       'windex': windex,
   2168       'path': path,
   2169       'value': value,
   2170     }
   2171     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2172 
   2173   def SendWebkitKeyEvent(self, key_type, key_code, tab_index=0, windex=0):
   2174     """Send a webkit key event to the browser.
   2175 
   2176     Args:
   2177       key_type: the raw key type such as 0 for up and 3 for down.
   2178       key_code: the hex value associated with the keypress (virtual key code).
   2179       tab_index: tab index to work on. Defaults to 0 (first tab).
   2180       windex: window index to work on. Defaults to 0 (first window).
   2181     """
   2182     cmd_dict = {
   2183       'command': 'SendWebkitKeyEvent',
   2184       'type': key_type,
   2185       'text': '',
   2186       'isSystemKey': False,
   2187       'unmodifiedText': '',
   2188       'nativeKeyCode': 0,
   2189       'windowsKeyCode': key_code,
   2190       'modifiers': 0,
   2191       'windex': windex,
   2192       'tab_index': tab_index,
   2193     }
   2194     # Sending request for key event.
   2195     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2196 
   2197   def SendWebkitCharEvent(self, char, tab_index=0, windex=0):
   2198     """Send a webkit char to the browser.
   2199 
   2200     Args:
   2201       char: the char value to be sent to the browser.
   2202       tab_index: tab index to work on. Defaults to 0 (first tab).
   2203       windex: window index to work on. Defaults to 0 (first window).
   2204     """
   2205     cmd_dict = {
   2206       'command': 'SendWebkitKeyEvent',
   2207       'type': 2,  # kCharType
   2208       'text': char,
   2209       'isSystemKey': False,
   2210       'unmodifiedText': char,
   2211       'nativeKeyCode': 0,
   2212       'windowsKeyCode': ord((char).upper()),
   2213       'modifiers': 0,
   2214       'windex': windex,
   2215       'tab_index': tab_index,
   2216     }
   2217     # Sending request for a char.
   2218     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2219 
   2220   def SetDownloadShelfVisible(self, is_visible, windex=0):
   2221     """Set download shelf visibility for the specified browser window.
   2222 
   2223     Args:
   2224       is_visible: A boolean indicating the desired shelf visibility.
   2225       windex: The window index, defaults to 0 (the first window).
   2226 
   2227     Raises:
   2228       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2229     """
   2230     cmd_dict = {
   2231       'command': 'SetDownloadShelfVisible',
   2232       'is_visible': is_visible,
   2233       'windex': windex,
   2234     }
   2235     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2236 
   2237   def IsDownloadShelfVisible(self, windex=0):
   2238     """Determine whether the download shelf is visible in the given window.
   2239 
   2240     Args:
   2241       windex: The window index, defaults to 0 (the first window).
   2242 
   2243     Returns:
   2244       A boolean indicating the shelf visibility.
   2245 
   2246     Raises:
   2247       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2248     """
   2249     cmd_dict = {
   2250       'command': 'IsDownloadShelfVisible',
   2251       'windex': windex,
   2252     }
   2253     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['is_visible']
   2254 
   2255   def GetDownloadDirectory(self, tab_index=None, windex=0):
   2256     """Get the path to the download directory.
   2257 
   2258     Warning: Depending on the concept of an active tab is dangerous as it can
   2259     change during the test. Always supply a tab_index explicitly.
   2260 
   2261     Args:
   2262       tab_index: The index of the tab to work on. Defaults to the active tab.
   2263       windex: The index of the browser window to work on. Defaults to 0.
   2264 
   2265     Returns:
   2266       The path to the download directory as a FilePath object.
   2267 
   2268     Raises:
   2269       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2270     """
   2271     if tab_index is None:
   2272       tab_index = self.GetActiveTabIndex(windex)
   2273     cmd_dict = {
   2274       'command': 'GetDownloadDirectory',
   2275       'tab_index': tab_index,
   2276       'windex': windex,
   2277     }
   2278     return FilePath(str(self._GetResultFromJSONRequest(cmd_dict,
   2279                                                        windex=None)['path']))
   2280 
   2281   def WaitForAllDownloadsToComplete(self, pre_download_ids=[], windex=0,
   2282                                     timeout=-1):
   2283     """Wait for all pending downloads to complete.
   2284 
   2285     This function assumes that any downloads to wait for have already been
   2286     triggered and have started (it is ok if those downloads complete before this
   2287     function is called).
   2288 
   2289     Args:
   2290       pre_download_ids: A list of numbers representing the IDs of downloads that
   2291                         exist *before* downloads to wait for have been
   2292                         triggered. Defaults to []; use GetDownloadsInfo() to get
   2293                         these IDs (only necessary if a test previously
   2294                         downloaded files).
   2295       windex: The window index, defaults to 0 (the first window).
   2296       timeout: The maximum amount of time (in milliseconds) to wait for
   2297                downloads to complete.
   2298     """
   2299     cmd_dict = {
   2300       'command': 'WaitForAllDownloadsToComplete',
   2301       'pre_download_ids': pre_download_ids,
   2302     }
   2303     self._GetResultFromJSONRequest(cmd_dict, windex=windex, timeout=timeout)
   2304 
   2305   def PerformActionOnDownload(self, id, action, window_index=0):
   2306     """Perform the given action on the download with the given id.
   2307 
   2308     Args:
   2309       id: The id of the download.
   2310       action: The action to perform on the download.
   2311               Possible actions:
   2312                 'open': Opens the download (waits until it has completed first).
   2313                 'toggle_open_files_like_this': Toggles the 'Always Open Files
   2314                     Of This Type' option.
   2315                 'remove': Removes the file from downloads (not from disk).
   2316                 'decline_dangerous_download': Equivalent to 'Discard' option
   2317                     after downloading a dangerous download (ex. an executable).
   2318                 'save_dangerous_download': Equivalent to 'Save' option after
   2319                     downloading a dangerous file.
   2320                 'pause': Pause the download.  If the download completed before
   2321                     this call or is already paused, it's a no-op.
   2322                 'resume': Resume the download.  If the download completed before
   2323                     this call or was not paused, it's a no-op.
   2324                 'cancel': Cancel the download.
   2325       window_index: The window index, default is 0.
   2326 
   2327     Returns:
   2328       A dictionary representing the updated download item (except in the case
   2329       of 'decline_dangerous_download', 'toggle_open_files_like_this', and
   2330       'remove', which return an empty dict).
   2331       Example dictionary:
   2332       { u'PercentComplete': 100,
   2333         u'file_name': u'file.txt',
   2334         u'full_path': u'/path/to/file.txt',
   2335         u'id': 0,
   2336         u'is_otr': False,
   2337         u'is_paused': False,
   2338         u'is_temporary': False,
   2339         u'open_when_complete': False,
   2340         u'referrer_url': u'',
   2341         u'state': u'COMPLETE',
   2342         u'danger_type': u'DANGEROUS_FILE',
   2343         u'url':  u'file://url/to/file.txt'
   2344       }
   2345     """
   2346     cmd_dict = {  # Prepare command for the json interface
   2347       'command': 'PerformActionOnDownload',
   2348       'id': id,
   2349       'action': action
   2350     }
   2351     return self._GetResultFromJSONRequest(cmd_dict, windex=window_index)
   2352 
   2353   def DownloadAndWaitForStart(self, file_url, windex=0):
   2354     """Trigger download for the given url and wait for downloads to start.
   2355 
   2356     It waits for download by looking at the download info from Chrome, so
   2357     anything which isn't registered by the history service won't be noticed.
   2358     This is not thread-safe, but it's fine to call this method to start
   2359     downloading multiple files in parallel. That is after starting a
   2360     download, it's fine to start another one even if the first one hasn't
   2361     completed.
   2362     """
   2363     try:
   2364       num_downloads = len(self.GetDownloadsInfo(windex).Downloads())
   2365     except JSONInterfaceError:
   2366       num_downloads = 0
   2367 
   2368     self.NavigateToURL(file_url, windex)  # Trigger download.
   2369     # It might take a while for the download to kick in, hold on until then.
   2370     self.assertTrue(self.WaitUntil(
   2371         lambda: len(self.GetDownloadsInfo(windex).Downloads()) >
   2372                 num_downloads))
   2373 
   2374   def SetWindowDimensions(
   2375       self, x=None, y=None, width=None, height=None, windex=0):
   2376     """Set window dimensions.
   2377 
   2378     All args are optional and current values will be preserved.
   2379     Arbitrarily large values will be handled gracefully by the browser.
   2380 
   2381     Args:
   2382       x: window origin x
   2383       y: window origin y
   2384       width: window width
   2385       height: window height
   2386       windex: window index to work on. Defaults to 0 (first window)
   2387     """
   2388     cmd_dict = {  # Prepare command for the json interface
   2389       'command': 'SetWindowDimensions',
   2390     }
   2391     if x:
   2392       cmd_dict['x'] = x
   2393     if y:
   2394       cmd_dict['y'] = y
   2395     if width:
   2396       cmd_dict['width'] = width
   2397     if height:
   2398       cmd_dict['height'] = height
   2399     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2400 
   2401   def WaitForInfobarCount(self, count, windex=0, tab_index=0):
   2402     """Wait until infobar count becomes |count|.
   2403 
   2404     Note: Wait duration is capped by the automation timeout.
   2405 
   2406     Args:
   2407       count: requested number of infobars
   2408       windex: window index.  Defaults to 0 (first window)
   2409       tab_index: tab index  Defaults to 0 (first tab)
   2410 
   2411     Raises:
   2412       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2413     """
   2414     # TODO(phajdan.jr): We need a solid automation infrastructure to handle
   2415     # these cases. See crbug.com/53647.
   2416     def _InfobarCount():
   2417       windows = self.GetBrowserInfo()['windows']
   2418       if windex >= len(windows):  # not enough windows
   2419         return -1
   2420       tabs = windows[windex]['tabs']
   2421       if tab_index >= len(tabs):  # not enough tabs
   2422         return -1
   2423       return len(tabs[tab_index]['infobars'])
   2424 
   2425     return self.WaitUntil(_InfobarCount, expect_retval=count)
   2426 
   2427   def PerformActionOnInfobar(
   2428       self, action, infobar_index, windex=0, tab_index=0):
   2429     """Perform actions on an infobar.
   2430 
   2431     Args:
   2432       action: the action to be performed.
   2433               Actions depend on the type of the infobar.  The user needs to
   2434               call the right action for the right infobar.
   2435               Valid inputs are:
   2436               - "dismiss": closes the infobar (for all infobars)
   2437               - "accept", "cancel": click accept / cancel (for confirm infobars)
   2438               - "allow", "deny": click allow / deny (for media stream infobars)
   2439       infobar_index: 0-based index of the infobar on which to perform the action
   2440       windex: 0-based window index  Defaults to 0 (first window)
   2441       tab_index: 0-based tab index.  Defaults to 0 (first tab)
   2442 
   2443     Raises:
   2444       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2445     """
   2446     cmd_dict = {
   2447       'command': 'PerformActionOnInfobar',
   2448       'action': action,
   2449       'infobar_index': infobar_index,
   2450       'tab_index': tab_index,
   2451     }
   2452     if action not in ('dismiss', 'accept', 'allow', 'deny', 'cancel'):
   2453       raise JSONInterfaceError('Invalid action %s' % action)
   2454     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2455 
   2456   def GetBrowserInfo(self):
   2457     """Return info about the browser.
   2458 
   2459     This includes things like the version number, the executable name,
   2460     executable path, pid info about the renderer/plugin/extension processes,
   2461     window dimensions. (See sample below)
   2462 
   2463     For notification pid info, see 'GetActiveNotifications'.
   2464 
   2465     Returns:
   2466       a dictionary
   2467 
   2468       Sample:
   2469       { u'browser_pid': 93737,
   2470         # Child processes are the processes for plugins and other workers.
   2471         u'child_process_path': u'.../Chromium.app/Contents/'
   2472                                 'Versions/6.0.412.0/Chromium Helper.app/'
   2473                                 'Contents/MacOS/Chromium Helper',
   2474         u'child_processes': [ { u'name': u'Shockwave Flash',
   2475                                 u'pid': 93766,
   2476                                 u'type': u'Plug-in'}],
   2477         u'extension_views': [ {
   2478           u'name': u'Webpage Screenshot',
   2479           u'pid': 93938,
   2480           u'extension_id': u'dgcoklnmbeljaehamekjpeidmbicddfj',
   2481           u'url': u'chrome-extension://dgcoklnmbeljaehamekjpeidmbicddfj/'
   2482                     'bg.html',
   2483           u'loaded': True,
   2484           u'view': {
   2485             u'render_process_id': 2,
   2486             u'render_view_id': 1},
   2487           u'view_type': u'EXTENSION_BACKGROUND_PAGE'}]
   2488         u'properties': {
   2489           u'BrowserProcessExecutableName': u'Chromium',
   2490           u'BrowserProcessExecutablePath': u'Chromium.app/Contents/MacOS/'
   2491                                             'Chromium',
   2492           u'ChromeVersion': u'6.0.412.0',
   2493           u'HelperProcessExecutableName': u'Chromium Helper',
   2494           u'HelperProcessExecutablePath': u'Chromium Helper.app/Contents/'
   2495                                             'MacOS/Chromium Helper',
   2496           u'command_line_string': "COMMAND_LINE_STRING --WITH-FLAGS",
   2497           u'branding': 'Chromium',
   2498           u'is_official': False,}
   2499         # The order of the windows and tabs listed here will be the same as
   2500         # what shows up on screen.
   2501         u'windows': [ { u'index': 0,
   2502                         u'height': 1134,
   2503                         u'incognito': False,
   2504                         u'profile_path': u'Default',
   2505                         u'fullscreen': False,
   2506                         u'visible_page_actions':
   2507                           [u'dgcoklnmbeljaehamekjpeidmbicddfj',
   2508                            u'osfcklnfasdofpcldmalwpicslasdfgd']
   2509                         u'selected_tab': 0,
   2510                         u'tabs': [ {
   2511                           u'index': 0,
   2512                           u'infobars': [],
   2513                           u'pinned': True,
   2514                           u'renderer_pid': 93747,
   2515                           u'url': u'http://www.google.com/' }, {
   2516                           u'index': 1,
   2517                           u'infobars': [],
   2518                           u'pinned': False,
   2519                           u'renderer_pid': 93919,
   2520                           u'url': u'https://chrome.google.com/'}, {
   2521                           u'index': 2,
   2522                           u'infobars': [ {
   2523                             u'buttons': [u'Allow', u'Deny'],
   2524                             u'link_text': u'Learn more',
   2525                             u'text': u'slides.html5rocks.com wants to track '
   2526                                       'your physical location',
   2527                             u'type': u'confirm_infobar'}],
   2528                           u'pinned': False,
   2529                           u'renderer_pid': 93929,
   2530                           u'url': u'http://slides.html5rocks.com/#slide14'},
   2531                             ],
   2532                         u'type': u'tabbed',
   2533                         u'width': 925,
   2534                         u'x': 26,
   2535                         u'y': 44}]}
   2536 
   2537     Raises:
   2538       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2539     """
   2540     cmd_dict = {  # Prepare command for the json interface
   2541       'command': 'GetBrowserInfo',
   2542     }
   2543     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2544 
   2545   def IsAura(self):
   2546     """Is this Aura?"""
   2547     return self.GetBrowserInfo()['properties']['aura']
   2548 
   2549   def GetProcessInfo(self):
   2550     """Returns information about browser-related processes that currently exist.
   2551 
   2552     This will also return information about other currently-running browsers
   2553     besides just Chrome.
   2554 
   2555     Returns:
   2556       A dictionary containing browser-related process information as identified
   2557       by class MemoryDetails in src/chrome/browser/memory_details.h.  The
   2558       dictionary contains a single key 'browsers', mapped to a list of
   2559       dictionaries containing information about each browser process name.
   2560       Each of those dictionaries contains a key 'processes', mapped to a list
   2561       of dictionaries containing the specific information for each process
   2562       with the given process name.
   2563 
   2564       The memory values given in |committed_mem| and |working_set_mem| are in
   2565       KBytes.
   2566 
   2567       Sample:
   2568       { 'browsers': [ { 'name': 'Chromium',
   2569                         'process_name': 'chrome',
   2570                         'processes': [ { 'child_process_type': 'Browser',
   2571                                          'committed_mem': { 'image': 0,
   2572                                                             'mapped': 0,
   2573                                                             'priv': 0},
   2574                                          'is_diagnostics': False,
   2575                                          'num_processes': 1,
   2576                                          'pid': 7770,
   2577                                          'product_name': '',
   2578                                          'renderer_type': 'Unknown',
   2579                                          'titles': [],
   2580                                          'version': '',
   2581                                          'working_set_mem': { 'priv': 43672,
   2582                                                               'shareable': 0,
   2583                                                               'shared': 59251}},
   2584                                        { 'child_process_type': 'Tab',
   2585                                          'committed_mem': { 'image': 0,
   2586                                                             'mapped': 0,
   2587                                                             'priv': 0},
   2588                                          'is_diagnostics': False,
   2589                                          'num_processes': 1,
   2590                                          'pid': 7791,
   2591                                          'product_name': '',
   2592                                          'renderer_type': 'Tab',
   2593                                          'titles': ['about:blank'],
   2594                                          'version': '',
   2595                                          'working_set_mem': { 'priv': 16768,
   2596                                                               'shareable': 0,
   2597                                                               'shared': 26256}},
   2598                                        ...<more processes>...]}]}
   2599 
   2600     Raises:
   2601       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2602     """
   2603     cmd_dict = {  # Prepare command for the json interface.
   2604       'command': 'GetProcessInfo',
   2605     }
   2606     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2607 
   2608   def GetNavigationInfo(self, tab_index=0, windex=0):
   2609     """Get info about the navigation state of a given tab.
   2610 
   2611     Args:
   2612       tab_index: The tab index, default is 0.
   2613       window_index: The window index, default is 0.
   2614 
   2615     Returns:
   2616       a dictionary.
   2617       Sample:
   2618 
   2619       { u'favicon_url': u'https://www.google.com/favicon.ico',
   2620         u'page_type': u'NORMAL_PAGE',
   2621         u'ssl': { u'displayed_insecure_content': False,
   2622                   u'ran_insecure_content': False,
   2623                   u'security_style': u'SECURITY_STYLE_AUTHENTICATED'}}
   2624 
   2625       Values for security_style can be:
   2626         SECURITY_STYLE_UNKNOWN
   2627         SECURITY_STYLE_UNAUTHENTICATED
   2628         SECURITY_STYLE_AUTHENTICATION_BROKEN
   2629         SECURITY_STYLE_AUTHENTICATED
   2630 
   2631       Values for page_type can be:
   2632         NORMAL_PAGE
   2633         ERROR_PAGE
   2634         INTERSTITIAL_PAGE
   2635     """
   2636     cmd_dict = {  # Prepare command for the json interface
   2637       'command': 'GetNavigationInfo',
   2638       'tab_index': tab_index,
   2639     }
   2640     return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   2641 
   2642   def GetSecurityState(self, tab_index=0, windex=0):
   2643     """Get security details for a given tab.
   2644 
   2645     Args:
   2646       tab_index: The tab index, default is 0.
   2647       window_index: The window index, default is 0.
   2648 
   2649     Returns:
   2650       a dictionary.
   2651       Sample:
   2652       { "security_style": SECURITY_STYLE_AUTHENTICATED,
   2653         "ssl_cert_status": 3,  // bitmask of status flags
   2654         "insecure_content_status": 1,  // bitmask of status flags
   2655       }
   2656     """
   2657     cmd_dict = {  # Prepare command for the json interface
   2658       'command': 'GetSecurityState',
   2659       'tab_index': tab_index,
   2660       'windex': windex,
   2661     }
   2662     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2663 
   2664   def GetHistoryInfo(self, search_text='', windex=0):
   2665     """Return info about browsing history.
   2666 
   2667     Args:
   2668       search_text: the string to search in history.  Defaults to empty string
   2669                    which means that all history would be returned. This is
   2670                    functionally equivalent to searching for a text in the
   2671                    chrome://history UI. So partial matches work too.
   2672                    When non-empty, the history items returned will contain a
   2673                    "snippet" field corresponding to the snippet visible in
   2674                    the chrome://history/ UI.
   2675       windex: index of the browser window, defaults to 0.
   2676 
   2677     Returns:
   2678       an instance of history_info.HistoryInfo
   2679     """
   2680     cmd_dict = {  # Prepare command for the json interface
   2681       'command': 'GetHistoryInfo',
   2682       'search_text': search_text,
   2683     }
   2684     return history_info.HistoryInfo(
   2685         self._GetResultFromJSONRequest(cmd_dict, windex=windex))
   2686 
   2687   def InstallExtension(self, extension_path, with_ui=False, from_webstore=None,
   2688                        windex=0, tab_index=0):
   2689     """Installs an extension from the given path.
   2690 
   2691     The path must be absolute and may be a crx file or an unpacked extension
   2692     directory. Returns the extension ID if successfully installed and loaded.
   2693     Otherwise, throws an exception. The extension must not already be installed.
   2694 
   2695     Args:
   2696       extension_path: The absolute path to the extension to install. If the
   2697                       extension is packed, it must have a .crx extension.
   2698       with_ui: Whether the extension install confirmation UI should be shown.
   2699       from_webstore: If True, forces a .crx extension to be recognized as one
   2700           from the webstore. Can be used to force install an extension with
   2701           'experimental' permissions.
   2702       windex: Integer index of the browser window to use; defaults to 0
   2703               (first window).
   2704 
   2705     Returns:
   2706       The ID of the installed extension.
   2707 
   2708     Raises:
   2709       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2710     """
   2711     cmd_dict = {
   2712         'command': 'InstallExtension',
   2713         'path': extension_path,
   2714         'with_ui': with_ui,
   2715         'windex': windex,
   2716         'tab_index': tab_index,
   2717     }
   2718 
   2719     if from_webstore:
   2720       cmd_dict['from_webstore'] = True
   2721     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['id']
   2722 
   2723   def GetExtensionsInfo(self, windex=0):
   2724     """Returns information about all installed extensions.
   2725 
   2726     Args:
   2727       windex: Integer index of the browser window to use; defaults to 0
   2728               (first window).
   2729 
   2730     Returns:
   2731       A list of dictionaries representing each of the installed extensions.
   2732       Example:
   2733       [ { u'api_permissions': [u'bookmarks', u'experimental', u'tabs'],
   2734           u'background_url': u'',
   2735           u'description': u'Bookmark Manager',
   2736           u'effective_host_permissions': [u'chrome://favicon/*',
   2737                                           u'chrome://resources/*'],
   2738           u'host_permissions': [u'chrome://favicon/*', u'chrome://resources/*'],
   2739           u'id': u'eemcgdkfndhakfknompkggombfjjjeno',
   2740           u'is_component': True,
   2741           u'is_internal': False,
   2742           u'name': u'Bookmark Manager',
   2743           u'options_url': u'',
   2744           u'public_key': u'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQcByy+eN9jza\
   2745                            zWF/DPn7NW47sW7lgmpk6eKc0BQM18q8hvEM3zNm2n7HkJv/R6f\
   2746                            U+X5mtqkDuKvq5skF6qqUF4oEyaleWDFhd1xFwV7JV+/DU7bZ00\
   2747                            w2+6gzqsabkerFpoP33ZRIw7OviJenP0c0uWqDWF8EGSyMhB3tx\
   2748                            qhOtiQIDAQAB',
   2749           u'version': u'0.1' },
   2750         { u'api_permissions': [...],
   2751           u'background_url': u'chrome-extension://\
   2752                                lkdedmbpkaiahjjibfdmpoefffnbdkli/\
   2753                                background.html',
   2754           u'description': u'Extension which lets you read your Facebook news \
   2755                             feed and wall. You can also post status updates.',
   2756           u'effective_host_permissions': [...],
   2757           u'host_permissions': [...],
   2758           u'id': u'lkdedmbpkaiahjjibfdmpoefffnbdkli',
   2759           u'name': u'Facebook for Google Chrome',
   2760           u'options_url': u'',
   2761           u'public_key': u'...',
   2762           u'version': u'2.0.9'
   2763           u'is_enabled': True,
   2764           u'allowed_in_incognito': True} ]
   2765     """
   2766     cmd_dict = {  # Prepare command for the json interface
   2767       'command': 'GetExtensionsInfo',
   2768       'windex': windex,
   2769     }
   2770     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['extensions']
   2771 
   2772   def UninstallExtensionById(self, id, windex=0):
   2773     """Uninstall the extension with the given id.
   2774 
   2775     Args:
   2776       id: The string id of the extension.
   2777       windex: Integer index of the browser window to use; defaults to 0
   2778               (first window).
   2779 
   2780     Returns:
   2781       True, if the extension was successfully uninstalled, or
   2782       False, otherwise.
   2783     """
   2784     cmd_dict = {  # Prepare command for the json interface
   2785       'command': 'UninstallExtensionById',
   2786       'id': id,
   2787       'windex': windex,
   2788     }
   2789     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['success']
   2790 
   2791   def SetExtensionStateById(self, id, enable, allow_in_incognito, windex=0):
   2792     """Set extension state: enable/disable, allow/disallow in incognito mode.
   2793 
   2794     Args:
   2795       id: The string id of the extension.
   2796       enable: A boolean, enable extension.
   2797       allow_in_incognito: A boolean, allow extension in incognito.
   2798       windex: Integer index of the browser window to use; defaults to 0
   2799               (first window).
   2800     """
   2801     cmd_dict = {  # Prepare command for the json interface
   2802       'command': 'SetExtensionStateById',
   2803       'id': id,
   2804       'enable': enable,
   2805       'allow_in_incognito': allow_in_incognito,
   2806       'windex': windex,
   2807     }
   2808     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2809 
   2810   def TriggerPageActionById(self, id, tab_index=0, windex=0):
   2811     """Trigger page action asynchronously in the active tab.
   2812 
   2813     The page action icon must be displayed before invoking this function.
   2814 
   2815     Args:
   2816       id: The string id of the extension.
   2817       tab_index: Integer index of the tab to use; defaults to 0 (first tab).
   2818       windex: Integer index of the browser window to use; defaults to 0
   2819               (first window).
   2820     """
   2821     cmd_dict = {  # Prepare command for the json interface
   2822       'command': 'TriggerPageActionById',
   2823       'id': id,
   2824       'windex': windex,
   2825       'tab_index': tab_index,
   2826     }
   2827     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2828 
   2829   def TriggerBrowserActionById(self, id, tab_index=0, windex=0):
   2830     """Trigger browser action asynchronously in the active tab.
   2831 
   2832     Args:
   2833       id: The string id of the extension.
   2834       tab_index: Integer index of the tab to use; defaults to 0 (first tab).
   2835       windex: Integer index of the browser window to use; defaults to 0
   2836               (first window).
   2837     """
   2838     cmd_dict = {  # Prepare command for the json interface
   2839       'command': 'TriggerBrowserActionById',
   2840       'id': id,
   2841       'windex': windex,
   2842       'tab_index': tab_index,
   2843     }
   2844     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2845 
   2846   def UpdateExtensionsNow(self, windex=0):
   2847     """Auto-updates installed extensions.
   2848 
   2849     Waits until all extensions are updated, loaded, and ready for use.
   2850     This is equivalent to clicking the "Update extensions now" button on the
   2851     chrome://extensions page.
   2852 
   2853     Args:
   2854       windex: Integer index of the browser window to use; defaults to 0
   2855               (first window).
   2856 
   2857     Raises:
   2858       pyauto_errors.JSONInterfaceError if the automation returns an error.
   2859     """
   2860     cmd_dict = {  # Prepare command for the json interface.
   2861       'command': 'UpdateExtensionsNow',
   2862       'windex': windex,
   2863     }
   2864     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   2865 
   2866   def WaitUntilExtensionViewLoaded(self, name=None, extension_id=None,
   2867                                    url=None, view_type=None):
   2868     """Wait for a loaded extension view matching all the given properties.
   2869 
   2870     If no matching extension views are found, wait for one to be loaded.
   2871     If there are more than one matching extension view, return one at random.
   2872     Uses WaitUntil so timeout is capped by automation timeout.
   2873     Refer to extension_view dictionary returned in GetBrowserInfo()
   2874     for sample input/output values.
   2875 
   2876     Args:
   2877       name: (optional) Name of the extension.
   2878       extension_id: (optional) ID of the extension.
   2879       url: (optional) URL of the extension view.
   2880       view_type: (optional) Type of the extension view.
   2881         ['EXTENSION_BACKGROUND_PAGE'|'EXTENSION_POPUP'|'EXTENSION_INFOBAR'|
   2882          'EXTENSION_DIALOG']
   2883 
   2884     Returns:
   2885       The 'view' property of the extension view.
   2886       None, if no view loaded.
   2887 
   2888     Raises:
   2889       pyauto_errors.JSONInterfaceError if the automation returns an error.
   2890     """
   2891     def _GetExtensionViewLoaded():
   2892       extension_views = self.GetBrowserInfo()['extension_views']
   2893       for extension_view in extension_views:
   2894         if ((name and name != extension_view['name']) or
   2895             (extension_id and extension_id != extension_view['extension_id']) or
   2896             (url and url != extension_view['url']) or
   2897             (view_type and view_type != extension_view['view_type'])):
   2898           continue
   2899         if extension_view['loaded']:
   2900           return extension_view['view']
   2901       return False
   2902 
   2903     if self.WaitUntil(lambda: _GetExtensionViewLoaded()):
   2904       return _GetExtensionViewLoaded()
   2905     return None
   2906 
   2907   def WaitUntilExtensionViewClosed(self, view):
   2908     """Wait for the given extension view to to be closed.
   2909 
   2910     Uses WaitUntil so timeout is capped by automation timeout.
   2911     Refer to extension_view dictionary returned by GetBrowserInfo()
   2912     for sample input value.
   2913 
   2914     Args:
   2915       view: 'view' property of extension view.
   2916 
   2917     Raises:
   2918       pyauto_errors.JSONInterfaceError if the automation returns an error.
   2919     """
   2920     def _IsExtensionViewClosed():
   2921       extension_views = self.GetBrowserInfo()['extension_views']
   2922       for extension_view in extension_views:
   2923         if view == extension_view['view']:
   2924           return False
   2925       return True
   2926 
   2927     return self.WaitUntil(lambda: _IsExtensionViewClosed())
   2928 
   2929   def GetPluginsInfo(self, windex=0):
   2930     """Return info about plugins.
   2931 
   2932     This is the info available from about:plugins
   2933 
   2934     Returns:
   2935       an instance of plugins_info.PluginsInfo
   2936     """
   2937     return plugins_info.PluginsInfo(
   2938         self._GetResultFromJSONRequest({'command': 'GetPluginsInfo'},
   2939                                        windex=windex))
   2940 
   2941   def EnablePlugin(self, path):
   2942     """Enable the plugin at the given path.
   2943 
   2944     Use GetPluginsInfo() to fetch path info about a plugin.
   2945 
   2946     Raises:
   2947       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2948     """
   2949     cmd_dict = {
   2950       'command': 'EnablePlugin',
   2951       'path': path,
   2952     }
   2953     self._GetResultFromJSONRequest(cmd_dict)
   2954 
   2955   def DisablePlugin(self, path):
   2956     """Disable the plugin at the given path.
   2957 
   2958     Use GetPluginsInfo() to fetch path info about a plugin.
   2959 
   2960     Raises:
   2961       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   2962     """
   2963     cmd_dict = {
   2964       'command': 'DisablePlugin',
   2965       'path': path,
   2966     }
   2967     self._GetResultFromJSONRequest(cmd_dict)
   2968 
   2969   def GetTabContents(self, tab_index=0, window_index=0):
   2970     """Get the html contents of a tab (a la "view source").
   2971 
   2972     As an implementation detail, this saves the html in a file, reads
   2973     the file into a buffer, then deletes it.
   2974 
   2975     Args:
   2976       tab_index: tab index, defaults to 0.
   2977       window_index: window index, defaults to 0.
   2978     Returns:
   2979       html content of a page as a string.
   2980     """
   2981     tempdir = tempfile.mkdtemp()
   2982     # Make it writable by chronos on chromeos
   2983     os.chmod(tempdir, 0777)
   2984     filename = os.path.join(tempdir, 'content.html')
   2985     cmd_dict = {  # Prepare command for the json interface
   2986       'command': 'SaveTabContents',
   2987       'tab_index': tab_index,
   2988       'filename': filename
   2989     }
   2990     self._GetResultFromJSONRequest(cmd_dict, windex=window_index)
   2991     try:
   2992       f = open(filename)
   2993       all_data = f.read()
   2994       f.close()
   2995       return all_data
   2996     finally:
   2997       shutil.rmtree(tempdir, ignore_errors=True)
   2998 
   2999   def AddSavedPassword(self, password_dict, windex=0):
   3000     """Adds the given username-password combination to the saved passwords.
   3001 
   3002     Args:
   3003       password_dict: a dictionary that represents a password. Example:
   3004       { 'username_value': 'user (at] example.com',        # Required
   3005         'password_value': 'test.password',           # Required
   3006         'signon_realm': 'https://www.example.com/',  # Required
   3007         'time': 1279317810.0,                        # Can get from time.time()
   3008         'origin_url': 'https://www.example.com/login',
   3009         'username_element': 'username',              # The HTML element
   3010         'password_element': 'password',              # The HTML element
   3011         'submit_element': 'submit',                  # The HTML element
   3012         'action_target': 'https://www.example.com/login/',
   3013         'blacklist': False }
   3014       windex: window index; defaults to 0 (first window).
   3015 
   3016     *Blacklist notes* To blacklist a site, add a blacklist password with the
   3017     following dictionary items: origin_url, signon_realm, username_element,
   3018     password_element, action_target, and 'blacklist': True. Then all sites that
   3019     have password forms matching those are blacklisted.
   3020 
   3021     Returns:
   3022       True if adding the password succeeded, false otherwise. In incognito
   3023       mode, adding the password should fail.
   3024 
   3025     Raises:
   3026       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3027     """
   3028     cmd_dict = {  # Prepare command for the json interface
   3029       'command': 'AddSavedPassword',
   3030       'password': password_dict
   3031     }
   3032     return self._GetResultFromJSONRequest(
   3033         cmd_dict, windex=windex)['password_added']
   3034 
   3035   def RemoveSavedPassword(self, password_dict, windex=0):
   3036     """Removes the password matching the provided password dictionary.
   3037 
   3038     Args:
   3039       password_dict: A dictionary that represents a password.
   3040                      For an example, see the dictionary in AddSavedPassword.
   3041       windex: The window index, default is 0 (first window).
   3042     """
   3043     cmd_dict = {  # Prepare command for the json interface
   3044       'command': 'RemoveSavedPassword',
   3045       'password': password_dict
   3046     }
   3047     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   3048 
   3049   def GetSavedPasswords(self):
   3050     """Return the passwords currently saved.
   3051 
   3052     Returns:
   3053       A list of dictionaries representing each password. For an example
   3054       dictionary see AddSavedPassword documentation. The overall structure will
   3055       be:
   3056       [ {password1 dictionary}, {password2 dictionary} ]
   3057     """
   3058     cmd_dict = {  # Prepare command for the json interface
   3059       'command': 'GetSavedPasswords'
   3060     }
   3061     return self._GetResultFromJSONRequest(cmd_dict)['passwords']
   3062 
   3063   def SetTheme(self, crx_file_path, windex=0):
   3064     """Installs the given theme synchronously.
   3065 
   3066     A theme file is a file with a .crx suffix, like an extension.  The theme
   3067     file must be specified with an absolute path.  This method call waits until
   3068     the theme is installed and will trigger the "theme installed" infobar.
   3069     If the install is unsuccessful, will throw an exception.
   3070 
   3071     Uses InstallExtension().
   3072 
   3073     Returns:
   3074       The ID of the installed theme.
   3075 
   3076     Raises:
   3077       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3078     """
   3079     return self.InstallExtension(crx_file_path, True, windex)
   3080 
   3081   def GetActiveNotifications(self):
   3082     """Gets a list of the currently active/shown HTML5 notifications.
   3083 
   3084     Returns:
   3085       a list containing info about each active notification, with the
   3086       first item in the list being the notification on the bottom of the
   3087       notification stack. The 'content_url' key can refer to a URL or a data
   3088       URI. The 'pid' key-value pair may be invalid if the notification is
   3089       closing.
   3090 
   3091     SAMPLE:
   3092     [ { u'content_url': u'data:text/html;charset=utf-8,%3C!DOCTYPE%l%3E%0Atm...'
   3093         u'display_source': 'www.corp.google.com',
   3094         u'origin_url': 'http://www.corp.google.com/',
   3095         u'pid': 8505},
   3096       { u'content_url': 'http://www.gmail.com/special_notification.html',
   3097         u'display_source': 'www.gmail.com',
   3098         u'origin_url': 'http://www.gmail.com/',
   3099         u'pid': 9291}]
   3100 
   3101     Raises:
   3102       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3103     """
   3104     return [x for x in self.GetAllNotifications() if 'pid' in x]
   3105 
   3106   def GetAllNotifications(self):
   3107     """Gets a list of all active and queued HTML5 notifications.
   3108 
   3109     An active notification is one that is currently shown to the user. Chrome's
   3110     notification system will limit the number of notifications shown (currently
   3111     by only allowing a certain percentage of the screen to be taken up by them).
   3112     A notification will be queued if there are too many active notifications.
   3113     Once other notifications are closed, another will be shown from the queue.
   3114 
   3115     Returns:
   3116       a list containing info about each notification, with the first
   3117       item in the list being the notification on the bottom of the
   3118       notification stack. The 'content_url' key can refer to a URL or a data
   3119       URI. The 'pid' key-value pair will only be present for active
   3120       notifications.
   3121 
   3122     SAMPLE:
   3123     [ { u'content_url': u'data:text/html;charset=utf-8,%3C!DOCTYPE%l%3E%0Atm...'
   3124         u'display_source': 'www.corp.google.com',
   3125         u'origin_url': 'http://www.corp.google.com/',
   3126         u'pid': 8505},
   3127       { u'content_url': 'http://www.gmail.com/special_notification.html',
   3128         u'display_source': 'www.gmail.com',
   3129         u'origin_url': 'http://www.gmail.com/'}]
   3130 
   3131     Raises:
   3132       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3133     """
   3134     cmd_dict = {
   3135       'command': 'GetAllNotifications',
   3136     }
   3137     return self._GetResultFromJSONRequest(cmd_dict)['notifications']
   3138 
   3139   def CloseNotification(self, index):
   3140     """Closes the active HTML5 notification at the given index.
   3141 
   3142     Args:
   3143       index: the index of the notification to close. 0 refers to the
   3144              notification on the bottom of the notification stack.
   3145 
   3146     Raises:
   3147       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3148     """
   3149     cmd_dict = {
   3150       'command': 'CloseNotification',
   3151       'index': index,
   3152     }
   3153     return self._GetResultFromJSONRequest(cmd_dict)
   3154 
   3155   def WaitForNotificationCount(self, count):
   3156     """Waits for the number of active HTML5 notifications to reach the given
   3157     count.
   3158 
   3159     Raises:
   3160       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3161     """
   3162     cmd_dict = {
   3163       'command': 'WaitForNotificationCount',
   3164       'count': count,
   3165     }
   3166     self._GetResultFromJSONRequest(cmd_dict)
   3167 
   3168   def FindInPage(self, search_string, forward=True,
   3169                  match_case=False, find_next=False,
   3170                  tab_index=0, windex=0, timeout=-1):
   3171     """Find the match count for the given search string and search parameters.
   3172     This is equivalent to using the find box.
   3173 
   3174     Args:
   3175       search_string: The string to find on the page.
   3176       forward: Boolean to set if the search direction is forward or backwards
   3177       match_case: Boolean to set for case sensitive search.
   3178       find_next: Boolean to set to continue the search or start from beginning.
   3179       tab_index: The tab index, default is 0.
   3180       windex: The window index, default is 0.
   3181       timeout: request timeout (in milliseconds), default is -1.
   3182 
   3183     Returns:
   3184       number of matches found for the given search string and parameters
   3185     SAMPLE:
   3186     { u'match_count': 10,
   3187       u'match_left': 100,
   3188       u'match_top': 100,
   3189       u'match_right': 200,
   3190       u'match_bottom': 200}
   3191 
   3192     Raises:
   3193       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3194     """
   3195     cmd_dict = {
   3196       'command': 'FindInPage',
   3197       'tab_index' : tab_index,
   3198       'search_string' : search_string,
   3199       'forward' : forward,
   3200       'match_case' : match_case,
   3201       'find_next' : find_next,
   3202     }
   3203     return self._GetResultFromJSONRequest(cmd_dict, windex=windex,
   3204                                           timeout=timeout)
   3205 
   3206   def OpenFindInPage(self, windex=0):
   3207     """Opens the "Find in Page" box.
   3208 
   3209     Args:
   3210       windex: Index of the window; defaults to 0.
   3211 
   3212     Raises:
   3213       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3214     """
   3215     cmd_dict = {
   3216       'command': 'OpenFindInPage',
   3217       'windex' : windex,
   3218     }
   3219     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   3220 
   3221   def IsFindInPageVisible(self, windex=0):
   3222     """Returns the visibility of the "Find in Page" box.
   3223 
   3224     Args:
   3225       windex: Index of the window; defaults to 0.
   3226 
   3227     Returns:
   3228       A boolean indicating the visibility state of the "Find in Page" box.
   3229 
   3230     Raises:
   3231       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3232     """
   3233     cmd_dict = {
   3234       'command': 'IsFindInPageVisible',
   3235       'windex' : windex,
   3236     }
   3237     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['is_visible']
   3238 
   3239 
   3240   def AddDomEventObserver(self, event_name='', automation_id=-1,
   3241                           recurring=False):
   3242     """Adds a DomEventObserver associated with the AutomationEventQueue.
   3243 
   3244     An app raises a matching event in Javascript by calling:
   3245     window.domAutomationController.sendWithId(automation_id, event_name)
   3246 
   3247     Args:
   3248       event_name: The event name to watch for. By default an event is raised
   3249                   for any message.
   3250       automation_id: The Automation Id of the sent message. By default all
   3251                      messages sent from the window.domAutomationController are
   3252                      observed. Note that other PyAuto functions also send
   3253                      messages through window.domAutomationController with
   3254                      arbirary Automation Ids and they will be observed.
   3255       recurring: If False the observer will be removed after it generates one
   3256                  event, otherwise it will continue observing and generating
   3257                  events until explicity removed with RemoveEventObserver(id).
   3258 
   3259     Returns:
   3260       The id of the created observer, which can be used with GetNextEvent(id)
   3261       and RemoveEventObserver(id).
   3262 
   3263     Raises:
   3264       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3265     """
   3266     cmd_dict = {
   3267       'command': 'AddDomEventObserver',
   3268       'event_name': event_name,
   3269       'automation_id': automation_id,
   3270       'recurring': recurring,
   3271     }
   3272     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['observer_id']
   3273 
   3274   def AddDomMutationObserver(self, mutation_type, xpath,
   3275                              attribute='textContent', expected_value=None,
   3276                              automation_id=44444,
   3277                              exec_js=None, **kwargs):
   3278     """Sets up an event observer watching for a specific DOM mutation.
   3279 
   3280     Creates an observer that raises an event when a mutation of the given type
   3281     occurs on a DOM node specified by |selector|.
   3282 
   3283     Args:
   3284       mutation_type: One of 'add', 'remove', 'change', or 'exists'.
   3285       xpath: An xpath specifying the DOM node to watch. The node must already
   3286           exist if |mutation_type| is 'change'.
   3287       attribute: Attribute to match |expected_value| against, if given. Defaults
   3288           to 'textContent'.
   3289       expected_value: Optional regular expression to match against the node's
   3290           textContent attribute after the mutation. Defaults to None.
   3291       automation_id: The automation_id used to route the observer javascript
   3292           messages. Defaults to 44444.
   3293       exec_js: A callable of the form f(self, js, **kwargs) used to inject the
   3294           MutationObserver javascript. Defaults to None, which uses
   3295           PyUITest.ExecuteJavascript.
   3296 
   3297       Any additional keyword arguments are passed on to ExecuteJavascript and
   3298       can be used to select the tab where the DOM MutationObserver is created.
   3299 
   3300     Returns:
   3301       The id of the created observer, which can be used with GetNextEvent(id)
   3302       and RemoveEventObserver(id).
   3303 
   3304     Raises:
   3305       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3306       pyauto_errors.JavascriptRuntimeError if the injected javascript
   3307           MutationObserver returns an error.
   3308     """
   3309     assert mutation_type in ('add', 'remove', 'change', 'exists'), \
   3310         'Unexpected value "%s" for mutation_type.' % mutation_type
   3311     cmd_dict = {
   3312       'command': 'AddDomEventObserver',
   3313       'event_name': '__dom_mutation_observer__:$(id)',
   3314       'automation_id': automation_id,
   3315       'recurring': False,
   3316     }
   3317     observer_id = (
   3318         self._GetResultFromJSONRequest(cmd_dict, windex=None)['observer_id'])
   3319     expected_string = ('null' if expected_value is None else '"%s"' %
   3320                        expected_value.replace('"', r'\"'))
   3321     jsfile = os.path.join(os.path.abspath(os.path.dirname(__file__)),
   3322                           'dom_mutation_observer.js')
   3323     with open(jsfile, 'r') as f:
   3324       js = ('(' + f.read() + ')(%d, %d, "%s", "%s", "%s", %s);' %
   3325             (automation_id, observer_id, mutation_type,
   3326              xpath.replace('"', r'\"'), attribute, expected_string))
   3327     exec_js = exec_js or PyUITest.ExecuteJavascript
   3328     try:
   3329       jsreturn = exec_js(self, js, **kwargs)
   3330     except JSONInterfaceError:
   3331       raise JSONInterfaceError('Failed to inject DOM mutation observer.')
   3332     if jsreturn != 'success':
   3333       self.RemoveEventObserver(observer_id)
   3334       raise JavascriptRuntimeError(jsreturn)
   3335     return observer_id
   3336 
   3337   def WaitForDomNode(self, xpath, attribute='textContent',
   3338                      expected_value=None, exec_js=None, timeout=-1,
   3339                      msg='Expected DOM node failed to appear.', **kwargs):
   3340     """Waits until a node specified by an xpath exists in the DOM.
   3341 
   3342     NOTE: This does NOT poll. It returns as soon as the node appears, or
   3343       immediately if the node already exists.
   3344 
   3345     Args:
   3346       xpath: An xpath specifying the DOM node to watch.
   3347       attribute: Attribute to match |expected_value| against, if given. Defaults
   3348           to 'textContent'.
   3349       expected_value: Optional regular expression to match against the node's
   3350           textContent attribute. Defaults to None.
   3351       exec_js: A callable of the form f(self, js, **kwargs) used to inject the
   3352           MutationObserver javascript. Defaults to None, which uses
   3353           PyUITest.ExecuteJavascript.
   3354       msg: An optional error message used if a JSONInterfaceError is caught
   3355           while waiting for the DOM node to appear.
   3356       timeout: Time to wait for the node to exist before raising an exception,
   3357           defaults to the default automation timeout.
   3358 
   3359       Any additional keyword arguments are passed on to ExecuteJavascript and
   3360       can be used to select the tab where the DOM MutationObserver is created.
   3361 
   3362     Raises:
   3363       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3364       pyauto_errors.JavascriptRuntimeError if the injected javascript
   3365           MutationObserver returns an error.
   3366     """
   3367     observer_id = self.AddDomMutationObserver('exists', xpath, attribute,
   3368                                               expected_value, exec_js=exec_js,
   3369                                               **kwargs)
   3370     try:
   3371       self.GetNextEvent(observer_id, timeout=timeout)
   3372     except JSONInterfaceError:
   3373       raise JSONInterfaceError(msg)
   3374 
   3375   def GetNextEvent(self, observer_id=-1, blocking=True, timeout=-1):
   3376     """Waits for an observed event to occur.
   3377 
   3378     The returned event is removed from the Event Queue. If there is already a
   3379     matching event in the queue it is returned immediately, otherwise the call
   3380     blocks until a matching event occurs. If blocking is disabled and no
   3381     matching event is in the queue this function will immediately return None.
   3382 
   3383     Args:
   3384       observer_id: The id of the observer to wait for, matches any event by
   3385                    default.
   3386       blocking: If True waits until there is a matching event in the queue,
   3387                 if False and there is no event waiting in the queue returns None
   3388                 immediately.
   3389       timeout: Time to wait for a matching event, defaults to the default
   3390                automation timeout.
   3391 
   3392     Returns:
   3393       Event response dictionary, or None if blocking is disabled and there is no
   3394       matching event in the queue.
   3395       SAMPLE:
   3396       { 'observer_id': 1,
   3397         'name': 'login completed',
   3398         'type': 'raised_event'}
   3399 
   3400     Raises:
   3401       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3402     """
   3403     cmd_dict = {
   3404       'command': 'GetNextEvent',
   3405       'observer_id' : observer_id,
   3406       'blocking' : blocking,
   3407     }
   3408     return self._GetResultFromJSONRequest(cmd_dict, windex=None,
   3409                                           timeout=timeout)
   3410 
   3411   def RemoveEventObserver(self, observer_id):
   3412     """Removes an Event Observer from the AutomationEventQueue.
   3413 
   3414     Expects a valid observer_id.
   3415 
   3416     Args:
   3417       observer_id: The id of the observer to remove.
   3418 
   3419     Raises:
   3420       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3421     """
   3422     cmd_dict = {
   3423       'command': 'RemoveEventObserver',
   3424       'observer_id' : observer_id,
   3425     }
   3426     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   3427 
   3428   def ClearEventQueue(self):
   3429     """Removes all events currently in the AutomationEventQueue.
   3430 
   3431     Raises:
   3432       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3433     """
   3434     cmd_dict = {
   3435       'command': 'ClearEventQueue',
   3436     }
   3437     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   3438 
   3439   def WaitUntilNavigationCompletes(self, tab_index=0, windex=0):
   3440     """Wait until the specified tab is done navigating.
   3441 
   3442     It is safe to call ExecuteJavascript() as soon as the call returns. If
   3443     there is no outstanding navigation the call will return immediately.
   3444 
   3445     Args:
   3446       tab_index: index of the tab.
   3447       windex: index of the window.
   3448 
   3449     Raises:
   3450       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3451     """
   3452     cmd_dict = {
   3453       'command': 'WaitUntilNavigationCompletes',
   3454       'tab_index': tab_index,
   3455       'windex': windex,
   3456     }
   3457     return self._GetResultFromJSONRequest(cmd_dict)
   3458 
   3459   def ExecuteJavascript(self, js, tab_index=0, windex=0, frame_xpath=''):
   3460     """Executes a script in the specified frame of a tab.
   3461 
   3462     By default, execute the script in the top frame of the first tab in the
   3463     first window. The invoked javascript function must send a result back via
   3464     the domAutomationController.send function, or this function will never
   3465     return.
   3466 
   3467     Args:
   3468       js: script to be executed.
   3469       windex: index of the window.
   3470       tab_index: index of the tab.
   3471       frame_xpath: XPath of the frame to execute the script.  Default is no
   3472       frame. Example: '//frames[1]'.
   3473 
   3474     Returns:
   3475       a value that was sent back via the domAutomationController.send method
   3476 
   3477     Raises:
   3478       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3479     """
   3480     cmd_dict = {
   3481       'command': 'ExecuteJavascript',
   3482       'javascript' : js,
   3483       'windex' : windex,
   3484       'tab_index' : tab_index,
   3485       'frame_xpath' : frame_xpath,
   3486     }
   3487     result = self._GetResultFromJSONRequest(cmd_dict)['result']
   3488     # Wrap result in an array before deserializing because valid JSON has an
   3489     # array or an object as the root.
   3490     json_string = '[' + result + ']'
   3491     return json.loads(json_string)[0]
   3492 
   3493   def ExecuteJavascriptInRenderView(self, js, view, frame_xpath=''):
   3494     """Executes a script in the specified frame of an render view.
   3495 
   3496     The invoked javascript function must send a result back via the
   3497     domAutomationController.send function, or this function will never return.
   3498 
   3499     Args:
   3500       js: script to be executed.
   3501       view: A dictionary representing a unique id for the render view as
   3502       returned for example by.
   3503       self.GetBrowserInfo()['extension_views'][]['view'].
   3504       Example:
   3505       { 'render_process_id': 1,
   3506         'render_view_id' : 2}
   3507 
   3508       frame_xpath: XPath of the frame to execute the script. Default is no
   3509       frame. Example:
   3510       '//frames[1]'
   3511 
   3512     Returns:
   3513       a value that was sent back via the domAutomationController.send method
   3514 
   3515     Raises:
   3516       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3517     """
   3518     cmd_dict = {
   3519       'command': 'ExecuteJavascriptInRenderView',
   3520       'javascript' : js,
   3521       'view' : view,
   3522       'frame_xpath' : frame_xpath,
   3523     }
   3524     result = self._GetResultFromJSONRequest(cmd_dict, windex=None)['result']
   3525     # Wrap result in an array before deserializing because valid JSON has an
   3526     # array or an object as the root.
   3527     json_string = '[' + result + ']'
   3528     return json.loads(json_string)[0]
   3529 
   3530   def ExecuteJavascriptInOOBEWebUI(self, js, frame_xpath=''):
   3531     """Executes a script in the specified frame of the OOBE WebUI.
   3532 
   3533     By default, execute the script in the top frame of the OOBE window. This
   3534     also works for all OOBE pages, including the enterprise enrollment
   3535     screen and login page. The invoked javascript function must send a result
   3536     back via the domAutomationController.send function, or this function will
   3537     never return.
   3538 
   3539     Args:
   3540       js: Script to be executed.
   3541       frame_xpath: XPath of the frame to execute the script. Default is no
   3542           frame. Example: '//frames[1]'
   3543 
   3544     Returns:
   3545       A value that was sent back via the domAutomationController.send method.
   3546 
   3547     Raises:
   3548       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3549     """
   3550     cmd_dict = {
   3551       'command': 'ExecuteJavascriptInOOBEWebUI',
   3552 
   3553       'javascript': js,
   3554       'frame_xpath': frame_xpath,
   3555     }
   3556     result = self._GetResultFromJSONRequest(cmd_dict, windex=None)['result']
   3557     # Wrap result in an array before deserializing because valid JSON has an
   3558     # array or an object as the root.
   3559     return json.loads('[' + result + ']')[0]
   3560 
   3561 
   3562   def GetDOMValue(self, expr, tab_index=0, windex=0, frame_xpath=''):
   3563     """Executes a Javascript expression and returns the value.
   3564 
   3565     This is a wrapper for ExecuteJavascript, eliminating the need to
   3566     explicitly call domAutomationController.send function.
   3567 
   3568     Args:
   3569       expr: expression value to be returned.
   3570       tab_index: index of the tab.
   3571       windex: index of the window.
   3572       frame_xpath: XPath of the frame to execute the script.  Default is no
   3573       frame. Example: '//frames[1]'.
   3574 
   3575     Returns:
   3576       a string that was sent back via the domAutomationController.send method.
   3577     """
   3578     js = 'window.domAutomationController.send(%s);' % expr
   3579     return self.ExecuteJavascript(js, tab_index, windex, frame_xpath)
   3580 
   3581   def CallJavascriptFunc(self, function, args=[], tab_index=0, windex=0):
   3582     """Executes a script which calls a given javascript function.
   3583 
   3584     The invoked javascript function must send a result back via the
   3585     domAutomationController.send function, or this function will never return.
   3586 
   3587     Defaults to first tab in first window.
   3588 
   3589     Args:
   3590       function: name of the function.
   3591       args: list of all the arguments to pass into the called function. These
   3592             should be able to be converted to a string using the |str| function.
   3593       tab_index: index of the tab within the given window.
   3594       windex: index of the window.
   3595 
   3596     Returns:
   3597       a string that was sent back via the domAutomationController.send method
   3598     """
   3599     converted_args = map(lambda arg: json.dumps(arg), args)
   3600     js = '%s(%s)' % (function, ', '.join(converted_args))
   3601     logging.debug('Executing javascript: %s', js)
   3602     return self.ExecuteJavascript(js, tab_index, windex)
   3603 
   3604   def HeapProfilerDump(self, process_type, reason, tab_index=0, windex=0):
   3605     """Dumps a heap profile. It works only on Linux and ChromeOS.
   3606 
   3607     We need an environment variable "HEAPPROFILE" set to a directory and a
   3608     filename prefix, for example, "/tmp/prof".  In a case of this example,
   3609     heap profiles will be dumped into "/tmp/prof.(pid).0002.heap",
   3610     "/tmp/prof.(pid).0003.heap", and so on.  Nothing happens when this
   3611     function is called without the env.
   3612 
   3613     Also, this requires the --enable-memory-benchmarking command line flag.
   3614 
   3615     Args:
   3616       process_type: A string which is one of 'browser' or 'renderer'.
   3617       reason: A string which describes the reason for dumping a heap profile.
   3618               The reason will be included in the logged message.
   3619               Examples:
   3620                 'To check memory leaking'
   3621                 'For PyAuto tests'
   3622       tab_index: tab index to work on if 'process_type' == 'renderer'.
   3623           Defaults to 0 (first tab).
   3624       windex: window index to work on if 'process_type' == 'renderer'.
   3625           Defaults to 0 (first window).
   3626 
   3627     Raises:
   3628       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3629     """
   3630     assert process_type in ('browser', 'renderer')
   3631     if self.IsLinux():  # IsLinux() also implies IsChromeOS().
   3632       js = """
   3633           if (!chrome.memoryBenchmarking ||
   3634               !chrome.memoryBenchmarking.isHeapProfilerRunning()) {
   3635             domAutomationController.send('memory benchmarking disabled');
   3636           } else {
   3637             chrome.memoryBenchmarking.heapProfilerDump("%s", "%s");
   3638             domAutomationController.send('success');
   3639           }
   3640       """ % (process_type, reason.replace('"', '\\"'))
   3641       result = self.ExecuteJavascript(js, tab_index, windex)
   3642       if result != 'success':
   3643         raise JSONInterfaceError('Heap profiler dump failed: ' + result)
   3644     else:
   3645       logging.warn('Heap-profiling is not supported in this OS.')
   3646 
   3647   def GetNTPThumbnails(self):
   3648     """Return a list of info about the sites in the NTP most visited section.
   3649     SAMPLE:
   3650       [{ u'title': u'Google',
   3651          u'url': u'http://www.google.com'},
   3652        {
   3653          u'title': u'Yahoo',
   3654          u'url': u'http://www.yahoo.com'}]
   3655     """
   3656     return self._GetNTPInfo()['most_visited']
   3657 
   3658   def GetNTPThumbnailIndex(self, thumbnail):
   3659     """Returns the index of the given NTP thumbnail, or -1 if it is not shown.
   3660 
   3661     Args:
   3662       thumbnail: a thumbnail dict received from |GetNTPThumbnails|
   3663     """
   3664     thumbnails = self.GetNTPThumbnails()
   3665     for i in range(len(thumbnails)):
   3666       if thumbnails[i]['url'] == thumbnail['url']:
   3667         return i
   3668     return -1
   3669 
   3670   def RemoveNTPThumbnail(self, thumbnail):
   3671     """Removes the NTP thumbnail and returns true on success.
   3672 
   3673     Args:
   3674       thumbnail: a thumbnail dict received from |GetNTPThumbnails|
   3675     """
   3676     self._CheckNTPThumbnailShown(thumbnail)
   3677     cmd_dict = {
   3678       'command': 'RemoveNTPMostVisitedThumbnail',
   3679       'url': thumbnail['url']
   3680     }
   3681     self._GetResultFromJSONRequest(cmd_dict)
   3682 
   3683   def RestoreAllNTPThumbnails(self):
   3684     """Restores all the removed NTP thumbnails.
   3685     Note:
   3686       the default thumbnails may come back into the Most Visited sites
   3687       section after doing this
   3688     """
   3689     cmd_dict = {
   3690       'command': 'RestoreAllNTPMostVisitedThumbnails'
   3691     }
   3692     self._GetResultFromJSONRequest(cmd_dict)
   3693 
   3694   def GetNTPDefaultSites(self):
   3695     """Returns a list of URLs for all the default NTP sites, regardless of
   3696     whether they are showing or not.
   3697 
   3698     These sites are the ones present in the NTP on a fresh install of Chrome.
   3699     """
   3700     return self._GetNTPInfo()['default_sites']
   3701 
   3702   def RemoveNTPDefaultThumbnails(self):
   3703     """Removes all thumbnails for default NTP sites, regardless of whether they
   3704     are showing or not."""
   3705     cmd_dict = { 'command': 'RemoveNTPMostVisitedThumbnail' }
   3706     for site in self.GetNTPDefaultSites():
   3707       cmd_dict['url'] = site
   3708       self._GetResultFromJSONRequest(cmd_dict)
   3709 
   3710   def GetNTPRecentlyClosed(self):
   3711     """Return a list of info about the items in the NTP recently closed section.
   3712     SAMPLE:
   3713       [{
   3714          u'type': u'tab',
   3715          u'url': u'http://www.bing.com',
   3716          u'title': u'Bing',
   3717          u'timestamp': 2139082.03912,  # Seconds since epoch (Jan 1, 1970)
   3718          u'direction': u'ltr'},
   3719        {
   3720          u'type': u'window',
   3721          u'timestamp': 2130821.90812,
   3722          u'tabs': [
   3723          {
   3724            u'type': u'tab',
   3725            u'url': u'http://www.cnn.com',
   3726            u'title': u'CNN',
   3727            u'timestamp': 2129082.12098,
   3728            u'direction': u'ltr'}]},
   3729        {
   3730          u'type': u'tab',
   3731          u'url': u'http://www.altavista.com',
   3732          u'title': u'Altavista',
   3733          u'timestamp': 21390820.12903,
   3734          u'direction': u'rtl'}]
   3735     """
   3736     return self._GetNTPInfo()['recently_closed']
   3737 
   3738   def GetNTPApps(self):
   3739     """Retrieves information about the apps listed on the NTP.
   3740 
   3741     In the sample data below, the "launch_type" will be one of the following
   3742     strings: "pinned", "regular", "fullscreen", "window", or "unknown".
   3743 
   3744     SAMPLE:
   3745     [
   3746       {
   3747         u'app_launch_index': 2,
   3748         u'description': u'Web Store',
   3749         u'icon_big': u'chrome://theme/IDR_APP_DEFAULT_ICON',
   3750         u'icon_small': u'chrome://favicon/https://chrome.google.com/webstore',
   3751         u'id': u'ahfgeienlihckogmohjhadlkjgocpleb',
   3752         u'is_component_extension': True,
   3753         u'is_disabled': False,
   3754         u'launch_container': 2,
   3755         u'launch_type': u'regular',
   3756         u'launch_url': u'https://chrome.google.com/webstore',
   3757         u'name': u'Chrome Web Store',
   3758         u'options_url': u'',
   3759       },
   3760       {
   3761         u'app_launch_index': 1,
   3762         u'description': u'A countdown app',
   3763         u'icon_big': (u'chrome-extension://aeabikdlfbfeihglecobdkdflahfgcpd/'
   3764                       u'countdown128.png'),
   3765         u'icon_small': (u'chrome://favicon/chrome-extension://'
   3766                         u'aeabikdlfbfeihglecobdkdflahfgcpd/'
   3767                         u'launchLocalPath.html'),
   3768         u'id': u'aeabikdlfbfeihglecobdkdflahfgcpd',
   3769         u'is_component_extension': False,
   3770         u'is_disabled': False,
   3771         u'launch_container': 2,
   3772         u'launch_type': u'regular',
   3773         u'launch_url': (u'chrome-extension://aeabikdlfbfeihglecobdkdflahfgcpd/'
   3774                         u'launchLocalPath.html'),
   3775         u'name': u'Countdown',
   3776         u'options_url': u'',
   3777       }
   3778     ]
   3779 
   3780     Returns:
   3781       A list of dictionaries in which each dictionary contains the information
   3782       for a single app that appears in the "Apps" section of the NTP.
   3783     """
   3784     return self._GetNTPInfo()['apps']
   3785 
   3786   def _GetNTPInfo(self):
   3787     """Get info about the New Tab Page (NTP).
   3788 
   3789     This does not retrieve the actual info displayed in a particular NTP; it
   3790     retrieves the current state of internal data that would be used to display
   3791     an NTP.  This includes info about the apps, the most visited sites,
   3792     the recently closed tabs and windows, and the default NTP sites.
   3793 
   3794     SAMPLE:
   3795     {
   3796       u'apps': [ ... ],
   3797       u'most_visited': [ ... ],
   3798       u'recently_closed': [ ... ],
   3799       u'default_sites': [ ... ]
   3800     }
   3801 
   3802     Returns:
   3803       A dictionary containing all the NTP info. See details about the different
   3804       sections in their respective methods: GetNTPApps(), GetNTPThumbnails(),
   3805       GetNTPRecentlyClosed(), and GetNTPDefaultSites().
   3806 
   3807     Raises:
   3808       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3809     """
   3810     cmd_dict = {
   3811       'command': 'GetNTPInfo',
   3812     }
   3813     return self._GetResultFromJSONRequest(cmd_dict)
   3814 
   3815   def _CheckNTPThumbnailShown(self, thumbnail):
   3816     if self.GetNTPThumbnailIndex(thumbnail) == -1:
   3817       raise NTPThumbnailNotShownError()
   3818 
   3819   def LaunchApp(self, app_id, windex=0):
   3820     """Opens the New Tab Page and launches the specified app from it.
   3821 
   3822     This method will not return until after the contents of a new tab for the
   3823     launched app have stopped loading.
   3824 
   3825     Args:
   3826       app_id: The string ID of the app to launch.
   3827       windex: The index of the browser window to work on.  Defaults to 0 (the
   3828               first window).
   3829 
   3830     Raises:
   3831       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3832     """
   3833     self.AppendTab(GURL('chrome://newtab'), windex)  # Also activates this tab.
   3834     cmd_dict = {
   3835       'command': 'LaunchApp',
   3836       'id': app_id,
   3837     }
   3838     return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   3839 
   3840   def SetAppLaunchType(self, app_id, launch_type, windex=0):
   3841     """Sets the launch type for the specified app.
   3842 
   3843     Args:
   3844       app_id: The string ID of the app whose launch type should be set.
   3845       launch_type: The string launch type, which must be one of the following:
   3846                    'pinned': Launch in a pinned tab.
   3847                    'regular': Launch in a regular tab.
   3848                    'fullscreen': Launch in a fullscreen tab.
   3849                    'window': Launch in a new browser window.
   3850       windex: The index of the browser window to work on.  Defaults to 0 (the
   3851               first window).
   3852 
   3853     Raises:
   3854       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3855     """
   3856     self.assertTrue(launch_type in ('pinned', 'regular', 'fullscreen',
   3857                                     'window'),
   3858                     msg='Unexpected launch type value: "%s"' % launch_type)
   3859     cmd_dict = {
   3860       'command': 'SetAppLaunchType',
   3861       'id': app_id,
   3862       'launch_type': launch_type,
   3863     }
   3864     return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   3865 
   3866   def GetV8HeapStats(self, tab_index=0, windex=0):
   3867     """Returns statistics about the v8 heap in the renderer process for a tab.
   3868 
   3869     Args:
   3870       tab_index: The tab index, default is 0.
   3871       window_index: The window index, default is 0.
   3872 
   3873     Returns:
   3874       A dictionary containing v8 heap statistics. Memory values are in bytes.
   3875       Example:
   3876         { 'renderer_id': 6223,
   3877           'v8_memory_allocated': 21803776,
   3878           'v8_memory_used': 10565392 }
   3879     """
   3880     cmd_dict = {  # Prepare command for the json interface.
   3881       'command': 'GetV8HeapStats',
   3882       'tab_index': tab_index,
   3883     }
   3884     return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   3885 
   3886   def GetFPS(self, tab_index=0, windex=0):
   3887     """Returns the current FPS associated with the renderer process for a tab.
   3888 
   3889     FPS is the rendered frames per second.
   3890 
   3891     Args:
   3892       tab_index: The tab index, default is 0.
   3893       window_index: The window index, default is 0.
   3894 
   3895     Returns:
   3896       A dictionary containing FPS info.
   3897       Example:
   3898         { 'renderer_id': 23567,
   3899           'routing_id': 1,
   3900           'fps': 29.404298782348633 }
   3901     """
   3902     cmd_dict = {  # Prepare command for the json interface.
   3903       'command': 'GetFPS',
   3904       'tab_index': tab_index,
   3905     }
   3906     return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   3907 
   3908   def IsFullscreenForBrowser(self, windex=0):
   3909     """Returns true if the window is currently fullscreen and was initially
   3910     transitioned to fullscreen by a browser (vs tab) mode transition."""
   3911     return self._GetResultFromJSONRequest(
   3912       { 'command': 'IsFullscreenForBrowser' },
   3913       windex=windex).get('result')
   3914 
   3915   def IsFullscreenForTab(self, windex=0):
   3916     """Returns true if fullscreen has been caused by a tab."""
   3917     return self._GetResultFromJSONRequest(
   3918       { 'command': 'IsFullscreenForTab' },
   3919       windex=windex).get('result')
   3920 
   3921   def IsMouseLocked(self, windex=0):
   3922     """Returns true if the mouse is currently locked."""
   3923     return self._GetResultFromJSONRequest(
   3924       { 'command': 'IsMouseLocked' },
   3925       windex=windex).get('result')
   3926 
   3927   def IsMouseLockPermissionRequested(self, windex=0):
   3928     """Returns true if the user is currently prompted to give permision for
   3929     mouse lock."""
   3930     return self._GetResultFromJSONRequest(
   3931       { 'command': 'IsMouseLockPermissionRequested' },
   3932       windex=windex).get('result')
   3933 
   3934   def IsFullscreenPermissionRequested(self, windex=0):
   3935     """Returns true if the user is currently prompted to give permision for
   3936     fullscreen."""
   3937     return self._GetResultFromJSONRequest(
   3938       { 'command': 'IsFullscreenPermissionRequested' },
   3939       windex=windex).get('result')
   3940 
   3941   def IsFullscreenBubbleDisplayed(self, windex=0):
   3942     """Returns true if the fullscreen and mouse lock bubble is currently
   3943     displayed."""
   3944     return self._GetResultFromJSONRequest(
   3945       { 'command': 'IsFullscreenBubbleDisplayed' },
   3946       windex=windex).get('result')
   3947 
   3948   def IsFullscreenBubbleDisplayingButtons(self, windex=0):
   3949     """Returns true if the fullscreen and mouse lock bubble is currently
   3950     displayed and presenting buttons."""
   3951     return self._GetResultFromJSONRequest(
   3952       { 'command': 'IsFullscreenBubbleDisplayingButtons' },
   3953       windex=windex).get('result')
   3954 
   3955   def AcceptCurrentFullscreenOrMouseLockRequest(self, windex=0):
   3956     """Activate the accept button on the fullscreen and mouse lock bubble."""
   3957     return self._GetResultFromJSONRequest(
   3958       { 'command': 'AcceptCurrentFullscreenOrMouseLockRequest' },
   3959       windex=windex)
   3960 
   3961   def DenyCurrentFullscreenOrMouseLockRequest(self, windex=0):
   3962     """Activate the deny button on the fullscreen and mouse lock bubble."""
   3963     return self._GetResultFromJSONRequest(
   3964       { 'command': 'DenyCurrentFullscreenOrMouseLockRequest' },
   3965       windex=windex)
   3966 
   3967   def KillRendererProcess(self, pid):
   3968     """Kills the given renderer process.
   3969 
   3970     This will return only after the browser has received notice of the renderer
   3971     close.
   3972 
   3973     Args:
   3974       pid: the process id of the renderer to kill
   3975 
   3976     Raises:
   3977       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   3978     """
   3979     cmd_dict = {
   3980         'command': 'KillRendererProcess',
   3981         'pid': pid
   3982     }
   3983     return self._GetResultFromJSONRequest(cmd_dict)
   3984 
   3985   def NewWebDriver(self, port=0):
   3986     """Returns a new remote WebDriver instance.
   3987 
   3988     Args:
   3989       port: The port to start WebDriver on; by default the service selects an
   3990             open port. It is an error to request a port number and request a
   3991             different port later.
   3992 
   3993     Returns:
   3994       selenium.webdriver.remote.webdriver.WebDriver instance
   3995     """
   3996     from chrome_driver_factory import ChromeDriverFactory
   3997     global _CHROME_DRIVER_FACTORY
   3998     if _CHROME_DRIVER_FACTORY is None:
   3999       _CHROME_DRIVER_FACTORY = ChromeDriverFactory(port=port)
   4000     self.assertTrue(_CHROME_DRIVER_FACTORY.GetPort() == port or port == 0,
   4001                     msg='Requested a WebDriver on a specific port while already'
   4002                         ' running on a different port.')
   4003     return _CHROME_DRIVER_FACTORY.NewChromeDriver(self)
   4004 
   4005   def CreateNewAutomationProvider(self, channel_id):
   4006     """Creates a new automation provider.
   4007 
   4008     The provider will open a named channel in server mode.
   4009     Args:
   4010       channel_id: the channel_id to open the server channel with
   4011     """
   4012     cmd_dict = {
   4013         'command': 'CreateNewAutomationProvider',
   4014         'channel_id': channel_id
   4015     }
   4016     self._GetResultFromJSONRequest(cmd_dict)
   4017 
   4018   def OpenNewBrowserWindowWithNewProfile(self):
   4019     """Creates a new multi-profiles user, and then opens and shows a new
   4020     tabbed browser window with the new profile.
   4021 
   4022     This is equivalent to 'Add new user' action with multi-profiles.
   4023 
   4024     To account for crbug.com/108761 on Win XP, this call polls until the
   4025     profile count increments by 1.
   4026 
   4027     Raises:
   4028       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4029     """
   4030     num_profiles = len(self.GetMultiProfileInfo()['profiles'])
   4031     cmd_dict = {  # Prepare command for the json interface
   4032       'command': 'OpenNewBrowserWindowWithNewProfile'
   4033     }
   4034     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4035     # TODO(nirnimesh): Remove when crbug.com/108761 is fixed
   4036     self.WaitUntil(
   4037         lambda: len(self.GetMultiProfileInfo()['profiles']),
   4038         expect_retval=(num_profiles + 1))
   4039 
   4040   def OpenProfileWindow(self, path, num_loads=1):
   4041    """Open browser window for an existing profile.
   4042 
   4043    This is equivalent to picking a profile from the multi-profile menu.
   4044 
   4045    Multi-profile should be enabled and the requested profile should already
   4046    exist. Creates a new window for the given profile. Use
   4047    OpenNewBrowserWindowWithNewProfile() to create a new profile.
   4048 
   4049    Args:
   4050      path: profile path of the profile to be opened.
   4051      num_loads: the number of loads to wait for, when a new browser window
   4052                 is created.  Useful when restoring a window with many tabs.
   4053    """
   4054    cmd_dict = {  # Prepare command for the json interface
   4055     'command': 'OpenProfileWindow',
   4056     'path': path,
   4057     'num_loads': num_loads,
   4058    }
   4059    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4060 
   4061   def GetMultiProfileInfo(self):
   4062     """Fetch info about all multi-profile users.
   4063 
   4064     Returns:
   4065       A dictionary.
   4066       Sample:
   4067       {
   4068         'enabled': True,
   4069         'profiles': [{'name': 'First user',
   4070                       'path': '/tmp/.org.chromium.Chromium.Tyx17X/Default'},
   4071                      {'name': 'User 1',
   4072                       'path': '/tmp/.org.chromium.Chromium.Tyx17X/profile_1'}],
   4073       }
   4074 
   4075       Profiles will be listed in the same order as visible in preferences.
   4076 
   4077     Raises:
   4078       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4079     """
   4080     cmd_dict = {  # Prepare command for the json interface
   4081       'command': 'GetMultiProfileInfo'
   4082     }
   4083     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4084 
   4085   def RefreshPolicies(self):
   4086     """Refreshes all the available policy providers.
   4087 
   4088     Each policy provider will reload its policy source and push the updated
   4089     policies. This call waits for the new policies to be applied; any policies
   4090     installed before this call is issued are guaranteed to be ready after it
   4091     returns.
   4092     """
   4093     # TODO(craigdh): Determine the root cause of RefreshPolicies' flakiness.
   4094     #                See crosbug.com/30221
   4095     timeout = PyUITest.ActionTimeoutChanger(self, 3 * 60 * 1000)
   4096     cmd_dict = { 'command': 'RefreshPolicies' }
   4097     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4098 
   4099   def SubmitForm(self, form_id, tab_index=0, windex=0, frame_xpath=''):
   4100     """Submits the given form ID, and returns after it has been submitted.
   4101 
   4102     Args:
   4103       form_id: the id attribute of the form to submit.
   4104 
   4105     Returns: true on success.
   4106     """
   4107     js = """
   4108         document.getElementById("%s").submit();
   4109         window.addEventListener("unload", function() {
   4110           window.domAutomationController.send("done");
   4111         });
   4112     """ % form_id
   4113     if self.ExecuteJavascript(js, tab_index, windex, frame_xpath) != 'done':
   4114       return False
   4115     # Wait until the form is submitted and the page completes loading.
   4116     return self.WaitUntil(
   4117         lambda: self.GetDOMValue('document.readyState',
   4118                                  tab_index, windex, frame_xpath),
   4119         expect_retval='complete')
   4120 
   4121   def SimulateAsanMemoryBug(self):
   4122     """Simulates a memory bug for Address Sanitizer to catch.
   4123 
   4124     Address Sanitizer (if it was built it) will catch the bug and abort
   4125     the process.
   4126     This method returns immediately before it actually causes a crash.
   4127     """
   4128     cmd_dict = { 'command': 'SimulateAsanMemoryBug' }
   4129     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4130 
   4131   ## ChromeOS section
   4132 
   4133   def GetLoginInfo(self):
   4134     """Returns information about login and screen locker state.
   4135 
   4136     This includes things like whether a user is logged in, the username
   4137     of the logged in user, and whether the screen is locked.
   4138 
   4139     Returns:
   4140       A dictionary.
   4141       Sample:
   4142       { u'is_guest': False,
   4143         u'is_owner': True,
   4144         u'email': u'example (at] gmail.com',
   4145         u'user_image': 2,  # non-negative int, 'profile', 'file'
   4146         u'is_screen_locked': False,
   4147         u'login_ui_type': 'nativeui', # or 'webui'
   4148         u'is_logged_in': True}
   4149 
   4150     Raises:
   4151       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4152     """
   4153     cmd_dict = { 'command': 'GetLoginInfo' }
   4154     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4155 
   4156   def WaitForSessionManagerRestart(self, function):
   4157     """Call a function and wait for the ChromeOS session_manager to restart.
   4158 
   4159     Args:
   4160       function: The function to call.
   4161     """
   4162     assert callable(function)
   4163     pgrep_process = subprocess.Popen(['pgrep', 'session_manager'],
   4164                                      stdout=subprocess.PIPE)
   4165     old_pid = pgrep_process.communicate()[0].strip()
   4166     function()
   4167     return self.WaitUntil(lambda: self._IsSessionManagerReady(old_pid))
   4168 
   4169   def _WaitForInodeChange(self, path, function):
   4170     """Call a function and wait for the specified file path to change.
   4171 
   4172     Args:
   4173       path: The file path to check for changes.
   4174       function: The function to call.
   4175     """
   4176     assert callable(function)
   4177     old_inode = os.stat(path).st_ino
   4178     function()
   4179     return self.WaitUntil(lambda: self._IsInodeNew(path, old_inode))
   4180 
   4181   def ShowCreateAccountUI(self):
   4182     """Go to the account creation page.
   4183 
   4184     This is the same as clicking the "Create Account" link on the
   4185     ChromeOS login screen. Does not actually create a new account.
   4186     Should be displaying the login screen to work.
   4187 
   4188     Raises:
   4189       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4190     """
   4191     cmd_dict = { 'command': 'ShowCreateAccountUI' }
   4192     # See note below under LoginAsGuest(). ShowCreateAccountUI() logs
   4193     # the user in as guest in order to access the account creation page.
   4194     assert self._WaitForInodeChange(
   4195         self._named_channel_id,
   4196         lambda: self._GetResultFromJSONRequest(cmd_dict, windex=None)), \
   4197         'Chrome did not reopen the testing channel after login as guest.'
   4198     self.SetUp()
   4199 
   4200   def SkipToLogin(self, skip_image_selection=True):
   4201     """Skips OOBE to the login screen.
   4202 
   4203     Assumes that we're at the beginning of OOBE.
   4204 
   4205     Args:
   4206       skip_image_selection: Boolean indicating whether the user image selection
   4207                             screen should also be skipped.
   4208 
   4209     Raises:
   4210       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4211     """
   4212     cmd_dict = { 'command': 'SkipToLogin',
   4213                  'skip_image_selection': skip_image_selection }
   4214     result = self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4215     assert result['next_screen'] == 'login', 'Unexpected wizard transition'
   4216 
   4217   def GetOOBEScreenInfo(self):
   4218     """Queries info about the current OOBE screen.
   4219 
   4220     Returns:
   4221       A dictionary with the following keys:
   4222 
   4223       'screen_name': The title of the current OOBE screen as a string.
   4224 
   4225     Raises:
   4226       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4227     """
   4228     cmd_dict = { 'command': 'GetOOBEScreenInfo' }
   4229     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4230 
   4231   def AcceptOOBENetworkScreen(self):
   4232     """Accepts OOBE network screen and advances to the next one.
   4233 
   4234     Assumes that we're already at the OOBE network screen.
   4235 
   4236     Returns:
   4237       A dictionary with the following keys:
   4238 
   4239       'next_screen': The title of the next OOBE screen as a string.
   4240 
   4241     Raises:
   4242       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4243     """
   4244     cmd_dict = { 'command': 'AcceptOOBENetworkScreen' }
   4245     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4246 
   4247   def AcceptOOBEEula(self, accepted, usage_stats_reporting=False):
   4248     """Accepts OOBE EULA and advances to the next screen.
   4249 
   4250     Assumes that we're already at the OOBE EULA screen.
   4251 
   4252     Args:
   4253       accepted: Boolean indicating whether the EULA should be accepted.
   4254       usage_stats_reporting: Boolean indicating whether UMA should be enabled.
   4255 
   4256     Returns:
   4257       A dictionary with the following keys:
   4258 
   4259       'next_screen': The title of the next OOBE screen as a string.
   4260 
   4261     Raises:
   4262       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4263     """
   4264     cmd_dict = { 'command': 'AcceptOOBEEula',
   4265                  'accepted': accepted,
   4266                  'usage_stats_reporting': usage_stats_reporting }
   4267     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4268 
   4269   def CancelOOBEUpdate(self):
   4270     """Skips update on OOBE and advances to the next screen.
   4271 
   4272     Returns:
   4273       A dictionary with the following keys:
   4274 
   4275       'next_screen': The title of the next OOBE screen as a string.
   4276 
   4277     Raises:
   4278       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4279     """
   4280     cmd_dict = { 'command': 'CancelOOBEUpdate' }
   4281     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4282 
   4283   def PickUserImage(self, image):
   4284     """Chooses image for the newly created user.
   4285 
   4286     Should be called immediately after login.
   4287 
   4288     Args:
   4289       image_type: type of user image to choose. Possible values:
   4290         - "profile": Google profile image
   4291         - non-negative int: one of the default images
   4292 
   4293     Returns:
   4294       A dictionary with the following keys:
   4295 
   4296       'next_screen': The title of the next OOBE screen as a string.
   4297 
   4298     Raises:
   4299       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4300     """
   4301     cmd_dict = { 'command': 'PickUserImage',
   4302                  'image': image }
   4303     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4304 
   4305   def LoginAsGuest(self):
   4306     """Login to chromeos as a guest user.
   4307 
   4308     Waits until logged in.
   4309     Should be displaying the login screen to work.
   4310 
   4311     Raises:
   4312       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4313     """
   4314     cmd_dict = { 'command': 'LoginAsGuest' }
   4315     # Currently, logging in as guest causes session_manager to
   4316     # restart Chrome, which will close the testing channel.
   4317     # We need to call SetUp() again to reconnect to the new channel.
   4318     assert self._WaitForInodeChange(
   4319         self._named_channel_id,
   4320         lambda: self._GetResultFromJSONRequest(cmd_dict, windex=None)), \
   4321         'Chrome did not reopen the testing channel after login as guest.'
   4322     self.SetUp()
   4323 
   4324   def Login(self, username, password, timeout=120 * 1000):
   4325     """Login to chromeos.
   4326 
   4327     Waits until logged in and browser is ready.
   4328     Should be displaying the login screen to work.
   4329 
   4330     Note that in case of webui auth-extension-based login, gaia auth errors
   4331     will not be noticed here, because the browser has no knowledge of it. In
   4332     this case the GetNextEvent automation command will always time out.
   4333 
   4334     Args:
   4335       username: the username to log in as.
   4336       password: the user's password.
   4337       timeout: timeout in ms; defaults to two minutes.
   4338 
   4339     Returns:
   4340       An error string if an error occured.
   4341       None otherwise.
   4342 
   4343     Raises:
   4344       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4345     """
   4346     self._GetResultFromJSONRequest({'command': 'AddLoginEventObserver'},
   4347                                    windex=None)
   4348     cmd_dict = {
   4349         'command': 'SubmitLoginForm',
   4350         'username': username,
   4351         'password': password,
   4352     }
   4353     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4354     self.AddDomEventObserver('loginfail', automation_id=4444)
   4355     try:
   4356       if self.GetNextEvent(timeout=timeout).get('name') == 'loginfail':
   4357         raise JSONInterfaceError('Login denied by auth server.')
   4358     except JSONInterfaceError as e:
   4359       raise JSONInterfaceError('Login failed. Perhaps Chrome crashed, '
   4360                                'failed to start, or the login flow is '
   4361                                'broken? Error message: %s' % str(e))
   4362 
   4363   def Logout(self):
   4364     """Log out from ChromeOS and wait for session_manager to come up.
   4365 
   4366     This is equivalent to pressing the 'Sign out' button from the
   4367     aura shell tray when logged in.
   4368 
   4369     Should be logged in to work. Re-initializes the automation channel
   4370     after logout.
   4371     """
   4372     clear_profile_orig = self.get_clear_profile()
   4373     self.set_clear_profile(False)
   4374     assert self.GetLoginInfo()['is_logged_in'], \
   4375         'Trying to log out when already logged out.'
   4376     def _SignOut():
   4377       cmd_dict = { 'command': 'SignOut' }
   4378       self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4379     assert self.WaitForSessionManagerRestart(_SignOut), \
   4380         'Session manager did not restart after logout.'
   4381     self.__SetUp()
   4382     self.set_clear_profile(clear_profile_orig)
   4383 
   4384   def LockScreen(self):
   4385     """Locks the screen on chromeos.
   4386 
   4387     Waits until screen is locked.
   4388     Should be logged in and screen should not be locked to work.
   4389 
   4390     Raises:
   4391       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4392     """
   4393     cmd_dict = { 'command': 'LockScreen' }
   4394     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4395 
   4396   def UnlockScreen(self, password):
   4397     """Unlocks the screen on chromeos, authenticating the user's password first.
   4398 
   4399     Waits until screen is unlocked.
   4400     Screen locker should be active for this to work.
   4401 
   4402     Returns:
   4403       An error string if an error occured.
   4404       None otherwise.
   4405 
   4406     Raises:
   4407       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4408     """
   4409     cmd_dict = {
   4410         'command': 'UnlockScreen',
   4411         'password': password,
   4412     }
   4413     result = self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4414     return result.get('error_string')
   4415 
   4416   def SignoutInScreenLocker(self):
   4417     """Signs out of chromeos using the screen locker's "Sign out" feature.
   4418 
   4419     Effectively the same as clicking the "Sign out" link on the screen locker.
   4420     Screen should be locked for this to work.
   4421 
   4422     Raises:
   4423       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4424     """
   4425     cmd_dict = { 'command': 'SignoutInScreenLocker' }
   4426     assert self.WaitForSessionManagerRestart(
   4427         lambda: self._GetResultFromJSONRequest(cmd_dict, windex=None)), \
   4428         'Session manager did not restart after logout.'
   4429     self.__SetUp()
   4430 
   4431   def GetBatteryInfo(self):
   4432     """Get details about battery state.
   4433 
   4434     Returns:
   4435       A dictionary with the following keys:
   4436 
   4437       'battery_is_present': bool
   4438       'line_power_on': bool
   4439       if 'battery_is_present':
   4440         'battery_percentage': float (0 ~ 100)
   4441         'battery_fully_charged': bool
   4442         if 'line_power_on':
   4443           'battery_time_to_full': int (seconds)
   4444         else:
   4445           'battery_time_to_empty': int (seconds)
   4446 
   4447       If it is still calculating the time left, 'battery_time_to_full'
   4448       and 'battery_time_to_empty' will be absent.
   4449 
   4450       Use 'battery_fully_charged' instead of 'battery_percentage'
   4451       or 'battery_time_to_full' to determine whether the battery
   4452       is fully charged, since the percentage is only approximate.
   4453 
   4454       Sample:
   4455         { u'battery_is_present': True,
   4456           u'line_power_on': False,
   4457           u'battery_time_to_empty': 29617,
   4458           u'battery_percentage': 100.0,
   4459           u'battery_fully_charged': False }
   4460 
   4461     Raises:
   4462       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4463     """
   4464     cmd_dict = { 'command': 'GetBatteryInfo' }
   4465     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4466 
   4467   def GetPanelInfo(self):
   4468     """Get details about open ChromeOS panels.
   4469 
   4470     A panel is actually a type of browser window, so all of
   4471     this information is also available using GetBrowserInfo().
   4472 
   4473     Returns:
   4474       A dictionary.
   4475       Sample:
   4476       [{ 'incognito': False,
   4477          'renderer_pid': 4820,
   4478          'title': u'Downloads',
   4479          'url': u'chrome://active-downloads/'}]
   4480 
   4481     Raises:
   4482       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4483     """
   4484     panels = []
   4485     for browser in self.GetBrowserInfo()['windows']:
   4486       if browser['type'] != 'panel':
   4487         continue
   4488 
   4489       panel = {}
   4490       panels.append(panel)
   4491       tab = browser['tabs'][0]
   4492       panel['incognito'] = browser['incognito']
   4493       panel['renderer_pid'] = tab['renderer_pid']
   4494       panel['title'] = self.GetActiveTabTitle(browser['index'])
   4495       panel['url'] = tab['url']
   4496 
   4497     return panels
   4498 
   4499   def RestoreOnline(self):
   4500     """Returns the device from offline mode if GoOffline was used."""
   4501 
   4502     assert PyUITest.IsChromeOS()
   4503 
   4504     # Restores etherent connection
   4505     stdout, stderr = self.RunSuperuserActionOnChromeOS('TeardownBackchannel')
   4506 
   4507     if hasattr(self, 'bc_cellular_enabled') and self.bc_cellular_enabled:
   4508       self.ToggleNetworkDevice('cellular', True)
   4509     if hasattr(self, 'bc_wifi_enabled') and self.bc_wifi_enabled:
   4510       self.ToggleNetworkDevice('wifi', True)
   4511 
   4512     assert 'RuntimeError' not in stderr, stderr
   4513 
   4514   def GoOffline(self):
   4515     """Puts device in offline mode.
   4516 
   4517     The device is put into offline mode by disabling all network interfaces
   4518     but keeping the the wired ethernet interface up and faking shill/flimflam
   4519     into thinking there is no ethernet interface by renaming the interface.
   4520     This is so we can keep ssh connections over the wired connection alive.
   4521     """
   4522     assert PyUITest.IsChromeOS()
   4523     net_info = self.GetNetworkInfo()
   4524     self.bc_wifi_enabled = net_info.get('wifi_enabled')
   4525     self.bc_cellular_enabled = net_info.get('cellular_enabled')
   4526 
   4527     if self.bc_cellular_enabled:
   4528       self.ToggleNetworkDevice('cellular', False)
   4529     if self.bc_wifi_enabled:
   4530       self.ToggleNetworkDevice('wifi', False)
   4531 
   4532     stdout, stderr = self.RunSuperuserActionOnChromeOS('SetupBackchannel')
   4533     assert 'RuntimeError' not in stderr, stderr
   4534 
   4535   def GetNetworkInfo(self):
   4536     """Get details about ethernet, wifi, and cellular networks on chromeos.
   4537 
   4538     Returns:
   4539       A dictionary.
   4540       Sample:
   4541       { u'cellular_available': True,
   4542         u'cellular_enabled': False,
   4543         u'connected_ethernet': u'/service/ethernet_abcd',
   4544         u'connected_wifi': u'/service/wifi_abcd_1234_managed_none',
   4545         u'ethernet_available': True,
   4546         u'ethernet_enabled': True,
   4547         u'ethernet_networks':
   4548             { u'/service/ethernet_abcd':
   4549                 { u'device_path': u'/device/abcdeth',
   4550                   u'name': u'',
   4551                   u'service_path':
   4552                   u'/profile/default/ethernet_abcd',
   4553                   u'status': u'Connected'}
   4554               u'network_type': pyautolib.TYPE_ETHERNET },
   4555         u'remembered_wifi':
   4556             { u'/service/wifi_abcd_1234_managed_none':
   4557                 { u'device_path': u'',
   4558                   u'encrypted': False,
   4559                   u'encryption': u'',
   4560                   u'name': u'WifiNetworkName1',
   4561                   u'status': u'Unknown',
   4562                   u'strength': 0},
   4563               u'network_type': pyautolib.TYPE_WIFI
   4564             },
   4565         u'wifi_available': True,
   4566         u'wifi_enabled': True,
   4567         u'wifi_networks':
   4568             { u'/service/wifi_abcd_1234_managed_none':
   4569                 { u'device_path': u'/device/abcdwifi',
   4570                   u'encrypted': False,
   4571                   u'encryption': u'',
   4572                   u'name': u'WifiNetworkName1',
   4573                   u'status': u'Connected',
   4574                   u'strength': 76},
   4575               u'/service/wifi_abcd_1234_managed_802_1x':
   4576                   { u'encrypted': True,
   4577                     u'encryption': u'8021X',
   4578                     u'name': u'WifiNetworkName2',
   4579                     u'status': u'Idle',
   4580                     u'strength': 79}
   4581               u'network_type': pyautolib.TYPE_WIFI }}
   4582 
   4583 
   4584     Raises:
   4585       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4586     """
   4587     cmd_dict = { 'command': 'GetNetworkInfo' }
   4588     network_info = self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4589 
   4590     # Remembered networks do not have /service/ prepended to the service path
   4591     # even though wifi_networks does.  We want this prepended to allow for
   4592     # consistency and easy string comparison with wifi_networks.
   4593     remembered_wifi = {}
   4594     network_info['remembered_wifi'] = dict([('/service/' + k, v) for k, v in
   4595       network_info['remembered_wifi'].iteritems()])
   4596 
   4597     return network_info
   4598 
   4599   def GetConnectedWifi(self):
   4600     """Returns the SSID of the currently connected wifi network.
   4601 
   4602     Returns:
   4603       The SSID of the connected network or None if we're not connected.
   4604     """
   4605     service_list = self.GetNetworkInfo()
   4606     connected_service_path = service_list.get('connected_wifi')
   4607     if 'wifi_networks' in service_list and \
   4608        connected_service_path in service_list['wifi_networks']:
   4609        return service_list['wifi_networks'][connected_service_path]['name']
   4610 
   4611   def GetServicePath(self, ssid, encryption=None, timeout=30):
   4612     """Waits until the SSID is observed and returns its service path.
   4613 
   4614     Args:
   4615       ssid: String defining the SSID we are searching for.
   4616       encryption: Encryption type of the network; either None to return the
   4617                   first instance of network that matches the ssid, '' for
   4618                   an empty network, 'PSK', 'WEP' or '8021X'.
   4619       timeout: Duration to wait for ssid to appear.
   4620 
   4621     Returns:
   4622       The service path or None if SSID does not exist after timeout period.
   4623     """
   4624     def _GetServicePath():
   4625       service_list = self.GetNetworkInfo().get('wifi_networks', [])
   4626       for service_path, service_obj in service_list.iteritems():
   4627         if not (isinstance(service_obj, dict) and
   4628                 'encryption' in service_obj and
   4629                 'name' in service_obj):
   4630           continue
   4631 
   4632         service_encr = 'PSK' if service_obj['encryption'] in ['WPA', 'RSN']\
   4633                        else service_obj['encryption']
   4634 
   4635         if service_obj['name'] == ssid and \
   4636            (encryption == None or service_encr == encryption):
   4637           return service_path
   4638       self.NetworkScan()
   4639       return None
   4640 
   4641     service_path = self.WaitUntil(_GetServicePath, timeout=timeout,
   4642                                   retry_sleep=1, return_retval=True)
   4643     return service_path or None
   4644 
   4645   def NetworkScan(self):
   4646     """Causes ChromeOS to scan for available wifi networks.
   4647 
   4648     Blocks until scanning is complete.
   4649 
   4650     Returns:
   4651       The new list of networks obtained from GetNetworkInfo().
   4652 
   4653     Raises:
   4654       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4655     """
   4656     cmd_dict = { 'command': 'NetworkScan' }
   4657     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4658     return self.GetNetworkInfo()
   4659 
   4660   def ToggleNetworkDevice(self, device, enable):
   4661     """Enable or disable a network device on ChromeOS.
   4662 
   4663     Valid device names are ethernet, wifi, cellular.
   4664 
   4665     Raises:
   4666       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4667     """
   4668     cmd_dict = {
   4669         'command': 'ToggleNetworkDevice',
   4670         'device': device,
   4671         'enable': enable,
   4672     }
   4673     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4674 
   4675   PROXY_TYPE_DIRECT = 1
   4676   PROXY_TYPE_MANUAL = 2
   4677   PROXY_TYPE_PAC = 3
   4678 
   4679   def WaitUntilWifiNetworkAvailable(self, ssid, timeout=60, is_hidden=False):
   4680     """Waits until the given network is available.
   4681 
   4682     Routers that are just turned on may take up to 1 minute upon turning them
   4683     on to broadcast their SSID.
   4684 
   4685     Args:
   4686       ssid: SSID of the service we want to connect to.
   4687       timeout: timeout (in seconds)
   4688 
   4689     Raises:
   4690       Exception if timeout duration has been hit before wifi router is seen.
   4691 
   4692     Returns:
   4693       True, when the wifi network is seen within the timout period.
   4694       False, otherwise.
   4695     """
   4696     def _GotWifiNetwork():
   4697       # Returns non-empty array if desired SSID is available.
   4698       try:
   4699         return [wifi for wifi in
   4700                 self.NetworkScan().get('wifi_networks', {}).values()
   4701                 if wifi.get('name') == ssid]
   4702       except pyauto_errors.JSONInterfaceError:
   4703         # Temporary fix until crosbug.com/14174 is fixed.
   4704         # NetworkScan is only used in updating the list of networks so errors
   4705         # thrown by it are not critical to the results of wifi tests that use
   4706         # this method.
   4707         return False
   4708 
   4709     # The hidden AP's will always be on, thus we will assume it is ready to
   4710     # connect to.
   4711     if is_hidden:
   4712       return bool(_GotWifiNetwork())
   4713 
   4714     return self.WaitUntil(_GotWifiNetwork, timeout=timeout, retry_sleep=1)
   4715 
   4716   def ResetProxySettingsOnChromeOS(self):
   4717     """Public wrapper around proxysettings teardown functions."""
   4718     self.SetSharedProxies(False)
   4719     proxy_dict = {
   4720         'mode': 'direct'
   4721     }
   4722     self.SetProxySettingOnChromeOS(proxy_dict)
   4723 
   4724   def SetProxySettingOnChromeOS(self, proxy_config):
   4725     """Set the proxy config of the current network.
   4726 
   4727     Owner must be logged in for these to persist.
   4728     If user is not logged in or is logged in as non-owner or guest,
   4729     proxy settings do not persist across browser restarts or login/logout.
   4730 
   4731     Args:
   4732       proxy_config:   A dictionary following the format described in
   4733                       prefs/proxy_config_dictionary.h.
   4734 
   4735     Raises:
   4736       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4737     """
   4738     cmd_dict = {
   4739         'command': 'SetProxySettings',
   4740         'proxy_config': json.dumps(proxy_config)
   4741     }
   4742     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4743 
   4744   def SetSharedProxies(self, value):
   4745     """Allows proxies on the shared networks.
   4746 
   4747     Args:
   4748       value: True/False to set and clear respectively.
   4749 
   4750     Raises:
   4751       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4752     """
   4753     cmd_dict = {
   4754         'command': 'SetSharedProxies',
   4755         'value': value,
   4756     }
   4757     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4758 
   4759   def ForgetAllRememberedNetworks(self):
   4760     """Forgets all networks that the device has marked as remembered."""
   4761     for service in self.GetNetworkInfo()['remembered_wifi']:
   4762       self.ForgetWifiNetwork(service)
   4763 
   4764   def ForgetWifiNetwork(self, service_path):
   4765     """Forget a remembered network by its service path.
   4766 
   4767     This function is equivalent to clicking the 'Forget Network' button in the
   4768     chrome://settings/internet page.  This function does not indicate whether
   4769     or not forget succeeded or failed.  It is up to the caller to call
   4770     GetNetworkInfo to check the updated remembered_wifi list to verify the
   4771     service has been removed.
   4772 
   4773     Args:
   4774       service_path: Flimflam path that defines the remembered network.
   4775 
   4776     Raises:
   4777       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4778     """
   4779     # Usually the service_path is prepended with '/service/', such as when the
   4780     # service path is retrieved from GetNetworkInfo.  ForgetWifiNetwork works
   4781     # only for service paths where this has already been stripped.
   4782     service_path = service_path.split('/service/')[-1]
   4783     cmd_dict = {
   4784         'command': 'ForgetWifiNetwork',
   4785         'service_path': service_path,
   4786     }
   4787     self._GetResultFromJSONRequest(cmd_dict, windex=None, timeout=50000)
   4788 
   4789   def ConnectToCellularNetwork(self):
   4790     """Connects to the available cellular network.
   4791 
   4792     Blocks until connection succeeds or fails.
   4793 
   4794     Returns:
   4795       An error string if an error occured.
   4796       None otherwise.
   4797 
   4798     Raises:
   4799       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4800     """
   4801     # Every device should only have one cellular network present, so we can
   4802     # scan for it.
   4803     cellular_networks = self.NetworkScan().get('cellular_networks', {}).keys()
   4804     self.assertTrue(cellular_networks, 'Could not find cellular service.')
   4805     service_path = cellular_networks[0]
   4806 
   4807     cmd_dict = {
   4808         'command': 'ConnectToCellularNetwork',
   4809         'service_path': service_path,
   4810     }
   4811     result = self._GetResultFromJSONRequest(
   4812         cmd_dict, windex=None, timeout=50000)
   4813     return result.get('error_string')
   4814 
   4815   def DisconnectFromCellularNetwork(self):
   4816     """Disconnect from the connected cellular network.
   4817 
   4818     Blocks until disconnect is complete.
   4819 
   4820     Raises:
   4821       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4822     """
   4823     cmd_dict = {
   4824         'command': 'DisconnectFromCellularNetwork',
   4825     }
   4826     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4827 
   4828   def ConnectToWifiNetwork(self, service_path, password='', shared=True):
   4829     """Connect to a wifi network by its service path.
   4830 
   4831     Blocks until connection succeeds or fails.
   4832 
   4833     Args:
   4834       service_path: Flimflam path that defines the wifi network.
   4835       password: Passphrase for connecting to the wifi network.
   4836       shared: Boolean value specifying whether the network should be shared.
   4837 
   4838     Returns:
   4839       An error string if an error occured.
   4840       None otherwise.
   4841 
   4842     Raises:
   4843       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4844     """
   4845     cmd_dict = {
   4846         'command': 'ConnectToWifiNetwork',
   4847         'service_path': service_path,
   4848         'password': password,
   4849         'shared': shared,
   4850     }
   4851     result = self._GetResultFromJSONRequest(
   4852         cmd_dict, windex=None, timeout=50000)
   4853     return result.get('error_string')
   4854 
   4855   def ConnectToHiddenWifiNetwork(self, ssid, security, password='',
   4856                                  shared=True, save_credentials=False):
   4857     """Connect to a wifi network by its service path.
   4858 
   4859     Blocks until connection succeeds or fails.
   4860 
   4861     Args:
   4862       ssid: The SSID of the network to connect to.
   4863       security: The network's security type. One of: 'SECURITY_NONE',
   4864                 'SECURITY_WEP', 'SECURITY_WPA', 'SECURITY_RSN', 'SECURITY_8021X'
   4865       password: Passphrase for connecting to the wifi network.
   4866       shared: Boolean value specifying whether the network should be shared.
   4867       save_credentials: Boolean value specifying whether 802.1x credentials are
   4868                         saved.
   4869 
   4870     Returns:
   4871       An error string if an error occured.
   4872       None otherwise.
   4873 
   4874     Raises:
   4875       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4876     """
   4877     assert security in ('SECURITY_NONE', 'SECURITY_WEP', 'SECURITY_WPA',
   4878                         'SECURITY_RSN', 'SECURITY_8021X')
   4879     cmd_dict = {
   4880         'command': 'ConnectToHiddenWifiNetwork',
   4881         'ssid': ssid,
   4882         'security': security,
   4883         'password': password,
   4884         'shared': shared,
   4885         'save_credentials': save_credentials,
   4886     }
   4887     result = self._GetResultFromJSONRequest(
   4888         cmd_dict, windex=None, timeout=50000)
   4889     return result.get('error_string')
   4890 
   4891   def DisconnectFromWifiNetwork(self):
   4892     """Disconnect from the connected wifi network.
   4893 
   4894     Blocks until disconnect is complete.
   4895 
   4896     Raises:
   4897       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4898     """
   4899     cmd_dict = {
   4900         'command': 'DisconnectFromWifiNetwork',
   4901     }
   4902     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4903 
   4904   def AddPrivateNetwork(self,
   4905                         hostname,
   4906                         service_name,
   4907                         provider_type,
   4908                         username,
   4909                         password,
   4910                         cert_id='',
   4911                         key=''):
   4912     """Add and connect to a private network.
   4913 
   4914     Blocks until connection succeeds or fails. This is equivalent to
   4915     'Add Private Network' in the network menu UI.
   4916 
   4917     Args:
   4918       hostname: Server hostname for the private network.
   4919       service_name: Service name that defines the private network. Do not
   4920                     add multiple services with the same name.
   4921       provider_type: Types are L2TP_IPSEC_PSK and L2TP_IPSEC_USER_CERT.
   4922                      Provider type OPEN_VPN is not yet supported.
   4923                      Type names returned by GetPrivateNetworkInfo will
   4924                      also work.
   4925       username: Username for connecting to the virtual network.
   4926       password: Passphrase for connecting to the virtual network.
   4927       cert_id: Certificate id for a L2TP_IPSEC_USER_CERT network.
   4928       key: Pre-shared key for a L2TP_IPSEC_PSK network.
   4929 
   4930     Returns:
   4931       An error string if an error occured.
   4932       None otherwise.
   4933 
   4934     Raises:
   4935       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4936     """
   4937     cmd_dict = {
   4938         'command': 'AddPrivateNetwork',
   4939         'hostname': hostname,
   4940         'service_name': service_name,
   4941         'provider_type': provider_type,
   4942         'username': username,
   4943         'password': password,
   4944         'cert_id': cert_id,
   4945         'key': key,
   4946     }
   4947     result = self._GetResultFromJSONRequest(
   4948         cmd_dict, windex=None, timeout=50000)
   4949     return result.get('error_string')
   4950 
   4951   def GetPrivateNetworkInfo(self):
   4952     """Get details about private networks on chromeos.
   4953 
   4954     Returns:
   4955       A dictionary including information about all remembered virtual networks
   4956       as well as the currently connected virtual network, if any.
   4957       Sample:
   4958       { u'connected': u'/service/vpn_123_45_67_89_test_vpn'}
   4959         u'/service/vpn_123_45_67_89_test_vpn':
   4960           { u'username': u'vpn_user',
   4961             u'name': u'test_vpn',
   4962             u'hostname': u'123.45.67.89',
   4963             u'key': u'abcde',
   4964             u'cert_id': u'',
   4965             u'password': u'zyxw123',
   4966             u'provider_type': u'L2TP_IPSEC_PSK'},
   4967         u'/service/vpn_111_11_11_11_test_vpn2':
   4968           { u'username': u'testerman',
   4969             u'name': u'test_vpn2',
   4970             u'hostname': u'111.11.11.11',
   4971             u'key': u'fghijklm',
   4972             u'cert_id': u'',
   4973             u'password': u'789mnop',
   4974             u'provider_type': u'L2TP_IPSEC_PSK'},
   4975 
   4976     Raises:
   4977       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4978     """
   4979     cmd_dict = { 'command': 'GetPrivateNetworkInfo' }
   4980     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   4981 
   4982   def ConnectToPrivateNetwork(self, service_path):
   4983     """Connect to a remembered private network by its service path.
   4984 
   4985     Blocks until connection succeeds or fails. The network must have been
   4986     previously added with all necessary connection details.
   4987 
   4988     Args:
   4989       service_path: Service name that defines the private network.
   4990 
   4991     Returns:
   4992       An error string if an error occured.
   4993       None otherwise.
   4994 
   4995     Raises:
   4996       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   4997     """
   4998     cmd_dict = {
   4999         'command': 'ConnectToPrivateNetwork',
   5000         'service_path': service_path,
   5001     }
   5002     result = self._GetResultFromJSONRequest(
   5003         cmd_dict, windex=None, timeout=50000)
   5004     return result.get('error_string')
   5005 
   5006   def DisconnectFromPrivateNetwork(self):
   5007     """Disconnect from the active private network.
   5008 
   5009     Expects a private network to be active.
   5010 
   5011     Raises:
   5012       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5013     """
   5014     cmd_dict = {
   5015         'command': 'DisconnectFromPrivateNetwork',
   5016     }
   5017     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5018 
   5019   def EnableSpokenFeedback(self, enabled):
   5020     """Enables or disables spoken feedback accessibility mode.
   5021 
   5022     Args:
   5023       enabled: Boolean value indicating the desired state of spoken feedback.
   5024 
   5025     Raises:
   5026       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5027     """
   5028     cmd_dict = {
   5029         'command': 'EnableSpokenFeedback',
   5030         'enabled': enabled,
   5031     }
   5032     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5033 
   5034   def IsSpokenFeedbackEnabled(self):
   5035     """Check whether spoken feedback accessibility mode is enabled.
   5036 
   5037     Returns:
   5038       True if spoken feedback is enabled, False otherwise.
   5039 
   5040     Raises:
   5041       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5042     """
   5043     cmd_dict = { 'command': 'IsSpokenFeedbackEnabled', }
   5044     result = self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5045     return result.get('spoken_feedback')
   5046 
   5047   def GetTimeInfo(self, windex=0):
   5048     """Gets info about the ChromeOS status bar clock.
   5049 
   5050     Set the 24-hour clock by using:
   5051       self.SetPrefs('settings.clock.use_24hour_clock', True)
   5052 
   5053     Returns:
   5054       a dictionary.
   5055       Sample:
   5056       {u'display_date': u'Tuesday, July 26, 2011',
   5057        u'display_time': u'4:30',
   5058        u'timezone': u'America/Los_Angeles'}
   5059 
   5060     Raises:
   5061       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5062     """
   5063     cmd_dict = { 'command': 'GetTimeInfo' }
   5064     if self.GetLoginInfo()['is_logged_in']:
   5065       return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
   5066     else:
   5067       return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5068 
   5069   def SetTimezone(self, timezone):
   5070     """Sets the timezone on ChromeOS. A user must be logged in.
   5071 
   5072     The timezone is the relative path to the timezone file in
   5073     /usr/share/zoneinfo. For example, /usr/share/zoneinfo/America/Los_Angeles is
   5074     'America/Los_Angeles'. For a list of valid timezones see
   5075     'chrome/browser/chromeos/system/timezone_settings.cc'.
   5076 
   5077     This method does not return indication of success or failure.
   5078     If the timezone is it falls back to a valid timezone.
   5079 
   5080     Raises:
   5081       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5082     """
   5083     cmd_dict = {
   5084         'command': 'SetTimezone',
   5085         'timezone': timezone,
   5086     }
   5087     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5088 
   5089   def UpdateCheck(self):
   5090     """Checks for a ChromeOS update. Blocks until finished updating.
   5091 
   5092     Raises:
   5093       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5094     """
   5095     cmd_dict = { 'command': 'UpdateCheck' }
   5096     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5097 
   5098   def GetVolumeInfo(self):
   5099     """Gets the volume and whether the device is muted.
   5100 
   5101     Returns:
   5102       a tuple.
   5103       Sample:
   5104       (47.763456790123456, False)
   5105 
   5106     Raises:
   5107       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5108     """
   5109     cmd_dict = { 'command': 'GetVolumeInfo' }
   5110     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5111 
   5112   def SetVolume(self, volume):
   5113     """Sets the volume on ChromeOS. Only valid if not muted.
   5114 
   5115     Args:
   5116       volume: The desired volume level as a percent from 0 to 100.
   5117 
   5118     Raises:
   5119       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5120     """
   5121     assert volume >= 0 and volume <= 100
   5122     cmd_dict = {
   5123         'command': 'SetVolume',
   5124         'volume': float(volume),
   5125     }
   5126     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5127 
   5128   def SetMute(self, mute):
   5129     """Sets whether ChromeOS is muted or not.
   5130 
   5131     Args:
   5132       mute: True to mute, False to unmute.
   5133 
   5134     Raises:
   5135       pyauto_errors.JSONInterfaceError if the automation call returns an error.
   5136     """
   5137     cmd_dict = { 'command': 'SetMute' }
   5138     cmd_dict = {
   5139         'command': 'SetMute',
   5140         'mute': mute,
   5141     }
   5142     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5143 
   5144   # HTML Terminal
   5145 
   5146   def OpenCrosh(self):
   5147     """Open crosh.
   5148 
   5149     Equivalent to pressing Ctrl-Alt-t.
   5150     Opens in the last active (non-incognito) window.
   5151 
   5152     Waits long enough for crosh to load, but does not wait for the crosh
   5153     prompt. Use WaitForHtermText() for that.
   5154     """
   5155     cmd_dict = { 'command': 'OpenCrosh' }
   5156     self._GetResultFromJSONRequest(cmd_dict, windex=None)
   5157 
   5158   def WaitForHtermText(self, text, msg=None, tab_index=0, windex=0):
   5159     """Waits for the given text in a hterm tab.
   5160 
   5161     Can be used to wait for the crosh> prompt or ssh prompt.
   5162 
   5163     This does not poll. It uses dom mutation observers to wait
   5164     for the given text to show up.
   5165 
   5166     Args:
   5167       text: the text to wait for. Can be a regex.
   5168       msg: the failure message to emit if the text could not be found.
   5169       tab_index: the tab for the hterm tab. Default: 0.
   5170       windex: the window index for the hterm tab. Default: 0.
   5171     """
   5172     self.WaitForDomNode(
   5173         xpath='//*[contains(text(), "%s")]' % text, frame_xpath='//iframe',
   5174         msg=msg, tab_index=tab_index, windex=windex)
   5175 
   5176   def GetHtermRowsText(self, start, end, tab_index=0, windex=0):
   5177     """Fetch rows from a html terminal tab.
   5178 
   5179     Works for both crosh and ssh tab.
   5180     Uses term_.getRowsText(start, end) javascript call.
   5181 
   5182     Args:
   5183       start: start line number (0-based).
   5184       end: the end line (one beyond the line of interest).
   5185       tab_index: the tab for the hterm tab. Default: 0.
   5186       windex: the window index for the hterm tab. Default: 0.
   5187     """
   5188     return self.ExecuteJavascript(
   5189         'domAutomationController.send(term_.getRowsText(%d, %d))' % (
   5190             start, end),
   5191         tab_index=tab_index, windex=windex)
   5192 
   5193   def SendKeysToHterm(self, text, tab_index=0, windex=0):
   5194     """Send keys to a html terminal tab.
   5195 
   5196     Works for both crosh and ssh tab.
   5197     Uses term_.onVTKeystroke(str) javascript call.
   5198 
   5199     Args:
   5200       text: the text to send.
   5201       tab_index: the tab for the hterm tab. Default: 0.
   5202       windex: the window index for the hterm tab. Default: 0.
   5203     """
   5204     return self.ExecuteJavascript(
   5205         'term_.onVTKeystroke("%s");'
   5206         'domAutomationController.send("done")' % text,
   5207         tab_index=tab_index, windex=windex)
   5208 
   5209 
   5210   def GetMemoryStatsChromeOS(self, duration):
   5211     """Identifies and returns different kinds of current memory usage stats.
   5212 
   5213     This function samples values each second for |duration| seconds, then
   5214     outputs the min, max, and ending values for each measurement type.
   5215 
   5216     Args:
   5217       duration: The number of seconds to sample data before outputting the
   5218           minimum, maximum, and ending values for each measurement type.
   5219 
   5220     Returns:
   5221       A dictionary containing memory usage information.  Each measurement type
   5222       is associated with the min, max, and ending values from among all
   5223       sampled values.  Values are specified in KB.
   5224       {
   5225         'gem_obj': {  # GPU memory usage.
   5226           'min': ...,
   5227           'max': ...,
   5228           'end': ...,
   5229         },
   5230         'gtt': { ... },  # GPU memory usage (graphics translation table).
   5231         'mem_free': { ... },  # CPU free memory.
   5232         'mem_available': { ... },  # CPU available memory.
   5233         'mem_shared': { ... },  # CPU shared memory.
   5234         'mem_cached': { ... },  # CPU cached memory.
   5235         'mem_anon': { ... },  # CPU anon memory (active + inactive).
   5236         'mem_file': { ... },  # CPU file memory (active + inactive).
   5237         'mem_slab': { ... },  # CPU slab memory.
   5238         'browser_priv': { ... },  # Chrome browser private memory.
   5239         'browser_shared': { ... },  # Chrome browser shared memory.
   5240         'gpu_priv': { ... },  # Chrome GPU private memory.
   5241         'gpu_shared': { ... },  # Chrome GPU shared memory.
   5242         'renderer_priv': { ... },  # Total private memory of all renderers.
   5243         'renderer_shared': { ... },  # Total shared memory of all renderers.
   5244       }
   5245     """
   5246     logging.debug('Sampling memory information for %d seconds...' % duration)
   5247     stats = {}
   5248 
   5249     for _ in xrange(duration):
   5250       # GPU memory.
   5251       gem_obj_path = '/sys/kernel/debug/dri/0/i915_gem_objects'
   5252       if os.path.exists(gem_obj_path):
   5253         p = subprocess.Popen('grep bytes %s' % gem_obj_path,
   5254                              stdout=subprocess.PIPE, shell=True)
   5255         stdout = p.communicate()[0]
   5256 
   5257         gem_obj = re.search(
   5258             '\d+ objects, (\d+) bytes\n', stdout).group(1)
   5259         if 'gem_obj' not in stats:
   5260           stats['gem_obj'] = []
   5261         stats['gem_obj'].append(int(gem_obj) / 1024.0)
   5262 
   5263       gtt_path = '/sys/kernel/debug/dri/0/i915_gem_gtt'
   5264       if os.path.exists(gtt_path):
   5265         p = subprocess.Popen('grep bytes %s' % gtt_path,
   5266                              stdout=subprocess.PIPE, shell=True)
   5267         stdout = p.communicate()[0]
   5268 
   5269         gtt = re.search(
   5270             'Total [\d]+ objects, ([\d]+) bytes', stdout).group(1)
   5271         if 'gtt' not in stats:
   5272           stats['gtt'] = []
   5273         stats['gtt'].append(int(gtt) / 1024.0)
   5274 
   5275       # CPU memory.
   5276       stdout = ''
   5277       with open('/proc/meminfo') as f:
   5278         stdout = f.read()
   5279       mem_free = re.search('MemFree:\s*([\d]+) kB', stdout).group(1)
   5280 
   5281       if 'mem_free' not in stats:
   5282         stats['mem_free'] = []
   5283       stats['mem_free'].append(int(mem_free))
   5284 
   5285       mem_dirty = re.search('Dirty:\s*([\d]+) kB', stdout).group(1)
   5286       mem_active_file = re.search(
   5287           'Active\(file\):\s*([\d]+) kB', stdout).group(1)
   5288       mem_inactive_file = re.search(
   5289           'Inactive\(file\):\s*([\d]+) kB', stdout).group(1)
   5290 
   5291       with open('/proc/sys/vm/min_filelist_kbytes') as f:
   5292         mem_min_file = f.read()
   5293 
   5294       # Available memory =
   5295       #     MemFree + ActiveFile + InactiveFile - DirtyMem - MinFileMem
   5296       if 'mem_available' not in stats:
   5297         stats['mem_available'] = []
   5298       stats['mem_available'].append(
   5299           int(mem_free) + int(mem_active_file) + int(mem_inactive_file) -
   5300           int(mem_dirty) - int(mem_min_file))
   5301 
   5302       mem_shared = re.search('Shmem:\s*([\d]+) kB', stdout).group(1)
   5303       if 'mem_shared' not in stats:
   5304         stats['mem_shared'] = []
   5305       stats['mem_shared'].append(int(mem_shared))
   5306 
   5307       mem_cached = re.search('Cached:\s*([\d]+) kB', stdout).group(1)
   5308       if 'mem_cached' not in stats:
   5309         stats['mem_cached'] = []
   5310       stats['mem_cached'].append(int(mem_cached))
   5311 
   5312       mem_anon_active = re.search('Active\(anon\):\s*([\d]+) kB',
   5313                                   stdout).group(1)
   5314       mem_anon_inactive = re.search('Inactive\(anon\):\s*([\d]+) kB',
   5315                                     stdout).group(1)
   5316       if 'mem_anon' not in stats:
   5317         stats['mem_anon'] = []
   5318       stats['mem_anon'].append(int(mem_anon_active) + int(mem_anon_inactive))
   5319 
   5320       mem_file_active = re.search('Active\(file\):\s*([\d]+) kB',
   5321                                   stdout).group(1)
   5322       mem_file_inactive = re.search('Inactive\(file\):\s*([\d]+) kB',
   5323                                     stdout).group(1)
   5324       if 'mem_file' not in stats:
   5325         stats['mem_file'] = []
   5326       stats['mem_file'].append(int(mem_file_active) + int(mem_file_inactive))
   5327 
   5328       mem_slab = re.search('Slab:\s*([\d]+) kB', stdout).group(1)
   5329       if 'mem_slab' not in stats:
   5330         stats['mem_slab'] = []
   5331       stats['mem_slab'].append(int(mem_slab))
   5332 
   5333       # Chrome process memory.
   5334       pinfo = self.GetProcessInfo()['browsers'][0]['processes']
   5335       total_renderer_priv = 0
   5336       total_renderer_shared = 0
   5337       for process in pinfo:
   5338         mem_priv = process['working_set_mem']['priv']
   5339         mem_shared = process['working_set_mem']['shared']
   5340         if process['child_process_type'] == 'Browser':
   5341           if 'browser_priv' not in stats:
   5342             stats['browser_priv'] = []
   5343             stats['browser_priv'].append(int(mem_priv))
   5344           if 'browser_shared' not in stats:
   5345             stats['browser_shared'] = []
   5346             stats['browser_shared'].append(int(mem_shared))
   5347         elif process['child_process_type'] == 'GPU':
   5348           if 'gpu_priv' not in stats:
   5349             stats['gpu_priv'] = []
   5350             stats['gpu_priv'].append(int(mem_priv))
   5351           if 'gpu_shared' not in stats:
   5352             stats['gpu_shared'] = []
   5353             stats['gpu_shared'].append(int(mem_shared))
   5354         elif process['child_process_type'] == 'Tab':
   5355           # Sum the memory of all renderer processes.
   5356           total_renderer_priv += int(mem_priv)
   5357           total_renderer_shared += int(mem_shared)
   5358       if 'renderer_priv' not in stats:
   5359         stats['renderer_priv'] = []
   5360         stats['renderer_priv'].append(int(total_renderer_priv))
   5361       if 'renderer_shared' not in stats:
   5362         stats['renderer_shared'] = []
   5363         stats['renderer_shared'].append(int(total_renderer_shared))
   5364 
   5365       time.sleep(1)
   5366 
   5367     # Compute min, max, and ending values to return.
   5368     result = {}
   5369     for measurement_type in stats:
   5370       values = stats[measurement_type]
   5371       result[measurement_type] = {
   5372         'min': min(values),
   5373         'max': max(values),
   5374         'end': values[-1],
   5375       }
   5376 
   5377     return result
   5378 
   5379   ## ChromeOS section -- end
   5380 
   5381 
   5382 class ExtraBrowser(PyUITest):
   5383   """Launches a new browser with some extra flags.
   5384 
   5385   The new browser is launched with its own fresh profile.
   5386   This class does not apply to ChromeOS.
   5387   """
   5388   def __init__(self, chrome_flags=[], methodName='runTest', **kwargs):
   5389     """Accepts extra chrome flags for launching a new browser instance.
   5390 
   5391     Args:
   5392       chrome_flags: list of extra flags when launching a new browser.
   5393     """
   5394     assert not PyUITest.IsChromeOS(), \
   5395         'This function cannot be used to launch a new browser in ChromeOS.'
   5396     PyUITest.__init__(self, methodName=methodName, **kwargs)
   5397     self._chrome_flags = chrome_flags
   5398     PyUITest.setUp(self)
   5399 
   5400   def __del__(self):
   5401     """Tears down the browser and then calls super class's destructor"""
   5402     PyUITest.tearDown(self)
   5403     PyUITest.__del__(self)
   5404 
   5405   def ExtraChromeFlags(self):
   5406     """Prepares the browser to launch with specified Chrome flags."""
   5407     return PyUITest.ExtraChromeFlags(self) + self._chrome_flags
   5408 
   5409 
   5410 class _RemoteProxy():
   5411   """Class for PyAuto remote method calls.
   5412 
   5413   Use this class along with RemoteHost.testRemoteHost to establish a PyAuto
   5414   connection with another machine and make remote PyAuto calls. The RemoteProxy
   5415   mimics a PyAuto object, so all json-style PyAuto calls can be made on it.
   5416 
   5417   The remote host acts as a dumb executor that receives method call requests,
   5418   executes them, and sends all of the results back to the RemoteProxy, including
   5419   the return value, thrown exceptions, and console output.
   5420 
   5421   The remote host should be running the same version of PyAuto as the proxy.
   5422   A mismatch could lead to undefined behavior.
   5423 
   5424   Example usage:
   5425     class MyTest(pyauto.PyUITest):
   5426       def testRemoteExample(self):
   5427         remote = pyauto._RemoteProxy(('127.0.0.1', 7410))
   5428         remote.NavigateToURL('http://www.google.com')
   5429         title = remote.GetActiveTabTitle()
   5430         self.assertEqual(title, 'Google')
   5431   """
   5432   class RemoteException(Exception):
   5433     pass
   5434 
   5435   def __init__(self, host):
   5436     self.RemoteConnect(host)
   5437 
   5438   def RemoteConnect(self, host):
   5439     begin = time.time()
   5440     while time.time() - begin < 50:
   5441       self._socket = socket.socket()
   5442       if not self._socket.connect_ex(host):
   5443         break
   5444       time.sleep(0.25)
   5445     else:
   5446       # Make one last attempt, but raise a socket error on failure.
   5447       self._socket = socket.socket()
   5448       self._socket.connect(host)
   5449 
   5450   def RemoteDisconnect(self):
   5451     if self._socket:
   5452       self._socket.shutdown(socket.SHUT_RDWR)
   5453       self._socket.close()
   5454       self._socket = None
   5455 
   5456   def CreateTarget(self, target):
   5457     """Registers the methods and creates a remote instance of a target.
   5458 
   5459     Any RPC calls will then be made on the remote target instance. Note that the
   5460     remote instance will be a brand new instance and will have none of the state
   5461     of the local instance. The target's class should have a constructor that
   5462     takes no arguments.
   5463     """
   5464     self._Call('CreateTarget', target.__class__)
   5465     self._RegisterClassMethods(target)
   5466 
   5467   def _RegisterClassMethods(self, remote_class):
   5468     # Make remote-call versions of all remote_class methods.
   5469     for method_name, _ in inspect.getmembers(remote_class, inspect.ismethod):
   5470       # Ignore private methods and duplicates.
   5471       if method_name[0] in string.letters and \
   5472         getattr(self, method_name, None) is None:
   5473         setattr(self, method_name, functools.partial(self._Call, method_name))
   5474 
   5475   def _Call(self, method_name, *args, **kwargs):
   5476     # Send request.
   5477     request = pickle.dumps((method_name, args, kwargs))
   5478     if self._socket.send(request) != len(request):
   5479       raise self.RemoteException('Error sending remote method call request.')
   5480 
   5481     # Receive response.
   5482     response = self._socket.recv(4096)
   5483     if not response:
   5484       raise self.RemoteException('Client disconnected during method call.')
   5485     result, stdout, stderr, exception = pickle.loads(response)
   5486 
   5487     # Print any output the client captured, throw any exceptions, and return.
   5488     sys.stdout.write(stdout)
   5489     sys.stderr.write(stderr)
   5490     if exception:
   5491       raise self.RemoteException('%s raised by remote client: %s' %
   5492                                  (exception[0], exception[1]))
   5493     return result
   5494 
   5495 
   5496 class PyUITestSuite(pyautolib.PyUITestSuiteBase, unittest.TestSuite):
   5497   """Base TestSuite for PyAuto UI tests."""
   5498 
   5499   def __init__(self, args):
   5500     pyautolib.PyUITestSuiteBase.__init__(self, args)
   5501 
   5502     # Figure out path to chromium binaries
   5503     browser_dir = os.path.normpath(os.path.dirname(pyautolib.__file__))
   5504     logging.debug('Loading pyauto libs from %s', browser_dir)
   5505     self.InitializeWithPath(pyautolib.FilePath(browser_dir))
   5506     os.environ['PATH'] = browser_dir + os.pathsep + os.environ['PATH']
   5507 
   5508     unittest.TestSuite.__init__(self)
   5509     cr_source_root = os.path.normpath(os.path.join(
   5510         os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
   5511     self.SetCrSourceRoot(pyautolib.FilePath(cr_source_root))
   5512 
   5513     # Start http server, if needed.
   5514     global _OPTIONS
   5515     if _OPTIONS and not _OPTIONS.no_http_server:
   5516       self._StartHTTPServer()
   5517     if _OPTIONS and _OPTIONS.remote_host:
   5518       self._ConnectToRemoteHosts(_OPTIONS.remote_host.split(','))
   5519 
   5520   def __del__(self):
   5521     # python unittest module is setup such that the suite gets deleted before
   5522     # the test cases, which is odd because our test cases depend on
   5523     # initializtions like exitmanager, autorelease pool provided by the
   5524     # suite. Forcibly delete the test cases before the suite.
   5525     del self._tests
   5526     pyautolib.PyUITestSuiteBase.__del__(self)
   5527 
   5528     global _HTTP_SERVER
   5529     if _HTTP_SERVER:
   5530       self._StopHTTPServer()
   5531 
   5532     global _CHROME_DRIVER_FACTORY
   5533     if _CHROME_DRIVER_FACTORY is not None:
   5534       _CHROME_DRIVER_FACTORY.Stop()
   5535 
   5536   def _StartHTTPServer(self):
   5537     """Start a local file server hosting data files over http://"""
   5538     global _HTTP_SERVER
   5539     assert not _HTTP_SERVER, 'HTTP Server already started'
   5540     http_data_dir = _OPTIONS.http_data_dir
   5541     http_server = pyautolib.SpawnedTestServer(
   5542         pyautolib.SpawnedTestServer.TYPE_HTTP,
   5543         '127.0.0.1',
   5544         pyautolib.FilePath(http_data_dir))
   5545     assert http_server.Start(), 'Could not start http server'
   5546     _HTTP_SERVER = http_server
   5547     logging.debug('Started http server at "%s".', http_data_dir)
   5548 
   5549   def _StopHTTPServer(self):
   5550     """Stop the local http server."""
   5551     global _HTTP_SERVER
   5552     assert _HTTP_SERVER, 'HTTP Server not yet started'
   5553     assert _HTTP_SERVER.Stop(), 'Could not stop http server'
   5554     _HTTP_SERVER = None
   5555     logging.debug('Stopped http server.')
   5556 
   5557   def _ConnectToRemoteHosts(self, addresses):
   5558     """Connect to remote PyAuto instances using a RemoteProxy.
   5559 
   5560     The RemoteHost instances must already be running."""
   5561     global _REMOTE_PROXY
   5562     assert not _REMOTE_PROXY, 'Already connected to a remote host.'
   5563     _REMOTE_PROXY = []
   5564     for address in addresses:
   5565       if address == 'localhost' or address == '127.0.0.1':
   5566         self._StartLocalRemoteHost()
   5567       _REMOTE_PROXY.append(_RemoteProxy((address, 7410)))
   5568 
   5569   def _StartLocalRemoteHost(self):
   5570     """Start a remote PyAuto instance on the local machine."""
   5571     # Add the path to our main class to the RemoteHost's
   5572     # environment, so it can load that class at runtime.
   5573     import __main__
   5574     main_path = os.path.dirname(__main__.__file__)
   5575     env = os.environ
   5576     if env.get('PYTHONPATH', None):
   5577       env['PYTHONPATH'] += ':' + main_path
   5578     else:
   5579       env['PYTHONPATH'] = main_path
   5580 
   5581     # Run it!
   5582     subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__),
   5583                                                    'remote_host.py')], env=env)
   5584 
   5585 
   5586 class _GTestTextTestResult(unittest._TextTestResult):
   5587   """A test result class that can print formatted text results to a stream.
   5588 
   5589   Results printed in conformance with gtest output format, like:
   5590   [ RUN        ] autofill.AutofillTest.testAutofillInvalid: "test desc."
   5591   [         OK ] autofill.AutofillTest.testAutofillInvalid
   5592   [ RUN        ] autofill.AutofillTest.testFillProfile: "test desc."
   5593   [         OK ] autofill.AutofillTest.testFillProfile
   5594   [ RUN        ] autofill.AutofillTest.testFillProfileCrazyCharacters: "Test."
   5595   [         OK ] autofill.AutofillTest.testFillProfileCrazyCharacters
   5596   """
   5597   def __init__(self, stream, descriptions, verbosity):
   5598     unittest._TextTestResult.__init__(self, stream, descriptions, verbosity)
   5599 
   5600   def _GetTestURI(self, test):
   5601     if sys.version_info[:2] <= (2, 4):
   5602       return '%s.%s' % (unittest._strclass(test.__class__),
   5603                         test._TestCase__testMethodName)
   5604     return '%s.%s.%s' % (test.__class__.__module__,
   5605                          test.__class__.__name__,
   5606                          test._testMethodName)
   5607 
   5608   def getDescription(self, test):
   5609     return '%s: "%s"' % (self._GetTestURI(test), test.shortDescription())
   5610 
   5611   def startTest(self, test):
   5612     unittest.TestResult.startTest(self, test)
   5613     self.stream.writeln('[ RUN        ] %s' % self.getDescription(test))
   5614 
   5615   def addSuccess(self, test):
   5616     unittest.TestResult.addSuccess(self, test)
   5617     self.stream.writeln('[         OK ] %s' % self._GetTestURI(test))
   5618 
   5619   def addError(self, test, err):
   5620     unittest.TestResult.addError(self, test, err)
   5621     self.stream.writeln('[      ERROR ] %s' % self._GetTestURI(test))
   5622 
   5623   def addFailure(self, test, err):
   5624     unittest.TestResult.addFailure(self, test, err)
   5625     self.stream.writeln('[     FAILED ] %s' % self._GetTestURI(test))
   5626 
   5627 
   5628 class PyAutoTextTestRunner(unittest.TextTestRunner):
   5629   """Test Runner for PyAuto tests that displays results in textual format.
   5630 
   5631   Results are displayed in conformance with gtest output.
   5632   """
   5633   def __init__(self, verbosity=1):
   5634     unittest.TextTestRunner.__init__(self,
   5635                                      stream=sys.stderr,
   5636                                      verbosity=verbosity)
   5637 
   5638   def _makeResult(self):
   5639     return _GTestTextTestResult(self.stream, self.descriptions, self.verbosity)
   5640 
   5641 
   5642 # Implementation inspired from unittest.main()
   5643 class Main(object):
   5644   """Main program for running PyAuto tests."""
   5645 
   5646   _options, _args = None, None
   5647   _tests_filename = 'PYAUTO_TESTS'
   5648   _platform_map = {
   5649     'win32':  'win',
   5650     'darwin': 'mac',
   5651     'linux2': 'linux',
   5652     'linux3': 'linux',
   5653     'chromeos': 'chromeos',
   5654   }
   5655 
   5656   def __init__(self):
   5657     self._ParseArgs()
   5658     self._Run()
   5659 
   5660   def _ParseArgs(self):
   5661     """Parse command line args."""
   5662     parser = optparse.OptionParser()
   5663     parser.add_option(
   5664         '', '--channel-id', type='string', default='',
   5665         help='Name of channel id, if using named interface.')
   5666     parser.add_option(
   5667         '', '--chrome-flags', type='string', default='',
   5668         help='Flags passed to Chrome.  This is in addition to the usual flags '
   5669              'like suppressing first-run dialogs, enabling automation.  '
   5670              'See chrome/common/chrome_switches.cc for the list of flags '
   5671              'chrome understands.')
   5672     parser.add_option(
   5673         '', '--http-data-dir', type='string',
   5674         default=os.path.join('chrome', 'test', 'data'),
   5675         help='Relative path from which http server should serve files.')
   5676     parser.add_option(
   5677         '-L', '--list-tests', action='store_true', default=False,
   5678         help='List all tests, and exit.')
   5679     parser.add_option(
   5680         '--shard',
   5681         help='Specify sharding params. Example: 1/3 implies split the list of '
   5682              'tests into 3 groups of which this is the 1st.')
   5683     parser.add_option(
   5684         '', '--log-file', type='string', default=None,
   5685         help='Provide a path to a file to which the logger will log')
   5686     parser.add_option(
   5687         '', '--no-http-server', action='store_true', default=False,
   5688         help='Do not start an http server to serve files in data dir.')
   5689     parser.add_option(
   5690         '', '--remote-host', type='string', default=None,
   5691         help='Connect to remote hosts for remote automation. If "localhost" '
   5692             '"127.0.0.1" is specified, a remote host will be launched '
   5693             'automatically on the local machine.')
   5694     parser.add_option(
   5695         '', '--repeat', type='int', default=1,
   5696         help='Number of times to repeat the tests. Useful to determine '
   5697              'flakiness. Defaults to 1.')
   5698     parser.add_option(
   5699         '-S', '--suite', type='string', default='FULL',
   5700         help='Name of the suite to load.  Defaults to "FULL".')
   5701     parser.add_option(
   5702         '-v', '--verbose', action='store_true', default=False,
   5703         help='Make PyAuto verbose.')
   5704     parser.add_option(
   5705         '-D', '--wait-for-debugger', action='store_true', default=False,
   5706         help='Block PyAuto on startup for attaching debugger.')
   5707 
   5708     self._options, self._args = parser.parse_args()
   5709     global _OPTIONS
   5710     _OPTIONS = self._options  # Export options so other classes can access.
   5711 
   5712     # Set up logging. All log messages will be prepended with a timestamp.
   5713     format = '%(asctime)s %(levelname)-8s %(message)s'
   5714 
   5715     level = logging.INFO
   5716     if self._options.verbose:
   5717       level=logging.DEBUG
   5718 
   5719     logging.basicConfig(level=level, format=format,
   5720                         filename=self._options.log_file)
   5721 
   5722   def TestsDir(self):
   5723     """Returns the path to dir containing tests.
   5724 
   5725     This is typically the dir containing the tests description file.
   5726     This method should be overridden by derived class to point to other dirs
   5727     if needed.
   5728     """
   5729     return os.path.dirname(__file__)
   5730 
   5731   @staticmethod
   5732   def _ImportTestsFromName(name):
   5733     """Get a list of all test names from the given string.
   5734 
   5735     Args:
   5736       name: dot-separated string for a module, a test case or a test method.
   5737             Examples: omnibox  (a module)
   5738                       omnibox.OmniboxTest  (a test case)
   5739                       omnibox.OmniboxTest.testA  (a test method)
   5740 
   5741     Returns:
   5742       [omnibox.OmniboxTest.testA, omnibox.OmniboxTest.testB, ...]
   5743     """
   5744     def _GetTestsFromTestCase(class_obj):
   5745       """Return all test method names from given class object."""
   5746       return [class_obj.__name__ + '.' + x for x in dir(class_obj) if
   5747               x.startswith('test')]
   5748 
   5749     def _GetTestsFromModule(module):
   5750       """Return all test method names from the given module object."""
   5751       tests = []
   5752       for name in dir(module):
   5753         obj = getattr(module, name)
   5754         if (isinstance(obj, (type, types.ClassType)) and
   5755             issubclass(obj, PyUITest) and obj != PyUITest):
   5756           tests.extend([module.__name__ + '.' + x for x in
   5757                         _GetTestsFromTestCase(obj)])
   5758       return tests
   5759 
   5760     module = None
   5761     # Locate the module
   5762     parts = name.split('.')
   5763     parts_copy = parts[:]
   5764     while parts_copy:
   5765       try:
   5766         module = __import__('.'.join(parts_copy))
   5767         break
   5768       except ImportError:
   5769         del parts_copy[-1]
   5770         if not parts_copy: raise
   5771     # We have the module. Pick the exact test method or class asked for.
   5772     parts = parts[1:]
   5773     obj = module
   5774     for part in parts:
   5775       obj = getattr(obj, part)
   5776 
   5777     if type(obj) == types.ModuleType:
   5778       return _GetTestsFromModule(obj)
   5779     elif (isinstance(obj, (type, types.ClassType)) and
   5780           issubclass(obj, PyUITest) and obj != PyUITest):
   5781       return [module.__name__ + '.' + x for x in _GetTestsFromTestCase(obj)]
   5782     elif type(obj) == types.UnboundMethodType:
   5783       return [name]
   5784     else:
   5785       logging.warn('No tests in "%s"', name)
   5786       return []
   5787 
   5788   def _HasTestCases(self, module_string):
   5789     """Determines if we have any PyUITest test case classes in the module
   5790        identified by |module_string|."""
   5791     module = __import__(module_string)
   5792     for name in dir(module):
   5793       obj = getattr(module, name)
   5794       if (isinstance(obj, (type, types.ClassType)) and
   5795           issubclass(obj, PyUITest)):
   5796         return True
   5797     return False
   5798 
   5799   def _ExpandTestNames(self, args):
   5800     """Returns a list of tests loaded from the given args.
   5801 
   5802     The given args can be either a module (ex: module1) or a testcase
   5803     (ex: module2.MyTestCase) or a test (ex: module1.MyTestCase.testX)
   5804     or a suite name (ex: @FULL). If empty, the tests in the already imported
   5805     modules are loaded.
   5806 
   5807     Args:
   5808       args: [module1, module2, module3.testcase, module4.testcase.testX]
   5809             These modules or test cases or tests should be importable.
   5810             Suites can be specified by prefixing @. Example: @FULL
   5811 
   5812       Returns:
   5813         a list of expanded test names.  Example:
   5814           [
   5815             'module1.TestCase1.testA',
   5816             'module1.TestCase1.testB',
   5817             'module2.TestCase2.testX',
   5818             'module3.testcase.testY',
   5819             'module4.testcase.testX'
   5820           ]
   5821     """
   5822 
   5823     def _TestsFromDescriptionFile(suite):
   5824       pyauto_tests_file = os.path.join(self.TestsDir(), self._tests_filename)
   5825       if suite:
   5826         logging.debug("Reading %s (@%s)", pyauto_tests_file, suite)
   5827       else:
   5828         logging.debug("Reading %s", pyauto_tests_file)
   5829       if not os.path.exists(pyauto_tests_file):
   5830         logging.warn("%s missing. Cannot load tests.", pyauto_tests_file)
   5831         return []
   5832       else:
   5833         return self._ExpandTestNamesFrom(pyauto_tests_file, suite)
   5834 
   5835     if not args:  # Load tests ourselves
   5836       if self._HasTestCases('__main__'):    # we are running a test script
   5837         module_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
   5838         args.append(module_name)   # run the test cases found in it
   5839       else:  # run tests from the test description file
   5840         args = _TestsFromDescriptionFile(self._options.suite)
   5841     else:  # Check args with @ prefix for suites
   5842       out_args = []
   5843       for arg in args:
   5844         if arg.startswith('@'):
   5845           suite = arg[1:]
   5846           out_args += _TestsFromDescriptionFile(suite)
   5847         else:
   5848           out_args.append(arg)
   5849       args = out_args
   5850     return args
   5851 
   5852   def _ExpandTestNamesFrom(self, filename, suite):
   5853     """Load test names from the given file.
   5854 
   5855     Args:
   5856       filename: the file to read the tests from
   5857       suite: the name of the suite to load from |filename|.
   5858 
   5859     Returns:
   5860       a list of test names
   5861       [module.testcase.testX, module.testcase.testY, ..]
   5862     """
   5863     suites = PyUITest.EvalDataFrom(filename)
   5864     platform = sys.platform
   5865     if PyUITest.IsChromeOS():  # check if it's chromeos
   5866       platform = 'chromeos'
   5867     assert platform in self._platform_map, '%s unsupported' % platform
   5868     def _NamesInSuite(suite_name):
   5869       logging.debug('Expanding suite %s', suite_name)
   5870       platforms = suites.get(suite_name)
   5871       names = platforms.get('all', []) + \
   5872               platforms.get(self._platform_map[platform], [])
   5873       ret = []
   5874       # Recursively include suites if any.  Suites begin with @.
   5875       for name in names:
   5876         if name.startswith('@'):  # Include another suite
   5877           ret.extend(_NamesInSuite(name[1:]))
   5878         else:
   5879           ret.append(name)
   5880       return ret
   5881 
   5882     assert suite in suites, '%s: No such suite in %s' % (suite, filename)
   5883     all_names = _NamesInSuite(suite)
   5884     args = []
   5885     excluded = []
   5886     # Find all excluded tests.  Excluded tests begin with '-'.
   5887     for name in all_names:
   5888       if name.startswith('-'):  # Exclude
   5889         excluded.extend(self._ImportTestsFromName(name[1:]))
   5890       else:
   5891         args.extend(self._ImportTestsFromName(name))
   5892     for name in excluded:
   5893       if name in args:
   5894         args.remove(name)
   5895       else:
   5896         logging.warn('Cannot exclude %s. Not included. Ignoring', name)
   5897     if excluded:
   5898       logging.debug('Excluded %d test(s): %s', len(excluded), excluded)
   5899     return args
   5900 
   5901   def _Run(self):
   5902     """Run the tests."""
   5903     if self._options.wait_for_debugger:
   5904       raw_input('Attach debugger to process %s and hit <enter> ' % os.getpid())
   5905 
   5906     suite_args = [sys.argv[0]]
   5907     chrome_flags = self._options.chrome_flags
   5908     # Set CHROME_HEADLESS. It enables crash reporter on posix.
   5909     os.environ['CHROME_HEADLESS'] = '1'
   5910     os.environ['EXTRA_CHROME_FLAGS'] = chrome_flags
   5911     test_names = self._ExpandTestNames(self._args)
   5912 
   5913     # Shard, if requested (--shard).
   5914     if self._options.shard:
   5915       matched = re.match('(\d+)/(\d+)', self._options.shard)
   5916       if not matched:
   5917         print >>sys.stderr, 'Invalid sharding params: %s' % self._options.shard
   5918         sys.exit(1)
   5919       shard_index = int(matched.group(1)) - 1
   5920       num_shards = int(matched.group(2))
   5921       if shard_index < 0 or shard_index >= num_shards:
   5922         print >>sys.stderr, 'Invalid sharding params: %s' % self._options.shard
   5923         sys.exit(1)
   5924       test_names = pyauto_utils.Shard(test_names, shard_index, num_shards)
   5925 
   5926     test_names *= self._options.repeat
   5927     logging.debug("Loading %d tests from %s", len(test_names), test_names)
   5928     if self._options.list_tests:  # List tests and exit
   5929       for name in test_names:
   5930         print name
   5931       sys.exit(0)
   5932     pyauto_suite = PyUITestSuite(suite_args)
   5933     loaded_tests = unittest.defaultTestLoader.loadTestsFromNames(test_names)
   5934     pyauto_suite.addTests(loaded_tests)
   5935     verbosity = 1
   5936     if self._options.verbose:
   5937       verbosity = 2
   5938     result = PyAutoTextTestRunner(verbosity=verbosity).run(pyauto_suite)
   5939     del loaded_tests  # Need to destroy test cases before the suite
   5940     del pyauto_suite
   5941     successful = result.wasSuccessful()
   5942     if not successful:
   5943       pyauto_tests_file = os.path.join(self.TestsDir(), self._tests_filename)
   5944       print >>sys.stderr, 'Tests can be disabled by editing %s. ' \
   5945                           'Ref: %s' % (pyauto_tests_file, _PYAUTO_DOC_URL)
   5946     sys.exit(not successful)
   5947 
   5948 
   5949 if __name__ == '__main__':
   5950   Main()
   5951