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