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