- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / test / pyautolib / pyauto.py
1 #!/usr/bin/env python
2 # Copyright 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """PyAuto: Python Interface to Chromium's Automation Proxy.
7
8 PyAuto uses swig to expose Automation Proxy interfaces to Python.
9 For complete documentation on the functionality available,
10 run pydoc on this file.
11
12 Ref: http://dev.chromium.org/developers/testing/pyauto
13
14
15 Include the following in your PyAuto test script to make it run standalone.
16
17 from pyauto import Main
18
19 if __name__ == '__main__':
20   Main()
21
22 This script can be used as an executable to fire off other scripts, similar
23 to unittest.py
24   python pyauto.py test_script
25 """
26
27 import cStringIO
28 import copy
29 import functools
30 import hashlib
31 import inspect
32 import logging
33 import optparse
34 import os
35 import pickle
36 import pprint
37 import re
38 import shutil
39 import signal
40 import socket
41 import stat
42 import string
43 import subprocess
44 import sys
45 import tempfile
46 import time
47 import types
48 import unittest
49 import urllib
50
51 import pyauto_paths
52
53
54 def _LocateBinDirs():
55   """Setup a few dirs where we expect to find dependency libraries."""
56   deps_dirs = [
57       os.path.dirname(__file__),
58       pyauto_paths.GetThirdPartyDir(),
59       os.path.join(pyauto_paths.GetThirdPartyDir(), 'webdriver', 'pylib'),
60   ]
61   sys.path += map(os.path.normpath, pyauto_paths.GetBuildDirs() + deps_dirs)
62
63 _LocateBinDirs()
64
65 _PYAUTO_DOC_URL = 'http://dev.chromium.org/developers/testing/pyauto'
66
67 try:
68   import pyautolib
69   # Needed so that all additional classes (like: FilePath, GURL) exposed by
70   # swig interface get available in this module.
71   from pyautolib import *
72 except ImportError:
73   print >>sys.stderr, 'Could not locate pyautolib shared libraries.  ' \
74                       'Did you build?\n  Documentation: %s' % _PYAUTO_DOC_URL
75   # Mac requires python2.5 even when not the default 'python' (e.g. 10.6)
76   if 'darwin' == sys.platform and sys.version_info[:2] != (2,5):
77     print  >>sys.stderr, '*\n* Perhaps use "python2.5", not "python" ?\n*'
78   raise
79
80 # Should go after sys.path is set appropriately
81 import bookmark_model
82 import download_info
83 import history_info
84 import omnibox_info
85 import plugins_info
86 import prefs_info
87 from pyauto_errors import AutomationCommandFail
88 from pyauto_errors import AutomationCommandTimeout
89 from pyauto_errors import JavascriptRuntimeError
90 from pyauto_errors import JSONInterfaceError
91 from pyauto_errors import NTPThumbnailNotShownError
92 import pyauto_utils
93 import simplejson as json  # found in third_party
94
95 _CHROME_DRIVER_FACTORY = None
96 _DEFAULT_AUTOMATION_TIMEOUT = 45
97 _HTTP_SERVER = None
98 _REMOTE_PROXY = None
99 _OPTIONS = None
100 _BROWSER_PID = None
101
102 class PyUITest(pyautolib.PyUITestBase, unittest.TestCase):
103   """Base class for UI Test Cases in Python.
104
105   A browser is created before executing each test, and is destroyed after
106   each test irrespective of whether the test passed or failed.
107
108   You should derive from this class and create methods with 'test' prefix,
109   and use methods inherited from PyUITestBase (the C++ side).
110
111   Example:
112
113     class MyTest(PyUITest):
114
115       def testNavigation(self):
116         self.NavigateToURL("http://www.google.com")
117         self.assertEqual("Google", self.GetActiveTabTitle())
118   """
119
120   def __init__(self, methodName='runTest', **kwargs):
121     """Initialize PyUITest.
122
123     When redefining __init__ in a derived class, make sure that:
124       o you make a call this __init__
125       o __init__ takes methodName as an arg. this is mandated by unittest module
126
127     Args:
128       methodName: the default method name. Internal use by unittest module
129
130       (The rest of the args can be in any order. They can even be skipped in
131        which case the defaults will be used.)
132
133       clear_profile: If True, clean the profile dir before use. Defaults to True
134       homepage: the home page. Defaults to "about:blank"
135     """
136     # Fetch provided keyword args, or fill in defaults.
137     clear_profile = kwargs.get('clear_profile', True)
138     homepage = kwargs.get('homepage', 'about:blank')
139     self._automation_timeout = _DEFAULT_AUTOMATION_TIMEOUT * 1000
140
141     pyautolib.PyUITestBase.__init__(self, clear_profile, homepage)
142     self.Initialize(pyautolib.FilePath(self.BrowserPath()))
143     unittest.TestCase.__init__(self, methodName)
144
145     # Give all pyauto tests easy access to pprint.PrettyPrinter functions.
146     self.pprint = pprint.pprint
147     self.pformat = pprint.pformat
148
149     # Set up remote proxies, if they were requested.
150     self.remotes = []
151     self.remote = None
152     global _REMOTE_PROXY
153     if _REMOTE_PROXY:
154       self.remotes = _REMOTE_PROXY
155       self.remote = _REMOTE_PROXY[0]
156
157   def __del__(self):
158     pyautolib.PyUITestBase.__del__(self)
159
160   def _SetExtraChromeFlags(self):
161     """Prepares the browser to launch with the specified extra Chrome flags.
162
163     This function is called right before the browser is launched for the first
164     time.
165     """
166     for flag in self.ExtraChromeFlags():
167       if flag.startswith('--'):
168         flag = flag[2:]
169       split_pos = flag.find('=')
170       if split_pos >= 0:
171         flag_name = flag[:split_pos]
172         flag_val = flag[split_pos + 1:]
173         self.AppendBrowserLaunchSwitch(flag_name, flag_val)
174       else:
175         self.AppendBrowserLaunchSwitch(flag)
176
177   def __SetUp(self):
178     named_channel_id = None
179     if _OPTIONS:
180       named_channel_id = _OPTIONS.channel_id
181     if self.IsChromeOS():  # Enable testing interface on ChromeOS.
182       if self.get_clear_profile():
183         self.CleanupBrowserProfileOnChromeOS()
184       self.EnableCrashReportingOnChromeOS()
185       if not named_channel_id:
186         named_channel_id = self.EnableChromeTestingOnChromeOS()
187     else:
188       self._SetExtraChromeFlags()  # Flags already previously set for ChromeOS.
189     if named_channel_id:
190       self._named_channel_id = named_channel_id
191       self.UseNamedChannelID(named_channel_id)
192     # Initialize automation and fire the browser (does not fire the browser
193     # on ChromeOS).
194     self.SetUp()
195
196     global _BROWSER_PID
197     try:
198       _BROWSER_PID = self.GetBrowserInfo()['browser_pid']
199     except JSONInterfaceError:
200       raise JSONInterfaceError('Unable to get browser_pid over automation '
201                                'channel on first attempt.  Something went very '
202                                'wrong.  Chrome probably did not launch.')
203
204     # Forcibly trigger all plugins to get registered.  crbug.com/94123
205     # Sometimes flash files loaded too quickly after firing browser
206     # ends up getting downloaded, which seems to indicate that the plugin
207     # hasn't been registered yet.
208     if not self.IsChromeOS():
209       self.GetPluginsInfo()
210
211     if (self.IsChromeOS() and not self.GetLoginInfo()['is_logged_in'] and
212         self.ShouldOOBESkipToLogin()):
213       if self.GetOOBEScreenInfo()['screen_name'] != 'login':
214         self.SkipToLogin()
215       if self.ShouldAutoLogin():
216         # Login with default creds.
217         sys.path.append('/usr/local')  # to import autotest libs
218         from autotest.cros import constants
219         creds = constants.CREDENTIALS['$default']
220         self.Login(creds[0], creds[1])
221         assert self.GetLoginInfo()['is_logged_in']
222         logging.info('Logged in as %s.' % creds[0])
223
224     # If we are connected to any RemoteHosts, create PyAuto
225     # instances on the remote sides and set them up too.
226     for remote in self.remotes:
227       remote.CreateTarget(self)
228       remote.setUp()
229
230   def setUp(self):
231     """Override this method to launch browser differently.
232
233     Can be used to prevent launching the browser window by default in case a
234     test wants to do some additional setup before firing browser.
235
236     When using the named interface, it connects to an existing browser
237     instance.
238
239     On ChromeOS, a browser showing the login window is started. Tests can
240     initiate a user session by calling Login() or LoginAsGuest(). Cryptohome
241     vaults or flimflam profiles left over by previous tests can be cleared by
242     calling RemoveAllCryptohomeVaults() respectively CleanFlimflamDirs() before
243     logging in to improve isolation. Note that clearing flimflam profiles
244     requires a flimflam restart, briefly taking down network connectivity and
245     slowing down the test. This should be done for tests that use flimflam only.
246     """
247     self.__SetUp()
248
249   def tearDown(self):
250     for remote in self.remotes:
251       remote.tearDown()
252
253     self.TearDown()  # Destroy browser
254
255   # Method required by the Python standard library unittest.TestCase.
256   def runTest(self):
257     pass
258
259   @staticmethod
260   def BrowserPath():
261     """Returns the path to Chromium binaries.
262
263     Expects the browser binaries to be in the
264     same location as the pyautolib binaries.
265     """
266     return os.path.normpath(os.path.dirname(pyautolib.__file__))
267
268   def ExtraChromeFlags(self):
269     """Return a list of extra chrome flags to use with Chrome for testing.
270
271     These are flags needed to facilitate testing.  Override this function to
272     use a custom set of Chrome flags.
273     """
274     auth_ext_path = ('/usr/local/autotest/deps/pyauto_dep/' +
275         'test_src/chrome/browser/resources/gaia_auth')
276     if self.IsChromeOS():
277       return [
278         '--homepage=about:blank',
279         '--allow-file-access',
280         '--allow-file-access-from-files',
281         '--enable-file-cookies',
282         '--disable-default-apps',
283         '--dom-automation',
284         '--skip-oauth-login',
285         # Enables injection of test content script for webui login automation
286         '--auth-ext-path=%s' % auth_ext_path,
287         # Enable automation provider, chromeos net and chromeos login logs
288         '--vmodule=*/browser/automation/*=2,*/chromeos/net/*=2,' +
289             '*/chromeos/login/*=2',
290       ]
291     else:
292       return []
293
294   def ShouldOOBESkipToLogin(self):
295     """Determine if we should skip the OOBE flow on ChromeOS.
296
297     This makes automation skip the OOBE flow during setUp() and land directly
298     to the login screen. Applies only if not logged in already.
299
300     Override and return False if OOBE flow is required, for OOBE tests, for
301     example. Calling this function directly will have no effect.
302
303     Returns:
304       True, if the OOBE should be skipped and automation should
305             go to the 'Add user' login screen directly
306       False, if the OOBE should not be skipped.
307     """
308     assert self.IsChromeOS()
309     return True
310
311   def ShouldAutoLogin(self):
312     """Determine if we should auto-login on ChromeOS at browser startup.
313
314     To be used for tests that expect user to be logged in before running test,
315     without caring which user. ShouldOOBESkipToLogin() should return True
316     for this to take effect.
317
318     Override and return False to not auto login, for tests where login is part
319     of the use case.
320
321     Returns:
322       True, if chrome should auto login after startup.
323       False, otherwise.
324     """
325     assert self.IsChromeOS()
326     return True
327
328   def CloseChromeOnChromeOS(self):
329     """Gracefully exit chrome on ChromeOS."""
330
331     def _GetListOfChromePids():
332       """Retrieves the list of currently-running Chrome process IDs.
333
334       Returns:
335         A list of strings, where each string represents a currently-running
336         'chrome' process ID.
337       """
338       proc = subprocess.Popen(['pgrep', '^chrome$'], stdout=subprocess.PIPE)
339       proc.wait()
340       return [x.strip() for x in proc.stdout.readlines()]
341
342     orig_pids = _GetListOfChromePids()
343     subprocess.call(['pkill', '^chrome$'])
344
345     def _AreOrigPidsDead(orig_pids):
346       """Determines whether all originally-running 'chrome' processes are dead.
347
348       Args:
349         orig_pids: A list of strings, where each string represents the PID for
350                    an originally-running 'chrome' process.
351
352       Returns:
353         True, if all originally-running 'chrome' processes have been killed, or
354         False otherwise.
355       """
356       for new_pid in _GetListOfChromePids():
357         if new_pid in orig_pids:
358           return False
359       return True
360
361     self.WaitUntil(lambda: _AreOrigPidsDead(orig_pids))
362
363   @staticmethod
364   def _IsRootSuid(path):
365     """Determine if |path| is a suid-root file."""
366     return os.path.isfile(path) and (os.stat(path).st_mode & stat.S_ISUID)
367
368   @staticmethod
369   def SuidPythonPath():
370     """Path to suid_python binary on ChromeOS.
371
372     This is typically in the same directory as pyautolib.py
373     """
374     return os.path.join(PyUITest.BrowserPath(), 'suid-python')
375
376   @staticmethod
377   def RunSuperuserActionOnChromeOS(action):
378     """Run the given action with superuser privs (on ChromeOS).
379
380     Uses the suid_actions.py script.
381
382     Args:
383       action: An action to perform.
384               See suid_actions.py for available options.
385
386     Returns:
387       (stdout, stderr)
388     """
389     assert PyUITest._IsRootSuid(PyUITest.SuidPythonPath()), \
390         'Did not find suid-root python at %s' % PyUITest.SuidPythonPath()
391     file_path = os.path.join(os.path.dirname(__file__), 'chromeos',
392                              'suid_actions.py')
393     args = [PyUITest.SuidPythonPath(), file_path, '--action=%s' % action]
394     proc = subprocess.Popen(
395         args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
396     stdout, stderr = proc.communicate()
397     return (stdout, stderr)
398
399   def EnableChromeTestingOnChromeOS(self):
400     """Enables the named automation interface on chromeos.
401
402     Restarts chrome so that you get a fresh instance.
403     Also sets some testing-friendly flags for chrome.
404
405     Expects suid python to be present in the same dir as pyautolib.py
406     """
407     assert PyUITest._IsRootSuid(self.SuidPythonPath()), \
408         'Did not find suid-root python at %s' % self.SuidPythonPath()
409     file_path = os.path.join(os.path.dirname(__file__), 'chromeos',
410                              'enable_testing.py')
411     args = [self.SuidPythonPath(), file_path]
412     # Pass extra chrome flags for testing
413     for flag in self.ExtraChromeFlags():
414       args.append('--extra-chrome-flags=%s' % flag)
415     assert self.WaitUntil(lambda: self._IsSessionManagerReady(0))
416     proc = subprocess.Popen(args, stdout=subprocess.PIPE)
417     automation_channel_path = proc.communicate()[0].strip()
418     assert len(automation_channel_path), 'Could not enable testing interface'
419     return automation_channel_path
420
421   @staticmethod
422   def EnableCrashReportingOnChromeOS():
423     """Enables crash reporting on ChromeOS.
424
425     Writes the "/home/chronos/Consent To Send Stats" file with a 32-char
426     readable string.  See comment in session_manager_setup.sh which does this
427     too.
428
429     Note that crash reporting will work only if breakpad is built in, ie in a
430     'Google Chrome' build (not Chromium).
431     """
432     consent_file = '/home/chronos/Consent To Send Stats'
433     def _HasValidConsentFile():
434       if not os.path.isfile(consent_file):
435         return False
436       stat = os.stat(consent_file)
437       return (len(open(consent_file).read()) and
438               (1000, 1000) == (stat.st_uid, stat.st_gid))
439     if not _HasValidConsentFile():
440       client_id = hashlib.md5('abcdefgh').hexdigest()
441       # Consent file creation and chown to chronos needs to be atomic
442       # to avoid races with the session_manager.  crosbug.com/18413
443       # Therefore, create a temp file, chown, then rename it as consent file.
444       temp_file = consent_file + '.tmp'
445       open(temp_file, 'w').write(client_id)
446       # This file must be owned by chronos:chronos!
447       os.chown(temp_file, 1000, 1000);
448       shutil.move(temp_file, consent_file)
449     assert _HasValidConsentFile(), 'Could not create %s' % consent_file
450
451   @staticmethod
452   def _IsSessionManagerReady(old_pid):
453     """Is the ChromeOS session_manager running and ready to accept DBus calls?
454
455     Called after session_manager is killed to know when it has restarted.
456
457     Args:
458       old_pid: The pid that session_manager had before it was killed,
459                to ensure that we don't look at the DBus interface
460                of an old session_manager process.
461     """
462     pgrep_process = subprocess.Popen(['pgrep', 'session_manager'],
463                                      stdout=subprocess.PIPE)
464     new_pid = pgrep_process.communicate()[0].strip()
465     if not new_pid or old_pid == new_pid:
466       return False
467
468     import dbus
469     try:
470       bus = dbus.SystemBus()
471       proxy = bus.get_object('org.chromium.SessionManager',
472                              '/org/chromium/SessionManager')
473       dbus.Interface(proxy, 'org.chromium.SessionManagerInterface')
474     except dbus.DBusException:
475       return False
476     return True
477
478   @staticmethod
479   def CleanupBrowserProfileOnChromeOS():
480     """Cleanup browser profile dir on ChromeOS.
481
482     This does not clear cryptohome.
483
484     Browser should not be running, or else there will be locked files.
485     """
486     profile_dir = '/home/chronos/user'
487     for item in os.listdir(profile_dir):
488       # Deleting .pki causes stateful partition to get erased.
489       if item not in ['log', 'flimflam'] and not item.startswith('.'):
490          pyauto_utils.RemovePath(os.path.join(profile_dir, item))
491
492     chronos_dir = '/home/chronos'
493     for item in os.listdir(chronos_dir):
494       if item != 'user' and not item.startswith('.'):
495         pyauto_utils.RemovePath(os.path.join(chronos_dir, item))
496
497   @staticmethod
498   def CleanupFlimflamDirsOnChromeOS():
499     """Clean the contents of flimflam profiles and restart flimflam."""
500     PyUITest.RunSuperuserActionOnChromeOS('CleanFlimflamDirs')
501
502   @staticmethod
503   def RemoveAllCryptohomeVaultsOnChromeOS():
504     """Remove any existing cryptohome vaults."""
505     PyUITest.RunSuperuserActionOnChromeOS('RemoveAllCryptohomeVaults')
506
507   @staticmethod
508   def _IsInodeNew(path, old_inode):
509     """Determine whether an inode has changed. POSIX only.
510
511     Args:
512       path: The file path to check for changes.
513       old_inode: The old inode number.
514
515     Returns:
516       True if the path exists and its inode number is different from old_inode.
517       False otherwise.
518     """
519     try:
520       stat_result = os.stat(path)
521     except OSError:
522       return False
523     if not stat_result:
524       return False
525     return stat_result.st_ino != old_inode
526
527   def RestartBrowser(self, clear_profile=True, pre_launch_hook=None):
528     """Restart the browser.
529
530     For use with tests that require to restart the browser.
531
532     Args:
533       clear_profile: If True, the browser profile is cleared before restart.
534                      Defaults to True, that is restarts browser with a clean
535                      profile.
536       pre_launch_hook: If specified, must be a callable that is invoked before
537                        the browser is started again. Not supported in ChromeOS.
538     """
539     if self.IsChromeOS():
540       assert pre_launch_hook is None, 'Not supported in ChromeOS'
541       self.TearDown()
542       if clear_profile:
543         self.CleanupBrowserProfileOnChromeOS()
544       self.CloseChromeOnChromeOS()
545       self.EnableChromeTestingOnChromeOS()
546       self.SetUp()
547       return
548     # Not chromeos
549     orig_clear_state = self.get_clear_profile()
550     self.CloseBrowserAndServer()
551     self.set_clear_profile(clear_profile)
552     if pre_launch_hook:
553       pre_launch_hook()
554     logging.debug('Restarting browser with clear_profile=%s',
555                   self.get_clear_profile())
556     self.LaunchBrowserAndServer()
557     self.set_clear_profile(orig_clear_state)  # Reset to original state.
558
559   @staticmethod
560   def DataDir():
561     """Returns the path to the data dir chrome/test/data."""
562     return os.path.normpath(
563         os.path.join(os.path.dirname(__file__), os.pardir, "data"))
564
565   @staticmethod
566   def ChromeOSDataDir():
567     """Returns the path to the data dir chromeos/test/data."""
568     return os.path.normpath(
569         os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
570                      "chromeos", "test", "data"))
571
572   @staticmethod
573   def GetFileURLForPath(*path):
574     """Get file:// url for the given path.
575
576     Also quotes the url using urllib.quote().
577
578     Args:
579       path: Variable number of strings that can be joined.
580     """
581     path_str = os.path.join(*path)
582     abs_path = os.path.abspath(path_str)
583     if sys.platform == 'win32':
584       # Don't quote the ':' in drive letter ( say, C: ) on win.
585       # Also, replace '\' with '/' as expected in a file:/// url.
586       drive, rest = os.path.splitdrive(abs_path)
587       quoted_path = drive.upper() + urllib.quote((rest.replace('\\', '/')))
588       return 'file:///' + quoted_path
589     else:
590       quoted_path = urllib.quote(abs_path)
591       return 'file://' + quoted_path
592
593   @staticmethod
594   def GetFileURLForDataPath(*relative_path):
595     """Get file:// url for the given path relative to the chrome test data dir.
596
597     Also quotes the url using urllib.quote().
598
599     Args:
600       relative_path: Variable number of strings that can be joined.
601     """
602     return PyUITest.GetFileURLForPath(PyUITest.DataDir(), *relative_path)
603
604   @staticmethod
605   def GetHttpURLForDataPath(*relative_path):
606     """Get http:// url for the given path in the data dir.
607
608     The URL will be usable only after starting the http server.
609     """
610     global _HTTP_SERVER
611     assert _HTTP_SERVER, 'HTTP Server not yet started'
612     return _HTTP_SERVER.GetURL(os.path.join('files', *relative_path)).spec()
613
614   @staticmethod
615   def ContentDataDir():
616     """Get path to content/test/data."""
617     return os.path.join(PyUITest.DataDir(), os.pardir, os.pardir, os.pardir,
618         'content', 'test', 'data')
619
620   @staticmethod
621   def GetFileURLForContentDataPath(*relative_path):
622     """Get file:// url for the given path relative to content test data dir.
623
624     Also quotes the url using urllib.quote().
625
626     Args:
627       relative_path: Variable number of strings that can be joined.
628     """
629     return PyUITest.GetFileURLForPath(PyUITest.ContentDataDir(), *relative_path)
630
631   @staticmethod
632   def GetFtpURLForDataPath(ftp_server, *relative_path):
633     """Get ftp:// url for the given path in the data dir.
634
635     Args:
636       ftp_server: handle to ftp server, an instance of SpawnedTestServer
637       relative_path: any number of path elements
638
639     The URL will be usable only after starting the ftp server.
640     """
641     assert ftp_server, 'FTP Server not yet started'
642     return ftp_server.GetURL(os.path.join(*relative_path)).spec()
643
644   @staticmethod
645   def IsMac():
646     """Are we on Mac?"""
647     return 'darwin' == sys.platform
648
649   @staticmethod
650   def IsLinux():
651     """Are we on Linux? ChromeOS is linux too."""
652     return sys.platform.startswith('linux')
653
654   @staticmethod
655   def IsWin():
656     """Are we on Win?"""
657     return 'win32' == sys.platform
658
659   @staticmethod
660   def IsWin7():
661     """Are we on Windows 7?"""
662     if not PyUITest.IsWin():
663       return False
664     ver = sys.getwindowsversion()
665     return (ver[3], ver[0], ver[1]) == (2, 6, 1)
666
667   @staticmethod
668   def IsWinVista():
669     """Are we on Windows Vista?"""
670     if not PyUITest.IsWin():
671       return False
672     ver = sys.getwindowsversion()
673     return (ver[3], ver[0], ver[1]) == (2, 6, 0)
674
675   @staticmethod
676   def IsWinXP():
677     """Are we on Windows XP?"""
678     if not PyUITest.IsWin():
679       return False
680     ver = sys.getwindowsversion()
681     return (ver[3], ver[0], ver[1]) == (2, 5, 1)
682
683   @staticmethod
684   def IsChromeOS():
685     """Are we on ChromeOS (or Chromium OS)?
686
687     Checks for "CHROMEOS_RELEASE_NAME=" in /etc/lsb-release.
688     """
689     lsb_release = '/etc/lsb-release'
690     if not PyUITest.IsLinux() or not os.path.isfile(lsb_release):
691       return False
692     for line in open(lsb_release).readlines():
693       if line.startswith('CHROMEOS_RELEASE_NAME='):
694         return True
695     return False
696
697   @staticmethod
698   def IsPosix():
699     """Are we on Mac/Linux?"""
700     return PyUITest.IsMac() or PyUITest.IsLinux()
701
702   @staticmethod
703   def IsEnUS():
704     """Are we en-US?"""
705     # TODO: figure out the machine's langugage.
706     return True
707
708   @staticmethod
709   def GetPlatform():
710     """Return the platform name."""
711     # Since ChromeOS is also Linux, we check for it first.
712     if PyUITest.IsChromeOS():
713       return 'chromeos'
714     elif PyUITest.IsLinux():
715       return 'linux'
716     elif PyUITest.IsMac():
717       return 'mac'
718     elif PyUITest.IsWin():
719       return 'win'
720     else:
721       return 'unknown'
722
723   @staticmethod
724   def EvalDataFrom(filename):
725     """Return eval of python code from given file.
726
727     The datastructure used in the file will be preserved.
728     """
729     data_file = os.path.join(filename)
730     contents = open(data_file).read()
731     try:
732       ret = eval(contents)
733     except:
734       print >>sys.stderr, '%s is an invalid data file.' % data_file
735       raise
736     return ret
737
738   @staticmethod
739   def ChromeOSBoard():
740     """What is the ChromeOS board name"""
741     if PyUITest.IsChromeOS():
742       for line in open('/etc/lsb-release'):
743         line = line.strip()
744         if line.startswith('CHROMEOS_RELEASE_BOARD='):
745           return line.split('=')[1]
746     return None
747
748   @staticmethod
749   def Kill(pid):
750     """Terminate the given pid.
751
752     If the pid refers to a renderer, use KillRendererProcess instead.
753     """
754     if PyUITest.IsWin():
755       subprocess.call(['taskkill.exe', '/T', '/F', '/PID', str(pid)])
756     else:
757       os.kill(pid, signal.SIGTERM)
758
759   @staticmethod
760   def GetPrivateInfo():
761     """Fetch info from private_tests_info.txt in private dir.
762
763     Returns:
764       a dictionary of items from private_tests_info.txt
765     """
766     private_file = os.path.join(
767         PyUITest.DataDir(), 'pyauto_private', 'private_tests_info.txt')
768     assert os.path.exists(private_file), '%s missing' % private_file
769     return PyUITest.EvalDataFrom(private_file)
770
771   def WaitUntil(self, function, timeout=-1, retry_sleep=0.25, args=[],
772                 expect_retval=None, return_retval=False, debug=True):
773     """Poll on a condition until timeout.
774
775     Waits until the |function| evalues to |expect_retval| or until |timeout|
776     secs, whichever occurs earlier.
777
778     This is better than using a sleep, since it waits (almost) only as much
779     as needed.
780
781     WARNING: This method call should be avoided as far as possible in favor
782     of a real wait from chromium (like wait-until-page-loaded).
783     Only use in case there's really no better option.
784
785     EXAMPLES:-
786     Wait for "file.txt" to get created:
787       WaitUntil(os.path.exists, args=["file.txt"])
788
789     Same as above, but using lambda:
790       WaitUntil(lambda: os.path.exists("file.txt"))
791
792     Args:
793       function: the function whose truth value is to be evaluated
794       timeout: the max timeout (in secs) for which to wait. The default
795                action is to wait for kWaitForActionMaxMsec, as set in
796                ui_test.cc
797                Use None to wait indefinitely.
798       retry_sleep: the sleep interval (in secs) before retrying |function|.
799                    Defaults to 0.25 secs.
800       args: the args to pass to |function|
801       expect_retval: the expected return value for |function|. This forms the
802                      exit criteria. In case this is None (the default),
803                      |function|'s return value is checked for truth,
804                      so 'non-empty-string' should match with True
805       return_retval: If True, return the value returned by the last call to
806                      |function()|
807       debug: if True, displays debug info at each retry.
808
809     Returns:
810       The return value of the |function| (when return_retval == True)
811       True, if returning when |function| evaluated to True (when
812           return_retval == False)
813       False, when returning due to timeout
814     """
815     if timeout == -1:  # Default
816       timeout = self._automation_timeout / 1000.0
817     assert callable(function), "function should be a callable"
818     begin = time.time()
819     debug_begin = begin
820     retval = None
821     while timeout is None or time.time() - begin <= timeout:
822       retval = function(*args)
823       if (expect_retval is None and retval) or \
824          (expect_retval is not None and expect_retval == retval):
825         return retval if return_retval else True
826       if debug and time.time() - debug_begin > 5:
827         debug_begin += 5
828         if function.func_name == (lambda: True).func_name:
829           function_info = inspect.getsource(function).strip()
830         else:
831           function_info = '%s()' % function.func_name
832         logging.debug('WaitUntil(%s:%d %s) still waiting. '
833                       'Expecting %s. Last returned %s.',
834                       os.path.basename(inspect.getsourcefile(function)),
835                       inspect.getsourcelines(function)[1],
836                       function_info,
837                       True if expect_retval is None else expect_retval,
838                       retval)
839       time.sleep(retry_sleep)
840     return retval if return_retval else False
841
842   def StartFTPServer(self, data_dir):
843     """Start a local file server hosting data files over ftp://
844
845     Args:
846       data_dir: path where ftp files should be served
847
848     Returns:
849       handle to FTP Server, an instance of SpawnedTestServer
850     """
851     ftp_server = pyautolib.SpawnedTestServer(
852         pyautolib.SpawnedTestServer.TYPE_FTP,
853         '127.0.0.1',
854         pyautolib.FilePath(data_dir))
855     assert ftp_server.Start(), 'Could not start ftp server'
856     logging.debug('Started ftp server at "%s".', data_dir)
857     return ftp_server
858
859   def StopFTPServer(self, ftp_server):
860     """Stop the local ftp server."""
861     assert ftp_server, 'FTP Server not yet started'
862     assert ftp_server.Stop(), 'Could not stop ftp server'
863     logging.debug('Stopped ftp server.')
864
865   def StartHTTPServer(self, data_dir):
866     """Starts a local HTTP SpawnedTestServer serving files from |data_dir|.
867
868     Args:
869       data_dir: path where the SpawnedTestServer should serve files from.
870       This will be appended to the source dir to get the final document root.
871
872     Returns:
873       handle to the HTTP SpawnedTestServer
874     """
875     http_server = pyautolib.SpawnedTestServer(
876         pyautolib.SpawnedTestServer.TYPE_HTTP,
877         '127.0.0.1',
878         pyautolib.FilePath(data_dir))
879     assert http_server.Start(), 'Could not start HTTP server'
880     logging.debug('Started HTTP server at "%s".', data_dir)
881     return http_server
882
883   def StopHTTPServer(self, http_server):
884     assert http_server, 'HTTP server not yet started'
885     assert http_server.Stop(), 'Cloud not stop the HTTP server'
886     logging.debug('Stopped HTTP server.')
887
888   def StartHttpsServer(self, cert_type, data_dir):
889     """Starts a local HTTPS SpawnedTestServer serving files from |data_dir|.
890
891     Args:
892       cert_type: An instance of SSLOptions.ServerCertificate for three
893                  certificate types: ok, expired, or mismatch.
894       data_dir: The path where SpawnedTestServer should serve files from.
895                 This is appended to the source dir to get the final
896                 document root.
897
898     Returns:
899       Handle to the HTTPS SpawnedTestServer
900     """
901     https_server = pyautolib.SpawnedTestServer(
902         pyautolib.SpawnedTestServer.TYPE_HTTPS,
903         pyautolib.SSLOptions(cert_type),
904         pyautolib.FilePath(data_dir))
905     assert https_server.Start(), 'Could not start HTTPS server.'
906     logging.debug('Start HTTPS server at "%s".' % data_dir)
907     return https_server
908
909   def StopHttpsServer(self, https_server):
910     assert https_server, 'HTTPS server not yet started.'
911     assert https_server.Stop(), 'Could not stop the HTTPS server.'
912     logging.debug('Stopped HTTPS server.')
913
914   class ActionTimeoutChanger(object):
915     """Facilitate temporary changes to PyAuto command timeout.
916
917     Automatically resets to original timeout when object is destroyed.
918     """
919     _saved_timeout = -1  # Saved timeout value
920
921     def __init__(self, ui_test, new_timeout):
922       """Initialize.
923
924       Args:
925         ui_test: a PyUITest object
926         new_timeout: new timeout to use (in milli secs)
927       """
928       self._saved_timeout = ui_test._automation_timeout
929       ui_test._automation_timeout = new_timeout
930       self._ui_test = ui_test
931
932     def __del__(self):
933       """Reset command_execution_timeout_ms to original value."""
934       self._ui_test._automation_timeout = self._saved_timeout
935
936   class JavascriptExecutor(object):
937     """Abstract base class for JavaScript injection.
938
939     Derived classes should override Execute method."""
940     def Execute(self, script):
941       pass
942
943   class JavascriptExecutorInTab(JavascriptExecutor):
944     """Wrapper for injecting JavaScript in a tab."""
945     def __init__(self, ui_test, tab_index=0, windex=0, frame_xpath=''):
946       """Initialize.
947
948         Refer to ExecuteJavascript() for the complete argument list
949         description.
950
951       Args:
952         ui_test: a PyUITest object
953       """
954       self._ui_test = ui_test
955       self.windex = windex
956       self.tab_index = tab_index
957       self.frame_xpath = frame_xpath
958
959     def Execute(self, script):
960       """Execute script in the tab."""
961       return self._ui_test.ExecuteJavascript(script,
962                                              self.tab_index,
963                                              self.windex,
964                                              self.frame_xpath)
965
966   class JavascriptExecutorInRenderView(JavascriptExecutor):
967     """Wrapper for injecting JavaScript in an extension view."""
968     def __init__(self, ui_test, view, frame_xpath=''):
969       """Initialize.
970
971         Refer to ExecuteJavascriptInRenderView() for the complete argument list
972         description.
973
974       Args:
975         ui_test: a PyUITest object
976       """
977       self._ui_test = ui_test
978       self.view = view
979       self.frame_xpath = frame_xpath
980
981     def Execute(self, script):
982       """Execute script in the render view."""
983       return self._ui_test.ExecuteJavascriptInRenderView(script,
984                                                          self.view,
985                                                          self.frame_xpath)
986
987   def _GetResultFromJSONRequestDiagnostics(self):
988     """Same as _GetResultFromJSONRequest without throwing a timeout exception.
989
990     This method is used to diagnose if a command returns without causing a
991     timout exception to be thrown.  This should be used for debugging purposes
992     only.
993
994     Returns:
995       True if the request returned; False if it timed out.
996     """
997     result = self._SendJSONRequest(-1,
998              json.dumps({'command': 'GetBrowserInfo',}),
999              self._automation_timeout)
1000     if not result:
1001       # The diagnostic command did not complete, Chrome is probably in a bad
1002       # state
1003       return False
1004     return True
1005
1006   def _GetResultFromJSONRequest(self, cmd_dict, windex=0, timeout=-1):
1007     """Issue call over the JSON automation channel and fetch output.
1008
1009     This method packages the given dictionary into a json string, sends it
1010     over the JSON automation channel, loads the json output string returned,
1011     and returns it back as a dictionary.
1012
1013     Args:
1014       cmd_dict: the command dictionary. It must have a 'command' key
1015                 Sample:
1016                   {
1017                     'command': 'SetOmniboxText',
1018                     'text': text,
1019                   }
1020       windex: 0-based window index on which to work. Default: 0 (first window)
1021               Use -ve windex or None if the automation command does not apply
1022               to a browser window. Example: for chromeos login
1023
1024       timeout: request timeout (in milliseconds)
1025
1026     Returns:
1027       a dictionary for the output returned by the automation channel.
1028
1029     Raises:
1030       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1031     """
1032     if timeout == -1:  # Default
1033       timeout = self._automation_timeout
1034     if windex is None:  # Do not target any window
1035       windex = -1
1036     result = self._SendJSONRequest(windex, json.dumps(cmd_dict), timeout)
1037     if not result:
1038       additional_info = 'No information available.'
1039       # Windows does not support os.kill until Python 2.7.
1040       if not self.IsWin() and _BROWSER_PID:
1041         browser_pid_exists = True
1042         # Does the browser PID exist?
1043         try:
1044           # Does not actually kill the process
1045           os.kill(int(_BROWSER_PID), 0)
1046         except OSError:
1047           browser_pid_exists = False
1048         if browser_pid_exists:
1049           if self._GetResultFromJSONRequestDiagnostics():
1050             # Browser info, worked, that means this hook had a problem
1051             additional_info = ('The browser process ID %d still exists. '
1052                                'PyAuto was able to obtain browser info. It '
1053                                'is possible this hook is broken.'
1054                                % _BROWSER_PID)
1055           else:
1056             additional_info = ('The browser process ID %d still exists. '
1057                                'PyAuto was not able to obtain browser info. '
1058                                'It is possible the browser is hung.'
1059                                % _BROWSER_PID)
1060         else:
1061           additional_info = ('The browser process ID %d no longer exists. '
1062                              'Perhaps the browser crashed.' % _BROWSER_PID)
1063       elif not _BROWSER_PID:
1064         additional_info = ('The browser PID was not obtained. Does this test '
1065                            'have a unique startup configuration?')
1066       # Mask private data if it is in the JSON dictionary
1067       cmd_dict_copy = copy.copy(cmd_dict)
1068       if 'password' in cmd_dict_copy.keys():
1069         cmd_dict_copy['password'] = '**********'
1070       if 'username' in cmd_dict_copy.keys():
1071         cmd_dict_copy['username'] = 'removed_username'
1072       raise JSONInterfaceError('Automation call %s received empty response.  '
1073                                'Additional information:\n%s' % (cmd_dict_copy,
1074                                additional_info))
1075     ret_dict = json.loads(result)
1076     if ret_dict.has_key('error'):
1077       if ret_dict.get('is_interface_timeout'):
1078         raise AutomationCommandTimeout(ret_dict['error'])
1079       elif ret_dict.get('is_interface_error'):
1080         raise JSONInterfaceError(ret_dict['error'])
1081       else:
1082         raise AutomationCommandFail(ret_dict['error'])
1083     return ret_dict
1084
1085   def NavigateToURL(self, url, windex=0, tab_index=None, navigation_count=1):
1086     """Navigate the given tab to the given URL.
1087
1088     Note that this method also activates the corresponding tab/window if it's
1089     not active already. Blocks until |navigation_count| navigations have
1090     completed.
1091
1092     Args:
1093       url: The URL to which to navigate, can be a string or GURL object.
1094       windex: The index of the browser window to work on. Defaults to the first
1095           window.
1096       tab_index: The index of the tab to work on. Defaults to the active tab.
1097       navigation_count: the number of navigations to wait for. Defaults to 1.
1098
1099     Raises:
1100       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1101     """
1102     if isinstance(url, GURL):
1103       url = url.spec()
1104     if tab_index is None:
1105       tab_index = self.GetActiveTabIndex(windex)
1106     cmd_dict = {
1107         'command': 'NavigateToURL',
1108         'url': url,
1109         'windex': windex,
1110         'tab_index': tab_index,
1111         'navigation_count': navigation_count,
1112     }
1113     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1114
1115   def NavigateToURLAsync(self, url, windex=0, tab_index=None):
1116     """Initiate a URL navigation.
1117
1118     A wrapper for NavigateToURL with navigation_count set to 0.
1119     """
1120     self.NavigateToURL(url, windex, tab_index, 0)
1121
1122   def ApplyAccelerator(self, accelerator, windex=0):
1123     """Apply the accelerator with the given id.
1124
1125     Note that this method schedules the accelerator, but does not wait for it to
1126     actually finish doing anything.
1127
1128     Args:
1129       accelerator: The accelerator id, IDC_BACK, IDC_NEWTAB, etc. The list of
1130           ids can be found at chrome/app/chrome_command_ids.h.
1131       windex: The index of the browser window to work on. Defaults to the first
1132           window.
1133
1134     Raises:
1135       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1136     """
1137
1138     cmd_dict = {
1139         'command': 'ApplyAccelerator',
1140         'accelerator': accelerator,
1141         'windex': windex,
1142     }
1143     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1144
1145   def RunCommand(self, accelerator, windex=0):
1146     """Apply the accelerator with the given id and wait for it to finish.
1147
1148     This is like ApplyAccelerator except that it waits for the command to finish
1149     executing.
1150
1151     Args:
1152       accelerator: The accelerator id. The list of ids can be found at
1153           chrome/app/chrome_command_ids.h.
1154       windex: The index of the browser window to work on. Defaults to the first
1155           window.
1156
1157     Raises:
1158       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1159     """
1160     cmd_dict = {
1161         'command': 'RunCommand',
1162         'accelerator': accelerator,
1163         'windex': windex,
1164     }
1165     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1166
1167   def IsMenuCommandEnabled(self, accelerator, windex=0):
1168     """Check if a command is enabled for a window.
1169
1170     Returns true if the command with the given accelerator id is enabled on the
1171     given window.
1172
1173     Args:
1174       accelerator: The accelerator id. The list of ids can be found at
1175           chrome/app/chrome_command_ids.h.
1176       windex: The index of the browser window to work on. Defaults to the first
1177           window.
1178
1179     Returns:
1180       True if the command is enabled for the given window.
1181     """
1182     cmd_dict = {
1183         'command': 'IsMenuCommandEnabled',
1184         'accelerator': accelerator,
1185         'windex': windex,
1186     }
1187     return self._GetResultFromJSONRequest(cmd_dict, windex=None).get('enabled')
1188
1189   def TabGoForward(self, tab_index=0, windex=0):
1190     """Navigate a tab forward in history.
1191
1192     Equivalent to clicking the Forward button in the UI. Activates the tab as a
1193     side effect.
1194
1195     Raises:
1196       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1197     """
1198     self.ActivateTab(tab_index, windex)
1199     self.RunCommand(IDC_FORWARD, windex)
1200
1201   def TabGoBack(self, tab_index=0, windex=0):
1202     """Navigate a tab backwards in history.
1203
1204     Equivalent to clicking the Back button in the UI. Activates the tab as a
1205     side effect.
1206
1207     Raises:
1208       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1209     """
1210     self.ActivateTab(tab_index, windex)
1211     self.RunCommand(IDC_BACK, windex)
1212
1213   def ReloadTab(self, tab_index=0, windex=0):
1214     """Reload the given tab.
1215
1216     Blocks until the page has reloaded.
1217
1218     Args:
1219       tab_index: The index of the tab to reload. Defaults to 0.
1220       windex: The index of the browser window to work on. Defaults to the first
1221           window.
1222
1223     Raises:
1224       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1225     """
1226     self.ActivateTab(tab_index, windex)
1227     self.RunCommand(IDC_RELOAD, windex)
1228
1229   def CloseTab(self, tab_index=0, windex=0, wait_until_closed=True):
1230     """Close the given tab.
1231
1232     Note: Be careful closing the last tab in a window as it may close the
1233         browser.
1234
1235     Args:
1236       tab_index: The index of the tab to reload. Defaults to 0.
1237       windex: The index of the browser window to work on. Defaults to the first
1238           window.
1239       wait_until_closed: Whether to block until the tab finishes closing.
1240
1241     Raises:
1242       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1243     """
1244     cmd_dict = {
1245         'command': 'CloseTab',
1246         'tab_index': tab_index,
1247         'windex': windex,
1248         'wait_until_closed': wait_until_closed,
1249     }
1250     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1251
1252   def WaitForTabToBeRestored(self, tab_index=0, windex=0, timeout=-1):
1253     """Wait for the given tab to be restored.
1254
1255     Args:
1256       tab_index: The index of the tab to reload. Defaults to 0.
1257       windex: The index of the browser window to work on. Defaults to the first
1258           window.
1259       timeout: Timeout in milliseconds.
1260
1261     Raises:
1262       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1263     """
1264     cmd_dict = {
1265         'command': 'CloseTab',
1266         'tab_index': tab_index,
1267         'windex': windex,
1268     }
1269     self._GetResultFromJSONRequest(cmd_dict, windex=None, timeout=timeout)
1270
1271   def ReloadActiveTab(self, windex=0):
1272     """Reload an active tab.
1273
1274     Warning: Depending on the concept of an active tab is dangerous as it can
1275     change during the test. Use ReloadTab and supply a tab_index explicitly.
1276
1277     Args:
1278       windex: The index of the browser window to work on. Defaults to the first
1279           window.
1280
1281     Raises:
1282       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1283     """
1284     self.ReloadTab(self.GetActiveTabIndex(windex), windex)
1285
1286   def GetActiveTabIndex(self, windex=0):
1287     """Get the index of the currently active tab in the given browser window.
1288
1289     Warning: Depending on the concept of an active tab is dangerous as it can
1290     change during the test. Supply the tab_index explicitly, if possible.
1291
1292     Args:
1293       windex: The index of the browser window to work on. Defaults to the first
1294           window.
1295
1296     Returns:
1297       An integer index for the currently active tab.
1298     """
1299     cmd_dict = {
1300         'command': 'GetActiveTabIndex',
1301         'windex': windex,
1302     }
1303     return self._GetResultFromJSONRequest(cmd_dict,
1304                                           windex=None).get('tab_index')
1305
1306   def ActivateTab(self, tab_index=0, windex=0):
1307     """Activates the given tab in the specified window.
1308
1309     Warning: Depending on the concept of an active tab is dangerous as it can
1310     change during the test. Instead use functions that accept a tab_index
1311     explicitly.
1312
1313     Args:
1314       tab_index: Integer index of the tab to activate; defaults to 0.
1315       windex: Integer index of the browser window to use; defaults to the first
1316           window.
1317
1318     Raises:
1319       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1320     """
1321     cmd_dict = {
1322         'command': 'ActivateTab',
1323         'tab_index': tab_index,
1324         'windex': windex,
1325     }
1326     self.BringBrowserToFront(windex)
1327     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1328
1329   def BringBrowserToFront(self, windex=0):
1330     """Activate the browser's window and bring it to front.
1331
1332     Args:
1333       windex: Integer index of the browser window to use; defaults to the first
1334           window.
1335
1336     Raises:
1337       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1338     """
1339     cmd_dict = {
1340         'command': 'BringBrowserToFront',
1341         'windex': windex,
1342     }
1343     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1344
1345   def GetBrowserWindowCount(self):
1346     """Get the browser window count.
1347
1348     Args:
1349       None.
1350
1351     Returns:
1352       Integer count of the number of browser windows. Includes popups.
1353
1354     Raises:
1355       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1356     """
1357     cmd_dict = {'command': 'GetBrowserWindowCount'}
1358     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['count']
1359
1360   def OpenNewBrowserWindow(self, show):
1361     """Create a new browser window.
1362
1363     Args:
1364       show: Boolean indicating whether to show the window.
1365
1366     Raises:
1367       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1368     """
1369     cmd_dict = {
1370         'command': 'OpenNewBrowserWindow',
1371         'show': show,
1372     }
1373     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1374
1375   def CloseBrowserWindow(self, windex=0):
1376     """Create a new browser window.
1377
1378     Args:
1379       windex: Index of the browser window to close; defaults to 0.
1380
1381     Raises:
1382       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1383     """
1384     cmd_dict = {
1385         'command': 'CloseBrowserWindow',
1386         'windex': windex,
1387     }
1388     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1389
1390   def AppendTab(self, url, windex=0):
1391     """Append a new tab.
1392
1393     Creates a new tab at the end of given browser window and activates
1394     it. Blocks until the specified |url| is loaded.
1395
1396     Args:
1397       url: The url to load, can be string or a GURL object.
1398       windex: The index of the browser window to work on. Defaults to the first
1399           window.
1400
1401     Returns:
1402       True if the url loads successfully in the new tab. False otherwise.
1403
1404     Raises:
1405       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1406     """
1407     if isinstance(url, GURL):
1408       url = url.spec()
1409     cmd_dict = {
1410         'command': 'AppendTab',
1411         'url': url,
1412         'windex': windex,
1413     }
1414     return self._GetResultFromJSONRequest(cmd_dict, windex=None).get('result')
1415
1416   def GetTabCount(self, windex=0):
1417     """Gets the number of tab in the given browser window.
1418
1419     Args:
1420       windex: Integer index of the browser window to use; defaults to the first
1421           window.
1422
1423     Returns:
1424       The tab count.
1425
1426     Raises:
1427       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1428     """
1429     cmd_dict = {
1430         'command': 'GetTabCount',
1431         'windex': windex,
1432     }
1433     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['tab_count']
1434
1435   def GetTabInfo(self, tab_index=0, windex=0):
1436     """Gets information about the specified tab.
1437
1438     Args:
1439       tab_index: Integer index of the tab to activate; defaults to 0.
1440       windex: Integer index of the browser window to use; defaults to the first
1441           window.
1442
1443     Returns:
1444       A dictionary containing information about the tab.
1445       Example:
1446         { u'title': "Hello World",
1447           u'url': "http://foo.bar", }
1448
1449     Raises:
1450       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1451     """
1452     cmd_dict = {
1453         'command': 'GetTabInfo',
1454         'tab_index': tab_index,
1455         'windex': windex,
1456     }
1457     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
1458
1459   def GetActiveTabTitle(self, windex=0):
1460     """Gets the title of the active tab.
1461
1462     Warning: Depending on the concept of an active tab is dangerous as it can
1463     change during the test. Use GetTabInfo and supply a tab_index explicitly.
1464
1465     Args:
1466       windex: Integer index of the browser window to use; defaults to the first
1467           window.
1468
1469     Returns:
1470       The tab title as a string.
1471
1472     Raises:
1473       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1474     """
1475     return self.GetTabInfo(self.GetActiveTabIndex(windex), windex)['title']
1476
1477   def GetActiveTabURL(self, windex=0):
1478     """Gets the URL of the active tab.
1479
1480     Warning: Depending on the concept of an active tab is dangerous as it can
1481     change during the test. Use GetTabInfo and supply a tab_index explicitly.
1482
1483     Args:
1484       windex: Integer index of the browser window to use; defaults to the first
1485           window.
1486
1487     Returns:
1488       The tab URL as a GURL object.
1489
1490     Raises:
1491       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1492     """
1493     return GURL(str(self.GetTabInfo(self.GetActiveTabIndex(windex),
1494                                     windex)['url']))
1495
1496   def ActionOnSSLBlockingPage(self, tab_index=0, windex=0, proceed=True):
1497     """Take action on an interstitial page.
1498
1499     Calling this when an interstitial page is not showing is an error.
1500
1501     Args:
1502       tab_index: Integer index of the tab to activate; defaults to 0.
1503       windex: Integer index of the browser window to use; defaults to the first
1504           window.
1505       proceed: Whether to proceed to the URL or not.
1506
1507     Raises:
1508       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1509     """
1510     cmd_dict = {
1511         'command': 'ActionOnSSLBlockingPage',
1512         'tab_index': tab_index,
1513         'windex': windex,
1514         'proceed': proceed,
1515     }
1516     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
1517
1518   def GetBookmarkModel(self, windex=0):
1519     """Return the bookmark model as a BookmarkModel object.
1520
1521     This is a snapshot of the bookmark model; it is not a proxy and
1522     does not get updated as the bookmark model changes.
1523     """
1524     bookmarks_as_json = self._GetBookmarksAsJSON(windex)
1525     if not bookmarks_as_json:
1526       raise JSONInterfaceError('Could not resolve browser proxy.')
1527     return bookmark_model.BookmarkModel(bookmarks_as_json)
1528
1529   def _GetBookmarksAsJSON(self, windex=0):
1530     """Get bookmarks as a JSON dictionary; used by GetBookmarkModel()."""
1531     cmd_dict = {
1532         'command': 'GetBookmarksAsJSON',
1533         'windex': windex,
1534     }
1535     self.WaitForBookmarkModelToLoad(windex)
1536     return self._GetResultFromJSONRequest(cmd_dict,
1537                                           windex=None)['bookmarks_as_json']
1538
1539   def WaitForBookmarkModelToLoad(self, windex=0):
1540     """Gets the status of the bookmark bar as a dictionary.
1541
1542     Args:
1543       windex: Integer index of the browser window to use; defaults to the first
1544           window.
1545
1546     Raises:
1547       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1548     """
1549     cmd_dict = {
1550         'command': 'WaitForBookmarkModelToLoad',
1551         'windex': windex,
1552     }
1553     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
1554
1555   def GetBookmarkBarStatus(self, windex=0):
1556     """Gets the status of the bookmark bar as a dictionary.
1557
1558     Args:
1559       windex: Integer index of the browser window to use; defaults to the first
1560           window.
1561
1562     Returns:
1563       A dictionary.
1564       Example:
1565         { u'visible': True,
1566           u'animating': False,
1567           u'detached': False, }
1568
1569     Raises:
1570       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1571     """
1572     cmd_dict = {
1573         'command': 'GetBookmarkBarStatus',
1574         'windex': windex,
1575     }
1576     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
1577
1578   def GetBookmarkBarStatus(self, windex=0):
1579     """Gets the status of the bookmark bar as a dictionary.
1580
1581     Args:
1582       windex: Integer index of the browser window to use; defaults to the first
1583           window.
1584
1585     Returns:
1586       A dictionary.
1587       Example:
1588         { u'visible': True,
1589           u'animating': False,
1590           u'detached': False, }
1591
1592     Raises:
1593       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1594     """
1595     cmd_dict = {
1596         'command': 'GetBookmarkBarStatus',
1597         'windex': windex,
1598     }
1599     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
1600
1601   def GetBookmarkBarStatus(self, windex=0):
1602     """Gets the status of the bookmark bar as a dictionary.
1603
1604     Args:
1605       windex: Integer index of the browser window to use; defaults to the first
1606           window.
1607
1608     Returns:
1609       A dictionary.
1610       Example:
1611         { u'visible': True,
1612           u'animating': False,
1613           u'detached': False, }
1614
1615     Raises:
1616       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1617     """
1618     cmd_dict = {
1619         'command': 'GetBookmarkBarStatus',
1620         'windex': windex,
1621     }
1622     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
1623
1624   def GetBookmarkBarVisibility(self, windex=0):
1625     """Returns the visibility of the bookmark bar.
1626
1627     Args:
1628       windex: Integer index of the browser window to use; defaults to the first
1629           window.
1630
1631     Returns:
1632       True if the bookmark bar is visible, false otherwise.
1633
1634     Raises:
1635       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1636     """
1637     return self.GetBookmarkBarStatus(windex)['visible']
1638
1639   def AddBookmarkGroup(self, parent_id, index, title, windex=0):
1640     """Adds a bookmark folder.
1641
1642     Args:
1643       parent_id: The parent bookmark folder.
1644       index: The location in the parent's list to insert this bookmark folder.
1645       title: The name of the bookmark folder.
1646       windex: Integer index of the browser window to use; defaults to the first
1647           window.
1648
1649     Returns:
1650       True if the bookmark bar is detached, false otherwise.
1651
1652     Raises:
1653       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1654     """
1655     if isinstance(parent_id, basestring):
1656       parent_id = int(parent_id)
1657     cmd_dict = {
1658         'command': 'AddBookmark',
1659         'parent_id': parent_id,
1660         'index': index,
1661         'title': title,
1662         'is_folder': True,
1663         'windex': windex,
1664     }
1665     self.WaitForBookmarkModelToLoad(windex)
1666     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1667
1668   def AddBookmarkURL(self, parent_id, index, title, url, windex=0):
1669     """Add a bookmark URL.
1670
1671     Args:
1672       parent_id: The parent bookmark folder.
1673       index: The location in the parent's list to insert this bookmark.
1674       title: The name of the bookmark.
1675       url: The url of the bookmark.
1676       windex: Integer index of the browser window to use; defaults to the first
1677           window.
1678
1679     Raises:
1680       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1681     """
1682     if isinstance(parent_id, basestring):
1683       parent_id = int(parent_id)
1684     cmd_dict = {
1685         'command': 'AddBookmark',
1686         'parent_id': parent_id,
1687         'index': index,
1688         'title': title,
1689         'url': url,
1690         'is_folder': False,
1691         'windex': windex,
1692     }
1693     self.WaitForBookmarkModelToLoad(windex)
1694     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1695
1696   def ReparentBookmark(self, id, new_parent_id, index, windex=0):
1697     """Move a bookmark.
1698
1699     Args:
1700       id: The bookmark to move.
1701       new_parent_id: The new parent bookmark folder.
1702       index: The location in the parent's list to insert this bookmark.
1703       windex: Integer index of the browser window to use; defaults to the first
1704           window.
1705
1706     Raises:
1707       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1708     """
1709     if isinstance(id, basestring):
1710       id = int(id)
1711     if isinstance(new_parent_id, basestring):
1712       new_parent_id = int(new_parent_id)
1713     cmd_dict = {
1714         'command': 'ReparentBookmark',
1715         'id': id,
1716         'new_parent_id': new_parent_id,
1717         'index': index,
1718         'windex': windex,
1719     }
1720     self.WaitForBookmarkModelToLoad(windex)
1721     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1722
1723   def SetBookmarkTitle(self, id, title, windex=0):
1724     """Change the title of a bookmark.
1725
1726     Args:
1727       id: The bookmark to rename.
1728       title: The new title for the bookmark.
1729       windex: Integer index of the browser window to use; defaults to the first
1730           window.
1731
1732     Raises:
1733       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1734     """
1735     if isinstance(id, basestring):
1736       id = int(id)
1737     cmd_dict = {
1738         'command': 'SetBookmarkTitle',
1739         'id': id,
1740         'title': title,
1741         'windex': windex,
1742     }
1743     self.WaitForBookmarkModelToLoad(windex)
1744     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1745
1746   def SetBookmarkURL(self, id, url, windex=0):
1747     """Change the URL of a bookmark.
1748
1749     Args:
1750       id: The bookmark to change.
1751       url: The new url for the bookmark.
1752       windex: Integer index of the browser window to use; defaults to the first
1753           window.
1754
1755     Raises:
1756       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1757     """
1758     if isinstance(id, basestring):
1759       id = int(id)
1760     cmd_dict = {
1761         'command': 'SetBookmarkURL',
1762         'id': id,
1763         'url': url,
1764         'windex': windex,
1765     }
1766     self.WaitForBookmarkModelToLoad(windex)
1767     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1768
1769   def RemoveBookmark(self, id, windex=0):
1770     """Remove a bookmark.
1771
1772     Args:
1773       id: The bookmark to remove.
1774       windex: Integer index of the browser window to use; defaults to the first
1775           window.
1776
1777     Raises:
1778       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1779     """
1780     if isinstance(id, basestring):
1781       id = int(id)
1782     cmd_dict = {
1783         'command': 'RemoveBookmark',
1784         'id': id,
1785         'windex': windex,
1786     }
1787     self.WaitForBookmarkModelToLoad(windex)
1788     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1789
1790   def GetDownloadsInfo(self, windex=0):
1791     """Return info about downloads.
1792
1793     This includes all the downloads recognized by the history system.
1794
1795     Returns:
1796       an instance of downloads_info.DownloadInfo
1797     """
1798     return download_info.DownloadInfo(
1799         self._GetResultFromJSONRequest({'command': 'GetDownloadsInfo'},
1800                                        windex=windex))
1801
1802   def GetOmniboxInfo(self, windex=0):
1803     """Return info about Omnibox.
1804
1805     This represents a snapshot of the omnibox.  If you expect changes
1806     you need to call this method again to get a fresh snapshot.
1807     Note that this DOES NOT shift focus to the omnibox; you've to ensure that
1808     the omnibox is in focus or else you won't get any interesting info.
1809
1810     It's OK to call this even when the omnibox popup is not showing.  In this
1811     case however, there won't be any matches, but other properties (like the
1812     current text in the omnibox) will still be fetched.
1813
1814     Due to the nature of the omnibox, this function is sensitive to mouse
1815     focus.  DO NOT HOVER MOUSE OVER OMNIBOX OR CHANGE WINDOW FOCUS WHEN USING
1816     THIS METHOD.
1817
1818     Args:
1819       windex: the index of the browser window to work on.
1820               Default: 0 (first window)
1821
1822     Returns:
1823       an instance of omnibox_info.OmniboxInfo
1824     """
1825     return omnibox_info.OmniboxInfo(
1826         self._GetResultFromJSONRequest({'command': 'GetOmniboxInfo'},
1827                                        windex=windex))
1828
1829   def SetOmniboxText(self, text, windex=0):
1830     """Enter text into the omnibox. This shifts focus to the omnibox.
1831
1832     Args:
1833       text: the text to be set.
1834       windex: the index of the browser window to work on.
1835               Default: 0 (first window)
1836     """
1837     # Ensure that keyword data is loaded from the profile.
1838     # This would normally be triggered by the user inputting this text.
1839     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'})
1840     cmd_dict = {
1841         'command': 'SetOmniboxText',
1842         'text': text,
1843     }
1844     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
1845
1846   # TODO(ace): Remove this hack, update bug 62783.
1847   def WaitUntilOmniboxReadyHack(self, windex=0):
1848     """Wait until the omnibox is ready for input.
1849
1850     This is a hack workaround for linux platform, which returns from
1851     synchronous window creation methods before the omnibox is fully functional.
1852
1853     No-op on non-linux platforms.
1854
1855     Args:
1856       windex: the index of the browser to work on.
1857     """
1858     if self.IsLinux():
1859       return self.WaitUntil(
1860           lambda : self.GetOmniboxInfo(windex).Properties('has_focus'))
1861
1862   def WaitUntilOmniboxQueryDone(self, windex=0):
1863     """Wait until omnibox has finished populating results.
1864
1865     Uses WaitUntil() so the wait duration is capped by the timeout values
1866     used by automation, which WaitUntil() uses.
1867
1868     Args:
1869       windex: the index of the browser window to work on.
1870               Default: 0 (first window)
1871     """
1872     return self.WaitUntil(
1873         lambda : not self.GetOmniboxInfo(windex).IsQueryInProgress())
1874
1875   def OmniboxMovePopupSelection(self, count, windex=0):
1876     """Move omnibox popup selection up or down.
1877
1878     Args:
1879       count: number of rows by which to move.
1880              -ve implies down, +ve implies up
1881       windex: the index of the browser window to work on.
1882               Default: 0 (first window)
1883     """
1884     cmd_dict = {
1885         'command': 'OmniboxMovePopupSelection',
1886         'count': count,
1887     }
1888     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
1889
1890   def OmniboxAcceptInput(self, windex=0):
1891     """Accepts the current string of text in the omnibox.
1892
1893     This is equivalent to clicking or hiting enter on a popup selection.
1894     Blocks until the page loads.
1895
1896     Args:
1897       windex: the index of the browser window to work on.
1898               Default: 0 (first window)
1899     """
1900     cmd_dict = {
1901         'command': 'OmniboxAcceptInput',
1902     }
1903     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
1904
1905   def GetCookie(self, url, windex=0):
1906     """Get the value of the cookie at url in context of the specified browser.
1907
1908     Args:
1909       url: Either a GURL object or url string specifing the cookie url.
1910       windex: The index of the browser window to work on. Defaults to the first
1911           window.
1912
1913     Raises:
1914       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1915     """
1916     if isinstance(url, GURL):
1917       url = url.spec()
1918     cmd_dict = {
1919         'command': 'GetCookiesInBrowserContext',
1920         'url': url,
1921         'windex': windex,
1922     }
1923     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['cookies']
1924
1925   def DeleteCookie(self, url, cookie_name, windex=0):
1926     """Delete the cookie at url with name cookie_name.
1927
1928     Args:
1929       url: Either a GURL object or url string specifing the cookie url.
1930       cookie_name: The name of the cookie to delete as a string.
1931       windex: The index of the browser window to work on. Defaults to the first
1932           window.
1933
1934     Raises:
1935       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1936     """
1937     if isinstance(url, GURL):
1938       url = url.spec()
1939     cmd_dict = {
1940         'command': 'DeleteCookieInBrowserContext',
1941         'url': url,
1942         'cookie_name': cookie_name,
1943         'windex': windex,
1944     }
1945     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1946
1947   def SetCookie(self, url, value, windex=0):
1948     """Set the value of the cookie at url to value in the context of a browser.
1949
1950     Args:
1951       url: Either a GURL object or url string specifing the cookie url.
1952       value: A string to set as the cookie's value.
1953       windex: The index of the browser window to work on. Defaults to the first
1954           window.
1955
1956     Raises:
1957       pyauto_errors.JSONInterfaceError if the automation call returns an error.
1958     """
1959     if isinstance(url, GURL):
1960       url = url.spec()
1961     cmd_dict = {
1962         'command': 'SetCookieInBrowserContext',
1963         'url': url,
1964         'value': value,
1965         'windex': windex,
1966     }
1967     self._GetResultFromJSONRequest(cmd_dict, windex=None)
1968
1969   def GetSearchEngineInfo(self, windex=0):
1970     """Return info about search engines.
1971
1972     Args:
1973       windex: The window index, default is 0.
1974
1975     Returns:
1976       An ordered list of dictionaries describing info about each search engine.
1977
1978       Example:
1979         [ { u'display_url': u'{google:baseURL}search?q=%s',
1980             u'host': u'www.google.com',
1981             u'in_default_list': True,
1982             u'is_default': True,
1983             u'is_valid': True,
1984             u'keyword': u'google.com',
1985             u'path': u'/search',
1986             u'short_name': u'Google',
1987             u'supports_replacement': True,
1988             u'url': u'{google:baseURL}search?q={searchTerms}'},
1989           { u'display_url': u'http://search.yahoo.com/search?p=%s',
1990             u'host': u'search.yahoo.com',
1991             u'in_default_list': True,
1992             u'is_default': False,
1993             u'is_valid': True,
1994             u'keyword': u'yahoo.com',
1995             u'path': u'/search',
1996             u'short_name': u'Yahoo!',
1997             u'supports_replacement': True,
1998             u'url': u'http://search.yahoo.com/search?p={searchTerms}'},
1999     """
2000     # Ensure that the search engine profile is loaded into data model.
2001     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
2002                                    windex=windex)
2003     cmd_dict = {'command': 'GetSearchEngineInfo'}
2004     return self._GetResultFromJSONRequest(
2005         cmd_dict, windex=windex)['search_engines']
2006
2007   def AddSearchEngine(self, title, keyword, url, windex=0):
2008     """Add a search engine, as done through the search engines UI.
2009
2010     Args:
2011       title: name for search engine.
2012       keyword: keyword, used to initiate a custom search from omnibox.
2013       url: url template for this search engine's query.
2014            '%s' is replaced by search query string when used to search.
2015       windex: The window index, default is 0.
2016     """
2017     # Ensure that the search engine profile is loaded into data model.
2018     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
2019                                    windex=windex)
2020     cmd_dict = {'command': 'AddOrEditSearchEngine',
2021                 'new_title': title,
2022                 'new_keyword': keyword,
2023                 'new_url': url}
2024     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2025
2026   def EditSearchEngine(self, keyword, new_title, new_keyword, new_url,
2027                        windex=0):
2028     """Edit info for existing search engine.
2029
2030     Args:
2031       keyword: existing search engine keyword.
2032       new_title: new name for this search engine.
2033       new_keyword: new keyword for this search engine.
2034       new_url: new url for this search engine.
2035       windex: The window index, default is 0.
2036     """
2037     # Ensure that the search engine profile is loaded into data model.
2038     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
2039                                    windex=windex)
2040     cmd_dict = {'command': 'AddOrEditSearchEngine',
2041                 'keyword': keyword,
2042                 'new_title': new_title,
2043                 'new_keyword': new_keyword,
2044                 'new_url': new_url}
2045     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2046
2047   def DeleteSearchEngine(self, keyword, windex=0):
2048     """Delete search engine with given keyword.
2049
2050     Args:
2051       keyword: the keyword string of the search engine to delete.
2052       windex: The window index, default is 0.
2053     """
2054     # Ensure that the search engine profile is loaded into data model.
2055     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
2056                                    windex=windex)
2057     cmd_dict = {'command': 'PerformActionOnSearchEngine', 'keyword': keyword,
2058                 'action': 'delete'}
2059     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2060
2061   def MakeSearchEngineDefault(self, keyword, windex=0):
2062     """Make search engine with given keyword the default search.
2063
2064     Args:
2065       keyword: the keyword string of the search engine to make default.
2066       windex: The window index, default is 0.
2067     """
2068     # Ensure that the search engine profile is loaded into data model.
2069     self._GetResultFromJSONRequest({'command': 'LoadSearchEngineInfo'},
2070                                    windex=windex)
2071     cmd_dict = {'command': 'PerformActionOnSearchEngine', 'keyword': keyword,
2072                 'action': 'default'}
2073     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2074
2075   def GetLocalStatePrefsInfo(self):
2076     """Return info about preferences.
2077
2078     This represents a snapshot of the local state preferences. If you expect
2079     local state preferences to have changed, you need to call this method again
2080     to get a fresh snapshot.
2081
2082     Returns:
2083       an instance of prefs_info.PrefsInfo
2084     """
2085     return prefs_info.PrefsInfo(
2086         self._GetResultFromJSONRequest({'command': 'GetLocalStatePrefsInfo'},
2087                                        windex=None))
2088
2089   def SetLocalStatePrefs(self, path, value):
2090     """Set local state preference for the given path.
2091
2092     Preferences are stored by Chromium as a hierarchical dictionary.
2093     dot-separated paths can be used to refer to a particular preference.
2094     example: "session.restore_on_startup"
2095
2096     Some preferences are managed, that is, they cannot be changed by the
2097     user. It's up to the user to know which ones can be changed. Typically,
2098     the options available via Chromium preferences can be changed.
2099
2100     Args:
2101       path: the path the preference key that needs to be changed
2102             example: "session.restore_on_startup"
2103             One of the equivalent names in chrome/common/pref_names.h could
2104             also be used.
2105       value: the value to be set. It could be plain values like int, bool,
2106              string or complex ones like list.
2107              The user has to ensure that the right value is specified for the
2108              right key. It's useful to dump the preferences first to determine
2109              what type is expected for a particular preference path.
2110     """
2111     cmd_dict = {
2112       'command': 'SetLocalStatePrefs',
2113       'windex': 0,
2114       'path': path,
2115       'value': value,
2116     }
2117     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2118
2119   def GetPrefsInfo(self, windex=0):
2120     """Return info about preferences.
2121
2122     This represents a snapshot of the preferences. If you expect preferences
2123     to have changed, you need to call this method again to get a fresh
2124     snapshot.
2125
2126     Args:
2127       windex: The window index, default is 0.
2128     Returns:
2129       an instance of prefs_info.PrefsInfo
2130     """
2131     cmd_dict = {
2132       'command': 'GetPrefsInfo',
2133       'windex': windex,
2134     }
2135     return prefs_info.PrefsInfo(
2136         self._GetResultFromJSONRequest(cmd_dict, windex=None))
2137
2138   def SetPrefs(self, path, value, windex=0):
2139     """Set preference for the given path.
2140
2141     Preferences are stored by Chromium as a hierarchical dictionary.
2142     dot-separated paths can be used to refer to a particular preference.
2143     example: "session.restore_on_startup"
2144
2145     Some preferences are managed, that is, they cannot be changed by the
2146     user. It's up to the user to know which ones can be changed. Typically,
2147     the options available via Chromium preferences can be changed.
2148
2149     Args:
2150       path: the path the preference key that needs to be changed
2151             example: "session.restore_on_startup"
2152             One of the equivalent names in chrome/common/pref_names.h could
2153             also be used.
2154       value: the value to be set. It could be plain values like int, bool,
2155              string or complex ones like list.
2156              The user has to ensure that the right value is specified for the
2157              right key. It's useful to dump the preferences first to determine
2158              what type is expected for a particular preference path.
2159       windex: window index to work on. Defaults to 0 (first window).
2160     """
2161     cmd_dict = {
2162       'command': 'SetPrefs',
2163       'windex': windex,
2164       'path': path,
2165       'value': value,
2166     }
2167     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2168
2169   def SendWebkitKeyEvent(self, key_type, key_code, tab_index=0, windex=0):
2170     """Send a webkit key event to the browser.
2171
2172     Args:
2173       key_type: the raw key type such as 0 for up and 3 for down.
2174       key_code: the hex value associated with the keypress (virtual key code).
2175       tab_index: tab index to work on. Defaults to 0 (first tab).
2176       windex: window index to work on. Defaults to 0 (first window).
2177     """
2178     cmd_dict = {
2179       'command': 'SendWebkitKeyEvent',
2180       'type': key_type,
2181       'text': '',
2182       'isSystemKey': False,
2183       'unmodifiedText': '',
2184       'nativeKeyCode': 0,
2185       'windowsKeyCode': key_code,
2186       'modifiers': 0,
2187       'windex': windex,
2188       'tab_index': tab_index,
2189     }
2190     # Sending request for key event.
2191     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2192
2193   def SendWebkitCharEvent(self, char, tab_index=0, windex=0):
2194     """Send a webkit char to the browser.
2195
2196     Args:
2197       char: the char value to be sent to the browser.
2198       tab_index: tab index to work on. Defaults to 0 (first tab).
2199       windex: window index to work on. Defaults to 0 (first window).
2200     """
2201     cmd_dict = {
2202       'command': 'SendWebkitKeyEvent',
2203       'type': 2,  # kCharType
2204       'text': char,
2205       'isSystemKey': False,
2206       'unmodifiedText': char,
2207       'nativeKeyCode': 0,
2208       'windowsKeyCode': ord((char).upper()),
2209       'modifiers': 0,
2210       'windex': windex,
2211       'tab_index': tab_index,
2212     }
2213     # Sending request for a char.
2214     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2215
2216   def SetDownloadShelfVisible(self, is_visible, windex=0):
2217     """Set download shelf visibility for the specified browser window.
2218
2219     Args:
2220       is_visible: A boolean indicating the desired shelf visibility.
2221       windex: The window index, defaults to 0 (the first window).
2222
2223     Raises:
2224       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2225     """
2226     cmd_dict = {
2227       'command': 'SetDownloadShelfVisible',
2228       'is_visible': is_visible,
2229       'windex': windex,
2230     }
2231     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2232
2233   def IsDownloadShelfVisible(self, windex=0):
2234     """Determine whether the download shelf is visible in the given window.
2235
2236     Args:
2237       windex: The window index, defaults to 0 (the first window).
2238
2239     Returns:
2240       A boolean indicating the shelf visibility.
2241
2242     Raises:
2243       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2244     """
2245     cmd_dict = {
2246       'command': 'IsDownloadShelfVisible',
2247       'windex': windex,
2248     }
2249     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['is_visible']
2250
2251   def GetDownloadDirectory(self, tab_index=None, windex=0):
2252     """Get the path to the download directory.
2253
2254     Warning: Depending on the concept of an active tab is dangerous as it can
2255     change during the test. Always supply a tab_index explicitly.
2256
2257     Args:
2258       tab_index: The index of the tab to work on. Defaults to the active tab.
2259       windex: The index of the browser window to work on. Defaults to 0.
2260
2261     Returns:
2262       The path to the download directory as a FilePath object.
2263
2264     Raises:
2265       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2266     """
2267     if tab_index is None:
2268       tab_index = self.GetActiveTabIndex(windex)
2269     cmd_dict = {
2270       'command': 'GetDownloadDirectory',
2271       'tab_index': tab_index,
2272       'windex': windex,
2273     }
2274     return FilePath(str(self._GetResultFromJSONRequest(cmd_dict,
2275                                                        windex=None)['path']))
2276
2277   def WaitForAllDownloadsToComplete(self, pre_download_ids=[], windex=0,
2278                                     timeout=-1):
2279     """Wait for all pending downloads to complete.
2280
2281     This function assumes that any downloads to wait for have already been
2282     triggered and have started (it is ok if those downloads complete before this
2283     function is called).
2284
2285     Args:
2286       pre_download_ids: A list of numbers representing the IDs of downloads that
2287                         exist *before* downloads to wait for have been
2288                         triggered. Defaults to []; use GetDownloadsInfo() to get
2289                         these IDs (only necessary if a test previously
2290                         downloaded files).
2291       windex: The window index, defaults to 0 (the first window).
2292       timeout: The maximum amount of time (in milliseconds) to wait for
2293                downloads to complete.
2294     """
2295     cmd_dict = {
2296       'command': 'WaitForAllDownloadsToComplete',
2297       'pre_download_ids': pre_download_ids,
2298     }
2299     self._GetResultFromJSONRequest(cmd_dict, windex=windex, timeout=timeout)
2300
2301   def PerformActionOnDownload(self, id, action, window_index=0):
2302     """Perform the given action on the download with the given id.
2303
2304     Args:
2305       id: The id of the download.
2306       action: The action to perform on the download.
2307               Possible actions:
2308                 'open': Opens the download (waits until it has completed first).
2309                 'toggle_open_files_like_this': Toggles the 'Always Open Files
2310                     Of This Type' option.
2311                 'remove': Removes the file from downloads (not from disk).
2312                 'decline_dangerous_download': Equivalent to 'Discard' option
2313                     after downloading a dangerous download (ex. an executable).
2314                 'save_dangerous_download': Equivalent to 'Save' option after
2315                     downloading a dangerous file.
2316                 'pause': Pause the download.  If the download completed before
2317                     this call or is already paused, it's a no-op.
2318                 'resume': Resume the download.  If the download completed before
2319                     this call or was not paused, it's a no-op.
2320                 'cancel': Cancel the download.
2321       window_index: The window index, default is 0.
2322
2323     Returns:
2324       A dictionary representing the updated download item (except in the case
2325       of 'decline_dangerous_download', 'toggle_open_files_like_this', and
2326       'remove', which return an empty dict).
2327       Example dictionary:
2328       { u'PercentComplete': 100,
2329         u'file_name': u'file.txt',
2330         u'full_path': u'/path/to/file.txt',
2331         u'id': 0,
2332         u'is_otr': False,
2333         u'is_paused': False,
2334         u'is_temporary': False,
2335         u'open_when_complete': False,
2336         u'referrer_url': u'',
2337         u'state': u'COMPLETE',
2338         u'danger_type': u'DANGEROUS_FILE',
2339         u'url':  u'file://url/to/file.txt'
2340       }
2341     """
2342     cmd_dict = {  # Prepare command for the json interface
2343       'command': 'PerformActionOnDownload',
2344       'id': id,
2345       'action': action
2346     }
2347     return self._GetResultFromJSONRequest(cmd_dict, windex=window_index)
2348
2349   def DownloadAndWaitForStart(self, file_url, windex=0):
2350     """Trigger download for the given url and wait for downloads to start.
2351
2352     It waits for download by looking at the download info from Chrome, so
2353     anything which isn't registered by the history service won't be noticed.
2354     This is not thread-safe, but it's fine to call this method to start
2355     downloading multiple files in parallel. That is after starting a
2356     download, it's fine to start another one even if the first one hasn't
2357     completed.
2358     """
2359     try:
2360       num_downloads = len(self.GetDownloadsInfo(windex).Downloads())
2361     except JSONInterfaceError:
2362       num_downloads = 0
2363
2364     self.NavigateToURL(file_url, windex)  # Trigger download.
2365     # It might take a while for the download to kick in, hold on until then.
2366     self.assertTrue(self.WaitUntil(
2367         lambda: len(self.GetDownloadsInfo(windex).Downloads()) >
2368                 num_downloads))
2369
2370   def SetWindowDimensions(
2371       self, x=None, y=None, width=None, height=None, windex=0):
2372     """Set window dimensions.
2373
2374     All args are optional and current values will be preserved.
2375     Arbitrarily large values will be handled gracefully by the browser.
2376
2377     Args:
2378       x: window origin x
2379       y: window origin y
2380       width: window width
2381       height: window height
2382       windex: window index to work on. Defaults to 0 (first window)
2383     """
2384     cmd_dict = {  # Prepare command for the json interface
2385       'command': 'SetWindowDimensions',
2386     }
2387     if x:
2388       cmd_dict['x'] = x
2389     if y:
2390       cmd_dict['y'] = y
2391     if width:
2392       cmd_dict['width'] = width
2393     if height:
2394       cmd_dict['height'] = height
2395     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2396
2397   def WaitForInfobarCount(self, count, windex=0, tab_index=0):
2398     """Wait until infobar count becomes |count|.
2399
2400     Note: Wait duration is capped by the automation timeout.
2401
2402     Args:
2403       count: requested number of infobars
2404       windex: window index.  Defaults to 0 (first window)
2405       tab_index: tab index  Defaults to 0 (first tab)
2406
2407     Raises:
2408       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2409     """
2410     # TODO(phajdan.jr): We need a solid automation infrastructure to handle
2411     # these cases. See crbug.com/53647.
2412     def _InfobarCount():
2413       windows = self.GetBrowserInfo()['windows']
2414       if windex >= len(windows):  # not enough windows
2415         return -1
2416       tabs = windows[windex]['tabs']
2417       if tab_index >= len(tabs):  # not enough tabs
2418         return -1
2419       return len(tabs[tab_index]['infobars'])
2420
2421     return self.WaitUntil(_InfobarCount, expect_retval=count)
2422
2423   def PerformActionOnInfobar(
2424       self, action, infobar_index, windex=0, tab_index=0):
2425     """Perform actions on an infobar.
2426
2427     Args:
2428       action: the action to be performed.
2429               Actions depend on the type of the infobar.  The user needs to
2430               call the right action for the right infobar.
2431               Valid inputs are:
2432               - "dismiss": closes the infobar (for all infobars)
2433               - "accept", "cancel": click accept / cancel (for confirm infobars)
2434               - "allow", "deny": click allow / deny (for media stream infobars)
2435       infobar_index: 0-based index of the infobar on which to perform the action
2436       windex: 0-based window index  Defaults to 0 (first window)
2437       tab_index: 0-based tab index.  Defaults to 0 (first tab)
2438
2439     Raises:
2440       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2441     """
2442     cmd_dict = {
2443       'command': 'PerformActionOnInfobar',
2444       'action': action,
2445       'infobar_index': infobar_index,
2446       'tab_index': tab_index,
2447     }
2448     if action not in ('dismiss', 'accept', 'allow', 'deny', 'cancel'):
2449       raise JSONInterfaceError('Invalid action %s' % action)
2450     self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2451
2452   def GetBrowserInfo(self):
2453     """Return info about the browser.
2454
2455     This includes things like the version number, the executable name,
2456     executable path, pid info about the renderer/plugin/extension processes,
2457     window dimensions. (See sample below)
2458
2459     For notification pid info, see 'GetActiveNotifications'.
2460
2461     Returns:
2462       a dictionary
2463
2464       Sample:
2465       { u'browser_pid': 93737,
2466         # Child processes are the processes for plugins and other workers.
2467         u'child_process_path': u'.../Chromium.app/Contents/'
2468                                 'Versions/6.0.412.0/Chromium Helper.app/'
2469                                 'Contents/MacOS/Chromium Helper',
2470         u'child_processes': [ { u'name': u'Shockwave Flash',
2471                                 u'pid': 93766,
2472                                 u'type': u'Plug-in'}],
2473         u'extension_views': [ {
2474           u'name': u'Webpage Screenshot',
2475           u'pid': 93938,
2476           u'extension_id': u'dgcoklnmbeljaehamekjpeidmbicddfj',
2477           u'url': u'chrome-extension://dgcoklnmbeljaehamekjpeidmbicddfj/'
2478                     'bg.html',
2479           u'loaded': True,
2480           u'view': {
2481             u'render_process_id': 2,
2482             u'render_view_id': 1},
2483           u'view_type': u'EXTENSION_BACKGROUND_PAGE'}]
2484         u'properties': {
2485           u'BrowserProcessExecutableName': u'Chromium',
2486           u'BrowserProcessExecutablePath': u'Chromium.app/Contents/MacOS/'
2487                                             'Chromium',
2488           u'ChromeVersion': u'6.0.412.0',
2489           u'HelperProcessExecutableName': u'Chromium Helper',
2490           u'HelperProcessExecutablePath': u'Chromium Helper.app/Contents/'
2491                                             'MacOS/Chromium Helper',
2492           u'command_line_string': "COMMAND_LINE_STRING --WITH-FLAGS",
2493           u'branding': 'Chromium',
2494           u'is_official': False,}
2495         # The order of the windows and tabs listed here will be the same as
2496         # what shows up on screen.
2497         u'windows': [ { u'index': 0,
2498                         u'height': 1134,
2499                         u'incognito': False,
2500                         u'profile_path': u'Default',
2501                         u'fullscreen': False,
2502                         u'visible_page_actions':
2503                           [u'dgcoklnmbeljaehamekjpeidmbicddfj',
2504                            u'osfcklnfasdofpcldmalwpicslasdfgd']
2505                         u'selected_tab': 0,
2506                         u'tabs': [ {
2507                           u'index': 0,
2508                           u'infobars': [],
2509                           u'pinned': True,
2510                           u'renderer_pid': 93747,
2511                           u'url': u'http://www.google.com/' }, {
2512                           u'index': 1,
2513                           u'infobars': [],
2514                           u'pinned': False,
2515                           u'renderer_pid': 93919,
2516                           u'url': u'https://chrome.google.com/'}, {
2517                           u'index': 2,
2518                           u'infobars': [ {
2519                             u'buttons': [u'Allow', u'Deny'],
2520                             u'link_text': u'Learn more',
2521                             u'text': u'slides.html5rocks.com wants to track '
2522                                       'your physical location',
2523                             u'type': u'confirm_infobar'}],
2524                           u'pinned': False,
2525                           u'renderer_pid': 93929,
2526                           u'url': u'http://slides.html5rocks.com/#slide14'},
2527                             ],
2528                         u'type': u'tabbed',
2529                         u'width': 925,
2530                         u'x': 26,
2531                         u'y': 44}]}
2532
2533     Raises:
2534       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2535     """
2536     cmd_dict = {  # Prepare command for the json interface
2537       'command': 'GetBrowserInfo',
2538     }
2539     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
2540
2541   def IsAura(self):
2542     """Is this Aura?"""
2543     return self.GetBrowserInfo()['properties']['aura']
2544
2545   def GetProcessInfo(self):
2546     """Returns information about browser-related processes that currently exist.
2547
2548     This will also return information about other currently-running browsers
2549     besides just Chrome.
2550
2551     Returns:
2552       A dictionary containing browser-related process information as identified
2553       by class MemoryDetails in src/chrome/browser/memory_details.h.  The
2554       dictionary contains a single key 'browsers', mapped to a list of
2555       dictionaries containing information about each browser process name.
2556       Each of those dictionaries contains a key 'processes', mapped to a list
2557       of dictionaries containing the specific information for each process
2558       with the given process name.
2559
2560       The memory values given in |committed_mem| and |working_set_mem| are in
2561       KBytes.
2562
2563       Sample:
2564       { 'browsers': [ { 'name': 'Chromium',
2565                         'process_name': 'chrome',
2566                         'processes': [ { 'child_process_type': 'Browser',
2567                                          'committed_mem': { 'image': 0,
2568                                                             'mapped': 0,
2569                                                             'priv': 0},
2570                                          'is_diagnostics': False,
2571                                          'num_processes': 1,
2572                                          'pid': 7770,
2573                                          'product_name': '',
2574                                          'renderer_type': 'Unknown',
2575                                          'titles': [],
2576                                          'version': '',
2577                                          'working_set_mem': { 'priv': 43672,
2578                                                               'shareable': 0,
2579                                                               'shared': 59251}},
2580                                        { 'child_process_type': 'Tab',
2581                                          'committed_mem': { 'image': 0,
2582                                                             'mapped': 0,
2583                                                             'priv': 0},
2584                                          'is_diagnostics': False,
2585                                          'num_processes': 1,
2586                                          'pid': 7791,
2587                                          'product_name': '',
2588                                          'renderer_type': 'Tab',
2589                                          'titles': ['about:blank'],
2590                                          'version': '',
2591                                          'working_set_mem': { 'priv': 16768,
2592                                                               'shareable': 0,
2593                                                               'shared': 26256}},
2594                                        ...<more processes>...]}]}
2595
2596     Raises:
2597       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2598     """
2599     cmd_dict = {  # Prepare command for the json interface.
2600       'command': 'GetProcessInfo',
2601     }
2602     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
2603
2604   def GetNavigationInfo(self, tab_index=0, windex=0):
2605     """Get info about the navigation state of a given tab.
2606
2607     Args:
2608       tab_index: The tab index, default is 0.
2609       window_index: The window index, default is 0.
2610
2611     Returns:
2612       a dictionary.
2613       Sample:
2614
2615       { u'favicon_url': u'https://www.google.com/favicon.ico',
2616         u'page_type': u'NORMAL_PAGE',
2617         u'ssl': { u'displayed_insecure_content': False,
2618                   u'ran_insecure_content': False,
2619                   u'security_style': u'SECURITY_STYLE_AUTHENTICATED'}}
2620
2621       Values for security_style can be:
2622         SECURITY_STYLE_UNKNOWN
2623         SECURITY_STYLE_UNAUTHENTICATED
2624         SECURITY_STYLE_AUTHENTICATION_BROKEN
2625         SECURITY_STYLE_AUTHENTICATED
2626
2627       Values for page_type can be:
2628         NORMAL_PAGE
2629         ERROR_PAGE
2630         INTERSTITIAL_PAGE
2631     """
2632     cmd_dict = {  # Prepare command for the json interface
2633       'command': 'GetNavigationInfo',
2634       'tab_index': tab_index,
2635     }
2636     return self._GetResultFromJSONRequest(cmd_dict, windex=windex)
2637
2638   def GetSecurityState(self, tab_index=0, windex=0):
2639     """Get security details for a given tab.
2640
2641     Args:
2642       tab_index: The tab index, default is 0.
2643       window_index: The window index, default is 0.
2644
2645     Returns:
2646       a dictionary.
2647       Sample:
2648       { "security_style": SECURITY_STYLE_AUTHENTICATED,
2649         "ssl_cert_status": 3,  // bitmask of status flags
2650         "insecure_content_status": 1,  // bitmask of status flags
2651       }
2652     """
2653     cmd_dict = {  # Prepare command for the json interface
2654       'command': 'GetSecurityState',
2655       'tab_index': tab_index,
2656       'windex': windex,
2657     }
2658     return self._GetResultFromJSONRequest(cmd_dict, windex=None)
2659
2660   def GetHistoryInfo(self, search_text='', windex=0):
2661     """Return info about browsing history.
2662
2663     Args:
2664       search_text: the string to search in history.  Defaults to empty string
2665                    which means that all history would be returned. This is
2666                    functionally equivalent to searching for a text in the
2667                    chrome://history UI. So partial matches work too.
2668                    When non-empty, the history items returned will contain a
2669                    "snippet" field corresponding to the snippet visible in
2670                    the chrome://history/ UI.
2671       windex: index of the browser window, defaults to 0.
2672
2673     Returns:
2674       an instance of history_info.HistoryInfo
2675     """
2676     cmd_dict = {  # Prepare command for the json interface
2677       'command': 'GetHistoryInfo',
2678       'search_text': search_text,
2679     }
2680     return history_info.HistoryInfo(
2681         self._GetResultFromJSONRequest(cmd_dict, windex=windex))
2682
2683   def InstallExtension(self, extension_path, with_ui=False, from_webstore=None,
2684                        windex=0, tab_index=0):
2685     """Installs an extension from the given path.
2686
2687     The path must be absolute and may be a crx file or an unpacked extension
2688     directory. Returns the extension ID if successfully installed and loaded.
2689     Otherwise, throws an exception. The extension must not already be installed.
2690
2691     Args:
2692       extension_path: The absolute path to the extension to install. If the
2693                       extension is packed, it must have a .crx extension.
2694       with_ui: Whether the extension install confirmation UI should be shown.
2695       from_webstore: If True, forces a .crx extension to be recognized as one
2696           from the webstore. Can be used to force install an extension with
2697           'experimental' permissions.
2698       windex: Integer index of the browser window to use; defaults to 0
2699               (first window).
2700
2701     Returns:
2702       The ID of the installed extension.
2703
2704     Raises:
2705       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2706     """
2707     cmd_dict = {
2708         'command': 'InstallExtension',
2709         'path': extension_path,
2710         'with_ui': with_ui,
2711         'windex': windex,
2712         'tab_index': tab_index,
2713     }
2714
2715     if from_webstore:
2716       cmd_dict['from_webstore'] = True
2717     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['id']
2718
2719   def GetExtensionsInfo(self, windex=0):
2720     """Returns information about all installed extensions.
2721
2722     Args:
2723       windex: Integer index of the browser window to use; defaults to 0
2724               (first window).
2725
2726     Returns:
2727       A list of dictionaries representing each of the installed extensions.
2728       Example:
2729       [ { u'api_permissions': [u'bookmarks', u'experimental', u'tabs'],
2730           u'background_url': u'',
2731           u'description': u'Bookmark Manager',
2732           u'effective_host_permissions': [u'chrome://favicon/*',
2733                                           u'chrome://resources/*'],
2734           u'host_permissions': [u'chrome://favicon/*', u'chrome://resources/*'],
2735           u'id': u'eemcgdkfndhakfknompkggombfjjjeno',
2736           u'is_component': True,
2737           u'is_internal': False,
2738           u'name': u'Bookmark Manager',
2739           u'options_url': u'',
2740           u'public_key': u'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQcByy+eN9jza\
2741                            zWF/DPn7NW47sW7lgmpk6eKc0BQM18q8hvEM3zNm2n7HkJv/R6f\
2742                            U+X5mtqkDuKvq5skF6qqUF4oEyaleWDFhd1xFwV7JV+/DU7bZ00\
2743                            w2+6gzqsabkerFpoP33ZRIw7OviJenP0c0uWqDWF8EGSyMhB3tx\
2744                            qhOtiQIDAQAB',
2745           u'version': u'0.1' },
2746         { u'api_permissions': [...],
2747           u'background_url': u'chrome-extension://\
2748                                lkdedmbpkaiahjjibfdmpoefffnbdkli/\
2749                                background.html',
2750           u'description': u'Extension which lets you read your Facebook news \
2751                             feed and wall. You can also post status updates.',
2752           u'effective_host_permissions': [...],
2753           u'host_permissions': [...],
2754           u'id': u'lkdedmbpkaiahjjibfdmpoefffnbdkli',
2755           u'name': u'Facebook for Google Chrome',
2756           u'options_url': u'',
2757           u'public_key': u'...',
2758           u'version': u'2.0.9'
2759           u'is_enabled': True,
2760           u'allowed_in_incognito': True} ]
2761     """
2762     cmd_dict = {  # Prepare command for the json interface
2763       'command': 'GetExtensionsInfo',
2764       'windex': windex,
2765     }
2766     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['extensions']
2767
2768   def UninstallExtensionById(self, id, windex=0):
2769     """Uninstall the extension with the given id.
2770
2771     Args:
2772       id: The string id of the extension.
2773       windex: Integer index of the browser window to use; defaults to 0
2774               (first window).
2775
2776     Returns:
2777       True, if the extension was successfully uninstalled, or
2778       False, otherwise.
2779     """
2780     cmd_dict = {  # Prepare command for the json interface
2781       'command': 'UninstallExtensionById',
2782       'id': id,
2783       'windex': windex,
2784     }
2785     return self._GetResultFromJSONRequest(cmd_dict, windex=None)['success']
2786
2787   def SetExtensionStateById(self, id, enable, allow_in_incognito, windex=0):
2788     """Set extension state: enable/disable, allow/disallow in incognito mode.
2789
2790     Args:
2791       id: The string id of the extension.
2792       enable: A boolean, enable extension.
2793       allow_in_incognito: A boolean, allow extension in incognito.
2794       windex: Integer index of the browser window to use; defaults to 0
2795               (first window).
2796     """
2797     cmd_dict = {  # Prepare command for the json interface
2798       'command': 'SetExtensionStateById',
2799       'id': id,
2800       'enable': enable,
2801       'allow_in_incognito': allow_in_incognito,
2802       'windex': windex,
2803     }
2804     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2805
2806   def TriggerPageActionById(self, id, tab_index=0, windex=0):
2807     """Trigger page action asynchronously in the active tab.
2808
2809     The page action icon must be displayed before invoking this function.
2810
2811     Args:
2812       id: The string id of the extension.
2813       tab_index: Integer index of the tab to use; defaults to 0 (first tab).
2814       windex: Integer index of the browser window to use; defaults to 0
2815               (first window).
2816     """
2817     cmd_dict = {  # Prepare command for the json interface
2818       'command': 'TriggerPageActionById',
2819       'id': id,
2820       'windex': windex,
2821       'tab_index': tab_index,
2822     }
2823     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2824
2825   def TriggerBrowserActionById(self, id, tab_index=0, windex=0):
2826     """Trigger browser action asynchronously in the active tab.
2827
2828     Args:
2829       id: The string id of the extension.
2830       tab_index: Integer index of the tab to use; defaults to 0 (first tab).
2831       windex: Integer index of the browser window to use; defaults to 0
2832               (first window).
2833     """
2834     cmd_dict = {  # Prepare command for the json interface
2835       'command': 'TriggerBrowserActionById',
2836       'id': id,
2837       'windex': windex,
2838       'tab_index': tab_index,
2839     }
2840     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2841
2842   def UpdateExtensionsNow(self, windex=0):
2843     """Auto-updates installed extensions.
2844
2845     Waits until all extensions are updated, loaded, and ready for use.
2846     This is equivalent to clicking the "Update extensions now" button on the
2847     chrome://extensions page.
2848
2849     Args:
2850       windex: Integer index of the browser window to use; defaults to 0
2851               (first window).
2852
2853     Raises:
2854       pyauto_errors.JSONInterfaceError if the automation returns an error.
2855     """
2856     cmd_dict = {  # Prepare command for the json interface.
2857       'command': 'UpdateExtensionsNow',
2858       'windex': windex,
2859     }
2860     self._GetResultFromJSONRequest(cmd_dict, windex=None)
2861
2862   def WaitUntilExtensionViewLoaded(self, name=None, extension_id=None,
2863                                    url=None, view_type=None):
2864     """Wait for a loaded extension view matching all the given properties.
2865
2866     If no matching extension views are found, wait for one to be loaded.
2867     If there are more than one matching extension view, return one at random.
2868     Uses WaitUntil so timeout is capped by automation timeout.
2869     Refer to extension_view dictionary returned in GetBrowserInfo()
2870     for sample input/output values.
2871
2872     Args:
2873       name: (optional) Name of the extension.
2874       extension_id: (optional) ID of the extension.
2875       url: (optional) URL of the extension view.
2876       view_type: (optional) Type of the extension view.
2877         ['EXTENSION_BACKGROUND_PAGE'|'EXTENSION_POPUP'|'EXTENSION_INFOBAR'|
2878          'EXTENSION_DIALOG']
2879
2880     Returns:
2881       The 'view' property of the extension view.
2882       None, if no view loaded.
2883
2884     Raises:
2885       pyauto_errors.JSONInterfaceError if the automation returns an error.
2886     """
2887     def _GetExtensionViewLoaded():
2888       extension_views = self.GetBrowserInfo()['extension_views']
2889       for extension_view in extension_views:
2890         if ((name and name != extension_view['name']) or
2891             (extension_id and extension_id != extension_view['extension_id']) or
2892             (url and url != extension_view['url']) or
2893             (view_type and view_type != extension_view['view_type'])):
2894           continue
2895         if extension_view['loaded']:
2896           return extension_view['view']
2897       return False
2898
2899     if self.WaitUntil(lambda: _GetExtensionViewLoaded()):
2900       return _GetExtensionViewLoaded()
2901     return None
2902
2903   def WaitUntilExtensionViewClosed(self, view):
2904     """Wait for the given extension view to to be closed.
2905
2906     Uses WaitUntil so timeout is capped by automation timeout.
2907     Refer to extension_view dictionary returned by GetBrowserInfo()
2908     for sample input value.
2909
2910     Args:
2911       view: 'view' property of extension view.
2912
2913     Raises:
2914       pyauto_errors.JSONInterfaceError if the automation returns an error.
2915     """
2916     def _IsExtensionViewClosed():
2917       extension_views = self.GetBrowserInfo()['extension_views']
2918       for extension_view in extension_views:
2919         if view == extension_view['view']:
2920           return False
2921       return True
2922
2923     return self.WaitUntil(lambda: _IsExtensionViewClosed())
2924
2925   def GetPluginsInfo(self, windex=0):
2926     """Return info about plugins.
2927
2928     This is the info available from about:plugins
2929
2930     Returns:
2931       an instance of plugins_info.PluginsInfo
2932     """
2933     return plugins_info.PluginsInfo(
2934         self._GetResultFromJSONRequest({'command': 'GetPluginsInfo'},
2935                                        windex=windex))
2936
2937   def EnablePlugin(self, path):
2938     """Enable the plugin at the given path.
2939
2940     Use GetPluginsInfo() to fetch path info about a plugin.
2941
2942     Raises:
2943       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2944     """
2945     cmd_dict = {
2946       'command': 'EnablePlugin',
2947       'path': path,
2948     }
2949     self._GetResultFromJSONRequest(cmd_dict)
2950
2951   def DisablePlugin(self, path):
2952     """Disable the plugin at the given path.
2953
2954     Use GetPluginsInfo() to fetch path info about a plugin.
2955
2956     Raises:
2957       pyauto_errors.JSONInterfaceError if the automation call returns an error.
2958     """
2959     cmd_dict = {
2960       'command': 'DisablePlugin',
2961       'path': path,
2962     }
2963     self._GetResultFromJSONRequest(cmd_dict)
2964
2965   def GetTabContents(self, tab_index=0, window_index=0):
2966     """Get the html contents of a tab (a la "view source").
2967
2968     As an implementation detail, this saves the html in a file, reads
2969     the file into a buffer, then deletes it.
2970
2971     Args:
2972       tab_index: tab index, defaults to 0.
2973       window_index: window index, defaults to 0.
2974     Returns:
2975       html content of a page as a string.
2976     """
2977     tempdir = tempfile.mkdtemp()
2978     # Make it writable by chronos on chromeos
2979     os.chmod(tempdir, 0777)
2980     filename = os.path.join(tempdir, 'content.html')
2981     cmd_dict = {  # Prepare command for the json interface
2982       'command': 'SaveTabContents',
2983       'tab_index': tab_index,
2984       'filename': filename
2985     }
2986     self._GetResultFromJSONRequest(cmd_dict, windex=window_index)
2987     try:
2988       f = open(filename)
2989       all_data = f.read()
2990       f.close()
2991       return all_data
2992     finally:
2993       shutil.rmtree(tempdir, ignore_errors=True)
2994
2995   def AddSavedPassword(self, password_dict, windex=0):
2996     """Adds the given username-password combination to the saved passwords.
2997
2998     Args:
2999       password_dict: a dictionary that represents a password. Example:
3000       { 'username_value': 'user@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
4858 class ExtraBrowser(PyUITest):
4859   """Launches a new browser with some extra flags.
4860
4861   The new browser is launched with its own fresh profile.
4862   This class does not apply to ChromeOS.
4863   """
4864   def __init__(self, chrome_flags=[], methodName='runTest', **kwargs):
4865     """Accepts extra chrome flags for launching a new browser instance.
4866
4867     Args:
4868       chrome_flags: list of extra flags when launching a new browser.
4869     """
4870     assert not PyUITest.IsChromeOS(), \
4871         'This function cannot be used to launch a new browser in ChromeOS.'
4872     PyUITest.__init__(self, methodName=methodName, **kwargs)
4873     self._chrome_flags = chrome_flags
4874     PyUITest.setUp(self)
4875
4876   def __del__(self):
4877     """Tears down the browser and then calls super class's destructor"""
4878     PyUITest.tearDown(self)
4879     PyUITest.__del__(self)
4880
4881   def ExtraChromeFlags(self):
4882     """Prepares the browser to launch with specified Chrome flags."""
4883     return PyUITest.ExtraChromeFlags(self) + self._chrome_flags
4884
4885
4886 class _RemoteProxy():
4887   """Class for PyAuto remote method calls.
4888
4889   Use this class along with RemoteHost.testRemoteHost to establish a PyAuto
4890   connection with another machine and make remote PyAuto calls. The RemoteProxy
4891   mimics a PyAuto object, so all json-style PyAuto calls can be made on it.
4892
4893   The remote host acts as a dumb executor that receives method call requests,
4894   executes them, and sends all of the results back to the RemoteProxy, including
4895   the return value, thrown exceptions, and console output.
4896
4897   The remote host should be running the same version of PyAuto as the proxy.
4898   A mismatch could lead to undefined behavior.
4899
4900   Example usage:
4901     class MyTest(pyauto.PyUITest):
4902       def testRemoteExample(self):
4903         remote = pyauto._RemoteProxy(('127.0.0.1', 7410))
4904         remote.NavigateToURL('http://www.google.com')
4905         title = remote.GetActiveTabTitle()
4906         self.assertEqual(title, 'Google')
4907   """
4908   class RemoteException(Exception):
4909     pass
4910
4911   def __init__(self, host):
4912     self.RemoteConnect(host)
4913
4914   def RemoteConnect(self, host):
4915     begin = time.time()
4916     while time.time() - begin < 50:
4917       self._socket = socket.socket()
4918       if not self._socket.connect_ex(host):
4919         break
4920       time.sleep(0.25)
4921     else:
4922       # Make one last attempt, but raise a socket error on failure.
4923       self._socket = socket.socket()
4924       self._socket.connect(host)
4925
4926   def RemoteDisconnect(self):
4927     if self._socket:
4928       self._socket.shutdown(socket.SHUT_RDWR)
4929       self._socket.close()
4930       self._socket = None
4931
4932   def CreateTarget(self, target):
4933     """Registers the methods and creates a remote instance of a target.
4934
4935     Any RPC calls will then be made on the remote target instance. Note that the
4936     remote instance will be a brand new instance and will have none of the state
4937     of the local instance. The target's class should have a constructor that
4938     takes no arguments.
4939     """
4940     self._Call('CreateTarget', target.__class__)
4941     self._RegisterClassMethods(target)
4942
4943   def _RegisterClassMethods(self, remote_class):
4944     # Make remote-call versions of all remote_class methods.
4945     for method_name, _ in inspect.getmembers(remote_class, inspect.ismethod):
4946       # Ignore private methods and duplicates.
4947       if method_name[0] in string.letters and \
4948         getattr(self, method_name, None) is None:
4949         setattr(self, method_name, functools.partial(self._Call, method_name))
4950
4951   def _Call(self, method_name, *args, **kwargs):
4952     # Send request.
4953     request = pickle.dumps((method_name, args, kwargs))
4954     if self._socket.send(request) != len(request):
4955       raise self.RemoteException('Error sending remote method call request.')
4956
4957     # Receive response.
4958     response = self._socket.recv(4096)
4959     if not response:
4960       raise self.RemoteException('Client disconnected during method call.')
4961     result, stdout, stderr, exception = pickle.loads(response)
4962
4963     # Print any output the client captured, throw any exceptions, and return.
4964     sys.stdout.write(stdout)
4965     sys.stderr.write(stderr)
4966     if exception:
4967       raise self.RemoteException('%s raised by remote client: %s' %
4968                                  (exception[0], exception[1]))
4969     return result
4970
4971
4972 class PyUITestSuite(pyautolib.PyUITestSuiteBase, unittest.TestSuite):
4973   """Base TestSuite for PyAuto UI tests."""
4974
4975   def __init__(self, args):
4976     pyautolib.PyUITestSuiteBase.__init__(self, args)
4977
4978     # Figure out path to chromium binaries
4979     browser_dir = os.path.normpath(os.path.dirname(pyautolib.__file__))
4980     logging.debug('Loading pyauto libs from %s', browser_dir)
4981     self.InitializeWithPath(pyautolib.FilePath(browser_dir))
4982     os.environ['PATH'] = browser_dir + os.pathsep + os.environ['PATH']
4983
4984     unittest.TestSuite.__init__(self)
4985     cr_source_root = os.path.normpath(os.path.join(
4986         os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
4987     self.SetCrSourceRoot(pyautolib.FilePath(cr_source_root))
4988
4989     # Start http server, if needed.
4990     global _OPTIONS
4991     if _OPTIONS and not _OPTIONS.no_http_server:
4992       self._StartHTTPServer()
4993     if _OPTIONS and _OPTIONS.remote_host:
4994       self._ConnectToRemoteHosts(_OPTIONS.remote_host.split(','))
4995
4996   def __del__(self):
4997     # python unittest module is setup such that the suite gets deleted before
4998     # the test cases, which is odd because our test cases depend on
4999     # initializtions like exitmanager, autorelease pool provided by the
5000     # suite. Forcibly delete the test cases before the suite.
5001     del self._tests
5002     pyautolib.PyUITestSuiteBase.__del__(self)
5003
5004     global _HTTP_SERVER
5005     if _HTTP_SERVER:
5006       self._StopHTTPServer()
5007
5008     global _CHROME_DRIVER_FACTORY
5009     if _CHROME_DRIVER_FACTORY is not None:
5010       _CHROME_DRIVER_FACTORY.Stop()
5011
5012   def _StartHTTPServer(self):
5013     """Start a local file server hosting data files over http://"""
5014     global _HTTP_SERVER
5015     assert not _HTTP_SERVER, 'HTTP Server already started'
5016     http_data_dir = _OPTIONS.http_data_dir
5017     http_server = pyautolib.SpawnedTestServer(
5018         pyautolib.SpawnedTestServer.TYPE_HTTP,
5019         '127.0.0.1',
5020         pyautolib.FilePath(http_data_dir))
5021     assert http_server.Start(), 'Could not start http server'
5022     _HTTP_SERVER = http_server
5023     logging.debug('Started http server at "%s".', http_data_dir)
5024
5025   def _StopHTTPServer(self):
5026     """Stop the local http server."""
5027     global _HTTP_SERVER
5028     assert _HTTP_SERVER, 'HTTP Server not yet started'
5029     assert _HTTP_SERVER.Stop(), 'Could not stop http server'
5030     _HTTP_SERVER = None
5031     logging.debug('Stopped http server.')
5032
5033   def _ConnectToRemoteHosts(self, addresses):
5034     """Connect to remote PyAuto instances using a RemoteProxy.
5035
5036     The RemoteHost instances must already be running."""
5037     global _REMOTE_PROXY
5038     assert not _REMOTE_PROXY, 'Already connected to a remote host.'
5039     _REMOTE_PROXY = []
5040     for address in addresses:
5041       if address == 'localhost' or address == '127.0.0.1':
5042         self._StartLocalRemoteHost()
5043       _REMOTE_PROXY.append(_RemoteProxy((address, 7410)))
5044
5045   def _StartLocalRemoteHost(self):
5046     """Start a remote PyAuto instance on the local machine."""
5047     # Add the path to our main class to the RemoteHost's
5048     # environment, so it can load that class at runtime.
5049     import __main__
5050     main_path = os.path.dirname(__main__.__file__)
5051     env = os.environ
5052     if env.get('PYTHONPATH', None):
5053       env['PYTHONPATH'] += ':' + main_path
5054     else:
5055       env['PYTHONPATH'] = main_path
5056
5057     # Run it!
5058     subprocess.Popen([sys.executable, os.path.join(os.path.dirname(__file__),
5059                                                    'remote_host.py')], env=env)
5060
5061
5062 class _GTestTextTestResult(unittest._TextTestResult):
5063   """A test result class that can print formatted text results to a stream.
5064
5065   Results printed in conformance with gtest output format, like:
5066   [ RUN        ] autofill.AutofillTest.testAutofillInvalid: "test desc."
5067   [         OK ] autofill.AutofillTest.testAutofillInvalid
5068   [ RUN        ] autofill.AutofillTest.testFillProfile: "test desc."
5069   [         OK ] autofill.AutofillTest.testFillProfile
5070   [ RUN        ] autofill.AutofillTest.testFillProfileCrazyCharacters: "Test."
5071   [         OK ] autofill.AutofillTest.testFillProfileCrazyCharacters
5072   """
5073   def __init__(self, stream, descriptions, verbosity):
5074     unittest._TextTestResult.__init__(self, stream, descriptions, verbosity)
5075
5076   def _GetTestURI(self, test):
5077     if sys.version_info[:2] <= (2, 4):
5078       return '%s.%s' % (unittest._strclass(test.__class__),
5079                         test._TestCase__testMethodName)
5080     return '%s.%s.%s' % (test.__class__.__module__,
5081                          test.__class__.__name__,
5082                          test._testMethodName)
5083
5084   def getDescription(self, test):
5085     return '%s: "%s"' % (self._GetTestURI(test), test.shortDescription())
5086
5087   def startTest(self, test):
5088     unittest.TestResult.startTest(self, test)
5089     self.stream.writeln('[ RUN        ] %s' % self.getDescription(test))
5090
5091   def addSuccess(self, test):
5092     unittest.TestResult.addSuccess(self, test)
5093     self.stream.writeln('[         OK ] %s' % self._GetTestURI(test))
5094
5095   def addError(self, test, err):
5096     unittest.TestResult.addError(self, test, err)
5097     self.stream.writeln('[      ERROR ] %s' % self._GetTestURI(test))
5098
5099   def addFailure(self, test, err):
5100     unittest.TestResult.addFailure(self, test, err)
5101     self.stream.writeln('[     FAILED ] %s' % self._GetTestURI(test))
5102
5103
5104 class PyAutoTextTestRunner(unittest.TextTestRunner):
5105   """Test Runner for PyAuto tests that displays results in textual format.
5106
5107   Results are displayed in conformance with gtest output.
5108   """
5109   def __init__(self, verbosity=1):
5110     unittest.TextTestRunner.__init__(self,
5111                                      stream=sys.stderr,
5112                                      verbosity=verbosity)
5113
5114   def _makeResult(self):
5115     return _GTestTextTestResult(self.stream, self.descriptions, self.verbosity)
5116
5117
5118 # Implementation inspired from unittest.main()
5119 class Main(object):
5120   """Main program for running PyAuto tests."""
5121
5122   _options, _args = None, None
5123   _tests_filename = 'PYAUTO_TESTS'
5124   _platform_map = {
5125     'win32':  'win',
5126     'darwin': 'mac',
5127     'linux2': 'linux',
5128     'linux3': 'linux',
5129     'chromeos': 'chromeos',
5130   }
5131
5132   def __init__(self):
5133     self._ParseArgs()
5134     self._Run()
5135
5136   def _ParseArgs(self):
5137     """Parse command line args."""
5138     parser = optparse.OptionParser()
5139     parser.add_option(
5140         '', '--channel-id', type='string', default='',
5141         help='Name of channel id, if using named interface.')
5142     parser.add_option(
5143         '', '--chrome-flags', type='string', default='',
5144         help='Flags passed to Chrome.  This is in addition to the usual flags '
5145              'like suppressing first-run dialogs, enabling automation.  '
5146              'See chrome/common/chrome_switches.cc for the list of flags '
5147              'chrome understands.')
5148     parser.add_option(
5149         '', '--http-data-dir', type='string',
5150         default=os.path.join('chrome', 'test', 'data'),
5151         help='Relative path from which http server should serve files.')
5152     parser.add_option(
5153         '-L', '--list-tests', action='store_true', default=False,
5154         help='List all tests, and exit.')
5155     parser.add_option(
5156         '--shard',
5157         help='Specify sharding params. Example: 1/3 implies split the list of '
5158              'tests into 3 groups of which this is the 1st.')
5159     parser.add_option(
5160         '', '--log-file', type='string', default=None,
5161         help='Provide a path to a file to which the logger will log')
5162     parser.add_option(
5163         '', '--no-http-server', action='store_true', default=False,
5164         help='Do not start an http server to serve files in data dir.')
5165     parser.add_option(
5166         '', '--remote-host', type='string', default=None,
5167         help='Connect to remote hosts for remote automation. If "localhost" '
5168             '"127.0.0.1" is specified, a remote host will be launched '
5169             'automatically on the local machine.')
5170     parser.add_option(
5171         '', '--repeat', type='int', default=1,
5172         help='Number of times to repeat the tests. Useful to determine '
5173              'flakiness. Defaults to 1.')
5174     parser.add_option(
5175         '-S', '--suite', type='string', default='FULL',
5176         help='Name of the suite to load.  Defaults to "FULL".')
5177     parser.add_option(
5178         '-v', '--verbose', action='store_true', default=False,
5179         help='Make PyAuto verbose.')
5180     parser.add_option(
5181         '-D', '--wait-for-debugger', action='store_true', default=False,
5182         help='Block PyAuto on startup for attaching debugger.')
5183
5184     self._options, self._args = parser.parse_args()
5185     global _OPTIONS
5186     _OPTIONS = self._options  # Export options so other classes can access.
5187
5188     # Set up logging. All log messages will be prepended with a timestamp.
5189     format = '%(asctime)s %(levelname)-8s %(message)s'
5190
5191     level = logging.INFO
5192     if self._options.verbose:
5193       level=logging.DEBUG
5194
5195     logging.basicConfig(level=level, format=format,
5196                         filename=self._options.log_file)
5197
5198   def TestsDir(self):
5199     """Returns the path to dir containing tests.
5200
5201     This is typically the dir containing the tests description file.
5202     This method should be overridden by derived class to point to other dirs
5203     if needed.
5204     """
5205     return os.path.dirname(__file__)
5206
5207   @staticmethod
5208   def _ImportTestsFromName(name):
5209     """Get a list of all test names from the given string.
5210
5211     Args:
5212       name: dot-separated string for a module, a test case or a test method.
5213             Examples: omnibox  (a module)
5214                       omnibox.OmniboxTest  (a test case)
5215                       omnibox.OmniboxTest.testA  (a test method)
5216
5217     Returns:
5218       [omnibox.OmniboxTest.testA, omnibox.OmniboxTest.testB, ...]
5219     """
5220     def _GetTestsFromTestCase(class_obj):
5221       """Return all test method names from given class object."""
5222       return [class_obj.__name__ + '.' + x for x in dir(class_obj) if
5223               x.startswith('test')]
5224
5225     def _GetTestsFromModule(module):
5226       """Return all test method names from the given module object."""
5227       tests = []
5228       for name in dir(module):
5229         obj = getattr(module, name)
5230         if (isinstance(obj, (type, types.ClassType)) and
5231             issubclass(obj, PyUITest) and obj != PyUITest):
5232           tests.extend([module.__name__ + '.' + x for x in
5233                         _GetTestsFromTestCase(obj)])
5234       return tests
5235
5236     module = None
5237     # Locate the module
5238     parts = name.split('.')
5239     parts_copy = parts[:]
5240     while parts_copy:
5241       try:
5242         module = __import__('.'.join(parts_copy))
5243         break
5244       except ImportError:
5245         del parts_copy[-1]
5246         if not parts_copy: raise
5247     # We have the module. Pick the exact test method or class asked for.
5248     parts = parts[1:]
5249     obj = module
5250     for part in parts:
5251       obj = getattr(obj, part)
5252
5253     if type(obj) == types.ModuleType:
5254       return _GetTestsFromModule(obj)
5255     elif (isinstance(obj, (type, types.ClassType)) and
5256           issubclass(obj, PyUITest) and obj != PyUITest):
5257       return [module.__name__ + '.' + x for x in _GetTestsFromTestCase(obj)]
5258     elif type(obj) == types.UnboundMethodType:
5259       return [name]
5260     else:
5261       logging.warn('No tests in "%s"', name)
5262       return []
5263
5264   def _HasTestCases(self, module_string):
5265     """Determines if we have any PyUITest test case classes in the module
5266        identified by |module_string|."""
5267     module = __import__(module_string)
5268     for name in dir(module):
5269       obj = getattr(module, name)
5270       if (isinstance(obj, (type, types.ClassType)) and
5271           issubclass(obj, PyUITest)):
5272         return True
5273     return False
5274
5275   def _ExpandTestNames(self, args):
5276     """Returns a list of tests loaded from the given args.
5277
5278     The given args can be either a module (ex: module1) or a testcase
5279     (ex: module2.MyTestCase) or a test (ex: module1.MyTestCase.testX)
5280     or a suite name (ex: @FULL). If empty, the tests in the already imported
5281     modules are loaded.
5282
5283     Args:
5284       args: [module1, module2, module3.testcase, module4.testcase.testX]
5285             These modules or test cases or tests should be importable.
5286             Suites can be specified by prefixing @. Example: @FULL
5287
5288       Returns:
5289         a list of expanded test names.  Example:
5290           [
5291             'module1.TestCase1.testA',
5292             'module1.TestCase1.testB',
5293             'module2.TestCase2.testX',
5294             'module3.testcase.testY',
5295             'module4.testcase.testX'
5296           ]
5297     """
5298
5299     def _TestsFromDescriptionFile(suite):
5300       pyauto_tests_file = os.path.join(self.TestsDir(), self._tests_filename)
5301       if suite:
5302         logging.debug("Reading %s (@%s)", pyauto_tests_file, suite)
5303       else:
5304         logging.debug("Reading %s", pyauto_tests_file)
5305       if not os.path.exists(pyauto_tests_file):
5306         logging.warn("%s missing. Cannot load tests.", pyauto_tests_file)
5307         return []
5308       else:
5309         return self._ExpandTestNamesFrom(pyauto_tests_file, suite)
5310
5311     if not args:  # Load tests ourselves
5312       if self._HasTestCases('__main__'):    # we are running a test script
5313         module_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
5314         args.append(module_name)   # run the test cases found in it
5315       else:  # run tests from the test description file
5316         args = _TestsFromDescriptionFile(self._options.suite)
5317     else:  # Check args with @ prefix for suites
5318       out_args = []
5319       for arg in args:
5320         if arg.startswith('@'):
5321           suite = arg[1:]
5322           out_args += _TestsFromDescriptionFile(suite)
5323         else:
5324           out_args.append(arg)
5325       args = out_args
5326     return args
5327
5328   def _ExpandTestNamesFrom(self, filename, suite):
5329     """Load test names from the given file.
5330
5331     Args:
5332       filename: the file to read the tests from
5333       suite: the name of the suite to load from |filename|.
5334
5335     Returns:
5336       a list of test names
5337       [module.testcase.testX, module.testcase.testY, ..]
5338     """
5339     suites = PyUITest.EvalDataFrom(filename)
5340     platform = sys.platform
5341     if PyUITest.IsChromeOS():  # check if it's chromeos
5342       platform = 'chromeos'
5343     assert platform in self._platform_map, '%s unsupported' % platform
5344     def _NamesInSuite(suite_name):
5345       logging.debug('Expanding suite %s', suite_name)
5346       platforms = suites.get(suite_name)
5347       names = platforms.get('all', []) + \
5348               platforms.get(self._platform_map[platform], [])
5349       ret = []
5350       # Recursively include suites if any.  Suites begin with @.
5351       for name in names:
5352         if name.startswith('@'):  # Include another suite
5353           ret.extend(_NamesInSuite(name[1:]))
5354         else:
5355           ret.append(name)
5356       return ret
5357
5358     assert suite in suites, '%s: No such suite in %s' % (suite, filename)
5359     all_names = _NamesInSuite(suite)
5360     args = []
5361     excluded = []
5362     # Find all excluded tests.  Excluded tests begin with '-'.
5363     for name in all_names:
5364       if name.startswith('-'):  # Exclude
5365         excluded.extend(self._ImportTestsFromName(name[1:]))
5366       else:
5367         args.extend(self._ImportTestsFromName(name))
5368     for name in excluded:
5369       if name in args:
5370         args.remove(name)
5371       else:
5372         logging.warn('Cannot exclude %s. Not included. Ignoring', name)
5373     if excluded:
5374       logging.debug('Excluded %d test(s): %s', len(excluded), excluded)
5375     return args
5376
5377   def _Run(self):
5378     """Run the tests."""
5379     if self._options.wait_for_debugger:
5380       raw_input('Attach debugger to process %s and hit <enter> ' % os.getpid())
5381
5382     suite_args = [sys.argv[0]]
5383     chrome_flags = self._options.chrome_flags
5384     # Set CHROME_HEADLESS. It enables crash reporter on posix.
5385     os.environ['CHROME_HEADLESS'] = '1'
5386     os.environ['EXTRA_CHROME_FLAGS'] = chrome_flags
5387     test_names = self._ExpandTestNames(self._args)
5388
5389     # Shard, if requested (--shard).
5390     if self._options.shard:
5391       matched = re.match('(\d+)/(\d+)', self._options.shard)
5392       if not matched:
5393         print >>sys.stderr, 'Invalid sharding params: %s' % self._options.shard
5394         sys.exit(1)
5395       shard_index = int(matched.group(1)) - 1
5396       num_shards = int(matched.group(2))
5397       if shard_index < 0 or shard_index >= num_shards:
5398         print >>sys.stderr, 'Invalid sharding params: %s' % self._options.shard
5399         sys.exit(1)
5400       test_names = pyauto_utils.Shard(test_names, shard_index, num_shards)
5401
5402     test_names *= self._options.repeat
5403     logging.debug("Loading %d tests from %s", len(test_names), test_names)
5404     if self._options.list_tests:  # List tests and exit
5405       for name in test_names:
5406         print name
5407       sys.exit(0)
5408     pyauto_suite = PyUITestSuite(suite_args)
5409     loaded_tests = unittest.defaultTestLoader.loadTestsFromNames(test_names)
5410     pyauto_suite.addTests(loaded_tests)
5411     verbosity = 1
5412     if self._options.verbose:
5413       verbosity = 2
5414     result = PyAutoTextTestRunner(verbosity=verbosity).run(pyauto_suite)
5415     del loaded_tests  # Need to destroy test cases before the suite
5416     del pyauto_suite
5417     successful = result.wasSuccessful()
5418     if not successful:
5419       pyauto_tests_file = os.path.join(self.TestsDir(), self._tests_filename)
5420       print >>sys.stderr, 'Tests can be disabled by editing %s. ' \
5421                           'Ref: %s' % (pyauto_tests_file, _PYAUTO_DOC_URL)
5422     sys.exit(not successful)
5423
5424
5425 if __name__ == '__main__':
5426   Main()