Home | History | Annotate | Download | only in pyautolib
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Includes different methods to drive chromoting UI."""
      6 
      7 import os
      8 import subprocess
      9 import sys
     10 import time
     11 
     12 from pyauto_errors import JSONInterfaceError
     13 
     14 
     15 class ChromotingMixIn(object):
     16   """MixIn for PyUITest that adds Chromoting-specific methods.
     17 
     18   Prepend it as a base class of a test to enable Chromoting functionality.
     19   This is a separate class from PyUITest to avoid namespace collisions.
     20 
     21   Example usage:
     22     class ChromotingExample(chromoting.ChromotingMixIn, pyauto.PyUITest):
     23       def testShare(self):
     24         app = self.InstallApp(self.GetWebappPath())
     25         self.LaunchApp(app)
     26         self.Authenticate()
     27         self.assertTrue(self.Share())
     28   """
     29 
     30   def _ExecuteJavascript(self, command, tab_index, windex):
     31     """Helper that returns immediately after running a Javascript command.
     32     """
     33     try:
     34       self.ExecuteJavascript(
     35           '%s; window.domAutomationController.send("done");' % command,
     36           tab_index, windex)
     37       return True
     38     except JSONInterfaceError:
     39       print '_ExecuteJavascript threw JSONInterfaceError'
     40       return False
     41 
     42   def _WaitForJavascriptCondition(self, condition, tab_index, windex,
     43                                   timeout=-1):
     44     """Waits until the Javascript condition is true.
     45 
     46     This is different from a naive self.WaitUntil(lambda: self.GetDOMValue())
     47     because it uses Javascript to check the condition instead of Python.
     48 
     49     Returns: True if condition is satisfied or otherwise False.
     50     """
     51     try:
     52       return self.WaitUntil(lambda: self.GetDOMValue(
     53           '(%s) ? "1" : ""' % condition, tab_index, windex), timeout)
     54     except JSONInterfaceError:
     55       print '_WaitForJavascriptCondition threw JSONInterfaceError'
     56       return False
     57 
     58   def _ExecuteAndWaitForMode(self, command, mode, tab_index, windex):
     59     """ Executes JavaScript and wait for remoting app mode equal to
     60     the given mode.
     61 
     62     Returns: True if condition is satisfied or otherwise False.
     63     """
     64     if not self._ExecuteJavascript(command, tab_index, windex):
     65       return False
     66     return self._WaitForJavascriptCondition(
     67         'remoting.currentMode == remoting.AppMode.%s' % mode,
     68         tab_index, windex)
     69 
     70   def _ExecuteAndWaitForMajorMode(self, command, mode, tab_index, windex):
     71     """ Executes JavaScript and wait for remoting app major mode equal to
     72     the given mode.
     73 
     74     Returns: True if condition is satisfied or otherwise False.
     75     """
     76     if not self._ExecuteJavascript(command, tab_index, windex):
     77       return False
     78     return self._WaitForJavascriptCondition(
     79         'remoting.getMajorMode() == remoting.AppMode.%s' % mode,
     80         tab_index, windex)
     81 
     82   def GetWebappPath(self):
     83     """Returns the path to the webapp.
     84 
     85     Expects the webapp to be in the same place as the pyautolib binaries.
     86     """
     87     return os.path.join(self.BrowserPath(), 'remoting', 'remoting.webapp')
     88 
     89   def _GetHelperRunner(self):
     90     """Returns the python binary name that runs chromoting_helper.py."""
     91     if sys.platform.startswith('win'):
     92       return 'python'
     93     else:
     94       return 'suid-python'
     95 
     96   def _GetHelper(self):
     97     """Get chromoting_helper.py."""
     98     return os.path.join(os.path.dirname(__file__), 'chromoting_helper.py')
     99 
    100   def InstallHostDaemon(self):
    101     """Installs the host daemon."""
    102     subprocess.call([self._GetHelperRunner(), self._GetHelper(),
    103                      'install', self.BrowserPath()])
    104 
    105   def UninstallHostDaemon(self):
    106     """Uninstalls the host daemon."""
    107     subprocess.call([self._GetHelperRunner(), self._GetHelper(),
    108                      'uninstall', self.BrowserPath()])
    109 
    110   def ContinueAuth(self, tab_index=1, windex=0):
    111     """Starts authentication."""
    112     self.assertTrue(
    113         self._WaitForJavascriptCondition('window.remoting && remoting.oauth2',
    114                                          tab_index, windex),
    115         msg='Timed out while waiting for remoting app to finish loading.')
    116     self._ExecuteJavascript('remoting.oauth2.doAuthRedirect();',
    117                             tab_index, windex)
    118 
    119   def SignIn(self, email=None, password=None, otp=None,
    120                    tab_index=1, windex=0):
    121     """Logs a user in.
    122 
    123     PyAuto tests start with a clean profile, so Chromoting tests should call
    124     this for every run after launching the app. If email or password is
    125     omitted, the user can type it into the browser window manually.
    126     """
    127     self.assertTrue(
    128         self._WaitForJavascriptCondition('document.getElementById("signIn")',
    129                                          tab_index, windex),
    130         msg='Unable to redirect for authentication.')
    131 
    132     if email:
    133       self._ExecuteJavascript('document.getElementById("Email").value = "%s";'
    134                               'document.getElementById("Passwd").focus();'
    135                               % email, tab_index, windex)
    136 
    137     if password:
    138       self._ExecuteJavascript('document.getElementById("Passwd").value = "%s";'
    139                               'document.getElementById("signIn").click();'
    140                               % password, tab_index, windex)
    141 
    142     if otp:
    143       self.assertTrue(
    144           self._WaitForJavascriptCondition(
    145               'document.getElementById("smsVerifyPin")',
    146               tab_index, windex),
    147           msg='Invalid username or password.')
    148       self._ExecuteJavascript(
    149           'document.getElementById("smsUserPin").value = "%s";'
    150           'document.getElementById("smsVerifyPin").click();' % otp,
    151           tab_index, windex)
    152 
    153     # If the account adder screen appears, then skip it.
    154     self.assertTrue(
    155         self._WaitForJavascriptCondition(
    156             'document.getElementById("skip") || '
    157             'document.getElementById("submit_approve_access")',
    158             tab_index, windex),
    159         msg='No "skip adding account" or "approve access" link.')
    160     self._ExecuteJavascript(
    161         'if (document.getElementById("skip")) '
    162         '{ document.getElementById("skip").click(); }',
    163         tab_index, windex)
    164 
    165   def AllowAccess(self, tab_index=1, windex=0):
    166     """Allows access to chromoting webapp."""
    167     # Approve access.
    168     self.assertTrue(
    169         self._WaitForJavascriptCondition(
    170             'document.getElementById("submit_approve_access")',
    171             tab_index, windex),
    172         msg='Did not go to permission page.')
    173     self._WaitForJavascriptCondition(
    174         '!document.getElementById("submit_approve_access").disabled',
    175         tab_index, windex)
    176     self._ExecuteJavascript(
    177         'document.getElementById("submit_approve_access").click();',
    178         tab_index, windex)
    179 
    180     # Wait for some things to be ready.
    181     self.assertTrue(
    182         self._WaitForJavascriptCondition(
    183             'window.remoting && remoting.oauth2 && ' \
    184             'remoting.oauth2.isAuthenticated()',
    185             tab_index, windex),
    186         msg='OAuth2 authentication failed.')
    187     self.assertTrue(
    188         self._WaitForJavascriptCondition(
    189             'window.localStorage.getItem("remoting-email")',
    190             tab_index, windex),
    191         msg='Chromoting app did not reload after authentication.')
    192 
    193   def DenyAccess(self, tab_index=1, windex=0):
    194     """Deny and then allow access to chromoting webapp."""
    195     self.assertTrue(
    196         self._WaitForJavascriptCondition(
    197             'document.getElementById("submit_deny_access")',
    198             tab_index, windex),
    199         msg='Did not go to permission page.')
    200     self._WaitForJavascriptCondition(
    201         '!document.getElementById("submit_deny_access").disabled',
    202         tab_index, windex)
    203     self._ExecuteJavascript(
    204         'document.getElementById("submit_deny_access").click();',
    205         tab_index, windex)
    206 
    207   def SignOut(self, tab_index=1, windex=0):
    208     """Signs out from chromoting and signs back in."""
    209     self._ExecuteAndWaitForMode(
    210         'document.getElementById("sign-out").click();',
    211         'UNAUTHENTICATED', tab_index, windex)
    212 
    213   def Authenticate(self, tab_index=1, windex=0):
    214     """Finishes authentication flow for user."""
    215     self.ContinueAuth(tab_index, windex)
    216     account = self.GetPrivateInfo()['test_chromoting_account']
    217     self.host.SignIn(account['username'], account['password'], None,
    218                     tab_index, windex)
    219     self.host.AllowAccess(tab_index, windex)
    220 
    221   def StartMe2Me(self, tab_index=1, windex=0):
    222     """Starts Me2Me. """
    223     self._ExecuteJavascript(
    224         'document.getElementById("get-started-me2me").click();',
    225         tab_index, windex)
    226     self.assertTrue(
    227         self._WaitForJavascriptCondition(
    228             'document.getElementById("me2me-content").hidden == false',
    229             tab_index, windex),
    230         msg='No me2me content')
    231 
    232   def Share(self, tab_index=1, windex=0):
    233     """Generates an access code and waits for incoming connections.
    234 
    235     Returns:
    236       The access code on success; None otherwise.
    237     """
    238     self._ExecuteAndWaitForMode(
    239         'remoting.tryShare();',
    240         'HOST_WAITING_FOR_CONNECTION', tab_index, windex)
    241     return self.GetDOMValue(
    242         'document.getElementById("access-code-display").innerText',
    243         tab_index, windex)
    244 
    245   def CancelShare(self, tab_index=1, windex=0):
    246     """Stops sharing the desktop on the host side."""
    247     self.assertTrue(
    248         self._ExecuteAndWaitForMode(
    249             'remoting.cancelShare();',
    250             'HOST_SHARE_FINISHED', tab_index, windex),
    251         msg='Stopping sharing from the host side failed')
    252 
    253   def CleanupHostList(self, tab_index=1, windex=0):
    254     """Removes hosts due to failure on previous stop-daemon"""
    255     self.EnableConnectionsInstalled()
    256     this_host_name = self.GetDOMValue(
    257         'document.getElementById("this-host-name").textContent',
    258         tab_index, windex)
    259     if this_host_name.endswith(' (offline)'):
    260       this_host_name = this_host_name[:-10]
    261     self.DisableConnections()
    262 
    263     total_hosts = self.GetDOMValue(
    264         'document.getElementById("host-list").childNodes.length',
    265         tab_index, windex)
    266 
    267     # Start from the end while deleting bogus hosts
    268     index = total_hosts
    269     while index > 0:
    270       index -= 1
    271       try:
    272         hostname = self.GetDOMValue(
    273             'document.getElementById("host-list")'
    274             '.childNodes[%s].textContent' % index,
    275             tab_index, windex)
    276         if hostname == this_host_name or \
    277             hostname == this_host_name + ' (offline)':
    278           self._ExecuteJavascript(
    279               'document.getElementById("host-list")'
    280               '.childNodes[%s].childNodes[3].click()' % index,
    281               tab_index, windex)
    282           self._ExecuteJavascript(
    283               'document.getElementById("confirm-host-delete").click()',
    284               tab_index, windex)
    285       except JSONInterfaceError:
    286         print 'Ignore the error on deleting host'
    287 
    288     if self._WaitForJavascriptCondition(
    289             'document.getElementById("this-host-connect")'
    290             '.getAttribute("data-daemon-state") == "enabled"',
    291             tab_index, windex, 1):
    292       self.DisableConnections()
    293 
    294   def EnableConnectionsInstalled(self, pin_exercise=False,
    295                                  tab_index=1, windex=0):
    296     """Enables the remote connections on the host side."""
    297     if sys.platform.startswith('darwin'):
    298       subprocess.call([self._GetHelperRunner(), self._GetHelper(), 'enable'])
    299 
    300     self.assertTrue(
    301         self._ExecuteAndWaitForMode(
    302             'document.getElementById("start-daemon").click();',
    303             'HOST_SETUP_ASK_PIN', tab_index, windex),
    304         msg='Cannot start host setup')
    305     self.assertTrue(
    306         self._WaitForJavascriptCondition(
    307             'document.getElementById("ask-pin-form").hidden == false',
    308             tab_index, windex),
    309         msg='No ask pin dialog')
    310 
    311     if pin_exercise:
    312       # Cancels the pin prompt
    313       self._ExecuteJavascript(
    314           'document.getElementById("daemon-pin-cancel").click();',
    315           tab_index, windex)
    316 
    317       # Enables again
    318       self.assertTrue(
    319           self._ExecuteAndWaitForMode(
    320               'document.getElementById("start-daemon").click();',
    321               'HOST_SETUP_ASK_PIN', tab_index, windex),
    322           msg='Cannot start host setup')
    323 
    324       # Click ok without typing in pins
    325       self._ExecuteJavascript(
    326           'document.getElementById("daemon-pin-ok").click();',
    327           tab_index, windex)
    328       self.assertTrue(
    329           self._WaitForJavascriptCondition(
    330               'document.getElementById("daemon-pin-error-message")',
    331               tab_index, windex),
    332           msg='No pin error message')
    333 
    334       # Mis-matching pins
    335       self._ExecuteJavascript(
    336           'document.getElementById("daemon-pin-entry").value = "111111";',
    337           tab_index, windex)
    338       self._ExecuteJavascript(
    339           'document.getElementById("daemon-pin-confirm").value = "123456";',
    340           tab_index, windex)
    341       self.assertTrue(
    342           self._WaitForJavascriptCondition(
    343               'document.getElementById("daemon-pin-error-message")',
    344               tab_index, windex),
    345           msg='No pin error message')
    346 
    347     # Types in correct pins
    348     self._ExecuteJavascript(
    349         'document.getElementById("daemon-pin-entry").value = "111111";',
    350         tab_index, windex)
    351     self._ExecuteJavascript(
    352         'document.getElementById("daemon-pin-confirm").value = "111111";',
    353         tab_index, windex)
    354     self.assertTrue(
    355         self._ExecuteAndWaitForMode(
    356             'document.getElementById("daemon-pin-ok").click();',
    357             'HOST_SETUP_PROCESSING', tab_index, windex),
    358         msg='Host setup was not started')
    359 
    360     # Handles preference panes
    361     self.assertTrue(
    362         self._WaitForJavascriptCondition(
    363             'remoting.currentMode == remoting.AppMode.HOST_SETUP_DONE',
    364             tab_index, windex),
    365         msg='Host setup was not done')
    366 
    367     # Dismisses the host config done dialog
    368     self.assertTrue(
    369         self._WaitForJavascriptCondition(
    370             'document.getElementById("host-setup-dialog")'
    371             '.childNodes[5].hidden == false',
    372             tab_index, windex),
    373         msg='No host setup done dialog')
    374     self.assertTrue(
    375         self._ExecuteAndWaitForMode(
    376             'document.getElementById("host-config-done-dismiss").click();',
    377             'HOME', tab_index, windex),
    378         msg='Failed to dismiss host setup confirmation dialog')
    379 
    380   def EnableConnectionsUninstalledAndCancel(self, tab_index=1, windex=0):
    381     """Enables remote connections while host is not installed yet."""
    382     self.assertTrue(
    383         self._ExecuteAndWaitForMode(
    384             'document.getElementById("start-daemon").click();',
    385             'HOST_SETUP_INSTALL', tab_index, windex),
    386         msg='Cannot start host install')
    387     self.assertTrue(
    388         self._ExecuteAndWaitForMode(
    389             'document.getElementById("host-config-install-dismiss").click();',
    390             'HOME', tab_index, windex),
    391         msg='Failed to dismiss host install dialog')
    392 
    393   def DisableConnections(self, tab_index=1, windex=0):
    394     """Disables the remote connections on the host side."""
    395     if sys.platform.startswith('darwin'):
    396       subprocess.call([self._GetHelperRunner(), self._GetHelper(), 'disable'])
    397 
    398     # Re-try to make disabling connection more stable
    399     for _ in range (1, 4):
    400       self._ExecuteJavascript(
    401           'document.getElementById("stop-daemon").click();',
    402           tab_index, windex)
    403 
    404       # Immediately waiting for host-setup-dialog hidden sometimes times out
    405       # even though visually it is hidden. Add some sleep here
    406       time.sleep(2)
    407 
    408       if self._WaitForJavascriptCondition(
    409           'document.getElementById("host-setup-dialog")'
    410           '.childNodes[3].hidden == true',
    411           tab_index, windex, 1):
    412         break;
    413 
    414     self.assertTrue(
    415         self._ExecuteAndWaitForMode(
    416             'document.getElementById("host-config-done-dismiss").click();',
    417             'HOME', tab_index, windex),
    418         msg='Failed to dismiss host setup confirmation dialog')
    419 
    420   def Connect(self, access_code, tab_index=1, windex=0):
    421     """Connects to a Chromoting host and starts the session."""
    422     self.assertTrue(
    423         self._ExecuteAndWaitForMode(
    424             'document.getElementById("access-code-entry").value = "%s";'
    425             'remoting.connectIt2Me();' % access_code,
    426             'IN_SESSION', tab_index, windex),
    427         msg='Cannot connect it2me session')
    428 
    429   def ChangePin(self, pin='222222', tab_index=1, windex=0):
    430     """Changes pin for enabled host."""
    431     if sys.platform.startswith('darwin'):
    432       subprocess.call([self._GetHelperRunner(), self._GetHelper(), 'changepin'])
    433 
    434     self.assertTrue(
    435         self._ExecuteAndWaitForMode(
    436             'document.getElementById("change-daemon-pin").click();',
    437             'HOST_SETUP_ASK_PIN', tab_index, windex),
    438         msg='Cannot change daemon pin')
    439     self.assertTrue(
    440         self._WaitForJavascriptCondition(
    441             'document.getElementById("ask-pin-form").hidden == false',
    442             tab_index, windex),
    443         msg='No ask pin dialog')
    444 
    445     self._ExecuteJavascript(
    446         'document.getElementById("daemon-pin-entry").value = "' + pin + '";',
    447         tab_index, windex)
    448     self._ExecuteJavascript(
    449         'document.getElementById("daemon-pin-confirm").value = "' +
    450         pin + '";', tab_index, windex)
    451     self.assertTrue(
    452         self._ExecuteAndWaitForMode(
    453             'document.getElementById("daemon-pin-ok").click();',
    454             'HOST_SETUP_PROCESSING', tab_index, windex),
    455         msg='Host setup was not started')
    456 
    457     # Handles preference panes
    458     self.assertTrue(
    459         self._WaitForJavascriptCondition(
    460             'remoting.currentMode == remoting.AppMode.HOST_SETUP_DONE',
    461             tab_index, windex),
    462         msg='Host setup was not done')
    463 
    464     # Dismisses the host config done dialog
    465     self.assertTrue(
    466         self._WaitForJavascriptCondition(
    467             'document.getElementById("host-setup-dialog")'
    468             '.childNodes[5].hidden == false',
    469             tab_index, windex),
    470         msg='No host setup done dialog')
    471     self.assertTrue(
    472         self._ExecuteAndWaitForMode(
    473             'document.getElementById("host-config-done-dismiss").click();',
    474             'HOME', tab_index, windex),
    475         msg='Failed to dismiss host setup confirmation dialog')
    476 
    477   def ChangeName(self, new_name='Changed', tab_index=1, windex=0):
    478     """Changes the host name."""
    479     self._ExecuteJavascript(
    480         'document.getElementById("this-host-rename").click();',
    481         tab_index, windex)
    482     self._ExecuteJavascript(
    483         'document.getElementById("this-host-name").childNodes[0].value = "' +
    484         new_name + '";', tab_index, windex)
    485     self._ExecuteJavascript(
    486         'document.getElementById("this-host-rename").click();',
    487         tab_index, windex)
    488 
    489   def ConnectMe2Me(self, pin='111111', mode='IN_SESSION',
    490                    tab_index=1, windex=0):
    491     """Connects to a Chromoting host and starts the session."""
    492 
    493     # There is delay from the enabling remote connections to the host
    494     # showing up in the host list. We need to reload the web app to get
    495     # the host to show up. We will repeat this a few times to make sure
    496     # eventually host appears.
    497     for _ in range(1, 13):
    498       self._ExecuteJavascript(
    499           'window.location.reload();',
    500           tab_index, windex)
    501 
    502       # pyauto _GetResultFromJSONRequest throws JSONInterfaceError after
    503       # 45 seconds if ExecuteJavascript is called right after reload.
    504       # Waiting 2s here can avoid this. So instead of getting the error and
    505       # wait 45s, we wait 2s here. If the error still happens, the following
    506       # retry will handle that.
    507       time.sleep(2)
    508 
    509       # If this-host-connect is still not enabled, let's retry one more time.
    510       this_host_connect_enabled = False
    511       for _ in range(1, 3):
    512         daemon_state_enabled = self._WaitForJavascriptCondition(
    513             'document.getElementById("this-host-connect")'
    514             '.getAttribute("data-daemon-state") == "enabled"',
    515             tab_index, windex, 1)
    516         host_online = self._WaitForJavascriptCondition(
    517             'document.getElementById("this-host-name")'
    518             '.textContent.toString().indexOf("offline") == -1',
    519             tab_index, windex, 1)
    520         this_host_connect_enabled = daemon_state_enabled and host_online
    521         if this_host_connect_enabled:
    522           break
    523       if this_host_connect_enabled:
    524         break;
    525 
    526     # Clicking this-host-connect does work right after this-host-connect
    527     # is enabled. Need to retry.
    528     for _ in range(1, 4):
    529       self._ExecuteJavascript(
    530           'document.getElementById("this-host-connect").click();',
    531           tab_index, windex)
    532 
    533       # pyauto _GetResultFromJSONRequest throws JSONInterfaceError after
    534       # a long time out if WaitUntil is called right after click.
    535       # Waiting 2s here can avoid this.
    536       time.sleep(2)
    537 
    538       # If cannot detect that pin-form appears, retry one more time.
    539       pin_form_exposed = False
    540       for _ in range(1, 3):
    541         pin_form_exposed = self._WaitForJavascriptCondition(
    542             'document.getElementById("client-dialog")'
    543             '.childNodes[9].hidden == false',
    544             tab_index, windex, 1)
    545         if pin_form_exposed:
    546           break
    547 
    548       if pin_form_exposed:
    549         break
    550 
    551       # Dismiss connect failure dialog before retry
    552       if self._WaitForJavascriptCondition(
    553           'document.getElementById("client-dialog")'
    554           '.childNodes[25].hidden == false',
    555           tab_index, windex, 1):
    556         self._ExecuteJavascript(
    557             'document.getElementById("client-finished-me2me-button")'
    558             '.click();',
    559             tab_index, windex)
    560 
    561     self._ExecuteJavascript(
    562         'document.getElementById("pin-entry").value = "' + pin + '";',
    563         tab_index, windex)
    564     self.assertTrue(
    565         self._ExecuteAndWaitForMode(
    566             'document.getElementById("pin-form").childNodes[5].click();',
    567             mode, tab_index, windex),
    568         msg='Session was not started')
    569 
    570   def Disconnect(self, tab_index=1, windex=0):
    571     """Disconnects from the Chromoting it2me session on the client side."""
    572     self.assertTrue(
    573         self._ExecuteAndWaitForMode(
    574             'remoting.disconnect();',
    575             'CLIENT_SESSION_FINISHED_IT2ME', tab_index, windex),
    576         msg='Disconnecting it2me session from the client side failed')
    577 
    578   def DisconnectMe2Me(self, confirmation=True, tab_index=1, windex=0):
    579     """Disconnects from the Chromoting me2me session on the client side."""
    580     self.assertTrue(
    581         self._ExecuteAndWaitForMode(
    582             'remoting.disconnect();',
    583             'CLIENT_SESSION_FINISHED_ME2ME', tab_index, windex),
    584         msg='Disconnecting me2me session from the client side failed')
    585 
    586     if confirmation:
    587       self.assertTrue(
    588           self._ExecuteAndWaitForMode(
    589               'document.getElementById("client-finished-me2me-button")'
    590               '.click();', 'HOME', tab_index, windex),
    591           msg='Failed to dismiss session finished dialog')
    592 
    593   def ReconnectMe2Me(self, pin='111111', tab_index=1, windex=0):
    594     """Reconnects the me2me session."""
    595     self._ExecuteJavascript(
    596         'document.getElementById("client-reconnect-button").click();',
    597         tab_index, windex)
    598 
    599     # pyauto _GetResultFromJSONRequest throws JSONInterfaceError after
    600     # a long time out if WaitUntil is called right after click.
    601     time.sleep(2)
    602 
    603     # If cannot detect that pin-form appears, retry one more time.
    604     for _ in range(1, 3):
    605       pin_form_exposed = self._WaitForJavascriptCondition(
    606           'document.getElementById("client-dialog")'
    607           '.childNodes[9].hidden == false',
    608           tab_index, windex, 1)
    609       if pin_form_exposed:
    610         break
    611 
    612     self._ExecuteJavascript(
    613         'document.getElementById("pin-entry").value = "' + pin + '";',
    614         tab_index, windex)
    615     self.assertTrue(
    616         self._ExecuteAndWaitForMode(
    617             'document.getElementById("pin-form").childNodes[5].click();',
    618             'IN_SESSION', tab_index, windex),
    619         msg='Session was not started when reconnecting')
    620