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