pyauto.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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
8PyAuto uses swig to expose Automation Proxy interfaces to Python.
9For complete documentation on the functionality available,
10run pydoc on this file.
11
12Ref: http://dev.chromium.org/developers/testing/pyauto
13
14
15Include the following in your PyAuto test script to make it run standalone.
16
17from pyauto import Main
18
19if __name__ == '__main__':
20  Main()
21
22This script can be used as an executable to fire off other scripts, similar
23to unittest.py
24  python pyauto.py test_script
25"""
26
27import cStringIO
28import copy
29import functools
30import hashlib
31import inspect
32import logging
33import optparse
34import os
35import pickle
36import pprint
37import re
38import shutil
39import signal
40import socket
41import stat
42import string
43import subprocess
44import sys
45import tempfile
46import time
47import types
48import unittest
49import urllib
50
51import pyauto_paths
52
53
54def _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
67try:
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 *
72except 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
81import bookmark_model
82import download_info
83import history_info
84import omnibox_info
85import plugins_info
86import prefs_info
87from pyauto_errors import AutomationCommandFail
88from pyauto_errors import AutomationCommandTimeout
89from pyauto_errors import JavascriptRuntimeError
90from pyauto_errors import JSONInterfaceError
91from pyauto_errors import NTPThumbnailNotShownError
92import pyauto_utils
93import 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
102class PyUITest(pyautolib.PyUITestBase, unittest.TestCase):
103  """Base class for UI Test Cases in Python.
104
105  A browser is created before executing each test, and is destroyed after
106  each test irrespective of whether the test passed or failed.
107
108  You should derive from this class and create methods with 'test' prefix,
109  and use methods inherited from PyUITestBase (the C++ side).
110
111  Example:
112
113    class MyTest(PyUITest):
114
115      def testNavigation(self):
116        self.NavigateToURL("http://www.google.com")
117        self.assertEqual("Google", self.GetActiveTabTitle())
118  """
119
120  def __init__(self, methodName='runTest', **kwargs):
121    """Initialize PyUITest.
122
123    When redefining __init__ in a derived class, make sure that:
124      o you make a call this __init__
125      o __init__ takes methodName as an arg. this is mandated by unittest module
126
127    Args:
128      methodName: the default method name. Internal use by unittest module
129
130      (The rest of the args can be in any order. They can even be skipped in
131       which case the defaults will be used.)
132
133      clear_profile: If True, clean the profile dir before use. Defaults to True
134      homepage: the home page. Defaults to "about:blank"
135    """
136    # Fetch provided keyword args, or fill in defaults.
137    clear_profile = kwargs.get('clear_profile', True)
138    homepage = kwargs.get('homepage', 'about:blank')
139    self._automation_timeout = _DEFAULT_AUTOMATION_TIMEOUT * 1000
140
141    pyautolib.PyUITestBase.__init__(self, clear_profile, homepage)
142    self.Initialize(pyautolib.FilePath(self.BrowserPath()))
143    unittest.TestCase.__init__(self, methodName)
144
145    # Give all pyauto tests easy access to pprint.PrettyPrinter functions.
146    self.pprint = pprint.pprint
147    self.pformat = pprint.pformat
148
149    # Set up remote proxies, if they were requested.
150    self.remotes = []
151    self.remote = None
152    global _REMOTE_PROXY
153    if _REMOTE_PROXY:
154      self.remotes = _REMOTE_PROXY
155      self.remote = _REMOTE_PROXY[0]
156
157  def __del__(self):
158    pyautolib.PyUITestBase.__del__(self)
159
160  def _SetExtraChromeFlags(self):
161    """Prepares the browser to launch with the specified extra Chrome flags.
162
163    This function is called right before the browser is launched for the first
164    time.
165    """
166    for flag in self.ExtraChromeFlags():
167      if flag.startswith('--'):
168        flag = flag[2:]
169      split_pos = flag.find('=')
170      if split_pos >= 0:
171        flag_name = flag[:split_pos]
172        flag_val = flag[split_pos + 1:]
173        self.AppendBrowserLaunchSwitch(flag_name, flag_val)
174      else:
175        self.AppendBrowserLaunchSwitch(flag)
176
177  def __SetUp(self):
178    named_channel_id = None
179    if _OPTIONS:
180      named_channel_id = _OPTIONS.channel_id
181    if self.IsChromeOS():  # Enable testing interface on ChromeOS.
182      if self.get_clear_profile():
183        self.CleanupBrowserProfileOnChromeOS()
184      self.EnableCrashReportingOnChromeOS()
185      if not named_channel_id:
186        named_channel_id = self.EnableChromeTestingOnChromeOS()
187    else:
188      self._SetExtraChromeFlags()  # Flags already previously set for ChromeOS.
189    if named_channel_id:
190      self._named_channel_id = named_channel_id
191      self.UseNamedChannelID(named_channel_id)
192    # Initialize automation and fire the browser (does not fire the browser
193    # on ChromeOS).
194    self.SetUp()
195
196    global _BROWSER_PID
197    try:
198      _BROWSER_PID = self.GetBrowserInfo()['browser_pid']
199    except JSONInterfaceError:
200      raise JSONInterfaceError('Unable to get browser_pid over automation '
201                               'channel on first attempt.  Something went very '
202                               'wrong.  Chrome probably did not launch.')
203
204    # Forcibly trigger all plugins to get registered.  crbug.com/94123
205    # Sometimes flash files loaded too quickly after firing browser
206    # ends up getting downloaded, which seems to indicate that the plugin
207    # hasn't been registered yet.
208    if not self.IsChromeOS():
209      self.GetPluginsInfo()
210
211    # TODO(dtu): Remove this after crosbug.com/4558 is fixed.
212    if self.IsChromeOS():
213      self.WaitUntil(lambda: not self.GetNetworkInfo()['offline_mode'])
214
215    if (self.IsChromeOS() and not self.GetLoginInfo()['is_logged_in'] and
216        self.ShouldOOBESkipToLogin()):
217      if self.GetOOBEScreenInfo()['screen_name'] != 'login':
218        self.SkipToLogin()
219      if self.ShouldAutoLogin():
220        # Login with default creds.
221        sys.path.append('/usr/local')  # to import autotest libs
222        from autotest.cros import constants
223        creds = constants.CREDENTIALS['$default']
224        self.Login(creds[0], creds[1])
225        assert self.GetLoginInfo()['is_logged_in']
226        logging.info('Logged in as %s.' % creds[0])
227
228    # If we are connected to any RemoteHosts, create PyAuto
229    # instances on the remote sides and set them up too.
230    for remote in self.remotes:
231      remote.CreateTarget(self)
232      remote.setUp()
233
234  def setUp(self):
235    """Override this method to launch browser differently.
236
237    Can be used to prevent launching the browser window by default in case a
238    test wants to do some additional setup before firing browser.
239
240    When using the named interface, it connects to an existing browser
241    instance.
242
243    On ChromeOS, a browser showing the login window is started. Tests can
244    initiate a user session by calling Login() or LoginAsGuest(). Cryptohome
245    vaults or flimflam profiles left over by previous tests can be cleared by
246    calling RemoveAllCryptohomeVaults() respectively CleanFlimflamDirs() before
247    logging in to improve isolation. Note that clearing flimflam profiles
248    requires a flimflam restart, briefly taking down network connectivity and
249    slowing down the test. This should be done for tests that use flimflam only.
250    """
251    self.__SetUp()
252
253  def tearDown(self):
254    for remote in self.remotes:
255      remote.tearDown()
256
257    self.TearDown()  # Destroy browser
258
259  # Method required by the Python standard library unittest.TestCase.
260  def runTest(self):
261    pass
262
263  @staticmethod
264  def BrowserPath():
265    """Returns the path to Chromium binaries.
266
267    Expects the browser binaries to be in the
268    same location as the pyautolib binaries.
269    """
270    return os.path.normpath(os.path.dirname(pyautolib.__file__))
271
272  def ExtraChromeFlags(self):
273    """Return a list of extra chrome flags to use with Chrome for testing.
274
275    These are flags needed to facilitate testing.  Override this function to
276    use a custom set of Chrome flags.
277    """
278    auth_ext_path = ('/usr/local/autotest/deps/pyauto_dep/' +
279        'test_src/chrome/browser/resources/gaia_auth')
280    if self.IsChromeOS():
281      return [
282        '--homepage=about:blank',
283        '--allow-file-access',
284        '--allow-file-access-from-files',
285        '--enable-file-cookies',
286        '--disable-default-apps',
287        '--dom-automation',
288        '--skip-oauth-login',
289        # Enables injection of test content script for webui login automation
290        '--auth-ext-path=%s' % auth_ext_path,
291        # Enable automation provider and chromeos net logs
292        '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2',
293      ]
294    else:
295      return []
296
297  def ShouldOOBESkipToLogin(self):
298    """Determine if we should skip the OOBE flow on ChromeOS.
299
300    This makes automation skip the OOBE flow during setUp() and land directly
301    to the login screen. Applies only if not logged in already.
302
303    Override and return False if OOBE flow is required, for OOBE tests, for
304    example. Calling this function directly will have no effect.
305
306    Returns:
307      True, if the OOBE should be skipped and automation should
308            go to the 'Add user' login screen directly
309      False, if the OOBE should not be skipped.
310    """
311    assert self.IsChromeOS()
312    return True
313
314  def ShouldAutoLogin(self):
315    """Determine if we should auto-login on ChromeOS at browser startup.
316
317    To be used for tests that expect user to be logged in before running test,
318    without caring which user. ShouldOOBESkipToLogin() should return True
319    for this to take effect.
320
321    Override and return False to not auto login, for tests where login is part
322    of the use case.
323
324    Returns:
325      True, if chrome should auto login after startup.
326      False, otherwise.
327    """
328    assert self.IsChromeOS()
329    return True
330
331  def CloseChromeOnChromeOS(self):
332    """Gracefully exit chrome on ChromeOS."""
333
334    def _GetListOfChromePids():
335      """Retrieves the list of currently-running Chrome process IDs.
336
337      Returns:
338        A list of strings, where each string represents a currently-running
339        'chrome' process ID.
340      """
341      proc = subprocess.Popen(['pgrep', '^chrome$'], stdout=subprocess.PIPE)
342      proc.wait()
343      return [x.strip() for x in proc.stdout.readlines()]
344
345    orig_pids = _GetListOfChromePids()
346    subprocess.call(['pkill', '^chrome$'])
347
348    def _AreOrigPidsDead(orig_pids):
349      """Determines whether all originally-running 'chrome' processes are dead.
350
351      Args:
352        orig_pids: A list of strings, where each string represents the PID for
353                   an originally-running 'chrome' process.
354
355      Returns:
356        True, if all originally-running 'chrome' processes have been killed, or
357        False otherwise.
358      """
359      for new_pid in _GetListOfChromePids():
360        if new_pid in orig_pids:
361          return False
362      return True
363
364    self.WaitUntil(lambda: _AreOrigPidsDead(orig_pids))
365
366  @staticmethod
367  def _IsRootSuid(path):
368    """Determine if |path| is a suid-root file."""
369    return os.path.isfile(path) and (os.stat(path).st_mode & stat.S_ISUID)
370
371  @staticmethod
372  def SuidPythonPath():
373    """Path to suid_python binary on ChromeOS.
374
375    This is typically in the same directory as pyautolib.py
376    """
377    return os.path.join(PyUITest.BrowserPath(), 'suid-python')
378
379  @staticmethod
380  def RunSuperuserActionOnChromeOS(action):
381    """Run the given action with superuser privs (on ChromeOS).
382
383    Uses the suid_actions.py script.
384
385    Args:
386      action: An action to perform.
387              See suid_actions.py for available options.
388
389    Returns:
390      (stdout, stderr)
391    """
392    assert PyUITest._IsRootSuid(PyUITest.SuidPythonPath()), \
393        'Did not find suid-root python at %s' % PyUITest.SuidPythonPath()
394    file_path = os.path.join(os.path.dirname(__file__), 'chromeos',
395                             'suid_actions.py')
396    args = [PyUITest.SuidPythonPath(), file_path, '--action=%s' % action]
397    proc = subprocess.Popen(
398        args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
399    stdout, stderr = proc.communicate()
400    return (stdout, stderr)
401
402  def EnableChromeTestingOnChromeOS(self):
403    """Enables the named automation interface on chromeos.
404
405    Restarts chrome so that you get a fresh instance.
406    Also sets some testing-friendly flags for chrome.
407
408    Expects suid python to be present in the same dir as pyautolib.py
409    """
410    assert PyUITest._IsRootSuid(self.SuidPythonPath()), \
411        'Did not find suid-root python at %s' % self.SuidPythonPath()
412    file_path = os.path.join(os.path.dirname(__file__), 'chromeos',
413                             'enable_testing.py')
414    args = [self.SuidPythonPath(), file_path]
415    # Pass extra chrome flags for testing
416    for flag in self.ExtraChromeFlags():
417      args.append('--extra-chrome-flags=%s' % flag)
418    assert self.WaitUntil(lambda: self._IsSessionManagerReady(0))
419    proc = subprocess.Popen(args, stdout=subprocess.PIPE)
420    automation_channel_path = proc.communicate()[0].strip()
421    assert len(automation_channel_path), 'Could not enable testing interface'
422    return automation_channel_path
423
424  @staticmethod
425  def EnableCrashReportingOnChromeOS():
426    """Enables crash reporting on ChromeOS.
427
428    Writes the "/home/chronos/Consent To Send Stats" file with a 32-char
429    readable string.  See comment in session_manager_setup.sh which does this
430    too.
431
432    Note that crash reporting will work only if breakpad is built in, ie in a
433    'Google Chrome' build (not Chromium).
434    """
435    consent_file = '/home/chronos/Consent To Send Stats'
436    def _HasValidConsentFile():
437      if not os.path.isfile(consent_file):
438        return False
439      stat = os.stat(consent_file)
440      return (len(open(consent_file).read()) and
441              (1000, 1000) == (stat.st_uid, stat.st_gid))
442    if not _HasValidConsentFile():
443      client_id = hashlib.md5('abcdefgh').hexdigest()
444      # Consent file creation and chown to chronos needs to be atomic
445      # to avoid races with the session_manager.  crosbug.com/18413
446      # Therefore, create a temp file, chown, then rename it as consent file.
447      temp_file = consent_file + '.tmp'
448      open(temp_file, 'w').write(client_id)
449      # This file must be owned by chronos:chronos!
450      os.chown(temp_file, 1000, 1000);
451      shutil.move(temp_file, consent_file)
452    assert _HasValidConsentFile(), 'Could not create %s' % consent_file
453
454  @staticmethod
455  def _IsSessionManagerReady(old_pid):
456    """Is the ChromeOS session_manager running and ready to accept DBus calls?
457
458    Called after session_manager is killed to know when it has restarted.
459
460    Args:
461      old_pid: The pid that session_manager had before it was killed,
462               to ensure that we don't look at the DBus interface
463               of an old session_manager process.
464    """
465    pgrep_process = subprocess.Popen(['pgrep', 'session_manager'],
466                                     stdout=subprocess.PIPE)
467    new_pid = pgrep_process.communicate()[0].strip()
468    if not new_pid or old_pid == new_pid:
469      return False
470
471    import dbus
472    try:
473      bus = dbus.SystemBus()
474      proxy = bus.get_object('org.chromium.SessionManager',
475                             '/org/chromium/SessionManager')
476      dbus.Interface(proxy, 'org.chromium.SessionManagerInterface')
477    except dbus.DBusException:
478      return False
479    return True
480
481  @staticmethod
482  def CleanupBrowserProfileOnChromeOS():
483    """Cleanup browser profile dir on ChromeOS.
484
485    This does not clear cryptohome.
486
487    Browser should not be running, or else there will be locked files.
488    """
489    profile_dir = '/home/chronos/user'
490    for item in os.listdir(profile_dir):
491      # Deleting .pki causes stateful partition to get erased.
492      if item not in ['log', 'flimflam'] and not item.startswith('.'):
493         pyauto_utils.RemovePath(os.path.join(profile_dir, item))
494
495    chronos_dir = '/home/chronos'
496    for item in os.listdir(chronos_dir):
497      if item != 'user' and not item.startswith('.'):
498        pyauto_utils.RemovePath(os.path.join(chronos_dir, item))
499
500  @staticmethod
501  def CleanupFlimflamDirsOnChromeOS():
502    """Clean the contents of flimflam profiles and restart flimflam."""
503    PyUITest.RunSuperuserActionOnChromeOS('CleanFlimflamDirs')
504
505  @staticmethod
506  def RemoveAllCryptohomeVaultsOnChromeOS():
507    """Remove any existing cryptohome vaults."""
508    PyUITest.RunSuperuserActionOnChromeOS('RemoveAllCryptohomeVaults')
509
510  @staticmethod
511  def _IsInodeNew(path, old_inode):
512    """Determine whether an inode has changed. POSIX only.
513
514    Args:
515      path: The file path to check for changes.
516      old_inode: The old inode number.
517
518    Returns:
519      True if the path exists and its inode number is different from old_inode.
520      False otherwise.
521    """
522    try:
523      stat_result = os.stat(path)
524    except OSError:
525      return False
526    if not stat_result:
527      return False
528    return stat_result.st_ino != old_inode
529
530  def RestartBrowser(self, clear_profile=True, pre_launch_hook=None):
531    """Restart the browser.
532
533    For use with tests that require to restart the browser.
534
535    Args:
536      clear_profile: If True, the browser profile is cleared before restart.
537                     Defaults to True, that is restarts browser with a clean
538                     profile.
539      pre_launch_hook: If specified, must be a callable that is invoked before
540                       the browser is started again. Not supported in ChromeOS.
541    """
542    if self.IsChromeOS():
543      assert pre_launch_hook is None, 'Not supported in ChromeOS'
544      self.TearDown()
545      if clear_profile:
546        self.CleanupBrowserProfileOnChromeOS()
547      self.CloseChromeOnChromeOS()
548      self.EnableChromeTestingOnChromeOS()
549      self.SetUp()
550      return
551    # Not chromeos
552    orig_clear_state = self.get_clear_profile()
553    self.CloseBrowserAndServer()
554    self.set_clear_profile(clear_profile)
555    if pre_launch_hook:
556      pre_launch_hook()
557    logging.debug('Restarting browser with clear_profile=%s',
558                  self.get_clear_profile())
559    self.LaunchBrowserAndServer()
560    self.set_clear_profile(orig_clear_state)  # Reset to original state.
561
562  @staticmethod
563  def DataDir():
564    """Returns the path to the data dir chrome/test/data."""
565    return os.path.normpath(
566        os.path.join(os.path.dirname(__file__), os.pardir, "data"))
567
568  @staticmethod
569  def ChromeOSDataDir():
570    """Returns the path to the data dir chromeos/test/data."""
571    return os.path.normpath(
572        os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
573                     "chromeos", "test", "data"))
574
575  @staticmethod
576  def GetFileURLForPath(*path):
577    """Get file:// url for the given path.
578
579    Also quotes the url using urllib.quote().
580
581    Args:
582      path: Variable number of strings that can be joined.
583    """
584    path_str = os.path.join(*path)
585    abs_path = os.path.abspath(path_str)
586    if sys.platform == 'win32':
587      # Don't quote the ':' in drive letter ( say, C: ) on win.
588      # Also, replace '\' with '/' as expected in a file:/// url.
589      drive, rest = os.path.splitdrive(abs_path)
590      quoted_path = drive.upper() + urllib.quote((rest.replace('\\', '/')))
591      return 'file:///' + quoted_path
592    else:
593      quoted_path = urllib.quote(abs_path)
594      return 'file://' + quoted_path
595
596  @staticmethod
597  def GetFileURLForDataPath(*relative_path):
598    """Get file:// url for the given path relative to the chrome test data dir.
599
600    Also quotes the url using urllib.quote().
601
602    Args:
603      relative_path: Variable number of strings that can be joined.
604    """
605    return PyUITest.GetFileURLForPath(PyUITest.DataDir(), *relative_path)
606
607  @staticmethod
608  def GetHttpURLForDataPath(*relative_path):
609    """Get http:// url for the given path in the data dir.
610
611    The URL will be usable only after starting the http server.
612    """
613    global _HTTP_SERVER
614    assert _HTTP_SERVER, 'HTTP Server not yet started'
615    return _HTTP_SERVER.GetURL(os.path.join('files', *relative_path)).spec()
616
617  @staticmethod
618  def ContentDataDir():
619    """Get path to content/test/data."""
620    return os.path.join(PyUITest.DataDir(), os.pardir, os.pardir, os.pardir,
621        'content', 'test', 'data')
622
623  @staticmethod
624  def GetFileURLForContentDataPath(*relative_path):
625    """Get file:// url for the given path relative to content test data dir.
626
627    Also quotes the url using urllib.quote().
628
629    Args:
630      relative_path: Variable number of strings that can be joined.
631    """
632    return PyUITest.GetFileURLForPath(PyUITest.ContentDataDir(), *relative_path)
633
634  @staticmethod
635  def GetFtpURLForDataPath(ftp_server, *relative_path):
636    """Get ftp:// url for the given path in the data dir.
637
638    Args:
639      ftp_server: handle to ftp server, an instance of TestServer
640      relative_path: any number of path elements
641
642    The URL will be usable only after starting the ftp server.
643    """
644    assert ftp_server, 'FTP Server not yet started'
645    return ftp_server.GetURL(os.path.join(*relative_path)).spec()
646
647  @staticmethod
648  def IsMac():
649    """Are we on Mac?"""
650    return 'darwin' == sys.platform
651
652  @staticmethod
653  def IsLinux():
654    """Are we on Linux? ChromeOS is linux too."""
655    return sys.platform.startswith('linux')
656
657  @staticmethod
658  def IsWin():
659    """Are we on Win?"""
660    return 'win32' == sys.platform
661
662  @staticmethod
663  def IsWin7():
664    """Are we on Windows 7?"""
665    if not PyUITest.IsWin():
666      return False
667    ver = sys.getwindowsversion()
668    return (ver[3], ver[0], ver[1]) == (2, 6, 1)
669
670  @staticmethod
671  def IsWinVista():
672    """Are we on Windows Vista?"""
673    if not PyUITest.IsWin():
674      return False
675    ver = sys.getwindowsversion()
676    return (ver[3], ver[0], ver[1]) == (2, 6, 0)
677
678  @staticmethod
679  def IsWinXP():
680    """Are we on Windows XP?"""
681    if not PyUITest.IsWin():
682      return False
683    ver = sys.getwindowsversion()
684    return (ver[3], ver[0], ver[1]) == (2, 5, 1)
685
686  @staticmethod
687  def IsChromeOS():
688    """Are we on ChromeOS (or Chromium OS)?
689
690    Checks for "CHROMEOS_RELEASE_NAME=" in /etc/lsb-release.
691    """
692    lsb_release = '/etc/lsb-release'
693    if not PyUITest.IsLinux() or not os.path.isfile(lsb_release):
694      return False
695    for line in open(lsb_release).readlines():
696      if line.startswith('CHROMEOS_RELEASE_NAME='):
697        return True
698    return False
699
700  @staticmethod
701  def IsPosix():
702    """Are we on Mac/Linux?"""
703    return PyUITest.IsMac() or PyUITest.IsLinux()
704
705  @staticmethod
706  def IsEnUS():
707    """Are we en-US?"""
708    # TODO: figure out the machine's langugage.
709    return True
710
711  @staticmethod
712  def GetPlatform():
713    """Return the platform name."""
714    # Since ChromeOS is also Linux, we check for it first.
715    if PyUITest.IsChromeOS():
716      return 'chromeos'
717    elif PyUITest.IsLinux():
718      return 'linux'
719    elif PyUITest.IsMac():
720      return 'mac'
721    elif PyUITest.IsWin():
722      return 'win'
723    else:
724      return 'unknown'
725
726  @staticmethod
727  def EvalDataFrom(filename):
728    """Return eval of python code from given file.
729
730    The datastructure used in the file will be preserved.
731    """
732    data_file = os.path.join(filename)
733    contents = open(data_file).read()
734    try:
735      ret = eval(contents)
736    except:
737      print >>sys.stderr, '%s is an invalid data file.' % data_file
738      raise
739    return ret
740
741  @staticmethod
742  def ChromeOSBoard():
743    """What is the ChromeOS board name"""
744    if PyUITest.IsChromeOS():
745      for line in open('/etc/lsb-release'):
746        line = line.strip()
747        if line.startswith('CHROMEOS_RELEASE_BOARD='):
748          return line.split('=')[1]
749    return None
750
751  @staticmethod
752  def Kill(pid):
753    """Terminate the given pid.
754
755    If the pid refers to a renderer, use KillRendererProcess instead.
756    """
757    if PyUITest.IsWin():
758      subprocess.call(['taskkill.exe', '/T', '/F', '/PID', str(pid)])
759    else:
760      os.kill(pid, signal.SIGTERM)
761
762  @staticmethod
763  def GetPrivateInfo():
764    """Fetch info from private_tests_info.txt in private dir.
765
766    Returns:
767      a dictionary of items from private_tests_info.txt
768    """
769    private_file = os.path.join(
770        PyUITest.DataDir(), 'pyauto_private', 'private_tests_info.txt')
771    assert os.path.exists(private_file), '%s missing' % private_file
772    return PyUITest.EvalDataFrom(private_file)
773
774  def WaitUntil(self, function, timeout=-1, retry_sleep=0.25, args=[],
775                expect_retval=None, return_retval=False, debug=True):
776    """Poll on a condition until timeout.
777
778    Waits until the |function| evalues to |expect_retval| or until |timeout|
779    secs, whichever occurs earlier.
780
781    This is better than using a sleep, since it waits (almost) only as much
782    as needed.
783
784    WARNING: This method call should be avoided as far as possible in favor
785    of a real wait from chromium (like wait-until-page-loaded).
786    Only use in case there's really no better option.
787
788    EXAMPLES:-
789    Wait for "file.txt" to get created:
790      WaitUntil(os.path.exists, args=["file.txt"])
791
792    Same as above, but using lambda:
793      WaitUntil(lambda: os.path.exists("file.txt"))
794
795    Args:
796      function: the function whose truth value is to be evaluated
797      timeout: the max timeout (in secs) for which to wait. The default
798               action is to wait for kWaitForActionMaxMsec, as set in
799               ui_test.cc
800               Use None to wait indefinitely.
801      retry_sleep: the sleep interval (in secs) before retrying |function|.
802                   Defaults to 0.25 secs.
803      args: the args to pass to |function|
804      expect_retval: the expected return value for |function|. This forms the
805                     exit criteria. In case this is None (the default),
806                     |function|'s return value is checked for truth,
807                     so 'non-empty-string' should match with True
808      return_retval: If True, return the value returned by the last call to
809                     |function()|
810      debug: if True, displays debug info at each retry.
811
812    Returns:
813      The return value of the |function| (when return_retval == True)
814      True, if returning when |function| evaluated to True (when
815          return_retval == False)
816      False, when returning due to timeout
817    """
818    if timeout == -1:  # Default
819      timeout = self._automation_timeout / 1000.0
820    assert callable(function), "function should be a callable"
821    begin = time.time()
822    debug_begin = begin
823    retval = None
824    while timeout is None or time.time() - begin <= timeout:
825      retval = function(*args)
826      if (expect_retval is None and retval) or \
827         (expect_retval is not None and expect_retval == retval):
828        return retval if return_retval else True
829      if debug and time.time() - debug_begin > 5:
830        debug_begin += 5
831        if function.func_name == (lambda: True).func_name:
832          function_info = inspect.getsource(function).strip()
833        else:
834          function_info = '%s()' % function.func_name
835        logging.debug('WaitUntil(%s:%d %s) still waiting. '
836                      'Expecting %s. Last returned %s.',
837                      os.path.basename(inspect.getsourcefile(function)),
838                      inspect.getsourcelines(function)[1],
839                      function_info,
840                      True if expect_retval is None else expect_retval,
841                      retval)
842      time.sleep(retry_sleep)
843    return retval if return_retval else False
844
845  def StartFTPServer(self, data_dir):
846    """Start a local file server hosting data files over ftp://
847
848    Args:
849      data_dir: path where ftp files should be served
850
851    Returns:
852      handle to FTP Server, an instance of TestServer
853    """
854    ftp_server = pyautolib.TestServer(pyautolib.TestServer.TYPE_FTP,
855                                      '127.0.0.1',
856                                      pyautolib.FilePath(data_dir))
857    assert ftp_server.Start(), 'Could not start ftp server'
858    logging.debug('Started ftp server at "%s".', data_dir)
859    return ftp_server
860
861  def StopFTPServer(self, ftp_server):
862    """Stop the local ftp server."""
863    assert ftp_server, 'FTP Server not yet started'
864    assert ftp_server.Stop(), 'Could not stop ftp server'
865    logging.debug('Stopped ftp server.')
866
867  def StartHTTPServer(self, data_dir):
868    """Starts a local HTTP TestServer serving files from |data_dir|.
869
870    Args:
871      data_dir: path where the TestServer should serve files from. This will be
872      appended to the source dir to get the final document root.
873
874    Returns:
875      handle to the HTTP TestServer
876    """
877    http_server = pyautolib.TestServer(pyautolib.TestServer.TYPE_HTTP,
878                                       '127.0.0.1',
879                                       pyautolib.FilePath(data_dir))
880    assert http_server.Start(), 'Could not start HTTP server'
881    logging.debug('Started HTTP server at "%s".', data_dir)
882    return http_server
883
884  def StopHTTPServer(self, http_server):
885    assert http_server, 'HTTP server not yet started'
886    assert http_server.Stop(), 'Cloud not stop the HTTP server'
887    logging.debug('Stopped HTTP server.')
888
889  def StartHttpsServer(self, cert_type, data_dir):
890    """Starts a local HTTPS TestServer serving files from |data_dir|.
891
892    Args:
893      cert_type: An instance of SSLOptions.ServerCertificate for three
894                 certificate types: ok, expired, or mismatch.
895      data_dir: The path where TestServer should serve files from. This is
896                appended to the source dir to get the final document root.
897
898    Returns:
899      Handle to the HTTPS TestServer
900    """
901    https_server = pyautolib.TestServer(
902        pyautolib.TestServer.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 ImportSettings(self, import_from, first_run,
2996                    import_items, windex=0):
2997    """Import the specified import items from the specified browser.
2998
2999    Implements the features available in the "Import Settings" part of the
3000    first-run UI dialog.
3001
3002    Args:
3003      import_from: A string indicating which browser to import from. Possible
3004                   strings (depending on which browsers are installed on the
3005                   machine) are: 'Mozilla Firefox', 'Google Toolbar',
3006                   'Microsoft Internet Explorer', 'Safari'
3007      first_run: A boolean indicating whether this is the first run of
3008                 the browser.
3009                 If it is not the first run then:
3010                 1) Bookmarks are only imported to the bookmarks bar if there
3011                    aren't already bookmarks.
3012                 2) The bookmark bar is shown.
3013      import_items: A list of strings indicating which items to import.
3014                    Strings that can be in the list are:
3015                    HISTORY, FAVORITES, PASSWORDS, SEARCH_ENGINES, HOME_PAGE,
3016                    ALL (note: COOKIES is not supported by the browser yet)
3017      windex: window index, defaults to 0.
3018
3019    Raises:
3020      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3021    """
3022    cmd_dict = {  # Prepare command for the json interface
3023      'command': 'ImportSettings',
3024      'import_from': import_from,
3025      'first_run': first_run,
3026      'import_items': import_items
3027    }
3028    return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
3029
3030  def AddSavedPassword(self, password_dict, windex=0):
3031    """Adds the given username-password combination to the saved passwords.
3032
3033    Args:
3034      password_dict: a dictionary that represents a password. Example:
3035      { 'username_value': 'user@example.com',        # Required
3036        'password_value': 'test.password',           # Required
3037        'signon_realm': 'https://www.example.com/',  # Required
3038        'time': 1279317810.0,                        # Can get from time.time()
3039        'origin_url': 'https://www.example.com/login',
3040        'username_element': 'username',              # The HTML element
3041        'password_element': 'password',              # The HTML element
3042        'submit_element': 'submit',                  # The HTML element
3043        'action_target': 'https://www.example.com/login/',
3044        'blacklist': False }
3045      windex: window index; defaults to 0 (first window).
3046
3047    *Blacklist notes* To blacklist a site, add a blacklist password with the
3048    following dictionary items: origin_url, signon_realm, username_element,
3049    password_element, action_target, and 'blacklist': True. Then all sites that
3050    have password forms matching those are blacklisted.
3051
3052    Returns:
3053      True if adding the password succeeded, false otherwise. In incognito
3054      mode, adding the password should fail.
3055
3056    Raises:
3057      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3058    """
3059    cmd_dict = {  # Prepare command for the json interface
3060      'command': 'AddSavedPassword',
3061      'password': password_dict
3062    }
3063    return self._GetResultFromJSONRequest(
3064        cmd_dict, windex=windex)['password_added']
3065
3066  def RemoveSavedPassword(self, password_dict, windex=0):
3067    """Removes the password matching the provided password dictionary.
3068
3069    Args:
3070      password_dict: A dictionary that represents a password.
3071                     For an example, see the dictionary in AddSavedPassword.
3072      windex: The window index, default is 0 (first window).
3073    """
3074    cmd_dict = {  # Prepare command for the json interface
3075      'command': 'RemoveSavedPassword',
3076      'password': password_dict
3077    }
3078    self._GetResultFromJSONRequest(cmd_dict, windex=windex)
3079
3080  def GetSavedPasswords(self):
3081    """Return the passwords currently saved.
3082
3083    Returns:
3084      A list of dictionaries representing each password. For an example
3085      dictionary see AddSavedPassword documentation. The overall structure will
3086      be:
3087      [ {password1 dictionary}, {password2 dictionary} ]
3088    """
3089    cmd_dict = {  # Prepare command for the json interface
3090      'command': 'GetSavedPasswords'
3091    }
3092    return self._GetResultFromJSONRequest(cmd_dict)['passwords']
3093
3094  def SetTheme(self, crx_file_path, windex=0):
3095    """Installs the given theme synchronously.
3096
3097    A theme file is a file with a .crx suffix, like an extension.  The theme
3098    file must be specified with an absolute path.  This method call waits until
3099    the theme is installed and will trigger the "theme installed" infobar.
3100    If the install is unsuccessful, will throw an exception.
3101
3102    Uses InstallExtension().
3103
3104    Returns:
3105      The ID of the installed theme.
3106
3107    Raises:
3108      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3109    """
3110    return self.InstallExtension(crx_file_path, True, windex)
3111
3112  def GetActiveNotifications(self):
3113    """Gets a list of the currently active/shown HTML5 notifications.
3114
3115    Returns:
3116      a list containing info about each active notification, with the
3117      first item in the list being the notification on the bottom of the
3118      notification stack. The 'content_url' key can refer to a URL or a data
3119      URI. The 'pid' key-value pair may be invalid if the notification is
3120      closing.
3121
3122    SAMPLE:
3123    [ { u'content_url': u'data:text/html;charset=utf-8,%3C!DOCTYPE%l%3E%0Atm...'
3124        u'display_source': 'www.corp.google.com',
3125        u'origin_url': 'http://www.corp.google.com/',
3126        u'pid': 8505},
3127      { u'content_url': 'http://www.gmail.com/special_notification.html',
3128        u'display_source': 'www.gmail.com',
3129        u'origin_url': 'http://www.gmail.com/',
3130        u'pid': 9291}]
3131
3132    Raises:
3133      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3134    """
3135    return [x for x in self.GetAllNotifications() if 'pid' in x]
3136
3137  def GetAllNotifications(self):
3138    """Gets a list of all active and queued HTML5 notifications.
3139
3140    An active notification is one that is currently shown to the user. Chrome's
3141    notification system will limit the number of notifications shown (currently
3142    by only allowing a certain percentage of the screen to be taken up by them).
3143    A notification will be queued if there are too many active notifications.
3144    Once other notifications are closed, another will be shown from the queue.
3145
3146    Returns:
3147      a list containing info about each notification, with the first
3148      item in the list being the notification on the bottom of the
3149      notification stack. The 'content_url' key can refer to a URL or a data
3150      URI. The 'pid' key-value pair will only be present for active
3151      notifications.
3152
3153    SAMPLE:
3154    [ { u'content_url': u'data:text/html;charset=utf-8,%3C!DOCTYPE%l%3E%0Atm...'
3155        u'display_source': 'www.corp.google.com',
3156        u'origin_url': 'http://www.corp.google.com/',
3157        u'pid': 8505},
3158      { u'content_url': 'http://www.gmail.com/special_notification.html',
3159        u'display_source': 'www.gmail.com',
3160        u'origin_url': 'http://www.gmail.com/'}]
3161
3162    Raises:
3163      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3164    """
3165    cmd_dict = {
3166      'command': 'GetAllNotifications',
3167    }
3168    return self._GetResultFromJSONRequest(cmd_dict)['notifications']
3169
3170  def CloseNotification(self, index):
3171    """Closes the active HTML5 notification at the given index.
3172
3173    Args:
3174      index: the index of the notification to close. 0 refers to the
3175             notification on the bottom of the notification stack.
3176
3177    Raises:
3178      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3179    """
3180    cmd_dict = {
3181      'command': 'CloseNotification',
3182      'index': index,
3183    }
3184    return self._GetResultFromJSONRequest(cmd_dict)
3185
3186  def WaitForNotificationCount(self, count):
3187    """Waits for the number of active HTML5 notifications to reach the given
3188    count.
3189
3190    Raises:
3191      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3192    """
3193    cmd_dict = {
3194      'command': 'WaitForNotificationCount',
3195      'count': count,
3196    }
3197    self._GetResultFromJSONRequest(cmd_dict)
3198
3199  def FindInPage(self, search_string, forward=True,
3200                 match_case=False, find_next=False,
3201                 tab_index=0, windex=0, timeout=-1):
3202    """Find the match count for the given search string and search parameters.
3203    This is equivalent to using the find box.
3204
3205    Args:
3206      search_string: The string to find on the page.
3207      forward: Boolean to set if the search direction is forward or backwards
3208      match_case: Boolean to set for case sensitive search.
3209      find_next: Boolean to set to continue the search or start from beginning.
3210      tab_index: The tab index, default is 0.
3211      windex: The window index, default is 0.
3212      timeout: request timeout (in milliseconds), default is -1.
3213
3214    Returns:
3215      number of matches found for the given search string and parameters
3216    SAMPLE:
3217    { u'match_count': 10,
3218      u'match_left': 100,
3219      u'match_top': 100,
3220      u'match_right': 200,
3221      u'match_bottom': 200}
3222
3223    Raises:
3224      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3225    """
3226    cmd_dict = {
3227      'command': 'FindInPage',
3228      'tab_index' : tab_index,
3229      'search_string' : search_string,
3230      'forward' : forward,
3231      'match_case' : match_case,
3232      'find_next' : find_next,
3233    }
3234    return self._GetResultFromJSONRequest(cmd_dict, windex=windex,
3235                                          timeout=timeout)
3236
3237  def OpenFindInPage(self, windex=0):
3238    """Opens the "Find in Page" box.
3239
3240    Args:
3241      windex: Index of the window; defaults to 0.
3242
3243    Raises:
3244      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3245    """
3246    cmd_dict = {
3247      'command': 'OpenFindInPage',
3248      'windex' : windex,
3249    }
3250    self._GetResultFromJSONRequest(cmd_dict, windex=None)
3251
3252  def IsFindInPageVisible(self, windex=0):
3253    """Returns the visibility of the "Find in Page" box.
3254
3255    Args:
3256      windex: Index of the window; defaults to 0.
3257
3258    Returns:
3259      A boolean indicating the visibility state of the "Find in Page" box.
3260
3261    Raises:
3262      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3263    """
3264    cmd_dict = {
3265      'command': 'IsFindInPageVisible',
3266      'windex' : windex,
3267    }
3268    return self._GetResultFromJSONRequest(cmd_dict, windex=None)['is_visible']
3269
3270
3271  def AddDomEventObserver(self, event_name='', automation_id=-1,
3272                          recurring=False):
3273    """Adds a DomEventObserver associated with the AutomationEventQueue.
3274
3275    An app raises a matching event in Javascript by calling:
3276    window.domAutomationController.sendWithId(automation_id, event_name)
3277
3278    Args:
3279      event_name: The event name to watch for. By default an event is raised
3280                  for any message.
3281      automation_id: The Automation Id of the sent message. By default all
3282                     messages sent from the window.domAutomationController are
3283                     observed. Note that other PyAuto functions also send
3284                     messages through window.domAutomationController with
3285                     arbirary Automation Ids and they will be observed.
3286      recurring: If False the observer will be removed after it generates one
3287                 event, otherwise it will continue observing and generating
3288                 events until explicity removed with RemoveEventObserver(id).
3289
3290    Returns:
3291      The id of the created observer, which can be used with GetNextEvent(id)
3292      and RemoveEventObserver(id).
3293
3294    Raises:
3295      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3296    """
3297    cmd_dict = {
3298      'command': 'AddDomEventObserver',
3299      'event_name': event_name,
3300      'automation_id': automation_id,
3301      'recurring': recurring,
3302    }
3303    return self._GetResultFromJSONRequest(cmd_dict, windex=None)['observer_id']
3304
3305  def AddDomMutationObserver(self, mutation_type, xpath,
3306                             attribute='textContent', expected_value=None,
3307                             automation_id=44444,
3308                             exec_js=None, **kwargs):
3309    """Sets up an event observer watching for a specific DOM mutation.
3310
3311    Creates an observer that raises an event when a mutation of the given type
3312    occurs on a DOM node specified by |selector|.
3313
3314    Args:
3315      mutation_type: One of 'add', 'remove', 'change', or 'exists'.
3316      xpath: An xpath specifying the DOM node to watch. The node must already
3317          exist if |mutation_type| is 'change'.
3318      attribute: Attribute to match |expected_value| against, if given. Defaults
3319          to 'textContent'.
3320      expected_value: Optional regular expression to match against the node's
3321          textContent attribute after the mutation. Defaults to None.
3322      automation_id: The automation_id used to route the observer javascript
3323          messages. Defaults to 44444.
3324      exec_js: A callable of the form f(self, js, **kwargs) used to inject the
3325          MutationObserver javascript. Defaults to None, which uses
3326          PyUITest.ExecuteJavascript.
3327
3328      Any additional keyword arguments are passed on to ExecuteJavascript and
3329      can be used to select the tab where the DOM MutationObserver is created.
3330
3331    Returns:
3332      The id of the created observer, which can be used with GetNextEvent(id)
3333      and RemoveEventObserver(id).
3334
3335    Raises:
3336      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3337      pyauto_errors.JavascriptRuntimeError if the injected javascript
3338          MutationObserver returns an error.
3339    """
3340    assert mutation_type in ('add', 'remove', 'change', 'exists'), \
3341        'Unexpected value "%s" for mutation_type.' % mutation_type
3342    cmd_dict = {
3343      'command': 'AddDomEventObserver',
3344      'event_name': '__dom_mutation_observer__:$(id)',
3345      'automation_id': automation_id,
3346      'recurring': False,
3347    }
3348    observer_id = (
3349        self._GetResultFromJSONRequest(cmd_dict, windex=None)['observer_id'])
3350    expected_string = ('null' if expected_value is None else '"%s"' %
3351                       expected_value.replace('"', r'\"'))
3352    jsfile = os.path.join(os.path.abspath(os.path.dirname(__file__)),
3353                          'dom_mutation_observer.js')
3354    with open(jsfile, 'r') as f:
3355      js = ('(' + f.read() + ')(%d, %d, "%s", "%s", "%s", %s);' %
3356            (automation_id, observer_id, mutation_type,
3357             xpath.replace('"', r'\"'), attribute, expected_string))
3358    exec_js = exec_js or PyUITest.ExecuteJavascript
3359    try:
3360      jsreturn = exec_js(self, js, **kwargs)
3361    except JSONInterfaceError:
3362      raise JSONInterfaceError('Failed to inject DOM mutation observer.')
3363    if jsreturn != 'success':
3364      self.RemoveEventObserver(observer_id)
3365      raise JavascriptRuntimeError(jsreturn)
3366    return observer_id
3367
3368  def WaitForDomNode(self, xpath, attribute='textContent',
3369                     expected_value=None, exec_js=None, timeout=-1,
3370                     msg='Expected DOM node failed to appear.', **kwargs):
3371    """Waits until a node specified by an xpath exists in the DOM.
3372
3373    NOTE: This does NOT poll. It returns as soon as the node appears, or
3374      immediately if the node already exists.
3375
3376    Args:
3377      xpath: An xpath specifying the DOM node to watch.
3378      attribute: Attribute to match |expected_value| against, if given. Defaults
3379          to 'textContent'.
3380      expected_value: Optional regular expression to match against the node's
3381          textContent attribute. Defaults to None.
3382      exec_js: A callable of the form f(self, js, **kwargs) used to inject the
3383          MutationObserver javascript. Defaults to None, which uses
3384          PyUITest.ExecuteJavascript.
3385      msg: An optional error message used if a JSONInterfaceError is caught
3386          while waiting for the DOM node to appear.
3387      timeout: Time to wait for the node to exist before raising an exception,
3388          defaults to the default automation timeout.
3389
3390      Any additional keyword arguments are passed on to ExecuteJavascript and
3391      can be used to select the tab where the DOM MutationObserver is created.
3392
3393    Raises:
3394      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3395      pyauto_errors.JavascriptRuntimeError if the injected javascript
3396          MutationObserver returns an error.
3397    """
3398    observer_id = self.AddDomMutationObserver('exists', xpath, attribute,
3399                                              expected_value, exec_js=exec_js,
3400                                              **kwargs)
3401    try:
3402      self.GetNextEvent(observer_id, timeout=timeout)
3403    except JSONInterfaceError:
3404      raise JSONInterfaceError(msg)
3405
3406  def GetNextEvent(self, observer_id=-1, blocking=True, timeout=-1):
3407    """Waits for an observed event to occur.
3408
3409    The returned event is removed from the Event Queue. If there is already a
3410    matching event in the queue it is returned immediately, otherwise the call
3411    blocks until a matching event occurs. If blocking is disabled and no
3412    matching event is in the queue this function will immediately return None.
3413
3414    Args:
3415      observer_id: The id of the observer to wait for, matches any event by
3416                   default.
3417      blocking: If True waits until there is a matching event in the queue,
3418                if False and there is no event waiting in the queue returns None
3419                immediately.
3420      timeout: Time to wait for a matching event, defaults to the default
3421               automation timeout.
3422
3423    Returns:
3424      Event response dictionary, or None if blocking is disabled and there is no
3425      matching event in the queue.
3426      SAMPLE:
3427      { 'observer_id': 1,
3428        'name': 'login completed',
3429        'type': 'raised_event'}
3430
3431    Raises:
3432      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3433    """
3434    cmd_dict = {
3435      'command': 'GetNextEvent',
3436      'observer_id' : observer_id,
3437      'blocking' : blocking,
3438    }
3439    return self._GetResultFromJSONRequest(cmd_dict, windex=None,
3440                                          timeout=timeout)
3441
3442  def RemoveEventObserver(self, observer_id):
3443    """Removes an Event Observer from the AutomationEventQueue.
3444
3445    Expects a valid observer_id.
3446
3447    Args:
3448      observer_id: The id of the observer to remove.
3449
3450    Raises:
3451      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3452    """
3453    cmd_dict = {
3454      'command': 'RemoveEventObserver',
3455      'observer_id' : observer_id,
3456    }
3457    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
3458
3459  def ClearEventQueue(self):
3460    """Removes all events currently in the AutomationEventQueue.
3461
3462    Raises:
3463      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3464    """
3465    cmd_dict = {
3466      'command': 'ClearEventQueue',
3467    }
3468    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
3469
3470  def WaitUntilNavigationCompletes(self, tab_index=0, windex=0):
3471    """Wait until the specified tab is done navigating.
3472
3473    It is safe to call ExecuteJavascript() as soon as the call returns. If
3474    there is no outstanding navigation the call will return immediately.
3475
3476    Args:
3477      tab_index: index of the tab.
3478      windex: index of the window.
3479
3480    Raises:
3481      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3482    """
3483    cmd_dict = {
3484      'command': 'WaitUntilNavigationCompletes',
3485      'tab_index': tab_index,
3486      'windex': windex,
3487    }
3488    return self._GetResultFromJSONRequest(cmd_dict)
3489
3490  def ExecuteJavascript(self, js, tab_index=0, windex=0, frame_xpath=''):
3491    """Executes a script in the specified frame of a tab.
3492
3493    By default, execute the script in the top frame of the first tab in the
3494    first window. The invoked javascript function must send a result back via
3495    the domAutomationController.send function, or this function will never
3496    return.
3497
3498    Args:
3499      js: script to be executed.
3500      windex: index of the window.
3501      tab_index: index of the tab.
3502      frame_xpath: XPath of the frame to execute the script.  Default is no
3503      frame. Example: '//frames[1]'.
3504
3505    Returns:
3506      a value that was sent back via the domAutomationController.send method
3507
3508    Raises:
3509      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3510    """
3511    cmd_dict = {
3512      'command': 'ExecuteJavascript',
3513      'javascript' : js,
3514      'windex' : windex,
3515      'tab_index' : tab_index,
3516      'frame_xpath' : frame_xpath,
3517    }
3518    result = self._GetResultFromJSONRequest(cmd_dict)['result']
3519    # Wrap result in an array before deserializing because valid JSON has an
3520    # array or an object as the root.
3521    json_string = '[' + result + ']'
3522    return json.loads(json_string)[0]
3523
3524  def ExecuteJavascriptInRenderView(self, js, view, frame_xpath=''):
3525    """Executes a script in the specified frame of an render view.
3526
3527    The invoked javascript function must send a result back via the
3528    domAutomationController.send function, or this function will never return.
3529
3530    Args:
3531      js: script to be executed.
3532      view: A dictionary representing a unique id for the render view as
3533      returned for example by.
3534      self.GetBrowserInfo()['extension_views'][]['view'].
3535      Example:
3536      { 'render_process_id': 1,
3537        'render_view_id' : 2}
3538
3539      frame_xpath: XPath of the frame to execute the script. Default is no
3540      frame. Example:
3541      '//frames[1]'
3542
3543    Returns:
3544      a value that was sent back via the domAutomationController.send method
3545
3546    Raises:
3547      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3548    """
3549    cmd_dict = {
3550      'command': 'ExecuteJavascriptInRenderView',
3551      'javascript' : js,
3552      'view' : view,
3553      'frame_xpath' : frame_xpath,
3554    }
3555    result = self._GetResultFromJSONRequest(cmd_dict, windex=None)['result']
3556    # Wrap result in an array before deserializing because valid JSON has an
3557    # array or an object as the root.
3558    json_string = '[' + result + ']'
3559    return json.loads(json_string)[0]
3560
3561  def ExecuteJavascriptInOOBEWebUI(self, js, frame_xpath=''):
3562    """Executes a script in the specified frame of the OOBE WebUI.
3563
3564    By default, execute the script in the top frame of the OOBE window. This
3565    also works for all OOBE pages, including the enterprise enrollment
3566    screen and login page. The invoked javascript function must send a result
3567    back via the domAutomationController.send function, or this function will
3568    never return.
3569
3570    Args:
3571      js: Script to be executed.
3572      frame_xpath: XPath of the frame to execute the script. Default is no
3573          frame. Example: '//frames[1]'
3574
3575    Returns:
3576      A value that was sent back via the domAutomationController.send method.
3577
3578    Raises:
3579      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3580    """
3581    cmd_dict = {
3582      'command': 'ExecuteJavascriptInOOBEWebUI',
3583
3584      'javascript': js,
3585      'frame_xpath': frame_xpath,
3586    }
3587    result = self._GetResultFromJSONRequest(cmd_dict, windex=None)['result']
3588    # Wrap result in an array before deserializing because valid JSON has an
3589    # array or an object as the root.
3590    return json.loads('[' + result + ']')[0]
3591
3592
3593  def GetDOMValue(self, expr, tab_index=0, windex=0, frame_xpath=''):
3594    """Executes a Javascript expression and returns the value.
3595
3596    This is a wrapper for ExecuteJavascript, eliminating the need to
3597    explicitly call domAutomationController.send function.
3598
3599    Args:
3600      expr: expression value to be returned.
3601      tab_index: index of the tab.
3602      windex: index of the window.
3603      frame_xpath: XPath of the frame to execute the script.  Default is no
3604      frame. Example: '//frames[1]'.
3605
3606    Returns:
3607      a string that was sent back via the domAutomationController.send method.
3608    """
3609    js = 'window.domAutomationController.send(%s);' % expr
3610    return self.ExecuteJavascript(js, tab_index, windex, frame_xpath)
3611
3612  def CallJavascriptFunc(self, function, args=[], tab_index=0, windex=0):
3613    """Executes a script which calls a given javascript function.
3614
3615    The invoked javascript function must send a result back via the
3616    domAutomationController.send function, or this function will never return.
3617
3618    Defaults to first tab in first window.
3619
3620    Args:
3621      function: name of the function.
3622      args: list of all the arguments to pass into the called function. These
3623            should be able to be converted to a string using the |str| function.
3624      tab_index: index of the tab within the given window.
3625      windex: index of the window.
3626
3627    Returns:
3628      a string that was sent back via the domAutomationController.send method
3629    """
3630    converted_args = map(lambda arg: json.dumps(arg), args)
3631    js = '%s(%s)' % (function, ', '.join(converted_args))
3632    logging.debug('Executing javascript: %s', js)
3633    return self.ExecuteJavascript(js, tab_index, windex)
3634
3635  def HeapProfilerDump(self, process_type, reason, tab_index=0, windex=0):
3636    """Dumps a heap profile.  It works only on Linux and ChromeOS.
3637
3638    We need an environment variable "HEAPPROFILE" set to a directory and a
3639    filename prefix, for example, "/tmp/prof".  In a case of this example,
3640    heap profiles will be dumped into "/tmp/prof.(pid).0002.heap",
3641    "/tmp/prof.(pid).0003.heap", and so on.  Nothing happens when this
3642    function is called without the env.
3643
3644    Args:
3645      process_type: A string which is one of 'browser' or 'renderer'.
3646      reason: A string which describes the reason for dumping a heap profile.
3647              The reason will be included in the logged message.
3648              Examples:
3649                'To check memory leaking'
3650                'For PyAuto tests'
3651      tab_index: tab index to work on if 'process_type' == 'renderer'.
3652          Defaults to 0 (first tab).
3653      windex: window index to work on if 'process_type' == 'renderer'.
3654          Defaults to 0 (first window).
3655
3656    Raises:
3657      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3658    """
3659    assert process_type in ('browser', 'renderer')
3660    if self.IsLinux():  # IsLinux() also implies IsChromeOS().
3661      cmd_dict = {
3662        'command': 'HeapProfilerDump',
3663        'process_type': process_type,
3664        'reason': reason,
3665        'windex': windex,
3666        'tab_index': tab_index,
3667      }
3668      self._GetResultFromJSONRequest(cmd_dict)
3669    else:
3670      logging.warn('Heap-profiling is not supported in this OS.')
3671
3672  def GetNTPThumbnails(self):
3673    """Return a list of info about the sites in the NTP most visited section.
3674    SAMPLE:
3675      [{ u'title': u'Google',
3676         u'url': u'http://www.google.com'},
3677       {
3678         u'title': u'Yahoo',
3679         u'url': u'http://www.yahoo.com'}]
3680    """
3681    return self._GetNTPInfo()['most_visited']
3682
3683  def GetNTPThumbnailIndex(self, thumbnail):
3684    """Returns the index of the given NTP thumbnail, or -1 if it is not shown.
3685
3686    Args:
3687      thumbnail: a thumbnail dict received from |GetNTPThumbnails|
3688    """
3689    thumbnails = self.GetNTPThumbnails()
3690    for i in range(len(thumbnails)):
3691      if thumbnails[i]['url'] == thumbnail['url']:
3692        return i
3693    return -1
3694
3695  def RemoveNTPThumbnail(self, thumbnail):
3696    """Removes the NTP thumbnail and returns true on success.
3697
3698    Args:
3699      thumbnail: a thumbnail dict received from |GetNTPThumbnails|
3700    """
3701    self._CheckNTPThumbnailShown(thumbnail)
3702    cmd_dict = {
3703      'command': 'RemoveNTPMostVisitedThumbnail',
3704      'url': thumbnail['url']
3705    }
3706    self._GetResultFromJSONRequest(cmd_dict)
3707
3708  def RestoreAllNTPThumbnails(self):
3709    """Restores all the removed NTP thumbnails.
3710    Note:
3711      the default thumbnails may come back into the Most Visited sites
3712      section after doing this
3713    """
3714    cmd_dict = {
3715      'command': 'RestoreAllNTPMostVisitedThumbnails'
3716    }
3717    self._GetResultFromJSONRequest(cmd_dict)
3718
3719  def GetNTPDefaultSites(self):
3720    """Returns a list of URLs for all the default NTP sites, regardless of
3721    whether they are showing or not.
3722
3723    These sites are the ones present in the NTP on a fresh install of Chrome.
3724    """
3725    return self._GetNTPInfo()['default_sites']
3726
3727  def RemoveNTPDefaultThumbnails(self):
3728    """Removes all thumbnails for default NTP sites, regardless of whether they
3729    are showing or not."""
3730    cmd_dict = { 'command': 'RemoveNTPMostVisitedThumbnail' }
3731    for site in self.GetNTPDefaultSites():
3732      cmd_dict['url'] = site
3733      self._GetResultFromJSONRequest(cmd_dict)
3734
3735  def GetNTPRecentlyClosed(self):
3736    """Return a list of info about the items in the NTP recently closed section.
3737    SAMPLE:
3738      [{
3739         u'type': u'tab',
3740         u'url': u'http://www.bing.com',
3741         u'title': u'Bing',
3742         u'timestamp': 2139082.03912,  # Seconds since epoch (Jan 1, 1970)
3743         u'direction': u'ltr'},
3744       {
3745         u'type': u'window',
3746         u'timestamp': 2130821.90812,
3747         u'tabs': [
3748         {
3749           u'type': u'tab',
3750           u'url': u'http://www.cnn.com',
3751           u'title': u'CNN',
3752           u'timestamp': 2129082.12098,
3753           u'direction': u'ltr'}]},
3754       {
3755         u'type': u'tab',
3756         u'url': u'http://www.altavista.com',
3757         u'title': u'Altavista',
3758         u'timestamp': 21390820.12903,
3759         u'direction': u'rtl'}]
3760    """
3761    return self._GetNTPInfo()['recently_closed']
3762
3763  def GetNTPApps(self):
3764    """Retrieves information about the apps listed on the NTP.
3765
3766    In the sample data below, the "launch_type" will be one of the following
3767    strings: "pinned", "regular", "fullscreen", "window", or "unknown".
3768
3769    SAMPLE:
3770    [
3771      {
3772        u'app_launch_index': 2,
3773        u'description': u'Web Store',
3774        u'icon_big': u'chrome://theme/IDR_APP_DEFAULT_ICON',
3775        u'icon_small': u'chrome://favicon/https://chrome.google.com/webstore',
3776        u'id': u'ahfgeienlihckogmohjhadlkjgocpleb',
3777        u'is_component_extension': True,
3778        u'is_disabled': False,
3779        u'launch_container': 2,
3780        u'launch_type': u'regular',
3781        u'launch_url': u'https://chrome.google.com/webstore',
3782        u'name': u'Chrome Web Store',
3783        u'options_url': u'',
3784      },
3785      {
3786        u'app_launch_index': 1,
3787        u'description': u'A countdown app',
3788        u'icon_big': (u'chrome-extension://aeabikdlfbfeihglecobdkdflahfgcpd/'
3789                      u'countdown128.png'),
3790        u'icon_small': (u'chrome://favicon/chrome-extension://'
3791                        u'aeabikdlfbfeihglecobdkdflahfgcpd/'
3792                        u'launchLocalPath.html'),
3793        u'id': u'aeabikdlfbfeihglecobdkdflahfgcpd',
3794        u'is_component_extension': False,
3795        u'is_disabled': False,
3796        u'launch_container': 2,
3797        u'launch_type': u'regular',
3798        u'launch_url': (u'chrome-extension://aeabikdlfbfeihglecobdkdflahfgcpd/'
3799                        u'launchLocalPath.html'),
3800        u'name': u'Countdown',
3801        u'options_url': u'',
3802      }
3803    ]
3804
3805    Returns:
3806      A list of dictionaries in which each dictionary contains the information
3807      for a single app that appears in the "Apps" section of the NTP.
3808    """
3809    return self._GetNTPInfo()['apps']
3810
3811  def _GetNTPInfo(self):
3812    """Get info about the New Tab Page (NTP).
3813
3814    This does not retrieve the actual info displayed in a particular NTP; it
3815    retrieves the current state of internal data that would be used to display
3816    an NTP.  This includes info about the apps, the most visited sites,
3817    the recently closed tabs and windows, and the default NTP sites.
3818
3819    SAMPLE:
3820    {
3821      u'apps': [ ... ],
3822      u'most_visited': [ ... ],
3823      u'recently_closed': [ ... ],
3824      u'default_sites': [ ... ]
3825    }
3826
3827    Returns:
3828      A dictionary containing all the NTP info. See details about the different
3829      sections in their respective methods: GetNTPApps(), GetNTPThumbnails(),
3830      GetNTPRecentlyClosed(), and GetNTPDefaultSites().
3831
3832    Raises:
3833      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3834    """
3835    cmd_dict = {
3836      'command': 'GetNTPInfo',
3837    }
3838    return self._GetResultFromJSONRequest(cmd_dict)
3839
3840  def _CheckNTPThumbnailShown(self, thumbnail):
3841    if self.GetNTPThumbnailIndex(thumbnail) == -1:
3842      raise NTPThumbnailNotShownError()
3843
3844  def LaunchApp(self, app_id, windex=0):
3845    """Opens the New Tab Page and launches the specified app from it.
3846
3847    This method will not return until after the contents of a new tab for the
3848    launched app have stopped loading.
3849
3850    Args:
3851      app_id: The string ID of the app to launch.
3852      windex: The index of the browser window to work on.  Defaults to 0 (the
3853              first window).
3854
3855    Raises:
3856      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3857    """
3858    self.AppendTab(GURL('chrome://newtab'), windex)  # Also activates this tab.
3859    cmd_dict = {
3860      'command': 'LaunchApp',
3861      'id': app_id,
3862    }
3863    return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
3864
3865  def SetAppLaunchType(self, app_id, launch_type, windex=0):
3866    """Sets the launch type for the specified app.
3867
3868    Args:
3869      app_id: The string ID of the app whose launch type should be set.
3870      launch_type: The string launch type, which must be one of the following:
3871                   'pinned': Launch in a pinned tab.
3872                   'regular': Launch in a regular tab.
3873                   'fullscreen': Launch in a fullscreen tab.
3874                   'window': Launch in a new browser window.
3875      windex: The index of the browser window to work on.  Defaults to 0 (the
3876              first window).
3877
3878    Raises:
3879      pyauto_errors.JSONInterfaceError if the automation call returns an error.
3880    """
3881    self.assertTrue(launch_type in ('pinned', 'regular', 'fullscreen',
3882                                    'window'),
3883                    msg='Unexpected launch type value: "%s"' % launch_type)
3884    cmd_dict = {
3885      'command': 'SetAppLaunchType',
3886      'id': app_id,
3887      'launch_type': launch_type,
3888    }
3889    return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
3890
3891  def GetV8HeapStats(self, tab_index=0, windex=0):
3892    """Returns statistics about the v8 heap in the renderer process for a tab.
3893
3894    Args:
3895      tab_index: The tab index, default is 0.
3896      window_index: The window index, default is 0.
3897
3898    Returns:
3899      A dictionary containing v8 heap statistics. Memory values are in bytes.
3900      Example:
3901        { 'renderer_id': 6223,
3902          'v8_memory_allocated': 21803776,
3903          'v8_memory_used': 10565392 }
3904    """
3905    cmd_dict = {  # Prepare command for the json interface.
3906      'command': 'GetV8HeapStats',
3907      'tab_index': tab_index,
3908    }
3909    return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
3910
3911  def GetFPS(self, tab_index=0, windex=0):
3912    """Returns the current FPS associated with the renderer process for a tab.
3913
3914    FPS is the rendered frames per second.
3915
3916    Args:
3917      tab_index: The tab index, default is 0.
3918      window_index: The window index, default is 0.
3919
3920    Returns:
3921      A dictionary containing FPS info.
3922      Example:
3923        { 'renderer_id': 23567,
3924          'routing_id': 1,
3925          'fps': 29.404298782348633 }
3926    """
3927    cmd_dict = {  # Prepare command for the json interface.
3928      'command': 'GetFPS',
3929      'tab_index': tab_index,
3930    }
3931    return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
3932
3933  def IsFullscreenForBrowser(self, windex=0):
3934    """Returns true if the window is currently fullscreen and was initially
3935    transitioned to fullscreen by a browser (vs tab) mode transition."""
3936    return self._GetResultFromJSONRequest(
3937      { 'command': 'IsFullscreenForBrowser' },
3938      windex=windex).get('result')
3939
3940  def IsFullscreenForTab(self, windex=0):
3941    """Returns true if fullscreen has been caused by a tab."""
3942    return self._GetResultFromJSONRequest(
3943      { 'command': 'IsFullscreenForTab' },
3944      windex=windex).get('result')
3945
3946  def IsMouseLocked(self, windex=0):
3947    """Returns true if the mouse is currently locked."""
3948    return self._GetResultFromJSONRequest(
3949      { 'command': 'IsMouseLocked' },
3950      windex=windex).get('result')
3951
3952  def IsMouseLockPermissionRequested(self, windex=0):
3953    """Returns true if the user is currently prompted to give permision for
3954    mouse lock."""
3955    return self._GetResultFromJSONRequest(
3956      { 'command': 'IsMouseLockPermissionRequested' },
3957      windex=windex).get('result')
3958
3959  def IsFullscreenPermissionRequested(self, windex=0):
3960    """Returns true if the user is currently prompted to give permision for
3961    fullscreen."""
3962    return self._GetResultFromJSONRequest(
3963      { 'command': 'IsFullscreenPermissionRequested' },
3964      windex=windex).get('result')
3965
3966  def IsFullscreenBubbleDisplayed(self, windex=0):
3967    """Returns true if the fullscreen and mouse lock bubble is currently
3968    displayed."""
3969    return self._GetResultFromJSONRequest(
3970      { 'command': 'IsFullscreenBubbleDisplayed' },
3971      windex=windex).get('result')
3972
3973  def IsFullscreenBubbleDisplayingButtons(self, windex=0):
3974    """Returns true if the fullscreen and mouse lock bubble is currently
3975    displayed and presenting buttons."""
3976    return self._GetResultFromJSONRequest(
3977      { 'command': 'IsFullscreenBubbleDisplayingButtons' },
3978      windex=windex).get('result')
3979
3980  def AcceptCurrentFullscreenOrMouseLockRequest(self, windex=0):
3981    """Activate the accept button on the fullscreen and mouse lock bubble."""
3982    return self._GetResultFromJSONRequest(
3983      { 'command': 'AcceptCurrentFullscreenOrMouseLockRequest' },
3984      windex=windex)
3985
3986  def DenyCurrentFullscreenOrMouseLockRequest(self, windex=0):
3987    """Activate the deny button on the fullscreen and mouse lock bubble."""
3988    return self._GetResultFromJSONRequest(
3989      { 'command': 'DenyCurrentFullscreenOrMouseLockRequest' },
3990      windex=windex)
3991
3992  def KillRendererProcess(self, pid):
3993    """Kills the given renderer process.
3994
3995    This will return only after the browser has received notice of the renderer
3996    close.
3997
3998    Args:
3999      pid: the process id of the renderer to kill
4000
4001    Raises:
4002      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4003    """
4004    cmd_dict = {
4005        'command': 'KillRendererProcess',
4006        'pid': pid
4007    }
4008    return self._GetResultFromJSONRequest(cmd_dict)
4009
4010  def NewWebDriver(self, port=0):
4011    """Returns a new remote WebDriver instance.
4012
4013    Args:
4014      port: The port to start WebDriver on; by default the service selects an
4015            open port. It is an error to request a port number and request a
4016            different port later.
4017
4018    Returns:
4019      selenium.webdriver.remote.webdriver.WebDriver instance
4020    """
4021    from chrome_driver_factory import ChromeDriverFactory
4022    global _CHROME_DRIVER_FACTORY
4023    if _CHROME_DRIVER_FACTORY is None:
4024      _CHROME_DRIVER_FACTORY = ChromeDriverFactory(port=port)
4025    self.assertTrue(_CHROME_DRIVER_FACTORY.GetPort() == port or port == 0,
4026                    msg='Requested a WebDriver on a specific port while already'
4027                        ' running on a different port.')
4028    return _CHROME_DRIVER_FACTORY.NewChromeDriver(self)
4029
4030  def CreateNewAutomationProvider(self, channel_id):
4031    """Creates a new automation provider.
4032
4033    The provider will open a named channel in server mode.
4034    Args:
4035      channel_id: the channel_id to open the server channel with
4036    """
4037    cmd_dict = {
4038        'command': 'CreateNewAutomationProvider',
4039        'channel_id': channel_id
4040    }
4041    self._GetResultFromJSONRequest(cmd_dict)
4042
4043  def OpenNewBrowserWindowWithNewProfile(self):
4044    """Creates a new multi-profiles user, and then opens and shows a new
4045    tabbed browser window with the new profile.
4046
4047    This is equivalent to 'Add new user' action with multi-profiles.
4048
4049    To account for crbug.com/108761 on Win XP, this call polls until the
4050    profile count increments by 1.
4051
4052    Raises:
4053      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4054    """
4055    num_profiles = len(self.GetMultiProfileInfo()['profiles'])
4056    cmd_dict = {  # Prepare command for the json interface
4057      'command': 'OpenNewBrowserWindowWithNewProfile'
4058    }
4059    self._GetResultFromJSONRequest(cmd_dict, windex=None)
4060    # TODO(nirnimesh): Remove when crbug.com/108761 is fixed
4061    self.WaitUntil(
4062        lambda: len(self.GetMultiProfileInfo()['profiles']),
4063        expect_retval=(num_profiles + 1))
4064
4065  def OpenProfileWindow(self, path, num_loads=1):
4066   """Open browser window for an existing profile.
4067
4068   This is equivalent to picking a profile from the multi-profile menu.
4069
4070   Multi-profile should be enabled and the requested profile should already
4071   exist. Creates a new window for the given profile. Use
4072   OpenNewBrowserWindowWithNewProfile() to create a new profile.
4073
4074   Args:
4075     path: profile path of the profile to be opened.
4076     num_loads: the number of loads to wait for, when a new browser window
4077                is created.  Useful when restoring a window with many tabs.
4078   """
4079   cmd_dict = {  # Prepare command for the json interface
4080    'command': 'OpenProfileWindow',
4081    'path': path,
4082    'num_loads': num_loads,
4083   }
4084   return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4085
4086  def GetMultiProfileInfo(self):
4087    """Fetch info about all multi-profile users.
4088
4089    Returns:
4090      A dictionary.
4091      Sample:
4092      {
4093        'enabled': True,
4094        'profiles': [{'name': 'First user',
4095                      'path': '/tmp/.org.chromium.Chromium.Tyx17X/Default'},
4096                     {'name': 'User 1',
4097                      'path': '/tmp/.org.chromium.Chromium.Tyx17X/profile_1'}],
4098      }
4099
4100      Profiles will be listed in the same order as visible in preferences.
4101
4102    Raises:
4103      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4104    """
4105    cmd_dict = {  # Prepare command for the json interface
4106      'command': 'GetMultiProfileInfo'
4107    }
4108    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4109
4110  def RefreshPolicies(self):
4111    """Refreshes all the available policy providers.
4112
4113    Each policy provider will reload its policy source and push the updated
4114    policies. This call waits for the new policies to be applied; any policies
4115    installed before this call is issued are guaranteed to be ready after it
4116    returns.
4117    """
4118    # TODO(craigdh): Determine the root cause of RefreshPolicies' flakiness.
4119    #                See crosbug.com/30221
4120    timeout = PyUITest.ActionTimeoutChanger(self, 3 * 60 * 1000)
4121    cmd_dict = { 'command': 'RefreshPolicies' }
4122    self._GetResultFromJSONRequest(cmd_dict, windex=None)
4123
4124  def SubmitForm(self, form_id, tab_index=0, windex=0, frame_xpath=''):
4125    """Submits the given form ID, and returns after it has been submitted.
4126
4127    Args:
4128      form_id: the id attribute of the form to submit.
4129
4130    Returns: true on success.
4131    """
4132    js = """
4133        document.getElementById("%s").submit();
4134        window.addEventListener("unload", function() {
4135          window.domAutomationController.send("done");
4136        });
4137    """ % form_id
4138    if self.ExecuteJavascript(js, tab_index, windex, frame_xpath) != 'done':
4139      return False
4140    # Wait until the form is submitted and the page completes loading.
4141    return self.WaitUntil(
4142        lambda: self.GetDOMValue('document.readyState',
4143                                 tab_index, windex, frame_xpath),
4144        expect_retval='complete')
4145
4146  def SimulateAsanMemoryBug(self):
4147    """Simulates a memory bug for Address Sanitizer to catch.
4148
4149    Address Sanitizer (if it was built it) will catch the bug and abort
4150    the process.
4151    This method returns immediately before it actually causes a crash.
4152    """
4153    cmd_dict = { 'command': 'SimulateAsanMemoryBug' }
4154    self._GetResultFromJSONRequest(cmd_dict, windex=None)
4155
4156  ## ChromeOS section
4157
4158  def GetLoginInfo(self):
4159    """Returns information about login and screen locker state.
4160
4161    This includes things like whether a user is logged in, the username
4162    of the logged in user, and whether the screen is locked.
4163
4164    Returns:
4165      A dictionary.
4166      Sample:
4167      { u'is_guest': False,
4168        u'is_owner': True,
4169        u'email': u'example@gmail.com',
4170        u'user_image': 2,  # non-negative int, 'profile', 'file'
4171        u'is_screen_locked': False,
4172        u'login_ui_type': 'nativeui', # or 'webui'
4173        u'is_logged_in': True}
4174
4175    Raises:
4176      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4177    """
4178    cmd_dict = { 'command': 'GetLoginInfo' }
4179    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4180
4181  def WaitForSessionManagerRestart(self, function):
4182    """Call a function and wait for the ChromeOS session_manager to restart.
4183
4184    Args:
4185      function: The function to call.
4186    """
4187    assert callable(function)
4188    pgrep_process = subprocess.Popen(['pgrep', 'session_manager'],
4189                                     stdout=subprocess.PIPE)
4190    old_pid = pgrep_process.communicate()[0].strip()
4191    function()
4192    return self.WaitUntil(lambda: self._IsSessionManagerReady(old_pid))
4193
4194  def _WaitForInodeChange(self, path, function):
4195    """Call a function and wait for the specified file path to change.
4196
4197    Args:
4198      path: The file path to check for changes.
4199      function: The function to call.
4200    """
4201    assert callable(function)
4202    old_inode = os.stat(path).st_ino
4203    function()
4204    return self.WaitUntil(lambda: self._IsInodeNew(path, old_inode))
4205
4206  def ShowCreateAccountUI(self):
4207    """Go to the account creation page.
4208
4209    This is the same as clicking the "Create Account" link on the
4210    ChromeOS login screen. Does not actually create a new account.
4211    Should be displaying the login screen to work.
4212
4213    Raises:
4214      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4215    """
4216    cmd_dict = { 'command': 'ShowCreateAccountUI' }
4217    # See note below under LoginAsGuest(). ShowCreateAccountUI() logs
4218    # the user in as guest in order to access the account creation page.
4219    assert self._WaitForInodeChange(
4220        self._named_channel_id,
4221        lambda: self._GetResultFromJSONRequest(cmd_dict, windex=None)), \
4222        'Chrome did not reopen the testing channel after login as guest.'
4223    self.SetUp()
4224
4225  def SkipToLogin(self, skip_image_selection=True):
4226    """Skips OOBE to the login screen.
4227
4228    Assumes that we're at the beginning of OOBE.
4229
4230    Args:
4231      skip_image_selection: Boolean indicating whether the user image selection
4232                            screen should also be skipped.
4233
4234    Raises:
4235      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4236    """
4237    cmd_dict = { 'command': 'SkipToLogin',
4238                 'skip_image_selection': skip_image_selection }
4239    result = self._GetResultFromJSONRequest(cmd_dict, windex=None)
4240    assert result['next_screen'] == 'login', 'Unexpected wizard transition'
4241
4242  def GetOOBEScreenInfo(self):
4243    """Queries info about the current OOBE screen.
4244
4245    Returns:
4246      A dictionary with the following keys:
4247
4248      'screen_name': The title of the current OOBE screen as a string.
4249
4250    Raises:
4251      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4252    """
4253    cmd_dict = { 'command': 'GetOOBEScreenInfo' }
4254    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4255
4256  def AcceptOOBENetworkScreen(self):
4257    """Accepts OOBE network screen and advances to the next one.
4258
4259    Assumes that we're already at the OOBE network screen.
4260
4261    Returns:
4262      A dictionary with the following keys:
4263
4264      'next_screen': The title of the next OOBE screen as a string.
4265
4266    Raises:
4267      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4268    """
4269    cmd_dict = { 'command': 'AcceptOOBENetworkScreen' }
4270    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4271
4272  def AcceptOOBEEula(self, accepted, usage_stats_reporting=False):
4273    """Accepts OOBE EULA and advances to the next screen.
4274
4275    Assumes that we're already at the OOBE EULA screen.
4276
4277    Args:
4278      accepted: Boolean indicating whether the EULA should be accepted.
4279      usage_stats_reporting: Boolean indicating whether UMA should be enabled.
4280
4281    Returns:
4282      A dictionary with the following keys:
4283
4284      'next_screen': The title of the next OOBE screen as a string.
4285
4286    Raises:
4287      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4288    """
4289    cmd_dict = { 'command': 'AcceptOOBEEula',
4290                 'accepted': accepted,
4291                 'usage_stats_reporting': usage_stats_reporting }
4292    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4293
4294  def CancelOOBEUpdate(self):
4295    """Skips update on OOBE and advances to the next screen.
4296
4297    Returns:
4298      A dictionary with the following keys:
4299
4300      'next_screen': The title of the next OOBE screen as a string.
4301
4302    Raises:
4303      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4304    """
4305    cmd_dict = { 'command': 'CancelOOBEUpdate' }
4306    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4307
4308  def PickUserImage(self, image):
4309    """Chooses image for the newly created user.
4310
4311    Should be called immediately after login.
4312
4313    Args:
4314      image_type: type of user image to choose. Possible values:
4315        - "profile": Google profile image
4316        - non-negative int: one of the default images
4317
4318    Returns:
4319      A dictionary with the following keys:
4320
4321      'next_screen': The title of the next OOBE screen as a string.
4322
4323    Raises:
4324      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4325    """
4326    cmd_dict = { 'command': 'PickUserImage',
4327                 'image': image }
4328    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4329
4330  def LoginAsGuest(self):
4331    """Login to chromeos as a guest user.
4332
4333    Waits until logged in.
4334    Should be displaying the login screen to work.
4335
4336    Raises:
4337      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4338    """
4339    cmd_dict = { 'command': 'LoginAsGuest' }
4340    # Currently, logging in as guest causes session_manager to
4341    # restart Chrome, which will close the testing channel.
4342    # We need to call SetUp() again to reconnect to the new channel.
4343    assert self._WaitForInodeChange(
4344        self._named_channel_id,
4345        lambda: self._GetResultFromJSONRequest(cmd_dict, windex=None)), \
4346        'Chrome did not reopen the testing channel after login as guest.'
4347    self.SetUp()
4348
4349  def Login(self, username, password, timeout=120 * 1000):
4350    """Login to chromeos.
4351
4352    Waits until logged in and browser is ready.
4353    Should be displaying the login screen to work.
4354
4355    Note that in case of webui auth-extension-based login, gaia auth errors
4356    will not be noticed here, because the browser has no knowledge of it. In
4357    this case the GetNextEvent automation command will always time out.
4358
4359    Args:
4360      username: the username to log in as.
4361      password: the user's password.
4362      timeout: timeout in ms; defaults to two minutes.
4363
4364    Returns:
4365      An error string if an error occured.
4366      None otherwise.
4367
4368    Raises:
4369      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4370    """
4371    self._GetResultFromJSONRequest({'command': 'AddLoginEventObserver'},
4372                                   windex=None)
4373    cmd_dict = {
4374        'command': 'SubmitLoginForm',
4375        'username': username,
4376        'password': password,
4377    }
4378    self._GetResultFromJSONRequest(cmd_dict, windex=None)
4379    self.AddDomEventObserver('loginfail', automation_id=4444)
4380    try:
4381      if self.GetNextEvent(timeout=timeout).get('name') == 'loginfail':
4382        raise JSONInterfaceError('Login denied by auth server.')
4383    except JSONInterfaceError as e:
4384      raise JSONInterfaceError('Login failed. Perhaps Chrome crashed, '
4385                               'failed to start, or the login flow is '
4386                               'broken? Error message: %s' % str(e))
4387
4388  def Logout(self):
4389    """Log out from ChromeOS and wait for session_manager to come up.
4390
4391    This is equivalent to pressing the 'Sign out' button from the
4392    aura shell tray when logged in.
4393
4394    Should be logged in to work. Re-initializes the automation channel
4395    after logout.
4396    """
4397    clear_profile_orig = self.get_clear_profile()
4398    self.set_clear_profile(False)
4399    assert self.GetLoginInfo()['is_logged_in'], \
4400        'Trying to log out when already logged out.'
4401    def _SignOut():
4402      cmd_dict = { 'command': 'SignOut' }
4403      self._GetResultFromJSONRequest(cmd_dict, windex=None)
4404    assert self.WaitForSessionManagerRestart(_SignOut), \
4405        'Session manager did not restart after logout.'
4406    self.__SetUp()
4407    self.set_clear_profile(clear_profile_orig)
4408
4409  def LockScreen(self):
4410    """Locks the screen on chromeos.
4411
4412    Waits until screen is locked.
4413    Should be logged in and screen should not be locked to work.
4414
4415    Raises:
4416      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4417    """
4418    cmd_dict = { 'command': 'LockScreen' }
4419    self._GetResultFromJSONRequest(cmd_dict, windex=None)
4420
4421  def UnlockScreen(self, password):
4422    """Unlocks the screen on chromeos, authenticating the user's password first.
4423
4424    Waits until screen is unlocked.
4425    Screen locker should be active for this to work.
4426
4427    Returns:
4428      An error string if an error occured.
4429      None otherwise.
4430
4431    Raises:
4432      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4433    """
4434    cmd_dict = {
4435        'command': 'UnlockScreen',
4436        'password': password,
4437    }
4438    result = self._GetResultFromJSONRequest(cmd_dict, windex=None)
4439    return result.get('error_string')
4440
4441  def SignoutInScreenLocker(self):
4442    """Signs out of chromeos using the screen locker's "Sign out" feature.
4443
4444    Effectively the same as clicking the "Sign out" link on the screen locker.
4445    Screen should be locked for this to work.
4446
4447    Raises:
4448      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4449    """
4450    cmd_dict = { 'command': 'SignoutInScreenLocker' }
4451    assert self.WaitForSessionManagerRestart(
4452        lambda: self._GetResultFromJSONRequest(cmd_dict, windex=None)), \
4453        'Session manager did not restart after logout.'
4454    self.__SetUp()
4455
4456  def GetBatteryInfo(self):
4457    """Get details about battery state.
4458
4459    Returns:
4460      A dictionary with the following keys:
4461
4462      'battery_is_present': bool
4463      'line_power_on': bool
4464      if 'battery_is_present':
4465        'battery_percentage': float (0 ~ 100)
4466        'battery_fully_charged': bool
4467        if 'line_power_on':
4468          'battery_time_to_full': int (seconds)
4469        else:
4470          'battery_time_to_empty': int (seconds)
4471
4472      If it is still calculating the time left, 'battery_time_to_full'
4473      and 'battery_time_to_empty' will be absent.
4474
4475      Use 'battery_fully_charged' instead of 'battery_percentage'
4476      or 'battery_time_to_full' to determine whether the battery
4477      is fully charged, since the percentage is only approximate.
4478
4479      Sample:
4480        { u'battery_is_present': True,
4481          u'line_power_on': False,
4482          u'battery_time_to_empty': 29617,
4483          u'battery_percentage': 100.0,
4484          u'battery_fully_charged': False }
4485
4486    Raises:
4487      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4488    """
4489    cmd_dict = { 'command': 'GetBatteryInfo' }
4490    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4491
4492  def GetPanelInfo(self):
4493    """Get details about open ChromeOS panels.
4494
4495    A panel is actually a type of browser window, so all of
4496    this information is also available using GetBrowserInfo().
4497
4498    Returns:
4499      A dictionary.
4500      Sample:
4501      [{ 'incognito': False,
4502         'renderer_pid': 4820,
4503         'title': u'Downloads',
4504         'url': u'chrome://active-downloads/'}]
4505
4506    Raises:
4507      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4508    """
4509    panels = []
4510    for browser in self.GetBrowserInfo()['windows']:
4511      if browser['type'] != 'panel':
4512        continue
4513
4514      panel = {}
4515      panels.append(panel)
4516      tab = browser['tabs'][0]
4517      panel['incognito'] = browser['incognito']
4518      panel['renderer_pid'] = tab['renderer_pid']
4519      panel['title'] = self.GetActiveTabTitle(browser['index'])
4520      panel['url'] = tab['url']
4521
4522    return panels
4523
4524  def RestoreOnline(self):
4525    """Returns the device from offline mode if GoOffline was used."""
4526
4527    assert PyUITest.IsChromeOS()
4528
4529    # Restores etherent connection
4530    stdout, stderr = self.RunSuperuserActionOnChromeOS('TeardownBackchannel')
4531
4532    if hasattr(self, 'bc_cellular_enabled') and self.bc_cellular_enabled:
4533      self.ToggleNetworkDevice('cellular', True)
4534    if hasattr(self, 'bc_wifi_enabled') and self.bc_wifi_enabled:
4535      self.ToggleNetworkDevice('wifi', True)
4536
4537    assert 'RuntimeError' not in stderr, stderr
4538
4539  def GoOffline(self):
4540    """Puts device in offline mode.
4541
4542    The device is put into offline mode by disabling all network interfaces
4543    but keeping the the wired ethernet interface up and faking shill/flimflam
4544    into thinking there is no ethernet interface by renaming the interface.
4545    This is so we can keep ssh connections over the wired connection alive.
4546    """
4547    assert PyUITest.IsChromeOS()
4548    net_info = self.GetNetworkInfo()
4549    self.bc_wifi_enabled = net_info.get('wifi_enabled')
4550    self.bc_cellular_enabled = net_info.get('cellular_enabled')
4551
4552    if self.bc_cellular_enabled:
4553      self.ToggleNetworkDevice('cellular', False)
4554    if self.bc_wifi_enabled:
4555      self.ToggleNetworkDevice('wifi', False)
4556
4557    stdout, stderr = self.RunSuperuserActionOnChromeOS('SetupBackchannel')
4558    assert 'RuntimeError' not in stderr, stderr
4559
4560  def GetNetworkInfo(self):
4561    """Get details about ethernet, wifi, and cellular networks on chromeos.
4562
4563    Returns:
4564      A dictionary.
4565      Sample:
4566      { u'cellular_available': True,
4567        u'cellular_enabled': False,
4568        u'connected_ethernet': u'/service/ethernet_abcd',
4569        u'connected_wifi': u'/service/wifi_abcd_1234_managed_none',
4570        u'ethernet_available': True,
4571        u'ethernet_enabled': True,
4572        u'ethernet_networks':
4573            { u'/service/ethernet_abcd':
4574                { u'device_path': u'/device/abcdeth',
4575                  u'ip_address': u'11.22.33.44',
4576                  u'name': u'',
4577                  u'service_path':
4578                  u'/profile/default/ethernet_abcd',
4579                  u'status': u'Connected'}
4580              u'network_type': pyautolib.TYPE_ETHERNET },
4581        u'ip_address': u'11.22.33.44',
4582        u'remembered_wifi':
4583            { u'/service/wifi_abcd_1234_managed_none':
4584                { u'device_path': u'',
4585                  u'encrypted': False,
4586                  u'encryption': u'',
4587                  u'ip_address': '',
4588                  u'name': u'WifiNetworkName1',
4589                  u'status': u'Unknown',
4590                  u'strength': 0},
4591              u'network_type': pyautolib.TYPE_WIFI
4592            },
4593        u'wifi_available': True,
4594        u'wifi_enabled': True,
4595        u'wifi_networks':
4596            { u'/service/wifi_abcd_1234_managed_none':
4597                { u'device_path': u'/device/abcdwifi',
4598                  u'encrypted': False,
4599                  u'encryption': u'',
4600                  u'ip_address': u'123.123.123.123',
4601                  u'name': u'WifiNetworkName1',
4602                  u'status': u'Connected',
4603                  u'strength': 76},
4604              u'/service/wifi_abcd_1234_managed_802_1x':
4605                  { u'encrypted': True,
4606                    u'encryption': u'8021X',
4607                    u'ip_address': u'',
4608                    u'name': u'WifiNetworkName2',
4609                    u'status': u'Idle',
4610                    u'strength': 79}
4611              u'network_type': pyautolib.TYPE_WIFI }}
4612
4613
4614    Raises:
4615      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4616    """
4617    cmd_dict = { 'command': 'GetNetworkInfo' }
4618    network_info = self._GetResultFromJSONRequest(cmd_dict, windex=None)
4619
4620    # Remembered networks do not have /service/ prepended to the service path
4621    # even though wifi_networks does.  We want this prepended to allow for
4622    # consistency and easy string comparison with wifi_networks.
4623    remembered_wifi = {}
4624    network_info['remembered_wifi'] = dict([('/service/' + k, v) for k, v in
4625      network_info['remembered_wifi'].iteritems()])
4626
4627    return network_info
4628
4629  def GetConnectedWifi(self):
4630    """Returns the SSID of the currently connected wifi network.
4631
4632    Returns:
4633      The SSID of the connected network or None if we're not connected.
4634    """
4635    service_list = self.GetNetworkInfo()
4636    connected_service_path = service_list.get('connected_wifi')
4637    if 'wifi_networks' in service_list and \
4638       connected_service_path in service_list['wifi_networks']:
4639       return service_list['wifi_networks'][connected_service_path]['name']
4640
4641  def GetServicePath(self, ssid, encryption=None, timeout=30):
4642    """Waits until the SSID is observed and returns its service path.
4643
4644    Args:
4645      ssid: String defining the SSID we are searching for.
4646      encryption: Encryption type of the network; either None to return the
4647                  first instance of network that matches the ssid, '' for
4648                  an empty network, 'PSK', 'WEP' or '8021X'.
4649      timeout: Duration to wait for ssid to appear.
4650
4651    Returns:
4652      The service path or None if SSID does not exist after timeout period.
4653    """
4654    def _GetServicePath():
4655      service_list = self.GetNetworkInfo().get('wifi_networks', [])
4656      for service_path, service_obj in service_list.iteritems():
4657        if not (isinstance(service_obj, dict) and
4658                'encryption' in service_obj and
4659                'name' in service_obj):
4660          continue
4661
4662        service_encr = 'PSK' if service_obj['encryption'] in ['WPA', 'RSN']\
4663                       else service_obj['encryption']
4664
4665        if service_obj['name'] == ssid and \
4666           (encryption == None or service_encr == encryption):
4667          return service_path
4668      self.NetworkScan()
4669      return None
4670
4671    service_path = self.WaitUntil(_GetServicePath, timeout=timeout,
4672                                  retry_sleep=1, return_retval=True)
4673    return service_path or None
4674
4675  def NetworkScan(self):
4676    """Causes ChromeOS to scan for available wifi networks.
4677
4678    Blocks until scanning is complete.
4679
4680    Returns:
4681      The new list of networks obtained from GetNetworkInfo().
4682
4683    Raises:
4684      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4685    """
4686    cmd_dict = { 'command': 'NetworkScan' }
4687    self._GetResultFromJSONRequest(cmd_dict, windex=None)
4688    return self.GetNetworkInfo()
4689
4690  def ToggleNetworkDevice(self, device, enable):
4691    """Enable or disable a network device on ChromeOS.
4692
4693    Valid device names are ethernet, wifi, cellular.
4694
4695    Raises:
4696      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4697    """
4698    cmd_dict = {
4699        'command': 'ToggleNetworkDevice',
4700        'device': device,
4701        'enable': enable,
4702    }
4703    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4704
4705  PROXY_TYPE_DIRECT = 1
4706  PROXY_TYPE_MANUAL = 2
4707  PROXY_TYPE_PAC = 3
4708
4709  def WaitUntilWifiNetworkAvailable(self, ssid, timeout=60, is_hidden=False):
4710    """Waits until the given network is available.
4711
4712    Routers that are just turned on may take up to 1 minute upon turning them
4713    on to broadcast their SSID.
4714
4715    Args:
4716      ssid: SSID of the service we want to connect to.
4717      timeout: timeout (in seconds)
4718
4719    Raises:
4720      Exception if timeout duration has been hit before wifi router is seen.
4721
4722    Returns:
4723      True, when the wifi network is seen within the timout period.
4724      False, otherwise.
4725    """
4726    def _GotWifiNetwork():
4727      # Returns non-empty array if desired SSID is available.
4728      try:
4729        return [wifi for wifi in
4730                self.NetworkScan().get('wifi_networks', {}).values()
4731                if wifi.get('name') == ssid]
4732      except pyauto_errors.JSONInterfaceError:
4733        # Temporary fix until crosbug.com/14174 is fixed.
4734        # NetworkScan is only used in updating the list of networks so errors
4735        # thrown by it are not critical to the results of wifi tests that use
4736        # this method.
4737        return False
4738
4739    # The hidden AP's will always be on, thus we will assume it is ready to
4740    # connect to.
4741    if is_hidden:
4742      return bool(_GotWifiNetwork())
4743
4744    return self.WaitUntil(_GotWifiNetwork, timeout=timeout, retry_sleep=1)
4745
4746  def GetProxyTypeName(self, proxy_type):
4747    values = { self.PROXY_TYPE_DIRECT: 'Direct Internet connection',
4748               self.PROXY_TYPE_MANUAL: 'Manual proxy configuration',
4749               self.PROXY_TYPE_PAC: 'Automatic proxy configuration' }
4750    return values[proxy_type]
4751
4752  def GetProxySettingsOnChromeOS(self):
4753    """Get current proxy settings on Chrome OS.
4754
4755    Returns:
4756      A dictionary. See SetProxySetting() below
4757      for the full list of possible dictionary keys.
4758
4759      Samples:
4760      { u'ignorelist': [],
4761        u'single': False,
4762        u'type': 1}
4763
4764      { u'ignorelist': [u'www.example.com', u'www.example2.com'],
4765        u'single': True,
4766        u'singlehttp': u'24.27.78.152',
4767        u'singlehttpport': 1728,
4768        u'type': 2}
4769
4770      { u'ignorelist': [],
4771        u'pacurl': u'http://example.com/config.pac',
4772        u'single': False,
4773        u'type': 3}
4774
4775    Raises:
4776      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4777    """
4778    cmd_dict = { 'command': 'GetProxySettings' }
4779    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4780
4781  def _FindNamedNetwork(self, network_dict, name):
4782    """Finds a network by name.
4783
4784    Args:
4785      network_dict: network settings as returned by GetNetworkInfo.
4786      name: name of network we want to set proxy settings on.
4787
4788    Returns:
4789      A dictionary with service_path and network_type of the
4790      named network, when given a dictionary with all system
4791      network information as returned by GetNetworkInfo.
4792
4793      See GetNetworkInfo for a description of the input dictionary.
4794
4795      Samples:
4796      { u'network_type': 'wifi_networks',
4797        u'service_path': '/service/700'}
4798    """
4799    for (key, value) in network_dict.iteritems():
4800      if isinstance(value, dict):
4801        if 'name' in value:
4802          if value['name'] == name:
4803            network_info = {'service_path': key}
4804            return network_info
4805        else:
4806          # if key is a dict but it doesnt have a 'name' entry, go deeper
4807          network_info = self._FindNamedNetwork(value, name)
4808          # if only service path set, set type from networking dictionary
4809          if network_info != None and 'network_type' not in network_info:
4810            network_info['network_type'] = value['network_type']
4811          return network_info
4812    return None
4813
4814  def _GetNamedNetworkInfo(self, network_name):
4815    """Gets settings needed to enable shared proxies for the named network.
4816
4817    Args:
4818      network_name: name of network we want to set proxy settings on.
4819
4820    Returns:
4821      A dictionary with network_type and service_path.
4822      Samples:
4823      { u'network_type': '1',
4824        u'service_path': '/service/0'}
4825
4826    Raises:
4827      AutomationCommandFail if network name isn't found.
4828    """
4829    net = self.GetNetworkInfo()
4830    if network_name == 'NAME_UNKNOWN':
4831      if net.get('ethernet_available'):
4832        service_path = net.get('connected_ethernet')
4833        network_type = str(pyautolib.TYPE_ETHERNET)
4834      elif net.get('wifi_available'):
4835        service_path = net.get('connected_wifi')
4836        network_type = str(pyautolib.TYPE_WIFI)
4837      elif net.get('cellular_available'):
4838        service_path = net.get('connected_cellular')
4839        network_type = str(pyautolib.TYPE_CELLULAR)
4840      else:
4841        raise AutomationCommandFail('No network available.')
4842    else:
4843      named_network_info = self._FindNamedNetwork(net, network_name)
4844      if named_network_info == None:
4845        raise AutomationCommandFail('%s not found.' % network_name)
4846      service_path = named_network_info['service_path']
4847      network_type = named_network_info['network_type']
4848
4849    if not network_type:
4850      raise AutomationCommandFail('network type not found.')
4851    if not service_path:
4852      raise AutomationCommandFail('service path not found.')
4853    network_info = {'network type': network_type, 'service path': service_path}
4854    return network_info
4855
4856  def SetProxySettingOnChromeOS(self, proxy_dict):
4857    """Public wrapper around _SetProxySettingOnChromeOSCore, performs
4858       state setup and error checking.
4859
4860    Args:
4861      proxy_dict: dictionary of proxy settings, valid entries of which are
4862      what one would supply _SetProxySettingOnChromeOSCore
4863
4864    Raises:
4865      AutomationCommandFail if a necessary dictionary entries aren't found.
4866    """
4867    url_path = proxy_dict.get('url_path')
4868    proxy_url = proxy_dict.get('proxy_url')
4869    port_path = proxy_dict.get('port_path')
4870    proxy_port = proxy_dict.get('proxy_port')
4871
4872    if proxy_url is not None:
4873      if url_path is None:
4874        raise AutomationCommandFail('url_path needed to set proxy_url.')
4875        return
4876      self.SetSharedProxies(True)
4877      self.RefreshInternetDetails()
4878      self._SetProxySettingOnChromeOSCore('type', self.PROXY_TYPE_MANUAL)
4879      self._SetProxySettingOnChromeOSCore(url_path, proxy_url)
4880
4881    if proxy_port is not None:
4882      if port_path is None:
4883        raise AutomationCommandFail('port_path needed to set proxy_port.')
4884        return
4885      self._SetProxySettingOnChromeOSCore(port_path, proxy_port)
4886
4887  def ResetProxySettingsOnChromeOS(self):
4888    """Public wrapper around proxysettings teardown functions."""
4889    self.SetSharedProxies(False)
4890    self.RefreshInternetDetails()
4891    self._SetProxySettingOnChromeOSCore('type', self.PROXY_TYPE_DIRECT)
4892
4893  def _SetProxySettingOnChromeOSCore(self, key, value):
4894    """Set a proxy setting.
4895
4896    Owner must be logged in for these to persist.
4897    If user is not logged in or is logged in as non-owner or guest,
4898    proxy settings do not persist across browser restarts or login/logout.
4899
4900    Args:
4901      key: string describing type of proxy preference.
4902      value: value of proxy preference.
4903
4904    Valid settings are:
4905      'type': int - Type of proxy. Should be one of:
4906                     PROXY_TYPE_DIRECT, PROXY_TYPE_MANUAL, PROXY_TYPE_PAC.
4907      'ignorelist': list - The list of hosts and domains to ignore.
4908
4909      These settings set 'type' to PROXY_TYPE_MANUAL:
4910        'single': boolean - Whether to use the same proxy for all protocols.
4911
4912        These settings set 'single' to True:
4913          'singlehttp': string - If single is true, the proxy address to use.
4914          'singlehttpport': int - If single is true, the proxy port to use.
4915
4916        These settings set 'single' to False:
4917          'httpurl': string - HTTP proxy address.
4918          'httpport': int - HTTP proxy port.
4919          'httpsurl': string - Secure HTTP proxy address.
4920          'httpsport': int - Secure HTTP proxy port.
4921          'ftpurl': string - FTP proxy address.
4922          'ftpport': int - FTP proxy port.
4923          'socks': string - SOCKS host address.
4924          'socksport': int - SOCKS host port.
4925
4926      This setting sets 'type' to PROXY_TYPE_PAC:
4927        'pacurl': string - Autoconfiguration URL.
4928
4929    Examples:
4930      # Sets direct internet connection, no proxy.
4931      self.SetProxySettingOnChromeOS('type', self.PROXY_TYPE_DIRECT)
4932
4933      # Sets manual proxy configuration, same proxy for all protocols.
4934      self.SetProxySettingOnChromeOS('singlehttp', '24.27.78.152')
4935      self.SetProxySettingOnChromeOS('singlehttpport', 1728)
4936      self.SetProxySettingOnChromeOS('ignorelist',
4937                                     ['www.example.com', 'example2.com'])
4938
4939      # Sets automatic proxy configuration with the specified PAC url.
4940      self.SetProxySettingOnChromeOS('pacurl', 'http://example.com/config.pac')
4941
4942      # Sets httpproxy with specified url
4943      self.SetProxySettingOnChromeOS('httpurl', 10.10.10)
4944
4945    Raises:
4946      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4947    """
4948    cmd_dict = {
4949        'command': 'SetProxySettings',
4950        'key': key,
4951        'value': value,
4952    }
4953    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4954
4955  def SetSharedProxies(self, value):
4956    """Allows shared proxies on the named network.
4957
4958    Args:
4959      value: True/False to set and clear respectively.
4960
4961    Raises:
4962      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4963    """
4964    cmd_dict = {
4965        'command': 'SetSharedProxies',
4966        'value': value,
4967    }
4968    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
4969
4970  def RefreshInternetDetails(self, network_name='NAME_UNKNOWN'):
4971    """Updates network information
4972
4973    Args:
4974      network_name: name of the network we want to refresh settings for.
4975
4976    Raises:
4977      pyauto_errors.JSONInterfaceError if the automation call returns an error.
4978    """
4979    network_info = self._GetNamedNetworkInfo(network_name)
4980    cmd_dict = {
4981        'command': 'RefreshInternetDetails',
4982        'service path': network_info.get('service path'),
4983    }
4984    return self._GetResultFromJSONRequest(cmd_dict, None)
4985
4986  def ForgetAllRememberedNetworks(self):
4987    """Forgets all networks that the device has marked as remembered."""
4988    for service in self.GetNetworkInfo()['remembered_wifi']:
4989      self.ForgetWifiNetwork(service)
4990
4991  def ForgetWifiNetwork(self, service_path):
4992    """Forget a remembered network by its service path.
4993
4994    This function is equivalent to clicking the 'Forget Network' button in the
4995    chrome://settings/internet page.  This function does not indicate whether
4996    or not forget succeeded or failed.  It is up to the caller to call
4997    GetNetworkInfo to check the updated remembered_wifi list to verify the
4998    service has been removed.
4999
5000    Args:
5001      service_path: Flimflam path that defines the remembered network.
5002
5003    Raises:
5004      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5005    """
5006    # Usually the service_path is prepended with '/service/', such as when the
5007    # service path is retrieved from GetNetworkInfo.  ForgetWifiNetwork works
5008    # only for service paths where this has already been stripped.
5009    service_path = service_path.split('/service/')[-1]
5010    cmd_dict = {
5011        'command': 'ForgetWifiNetwork',
5012        'service_path': service_path,
5013    }
5014    self._GetResultFromJSONRequest(cmd_dict, windex=None, timeout=50000)
5015
5016  def ConnectToCellularNetwork(self):
5017    """Connects to the available cellular network.
5018
5019    Blocks until connection succeeds or fails.
5020
5021    Returns:
5022      An error string if an error occured.
5023      None otherwise.
5024
5025    Raises:
5026      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5027    """
5028    # Every device should only have one cellular network present, so we can
5029    # scan for it.
5030    cellular_networks = self.NetworkScan().get('cellular_networks', {}).keys()
5031    self.assertTrue(cellular_networks, 'Could not find cellular service.')
5032    service_path = cellular_networks[0]
5033
5034    cmd_dict = {
5035        'command': 'ConnectToCellularNetwork',
5036        'service_path': service_path,
5037    }
5038    result = self._GetResultFromJSONRequest(
5039        cmd_dict, windex=None, timeout=50000)
5040    return result.get('error_string')
5041
5042  def DisconnectFromCellularNetwork(self):
5043    """Disconnect from the connected cellular network.
5044
5045    Blocks until disconnect is complete.
5046
5047    Raises:
5048      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5049    """
5050    cmd_dict = {
5051        'command': 'DisconnectFromCellularNetwork',
5052    }
5053    self._GetResultFromJSONRequest(cmd_dict, windex=None)
5054
5055  def ConnectToWifiNetwork(self, service_path, password='', shared=True):
5056    """Connect to a wifi network by its service path.
5057
5058    Blocks until connection succeeds or fails.
5059
5060    Args:
5061      service_path: Flimflam path that defines the wifi network.
5062      password: Passphrase for connecting to the wifi network.
5063      shared: Boolean value specifying whether the network should be shared.
5064
5065    Returns:
5066      An error string if an error occured.
5067      None otherwise.
5068
5069    Raises:
5070      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5071    """
5072    cmd_dict = {
5073        'command': 'ConnectToWifiNetwork',
5074        'service_path': service_path,
5075        'password': password,
5076        'shared': shared,
5077    }
5078    result = self._GetResultFromJSONRequest(
5079        cmd_dict, windex=None, timeout=50000)
5080    return result.get('error_string')
5081
5082  def ConnectToHiddenWifiNetwork(self, ssid, security, password='',
5083                                 shared=True, save_credentials=False):
5084    """Connect to a wifi network by its service path.
5085
5086    Blocks until connection succeeds or fails.
5087
5088    Args:
5089      ssid: The SSID of the network to connect to.
5090      security: The network's security type. One of: 'SECURITY_NONE',
5091                'SECURITY_WEP', 'SECURITY_WPA', 'SECURITY_RSN', 'SECURITY_8021X'
5092      password: Passphrase for connecting to the wifi network.
5093      shared: Boolean value specifying whether the network should be shared.
5094      save_credentials: Boolean value specifying whether 802.1x credentials are
5095                        saved.
5096
5097    Returns:
5098      An error string if an error occured.
5099      None otherwise.
5100
5101    Raises:
5102      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5103    """
5104    assert security in ('SECURITY_NONE', 'SECURITY_WEP', 'SECURITY_WPA',
5105                        'SECURITY_RSN', 'SECURITY_8021X')
5106    cmd_dict = {
5107        'command': 'ConnectToHiddenWifiNetwork',
5108        'ssid': ssid,
5109        'security': security,
5110        'password': password,
5111        'shared': shared,
5112        'save_credentials': save_credentials,
5113    }
5114    result = self._GetResultFromJSONRequest(
5115        cmd_dict, windex=None, timeout=50000)
5116    return result.get('error_string')
5117
5118  def DisconnectFromWifiNetwork(self):
5119    """Disconnect from the connected wifi network.
5120
5121    Blocks until disconnect is complete.
5122
5123    Raises:
5124      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5125    """
5126    cmd_dict = {
5127        'command': 'DisconnectFromWifiNetwork',
5128    }
5129    self._GetResultFromJSONRequest(cmd_dict, windex=None)
5130
5131  def AddPrivateNetwork(self,
5132                        hostname,
5133                        service_name,
5134                        provider_type,
5135                        username,
5136                        password,
5137                        cert_nss='',
5138                        cert_id='',
5139                        key=''):
5140    """Add and connect to a private network.
5141
5142    Blocks until connection succeeds or fails. This is equivalent to
5143    'Add Private Network' in the network menu UI.
5144
5145    Args:
5146      hostname: Server hostname for the private network.
5147      service_name: Service name that defines the private network. Do not
5148                    add multiple services with the same name.
5149      provider_type: Types are L2TP_IPSEC_PSK and L2TP_IPSEC_USER_CERT.
5150                     Provider type OPEN_VPN is not yet supported.
5151                     Type names returned by GetPrivateNetworkInfo will
5152                     also work.
5153      username: Username for connecting to the virtual network.
5154      password: Passphrase for connecting to the virtual network.
5155      cert_nss: Certificate nss nickname for a L2TP_IPSEC_USER_CERT network.
5156      cert_id: Certificate id for a L2TP_IPSEC_USER_CERT network.
5157      key: Pre-shared key for a L2TP_IPSEC_PSK network.
5158
5159    Returns:
5160      An error string if an error occured.
5161      None otherwise.
5162
5163    Raises:
5164      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5165    """
5166    cmd_dict = {
5167        'command': 'AddPrivateNetwork',
5168        'hostname': hostname,
5169        'service_name': service_name,
5170        'provider_type': provider_type,
5171        'username': username,
5172        'password': password,
5173        'cert_nss': cert_nss,
5174        'cert_id': cert_id,
5175        'key': key,
5176    }
5177    result = self._GetResultFromJSONRequest(
5178        cmd_dict, windex=None, timeout=50000)
5179    return result.get('error_string')
5180
5181  def GetPrivateNetworkInfo(self):
5182    """Get details about private networks on chromeos.
5183
5184    Returns:
5185      A dictionary including information about all remembered virtual networks
5186      as well as the currently connected virtual network, if any.
5187      Sample:
5188      { u'connected': u'/service/vpn_123_45_67_89_test_vpn'}
5189        u'/service/vpn_123_45_67_89_test_vpn':
5190          { u'username': u'vpn_user',
5191            u'name': u'test_vpn',
5192            u'hostname': u'123.45.67.89',
5193            u'key': u'abcde',
5194            u'cert_id': u'',
5195            u'password': u'zyxw123',
5196            u'provider_type': u'L2TP_IPSEC_PSK'},
5197        u'/service/vpn_111_11_11_11_test_vpn2':
5198          { u'username': u'testerman',
5199            u'name': u'test_vpn2',
5200            u'hostname': u'111.11.11.11',
5201            u'key': u'fghijklm',
5202            u'cert_id': u'',
5203            u'password': u'789mnop',
5204            u'provider_type': u'L2TP_IPSEC_PSK'},
5205
5206    Raises:
5207      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5208    """
5209    cmd_dict = { 'command': 'GetPrivateNetworkInfo' }
5210    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5211
5212  def ConnectToPrivateNetwork(self, service_path):
5213    """Connect to a remembered private network by its service path.
5214
5215    Blocks until connection succeeds or fails. The network must have been
5216    previously added with all necessary connection details.
5217
5218    Args:
5219      service_path: Service name that defines the private network.
5220
5221    Returns:
5222      An error string if an error occured.
5223      None otherwise.
5224
5225    Raises:
5226      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5227    """
5228    cmd_dict = {
5229        'command': 'ConnectToPrivateNetwork',
5230        'service_path': service_path,
5231    }
5232    result = self._GetResultFromJSONRequest(
5233        cmd_dict, windex=None, timeout=50000)
5234    return result.get('error_string')
5235
5236  def DisconnectFromPrivateNetwork(self):
5237    """Disconnect from the active private network.
5238
5239    Expects a private network to be active.
5240
5241    Raises:
5242      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5243    """
5244    cmd_dict = {
5245        'command': 'DisconnectFromPrivateNetwork',
5246    }
5247    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5248
5249  def EnableSpokenFeedback(self, enabled):
5250    """Enables or disables spoken feedback accessibility mode.
5251
5252    Args:
5253      enabled: Boolean value indicating the desired state of spoken feedback.
5254
5255    Raises:
5256      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5257    """
5258    cmd_dict = {
5259        'command': 'EnableSpokenFeedback',
5260        'enabled': enabled,
5261    }
5262    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5263
5264  def IsSpokenFeedbackEnabled(self):
5265    """Check whether spoken feedback accessibility mode is enabled.
5266
5267    Returns:
5268      True if spoken feedback is enabled, False otherwise.
5269
5270    Raises:
5271      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5272    """
5273    cmd_dict = { 'command': 'IsSpokenFeedbackEnabled', }
5274    result = self._GetResultFromJSONRequest(cmd_dict, windex=None)
5275    return result.get('spoken_feedback')
5276
5277  def GetTimeInfo(self, windex=0):
5278    """Gets info about the ChromeOS status bar clock.
5279
5280    Set the 24-hour clock by using:
5281      self.SetPrefs('settings.clock.use_24hour_clock', True)
5282
5283    Returns:
5284      a dictionary.
5285      Sample:
5286      {u'display_date': u'Tuesday, July 26, 2011',
5287       u'display_time': u'4:30',
5288       u'timezone': u'America/Los_Angeles'}
5289
5290    Raises:
5291      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5292    """
5293    cmd_dict = { 'command': 'GetTimeInfo' }
5294    if self.GetLoginInfo()['is_logged_in']:
5295      return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
5296    else:
5297      return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5298
5299  def SetTimezone(self, timezone):
5300    """Sets the timezone on ChromeOS. A user must be logged in.
5301
5302    The timezone is the relative path to the timezone file in
5303    /usr/share/zoneinfo. For example, /usr/share/zoneinfo/America/Los_Angeles is
5304    'America/Los_Angeles'. For a list of valid timezones see
5305    'chrome/browser/chromeos/system/timezone_settings.cc'.
5306
5307    This method does not return indication of success or failure.
5308    If the timezone is it falls back to a valid timezone.
5309
5310    Raises:
5311      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5312    """
5313    cmd_dict = {
5314        'command': 'SetTimezone',
5315        'timezone': timezone,
5316    }
5317    self._GetResultFromJSONRequest(cmd_dict, windex=None)
5318
5319  def GetUpdateInfo(self):
5320    """Gets the status of the ChromeOS updater.
5321
5322    Returns:
5323      a dictionary.
5324      Samples:
5325      { u'status': u'idle',
5326        u'release_track': u'beta-channel'}
5327
5328      { u'status': u'downloading',
5329        u'release_track': u'beta-channel',
5330        u'download_progress': 0.1203236708350371,   # 0.0 ~ 1.0
5331        u'new_size': 152033593,                     # size of payload, in bytes
5332        u'last_checked_time': 1302055709}           # seconds since UNIX epoch
5333
5334    Raises:
5335      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5336    """
5337    cmd_dict = { 'command': 'GetUpdateInfo' }
5338    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5339
5340  def UpdateCheck(self):
5341    """Checks for a ChromeOS update. Blocks until finished updating.
5342
5343    Raises:
5344      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5345    """
5346    cmd_dict = { 'command': 'UpdateCheck' }
5347    self._GetResultFromJSONRequest(cmd_dict, windex=None)
5348
5349  def SetReleaseTrack(self, track):
5350    """Sets the release track (channel) of the ChromeOS updater.
5351
5352    Valid values for the track parameter are:
5353      'stable-channel', 'beta-channel', 'dev-channel'
5354
5355    Raises:
5356      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5357    """
5358    assert track in ('stable-channel', 'beta-channel', 'dev-channel'), \
5359        'Attempt to set release track to unknown release track "%s".' % track
5360    cmd_dict = {
5361        'command': 'SetReleaseTrack',
5362        'track': track,
5363    }
5364    self._GetResultFromJSONRequest(cmd_dict, windex=None)
5365
5366  def GetVolumeInfo(self):
5367    """Gets the volume and whether the device is muted.
5368
5369    Returns:
5370      a tuple.
5371      Sample:
5372      (47.763456790123456, False)
5373
5374    Raises:
5375      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5376    """
5377    cmd_dict = { 'command': 'GetVolumeInfo' }
5378    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5379
5380  def SetVolume(self, volume):
5381    """Sets the volume on ChromeOS. Only valid if not muted.
5382
5383    Args:
5384      volume: The desired volume level as a percent from 0 to 100.
5385
5386    Raises:
5387      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5388    """
5389    assert volume >= 0 and volume <= 100
5390    cmd_dict = {
5391        'command': 'SetVolume',
5392        'volume': float(volume),
5393    }
5394    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5395
5396  def SetMute(self, mute):
5397    """Sets whether ChromeOS is muted or not.
5398
5399    Args:
5400      mute: True to mute, False to unmute.
5401
5402    Raises:
5403      pyauto_errors.JSONInterfaceError if the automation call returns an error.
5404    """
5405    cmd_dict = { 'command': 'SetMute' }
5406    cmd_dict = {
5407        'command': 'SetMute',
5408        'mute': mute,
5409    }
5410    return self._GetResultFromJSONRequest(cmd_dict, windex=None)
5411
5412  # HTML Terminal
5413
5414  def OpenCrosh(self):
5415    """Open crosh.
5416
5417    Equivalent to pressing Ctrl-Alt-t.
5418    Opens in the last active (non-incognito) window.
5419
5420    Waits long enough for crosh to load, but does not wait for the crosh
5421    prompt. Use WaitForHtermText() for that.
5422    """
5423    cmd_dict = { 'command': 'OpenCrosh' }
5424    self._GetResultFromJSONRequest(cmd_dict, windex=None)
5425
5426  def WaitForHtermText(self, text, msg=None, tab_index=0, windex=0):
5427    """Waits for the given text in a hterm tab.
5428
5429    Can be used to wait for the crosh> prompt or ssh prompt.
5430
5431    This does not poll. It uses dom mutation observers to wait
5432    for the given text to show up.
5433
5434    Args:
5435      text: the text to wait for. Can be a regex.
5436      msg: the failure message to emit if the text could not be found.
5437      tab_index: the tab for the hterm tab. Default: 0.
5438      windex: the window index for the hterm tab. Default: 0.
5439    """
5440    self.WaitForDomNode(
5441        xpath='//*[contains(text(), "%s")]' % text, frame_xpath='//iframe',
5442        msg=msg, tab_index=tab_index, windex=windex)
5443
5444  def GetHtermRowsText(self, start, end, tab_index=0, windex=0):
5445    """Fetch rows from a html terminal tab.
5446
5447    Works for both crosh and ssh tab.
5448    Uses term_.getRowsText(start, end) javascript call.
5449
5450    Args:
5451      start: start line number (0-based).
5452      end: the end line (one beyond the line of interest).
5453      tab_index: the tab for the hterm tab. Default: 0.
5454      windex: the window index for the hterm tab. Default: 0.
5455    """
5456    return self.ExecuteJavascript(
5457        'domAutomationController.send(term_.getRowsText(%d, %d))' % (
5458            start, end),
5459        tab_index=tab_index, windex=windex)
5460
5461  def SendKeysToHterm(self, text, tab_index=0, windex=0):
5462    """Send keys to a html terminal tab.
5463
5464    Works for both crosh and ssh tab.
5465    Uses term_.onVTKeystroke(str) javascript call.
5466
5467    Args:
5468      text: the text to send.
5469      tab_index: the tab for the hterm tab. Default: 0.
5470      windex: the window index for the hterm tab. Default: 0.
5471    """
5472    return self.ExecuteJavascript(
5473        'term_.onVTKeystroke("%s");'
5474        'domAutomationController.send("done")' % text,
5475        tab_index=tab_index, windex=windex)
5476
5477
5478  def GetMemoryStatsChromeOS(self, duration):
5479    """Identifies and returns different kinds of current memory usage stats.
5480
5481    This function samples values each second for |duration| seconds, then
5482    outputs the min, max, and ending values for each measurement type.
5483
5484    Args:
5485      duration: The number of seconds to sample data before outputting the
5486          minimum, maximum, and ending values for each measurement type.
5487
5488    Returns:
5489      A dictionary containing memory usage information.  Each measurement type
5490      is associated with the min, max, and ending values from among all
5491      sampled values.  Values are specified in KB.
5492      {
5493        'gem_obj': {  # GPU memory usage.
5494          'min': ...,
5495          'max': ...,
5496          'end': ...,
5497        },
5498        'gtt': { ... },  # GPU memory usage (graphics translation table).
5499        'mem_free': { ... },  # CPU free memory.
5500        'mem_available': { ... },  # CPU available memory.
5501        'mem_shared': { ... },  # CPU shared memory.
5502        'mem_cached': { ... },  # CPU cached memory.
5503        'mem_anon': { ... },  # CPU anon memory (active + inactive).
5504        'mem_file': { ... },  # CPU file memory (active + inactive).
5505        'mem_slab': { ... },  # CPU slab memory.
5506        'browser_priv': { ... },  # Chrome browser private memory.
5507        'browser_shared': { ... },  # Chrome browser shared memory.
5508        'gpu_priv': { ... },  # Chrome GPU private memory.
5509        'gpu_shared': { ... },  # Chrome GPU shared memory.
5510        'renderer_priv': { ... },  # Total private memory of all renderers.
5511        'renderer_shared': { ... },  # Total shared memory of all renderers.
5512      }
5513    """
5514    logging.debug('Sampling memory information for %d seconds...' % duration)
5515    stats = {}
5516
5517    for _ in xrange(duration):
5518      # GPU memory.
5519      gem_obj_path = '/sys/kernel/debug/dri/0/i915_gem_objects'
5520      if os.path.exists(gem_obj_path):
5521        p = subprocess.Popen('grep bytes %s' % gem_obj_path,
5522                             stdout=subprocess.PIPE, shell=True)
5523        stdout = p.communicate()[0]
5524
5525        gem_obj = re.search(
5526            '\d+ objects, (\d+) bytes\n', stdout).group(1)
5527        if 'gem_obj' not in stats:
5528          stats['gem_obj'] = []
5529        stats['gem_obj'].append(int(gem_obj) / 1024.0)
5530
5531      gtt_path = '/sys/kernel/debug/dri/0/i915_gem_gtt'
5532      if os.path.exists(gtt_path):
5533        p = subprocess.Popen('grep bytes %s' % gtt_path,
5534                             stdout=subprocess.PIPE, shell=True)
5535        stdout = p.communicate()[0]
5536
5537        gtt = re.search(
5538            'Total [\d]+ objects, ([\d]+) bytes', stdout).group(1)
5539        if 'gtt' not in stats:
5540          stats['gtt'] = []
5541        stats['gtt'].append(int(gtt) / 1024.0)
5542
5543      # CPU memory.
5544      stdout = ''
5545      with open('/proc/meminfo') as f:
5546        stdout = f.read()
5547      mem_free = re.search('MemFree:\s*([\d]+) kB', stdout).group(1)
5548
5549      if 'mem_free' not in stats:
5550        stats['mem_free'] = []
5551      stats['mem_free'].append(int(mem_free))
5552
5553      mem_dirty = re.search('Dirty:\s*([\d]+) kB', stdout).group(1)
5554      mem_active_file = re.search(
5555          'Active\(file\):\s*([\d]+) kB', stdout).group(1)
5556      mem_inactive_file = re.search(
5557          'Inactive\(file\):\s*([\d]+) kB', stdout).group(1)
5558
5559      with open('/proc/sys/vm/min_filelist_kbytes') as f:
5560        mem_min_file = f.read()
5561
5562      # Available memory =
5563      #     MemFree + ActiveFile + InactiveFile - DirtyMem - MinFileMem
5564      if 'mem_available' not in stats:
5565        stats['mem_available'] = []
5566      stats['mem_available'].append(
5567          int(mem_free) + int(mem_active_file) + int(mem_inactive_file) -
5568          int(mem_dirty) - int(mem_min_file))
5569
5570      mem_shared = re.search('Shmem:\s*([\d]+) kB', stdout).group(1)
5571      if 'mem_shared' not in stats:
5572        stats['mem_shared'] = []
5573      stats['mem_shared'].append(int(mem_shared))
5574
5575      mem_cached = re.search('Cached:\s*([\d]+) kB', stdout).group(1)
5576      if 'mem_cached' not in stats:
5577        stats['mem_cached'] = []
5578      stats['mem_cached'].append(int(mem_cached))
5579
5580      mem_anon_active = re.search('Active\(anon\):\s*([\d]+) kB',
5581                                  stdout).group(1)
5582      mem_anon_inactive = re.search('Inactive\(anon\):\s*([\d]+) kB',
5583                                    stdout).group(1)
5584      if 'mem_anon' not in stats:
5585        stats['mem_anon'] = []
5586      stats['mem_anon'].append(int(mem_anon_active) + int(mem_anon_inactive))
5587
5588      mem_file_active = re.search('Active\(file\):\s*([\d]+) kB',
5589                                  stdout).group(1)
5590      mem_file_inactive = re.search('Inactive\(file\):\s*([\d]+) kB',
5591                                    stdout).group(1)
5592      if 'mem_file' not in stats:
5593        stats['mem_file'] = []
5594      stats['mem_file'].append(int(mem_file_active) + int(mem_file_inactive))
5595
5596      mem_slab = re.search('Slab:\s*([\d]+) kB', stdout).group(1)
5597      if 'mem_slab' not in stats:
5598        stats['mem_slab'] = []
5599      stats['mem_slab'].append(int(mem_slab))
5600
5601      # Chrome process memory.
5602      pinfo = self.GetProcessInfo()['browsers'][0]['processes']
5603      total_renderer_priv = 0
5604      total_renderer_shared = 0
5605      for process in pinfo:
5606        mem_priv = process['working_set_mem']['priv']
5607        mem_shared = process['working_set_mem']['shared']
5608        if process['child_process_type'] == 'Browser':
5609          if 'browser_priv' not in stats:
5610            stats['browser_priv'] = []
5611            stats['browser_priv'].append(int(mem_priv))
5612          if 'browser_shared' not in stats:
5613            stats['browser_shared'] = []
5614            stats['browser_shared'].append(int(mem_shared))
5615        elif process['child_process_type'] == 'GPU':
5616          if 'gpu_priv' not in stats:
5617            stats['gpu_priv'] = []
5618            stats['gpu_priv'].append(int(mem_priv))
5619          if 'gpu_shared' not in stats:
5620            stats['gpu_shared'] = []
5621            stats['gpu_shared'].append(int(mem_shared))
5622        elif process['child_process_type'] == 'Tab':
5623          # Sum the memory of all renderer processes.
5624          total_renderer_priv += int(mem_priv)
5625          total_renderer_shared += int(mem_shared)
5626      if 'renderer_priv' not in stats:
5627        stats['renderer_priv'] = []
5628        stats['renderer_priv'].append(int(total_renderer_priv))
5629      if 'renderer_shared' not in stats:
5630        stats['renderer_shared'] = []
5631        stats['renderer_shared'].append(int(total_renderer_shared))
5632
5633      time.sleep(1)
5634
5635    # Compute min, max, and ending values to return.
5636    result = {}
5637    for measurement_type in stats:
5638      values = stats[measurement_type]
5639      result[measurement_type] = {
5640        'min': min(values),
5641        'max': max(values),
5642        'end': values[-1],
5643      }
5644
5645    return result
5646
5647  ## ChromeOS section -- end
5648
5649
5650class ExtraBrowser(PyUITest):
5651  """Launches a new browser with some extra flags.
5652
5653  The new browser is launched with its own fresh profile.
5654  This class does not apply to ChromeOS.
5655  """
5656  def __init__(self, chrome_flags=[], methodName='runTest', **kwargs):
5657    """Accepts extra chrome flags for launching a new browser instance.
5658
5659    Args:
5660      chrome_flags: list of extra flags when launching a new browser.
5661    """
5662    assert not PyUITest.IsChromeOS(), \
5663        'This function cannot be used to launch a new browser in ChromeOS.'
5664    PyUITest.__init__(self, methodName=methodName, **kwargs)
5665    self._chrome_flags = chrome_flags
5666    PyUITest.setUp(self)
5667
5668  def __del__(self):
5669    """Tears down the browser and then calls super class's destructor"""
5670    PyUITest.tearDown(self)
5671    PyUITest.__del__(self)
5672
5673  def ExtraChromeFlags(self):
5674    """Prepares the browser to launch with specified Chrome flags."""
5675    return PyUITest.ExtraChromeFlags(self) + self._chrome_flags
5676
5677
5678class _RemoteProxy():
5679  """Class for PyAuto remote method calls.
5680
5681  Use this class along with RemoteHost.testRemoteHost to establish a PyAuto
5682  connection with another machine and make remote PyAuto calls. The RemoteProxy
5683  mimics a PyAuto object, so all json-style PyAuto calls can be made on it.
5684
5685  The remote host acts as a dumb executor that receives method call requests,
5686  executes them, and sends all of the results back to the RemoteProxy, including
5687  the return value, thrown exceptions, and console output.
5688
5689  The remote host should be running the same version of PyAuto as the proxy.
5690  A mismatch could lead to undefined behavior.
5691
5692  Example usage:
5693    class MyTest(pyauto.PyUITest):
5694      def testRemoteExample(self):
5695        remote = pyauto._RemoteProxy(('127.0.0.1', 7410))
5696        remote.NavigateToURL('http://www.google.com')
5697        title = remote.GetActiveTabTitle()
5698        self.assertEqual(title, 'Google')
5699  """
5700  class RemoteException(Exception):
5701    pass
5702
5703  def __init__(self, host):
5704    self.RemoteConnect(host)
5705
5706  def RemoteConnect(self, host):
5707    begin = time.time()
5708    while time.time() - begin < 50:
5709      self._socket = socket.socket()
5710      if not self._socket.connect_ex(host):
5711        break
5712      time.sleep(0.25)
5713    else:
5714      # Make one last attempt, but raise a socket error on failure.
5715      self._socket = socket.socket()
5716      self._socket.connect(host)
5717
5718  def RemoteDisconnect(self):
5719    if self._socket:
5720      self._socket.shutdown(socket.SHUT_RDWR)
5721      self._socket.close()
5722      self._socket = None
5723
5724  def CreateTarget(self, target):
5725    """Registers the methods and creates a remote instance of a target.
5726
5727    Any RPC calls will then be made on the remote target instance. Note that the
5728    remote instance will be a brand new instance and will have none of the state
5729    of the local instance. The target's class should have a constructor that
5730    takes no arguments.
5731    """
5732    self._Call('CreateTarget', target.__class__)
5733    self._RegisterClassMethods(target)
5734
5735  def _RegisterClassMethods(self, remote_class):
5736    # Make remote-call versions of all remote_class methods.
5737    for method_name, _ in inspect.getmembers(remote_class, inspect.ismethod):
5738      # Ignore private methods and duplicates.
5739      if method_name[0] in string.letters and \
5740        getattr(self, method_name, None) is None:
5741        setattr(self, method_name, functools.partial(self._Call, method_name))
5742
5743  def _Call(self, method_name, *args, **kwargs):
5744    # Send request.
5745    request = pickle.dumps((method_name, args, kwargs))
5746    if self._socket.send(request) != len(request):
5747      raise self.RemoteException('Error sending remote method call request.')
5748
5749    # Receive response.
5750    response = self._socket.recv(4096)
5751    if not response:
5752      raise self.RemoteException('Client disconnected during method call.')
5753    result, stdout, stderr, exception = pickle.loads(response)
5754
5755    # Print any output the client captured, throw any exceptions, and return.
5756    sys.stdout.write(stdout)
5757    sys.stderr.write(stderr)
5758    if exception:
5759      raise self.RemoteException('%s raised by remote client: %s' %
5760                                 (exception[0], exception[1]))
5761    return result
5762
5763
5764class PyUITestSuite(pyautolib.PyUITestSuiteBase, unittest.TestSuite):
5765  """Base TestSuite for PyAuto UI tests."""
5766
5767  def __init__(self, args):
5768    pyautolib.PyUITestSuiteBase.__init__(self, args)
5769
5770    # Figure out path to chromium binaries
5771    browser_dir = os.path.normpath(os.path.dirname(pyautolib.__file__))
5772    logging.debug('Loading pyauto libs from %s', browser_dir)
5773    self.InitializeWithPath(pyautolib.FilePath(browser_dir))
5774    os.environ['PATH'] = browser_dir + os.pathsep + os.environ['PATH']
5775
5776    unittest.TestSuite.__init__(self)
5777    cr_source_root = os.path.normpath(os.path.join(
5778        os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
5779    self.SetCrSourceRoot(pyautolib.FilePath(cr_source_root))
5780
5781    # Start http server, if needed.
5782    global _OPTIONS
5783    if _OPTIONS and not _OPTIONS.no_http_server:
5784      self._StartHTTPServer()
5785    if _OPTIONS and _OPTIONS.remote_host:
5786      self._ConnectToRemoteHosts(_OPTIONS.remote_host.split(','))
5787
5788  def __del__(self):
5789    # python unittest module is setup such that the suite gets deleted before
5790    # the test cases, which is odd because our test cases depend on
5791    # initializtions like exitmanager, autorelease pool provided by the
5792    # suite. Forcibly delete the test cases before the suite.
5793    del self._tests
5794    pyautolib.PyUITestSuiteBase.__del__(self)
5795
5796    global _HTTP_SERVER
5797    if _HTTP_SERVER:
5798      self._StopHTTPServer()
5799
5800    global _CHROME_DRIVER_FACTORY
5801    if _CHROME_DRIVER_FACTORY is not None:
5802      _CHROME_DRIVER_FACTORY.Stop()
5803
5804  def _StartHTTPServer(self):
5805    """Start a local file server hosting data files over http://"""
5806    global _HTTP_SERVER
5807    assert not _HTTP_SERVER, 'HTTP Server already started'
5808    http_data_dir = _OPTIONS.http_data_dir
5809    http_server = pyautolib.TestServer(pyautolib.TestServer.TYPE_HTTP,
5810                                       '127.0.0.1',
5811                                       pyautolib.FilePath(http_data_dir))
5812    assert http_server.Start(), 'Could not start http server'
5813    _HTTP_SERVER = http_server
5814    logging.debug('Started http server at "%s".', http_data_dir)
5815
5816  def _StopHTTPServer(self):
5817    """Stop the local http server."""
5818    global _HTTP_SERVER
5819    assert _HTTP_SERVER, 'HTTP Server not yet started'
5820    assert _HTTP_SERVER.Stop(), 'Could not stop http server'
5821    _HTTP_SERVER = None
5822    logging.debug('Stopped http server.')
5823
5824  def _ConnectToRemoteHosts(self, addresses):
5825    """Connect to remote PyAuto instances using a RemoteProxy.
5826
5827    The RemoteHost instances must already be running."""
5828    global _REMOTE_PROXY
5829    assert not _REMOTE_PROXY, 'Already connected to a remote host.'
5830    _REMOTE_PROXY = []
5831    for address in addresses:
5832      if address == 'localhost' or address == '127.0.0.1':
5833        self._StartLocalRemoteHost()
5834      _REMOTE_PROXY.append(_RemoteProxy((address, 7410)))
5835
5836  def _StartLocalRemoteHost(self):
5837    """Start a remote PyAuto instance on the local machine."""
5838    # Add the path to our main class to the RemoteHost's
5839    # environment, so it can load that class at runtime.
5840    import __main__
5841    main_path = os.path.dirname(__main__.__file__)
5842    env = os.environ
5843    if env.get('PYTHONPATH', None):
5844      env['PYTHONPATH'] += ':' + main_path
5845    else:
5846      env['PYTHONPATH'] = main_path
5847
5848    # Run it!
5849    subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__),
5850                                                   'remote_host.py')], env=env)
5851
5852
5853class _GTestTextTestResult(unittest._TextTestResult):
5854  """A test result class that can print formatted text results to a stream.
5855
5856  Results printed in conformance with gtest output format, like:
5857  [ RUN        ] autofill.AutofillTest.testAutofillInvalid: "test desc."
5858  [         OK ] autofill.AutofillTest.testAutofillInvalid
5859  [ RUN        ] autofill.AutofillTest.testFillProfile: "test desc."
5860  [         OK ] autofill.AutofillTest.testFillProfile
5861  [ RUN        ] autofill.AutofillTest.testFillProfileCrazyCharacters: "Test."
5862  [         OK ] autofill.AutofillTest.testFillProfileCrazyCharacters
5863  """
5864  def __init__(self, stream, descriptions, verbosity):
5865    unittest._TextTestResult.__init__(self, stream, descriptions, verbosity)
5866
5867  def _GetTestURI(self, test):
5868    if sys.version_info[:2] <= (2, 4):
5869      return '%s.%s' % (unittest._strclass(test.__class__),
5870                        test._TestCase__testMethodName)
5871    return '%s.%s.%s' % (test.__class__.__module__,
5872                         test.__class__.__name__,
5873                         test._testMethodName)
5874
5875  def getDescription(self, test):
5876    return '%s: "%s"' % (self._GetTestURI(test), test.shortDescription())
5877
5878  def startTest(self, test):
5879    unittest.TestResult.startTest(self, test)
5880    self.stream.writeln('[ RUN        ] %s' % self.getDescription(test))
5881
5882  def addSuccess(self, test):
5883    unittest.TestResult.addSuccess(self, test)
5884    self.stream.writeln('[         OK ] %s' % self._GetTestURI(test))
5885
5886  def addError(self, test, err):
5887    unittest.TestResult.addError(self, test, err)
5888    self.stream.writeln('[      ERROR ] %s' % self._GetTestURI(test))
5889
5890  def addFailure(self, test, err):
5891    unittest.TestResult.addFailure(self, test, err)
5892    self.stream.writeln('[     FAILED ] %s' % self._GetTestURI(test))
5893
5894
5895class PyAutoTextTestRunner(unittest.TextTestRunner):
5896  """Test Runner for PyAuto tests that displays results in textual format.
5897
5898  Results are displayed in conformance with gtest output.
5899  """
5900  def __init__(self, verbosity=1):
5901    unittest.TextTestRunner.__init__(self,
5902                                     stream=sys.stderr,
5903                                     verbosity=verbosity)
5904
5905  def _makeResult(self):
5906    return _GTestTextTestResult(self.stream, self.descriptions, self.verbosity)
5907
5908
5909# Implementation inspired from unittest.main()
5910class Main(object):
5911  """Main program for running PyAuto tests."""
5912
5913  _options, _args = None, None
5914  _tests_filename = 'PYAUTO_TESTS'
5915  _platform_map = {
5916    'win32':  'win',
5917    'darwin': 'mac',
5918    'linux2': 'linux',
5919    'linux3': 'linux',
5920    'chromeos': 'chromeos',
5921  }
5922
5923  def __init__(self):
5924    self._ParseArgs()
5925    self._Run()
5926
5927  def _ParseArgs(self):
5928    """Parse command line args."""
5929    parser = optparse.OptionParser()
5930    parser.add_option(
5931        '', '--channel-id', type='string', default='',
5932        help='Name of channel id, if using named interface.')
5933    parser.add_option(
5934        '', '--chrome-flags', type='string', default='',
5935        help='Flags passed to Chrome.  This is in addition to the usual flags '
5936             'like suppressing first-run dialogs, enabling automation.  '
5937             'See chrome/common/chrome_switches.cc for the list of flags '
5938             'chrome understands.')
5939    parser.add_option(
5940        '', '--http-data-dir', type='string',
5941        default=os.path.join('chrome', 'test', 'data'),
5942        help='Relative path from which http server should serve files.')
5943    parser.add_option(
5944        '-L', '--list-tests', action='store_true', default=False,
5945        help='List all tests, and exit.')
5946    parser.add_option(
5947        '--shard',
5948        help='Specify sharding params. Example: 1/3 implies split the list of '
5949             'tests into 3 groups of which this is the 1st.')
5950    parser.add_option(
5951        '', '--log-file', type='string', default=None,
5952        help='Provide a path to a file to which the logger will log')
5953    parser.add_option(
5954        '', '--no-http-server', action='store_true', default=False,
5955        help='Do not start an http server to serve files in data dir.')
5956    parser.add_option(
5957        '', '--remote-host', type='string', default=None,
5958        help='Connect to remote hosts for remote automation. If "localhost" '
5959            '"127.0.0.1" is specified, a remote host will be launched '
5960            'automatically on the local machine.')
5961    parser.add_option(
5962        '', '--repeat', type='int', default=1,
5963        help='Number of times to repeat the tests. Useful to determine '
5964             'flakiness. Defaults to 1.')
5965    parser.add_option(
5966        '-S', '--suite', type='string', default='FULL',
5967        help='Name of the suite to load.  Defaults to "FULL".')
5968    parser.add_option(
5969        '-v', '--verbose', action='store_true', default=False,
5970        help='Make PyAuto verbose.')
5971    parser.add_option(
5972        '-D', '--wait-for-debugger', action='store_true', default=False,
5973        help='Block PyAuto on startup for attaching debugger.')
5974
5975    self._options, self._args = parser.parse_args()
5976    global _OPTIONS
5977    _OPTIONS = self._options  # Export options so other classes can access.
5978
5979    # Set up logging. All log messages will be prepended with a timestamp.
5980    format = '%(asctime)s %(levelname)-8s %(message)s'
5981
5982    level = logging.INFO
5983    if self._options.verbose:
5984      level=logging.DEBUG
5985
5986    logging.basicConfig(level=level, format=format,
5987                        filename=self._options.log_file)
5988
5989  def TestsDir(self):
5990    """Returns the path to dir containing tests.
5991
5992    This is typically the dir containing the tests description file.
5993    This method should be overridden by derived class to point to other dirs
5994    if needed.
5995    """
5996    return os.path.dirname(__file__)
5997
5998  @staticmethod
5999  def _ImportTestsFromName(name):
6000    """Get a list of all test names from the given string.
6001
6002    Args:
6003      name: dot-separated string for a module, a test case or a test method.
6004            Examples: omnibox  (a module)
6005                      omnibox.OmniboxTest  (a test case)
6006                      omnibox.OmniboxTest.testA  (a test method)
6007
6008    Returns:
6009      [omnibox.OmniboxTest.testA, omnibox.OmniboxTest.testB, ...]
6010    """
6011    def _GetTestsFromTestCase(class_obj):
6012      """Return all test method names from given class object."""
6013      return [class_obj.__name__ + '.' + x for x in dir(class_obj) if
6014              x.startswith('test')]
6015
6016    def _GetTestsFromModule(module):
6017      """Return all test method names from the given module object."""
6018      tests = []
6019      for name in dir(module):
6020        obj = getattr(module, name)
6021        if (isinstance(obj, (type, types.ClassType)) and
6022            issubclass(obj, PyUITest) and obj != PyUITest):
6023          tests.extend([module.__name__ + '.' + x for x in
6024                        _GetTestsFromTestCase(obj)])
6025      return tests
6026
6027    module = None
6028    # Locate the module
6029    parts = name.split('.')
6030    parts_copy = parts[:]
6031    while parts_copy:
6032      try:
6033        module = __import__('.'.join(parts_copy))
6034        break
6035      except ImportError:
6036        del parts_copy[-1]
6037        if not parts_copy: raise
6038    # We have the module. Pick the exact test method or class asked for.
6039    parts = parts[1:]
6040    obj = module
6041    for part in parts:
6042      obj = getattr(obj, part)
6043
6044    if type(obj) == types.ModuleType:
6045      return _GetTestsFromModule(obj)
6046    elif (isinstance(obj, (type, types.ClassType)) and
6047          issubclass(obj, PyUITest) and obj != PyUITest):
6048      return [module.__name__ + '.' + x for x in _GetTestsFromTestCase(obj)]
6049    elif type(obj) == types.UnboundMethodType:
6050      return [name]
6051    else:
6052      logging.warn('No tests in "%s"', name)
6053      return []
6054
6055  def _HasTestCases(self, module_string):
6056    """Determines if we have any PyUITest test case classes in the module
6057       identified by |module_string|."""
6058    module = __import__(module_string)
6059    for name in dir(module):
6060      obj = getattr(module, name)
6061      if (isinstance(obj, (type, types.ClassType)) and
6062          issubclass(obj, PyUITest)):
6063        return True
6064    return False
6065
6066  def _ExpandTestNames(self, args):
6067    """Returns a list of tests loaded from the given args.
6068
6069    The given args can be either a module (ex: module1) or a testcase
6070    (ex: module2.MyTestCase) or a test (ex: module1.MyTestCase.testX)
6071    or a suite name (ex: @FULL). If empty, the tests in the already imported
6072    modules are loaded.
6073
6074    Args:
6075      args: [module1, module2, module3.testcase, module4.testcase.testX]
6076            These modules or test cases or tests should be importable.
6077            Suites can be specified by prefixing @. Example: @FULL
6078
6079      Returns:
6080        a list of expanded test names.  Example:
6081          [
6082            'module1.TestCase1.testA',
6083            'module1.TestCase1.testB',
6084            'module2.TestCase2.testX',
6085            'module3.testcase.testY',
6086            'module4.testcase.testX'
6087          ]
6088    """
6089
6090    def _TestsFromDescriptionFile(suite):
6091      pyauto_tests_file = os.path.join(self.TestsDir(), self._tests_filename)
6092      if suite:
6093        logging.debug("Reading %s (@%s)", pyauto_tests_file, suite)
6094      else:
6095        logging.debug("Reading %s", pyauto_tests_file)
6096      if not os.path.exists(pyauto_tests_file):
6097        logging.warn("%s missing. Cannot load tests.", pyauto_tests_file)
6098        return []
6099      else:
6100        return self._ExpandTestNamesFrom(pyauto_tests_file, suite)
6101
6102    if not args:  # Load tests ourselves
6103      if self._HasTestCases('__main__'):    # we are running a test script
6104        module_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
6105        args.append(module_name)   # run the test cases found in it
6106      else:  # run tests from the test description file
6107        args = _TestsFromDescriptionFile(self._options.suite)
6108    else:  # Check args with @ prefix for suites
6109      out_args = []
6110      for arg in args:
6111        if arg.startswith('@'):
6112          suite = arg[1:]
6113          out_args += _TestsFromDescriptionFile(suite)
6114        else:
6115          out_args.append(arg)
6116      args = out_args
6117    return args
6118
6119  def _ExpandTestNamesFrom(self, filename, suite):
6120    """Load test names from the given file.
6121
6122    Args:
6123      filename: the file to read the tests from
6124      suite: the name of the suite to load from |filename|.
6125
6126    Returns:
6127      a list of test names
6128      [module.testcase.testX, module.testcase.testY, ..]
6129    """
6130    suites = PyUITest.EvalDataFrom(filename)
6131    platform = sys.platform
6132    if PyUITest.IsChromeOS():  # check if it's chromeos
6133      platform = 'chromeos'
6134    assert platform in self._platform_map, '%s unsupported' % platform
6135    def _NamesInSuite(suite_name):
6136      logging.debug('Expanding suite %s', suite_name)
6137      platforms = suites.get(suite_name)
6138      names = platforms.get('all', []) + \
6139              platforms.get(self._platform_map[platform], [])
6140      ret = []
6141      # Recursively include suites if any.  Suites begin with @.
6142      for name in names:
6143        if name.startswith('@'):  # Include another suite
6144          ret.extend(_NamesInSuite(name[1:]))
6145        else:
6146          ret.append(name)
6147      return ret
6148
6149    assert suite in suites, '%s: No such suite in %s' % (suite, filename)
6150    all_names = _NamesInSuite(suite)
6151    args = []
6152    excluded = []
6153    # Find all excluded tests.  Excluded tests begin with '-'.
6154    for name in all_names:
6155      if name.startswith('-'):  # Exclude
6156        excluded.extend(self._ImportTestsFromName(name[1:]))
6157      else:
6158        args.extend(self._ImportTestsFromName(name))
6159    for name in excluded:
6160      if name in args:
6161        args.remove(name)
6162      else:
6163        logging.warn('Cannot exclude %s. Not included. Ignoring', name)
6164    if excluded:
6165      logging.debug('Excluded %d test(s): %s', len(excluded), excluded)
6166    return args
6167
6168  def _Run(self):
6169    """Run the tests."""
6170    if self._options.wait_for_debugger:
6171      raw_input('Attach debugger to process %s and hit <enter> ' % os.getpid())
6172
6173    suite_args = [sys.argv[0]]
6174    chrome_flags = self._options.chrome_flags
6175    # Set CHROME_HEADLESS. It enables crash reporter on posix.
6176    os.environ['CHROME_HEADLESS'] = '1'
6177    os.environ['EXTRA_CHROME_FLAGS'] = chrome_flags
6178    test_names = self._ExpandTestNames(self._args)
6179
6180    # Shard, if requested (--shard).
6181    if self._options.shard:
6182      matched = re.match('(\d+)/(\d+)', self._options.shard)
6183      if not matched:
6184        print >>sys.stderr, 'Invalid sharding params: %s' % self._options.shard
6185        sys.exit(1)
6186      shard_index = int(matched.group(1)) - 1
6187      num_shards = int(matched.group(2))
6188      if shard_index < 0 or shard_index >= num_shards:
6189        print >>sys.stderr, 'Invalid sharding params: %s' % self._options.shard
6190        sys.exit(1)
6191      test_names = pyauto_utils.Shard(test_names, shard_index, num_shards)
6192
6193    test_names *= self._options.repeat
6194    logging.debug("Loading %d tests from %s", len(test_names), test_names)
6195    if self._options.list_tests:  # List tests and exit
6196      for name in test_names:
6197        print name
6198      sys.exit(0)
6199    pyauto_suite = PyUITestSuite(suite_args)
6200    loaded_tests = unittest.defaultTestLoader.loadTestsFromNames(test_names)
6201    pyauto_suite.addTests(loaded_tests)
6202    verbosity = 1
6203    if self._options.verbose:
6204      verbosity = 2
6205    result = PyAutoTextTestRunner(verbosity=verbosity).run(pyauto_suite)
6206    del loaded_tests  # Need to destroy test cases before the suite
6207    del pyauto_suite
6208    successful = result.wasSuccessful()
6209    if not successful:
6210      pyauto_tests_file = os.path.join(self.TestsDir(), self._tests_filename)
6211      print >>sys.stderr, 'Tests can be disabled by editing %s. ' \
6212                          'Ref: %s' % (pyauto_tests_file, _PYAUTO_DOC_URL)
6213    sys.exit(not successful)
6214
6215
6216if __name__ == '__main__':
6217  Main()
6218