1# Copyright 2015 The Chromium OS 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 5import json 6import logging 7import os 8import time 9 10from autotest_lib.client.common_lib.cros import arc 11 12from autotest_lib.client.common_lib.cros.arc import is_android_container_alive 13 14from autotest_lib.client.bin import test 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.common_lib.cros import chrome 17from autotest_lib.client.common_lib.cros import enrollment 18from autotest_lib.client.common_lib.cros import policy 19from autotest_lib.client.cros import cryptohome 20from autotest_lib.client.cros import httpd 21from autotest_lib.client.cros.input_playback import keyboard 22from autotest_lib.client.cros.enterprise import enterprise_policy_utils 23from autotest_lib.client.cros.enterprise import policy_manager 24from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver 25from autotest_lib.client.common_lib import ui_utils 26 27from telemetry.core import exceptions 28 29CROSQA_FLAGS = [ 30 '--gaia-url=https://gaiastaging.corp.google.com', 31 '--lso-url=https://gaiastaging.corp.google.com', 32 '--google-apis-url=https://www-googleapis-test.sandbox.google.com', 33 '--oauth2-client-id=236834563817.apps.googleusercontent.com', 34 '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI', 35 ('--cloud-print-url=' 36 'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'), 37 '--ignore-urlfetcher-cert-requests'] 38CROSALPHA_FLAGS = [ 39 ('--cloud-print-url=' 40 'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'), 41 '--ignore-urlfetcher-cert-requests'] 42TESTDMS_FLAGS = [ 43 '--ignore-urlfetcher-cert-requests', 44 '--disable-policy-key-verification'] 45FLAGS_DICT = { 46 'prod': [], 47 'crosman-qa': CROSQA_FLAGS, 48 'crosman-alpha': CROSALPHA_FLAGS, 49 'dm-test': TESTDMS_FLAGS, 50 'dm-fake': TESTDMS_FLAGS 51} 52DMS_URL_DICT = { 53 'prod': 'http://m.google.com/devicemanagement/data/api', 54 'crosman-qa': 55 'https://crosman-qa.sandbox.google.com/devicemanagement/data/api', 56 'crosman-alpha': 57 'https://crosman-alpha.sandbox.google.com/devicemanagement/data/api', 58 'dm-test': 'http://chromium-dm-test.appspot.com/d/%s', 59 'dm-fake': 'http://127.0.0.1:%d/' 60} 61DMSERVER = '--device-management-url=%s' 62# Username and password for the fake dm server can be anything, since 63# they are not used to authenticate against GAIA. 64USERNAME = 'fake-user@managedchrome.com' 65PASSWORD = 'fakepassword' 66GAIA_ID = 'fake-gaia-id' 67 68 69class EnterprisePolicyTest(arc.ArcTest, test.test): 70 """Base class for Enterprise Policy Tests.""" 71 72 WEB_PORT = 8080 73 WEB_HOST = 'http://localhost:%d' % WEB_PORT 74 CHROME_POLICY_PAGE = 'chrome://policy' 75 CHROME_VERSION_PAGE = 'chrome://version' 76 77 def initialize(self, **kwargs): 78 """ 79 Initialize test parameters. 80 81 Consume the check_client_result parameter if this test was started 82 from a server test. 83 84 """ 85 self._initialize_enterprise_policy_test(**kwargs) 86 87 def _initialize_enterprise_policy_test( 88 self, env='dm-fake', dms_name=None, 89 username=USERNAME, password=PASSWORD, gaia_id=GAIA_ID, 90 set_auto_logout=None, **kwargs): 91 """ 92 Initialize test parameters and fake DM Server. 93 94 This function exists so that ARC++ tests (which inherit from the 95 ArcTest class) can also initialize a policy setup. 96 97 @param env: String environment of DMS and Gaia servers. 98 If wanting to point to a 'real' server use 'dm-test' 99 @param username: String user name login credential. 100 @param password: String password login credential. 101 @param gaia_id: String gaia_id login credential. 102 @param dms_name: String name of test DM Server. 103 @param kwargs: Not used. 104 105 """ 106 self.env = env 107 self.username = username 108 self.password = password 109 self.gaia_id = gaia_id 110 self.set_auto_logout = set_auto_logout 111 self.dms_name = dms_name 112 self.dms_is_fake = (env == 'dm-fake') 113 self.arc_enabled = False 114 self._enforce_variable_restrictions() 115 116 # Install protobufs and add import path. 117 policy.install_protobufs(self.autodir, self.job) 118 119 # Initialize later variables to prevent error after an early failure. 120 self._web_server = None 121 self.cr = None 122 123 # Start AutoTest DM Server if using local fake server. 124 if self.dms_is_fake: 125 self.fake_dm_server = enterprise_fake_dmserver.FakeDMServer() 126 self.fake_dm_server.start(self.tmpdir, self.debugdir) 127 128 # Get enterprise directory of shared resources. 129 client_dir = os.path.dirname(os.path.dirname(self.bindir)) 130 self.enterprise_dir = os.path.join(client_dir, 'cros/enterprise') 131 132 if self.set_auto_logout is not None: 133 self._auto_logout = self.set_auto_logout 134 135 # Log the test context parameters. 136 logging.info('Test Context Parameters:') 137 logging.info(' Environment: %r', self.env) 138 logging.info(' Username: %r', self.username) 139 logging.info(' Password: %r', self.password) 140 logging.info(' Test DMS Name: %r', self.dms_name) 141 142 def check_page_readiness(self, tab, command): 143 """ 144 Check to see if page has fully loaded. 145 146 @param tab: chrome tab loading the page. 147 @param command: JS command to be checked in the tab. 148 149 @returns True if loaded and False if not. 150 151 """ 152 try: 153 tab.EvaluateJavaScript(command) 154 return True 155 except exceptions.EvaluateException: 156 return False 157 158 def cleanup(self): 159 """Close out anything used by this test.""" 160 # Clean up AutoTest DM Server if using local fake server. 161 if self.dms_is_fake: 162 self.fake_dm_server.stop() 163 164 # Stop web server if it was started. 165 if self._web_server: 166 self._web_server.stop() 167 168 # Close Chrome instance if opened. 169 if self.cr and self._auto_logout: 170 self.cr.close() 171 172 # Cleanup the ARC test if needed. 173 if self.arc_enabled: 174 super(EnterprisePolicyTest, self).cleanup() 175 176 def start_webserver(self): 177 """Set up HTTP Server to serve pages from enterprise directory.""" 178 self._web_server = httpd.HTTPListener( 179 self.WEB_PORT, docroot=self.enterprise_dir) 180 self._web_server.run() 181 182 def _enforce_variable_restrictions(self): 183 """Validate class-level test context parameters. 184 185 @raises error.TestError if context parameter has an invalid value, 186 or a combination of parameters have incompatible values. 187 """ 188 # Verify |env| is a valid environment. 189 if self.env not in FLAGS_DICT: 190 raise error.TestError('Environment is invalid: %s' % self.env) 191 192 # Verify test |dms_name| is given if |env| is 'dm-test'. 193 if self.env == 'dm-test' and not self.dms_name: 194 raise error.TestError('dms_name must be given when using ' 195 'env=dm-test.') 196 if self.env != 'dm-test' and self.dms_name: 197 raise error.TestError('dms_name must not be given when not using ' 198 'env=dm-test.') 199 if self.env == 'prod' and self.username == USERNAME: 200 raise error.TestError('Cannot user real DMS server when using ' 201 'a fake test account.') 202 203 def setup_case(self, 204 user_policies={}, 205 suggested_user_policies={}, 206 device_policies={}, 207 extension_policies={}, 208 skip_policy_value_verification=False, 209 kiosk_mode=False, 210 enroll=False, 211 auto_login=True, 212 auto_logout=True, 213 init_network_controller=False, 214 arc_mode=None, 215 setup_arc=True, 216 use_clouddpc_test=None, 217 disable_default_apps=True, 218 real_gaia=False, 219 extension_paths=[], 220 extra_chrome_flags=[]): 221 """Set up DMS, log in, and verify policy values. 222 223 If the AutoTest fake DM Server is used, make a JSON policy blob 224 and upload it to the fake DM server. 225 226 Launch Chrome and sign in to Chrome OS. Examine the user's 227 cryptohome vault, to confirm user is signed in successfully. 228 229 @param user_policies: dict of mandatory user policies in 230 name -> value format. 231 @param suggested_user_policies: optional dict of suggested policies 232 in name -> value format. 233 @param device_policies: dict of device policies in 234 name -> value format. 235 @param extension_policies: dict of extension policies. 236 @param skip_policy_value_verification: True if setup_case should not 237 verify that the correct policy value shows on policy page. 238 @param enroll: True for enrollment instead of login. 239 @param auto_login: Sign in to chromeos. 240 @param auto_logout: Sign out of chromeos when test is complete. 241 @param init_network_controller: whether to init network controller. 242 @param arc_mode: whether to enable arc_mode on chrome.chrome(). 243 @param setup_arc: whether to run setup_arc in arc.Arctest. 244 @param use_clouddpc_test: whether to run the cloud dpc test. 245 @param extension_paths: list of extensions to install. 246 @param extra_chrome_flags: list of flags to add to Chrome. 247 248 @raises error.TestError if cryptohome vault is not mounted for user. 249 @raises error.TestFail if |policy_name| and |policy_value| are not 250 shown on the Policies page. 251 """ 252 253 # Need a real account, for now. Note: Even though the account is 'real' 254 # you can still use a fake DM server. 255 256 if (arc_mode and self.username == USERNAME) or real_gaia: 257 self.username = 'tester50@managedchrome.com' 258 self.password = 'Test0000' 259 260 self.pol_manager = policy_manager.Policy_Manager( 261 self.username, 262 self.fake_dm_server if hasattr(self, "fake_dm_server") else None) 263 264 self._auto_logout = auto_logout 265 self._kiosk_mode = kiosk_mode 266 267 self.pol_manager.configure_policies(user_policies, 268 suggested_user_policies, 269 device_policies, 270 extension_policies) 271 272 self._create_chrome(enroll=enroll, 273 auto_login=auto_login, 274 init_network_controller=init_network_controller, 275 extension_paths=extension_paths, 276 arc_mode=arc_mode, 277 disable_default_apps=disable_default_apps, 278 real_gaia=real_gaia, 279 extra_chrome_flags=extra_chrome_flags) 280 281 self.pol_manager.obtain_policies_from_device(self.cr.autotest_ext) 282 283 # Skip policy check upon request or if we enroll but don't log in. 284 if not skip_policy_value_verification and auto_login: 285 self.pol_manager.verify_policies() 286 287 if arc_mode: 288 self.start_arc(use_clouddpc_test, setup_arc) 289 290 def update_DM_Info(self): 291 """Force an update of the DM server from current policies.""" 292 if not self.pol_manager.fake_dm_server: 293 raise error.TestError('Cannot autoupdate DM sever without the fake' 294 ' DM Sever being configured') 295 self.pol_manager.updateDMServer() 296 297 def enable_Fake_DM_autoupdate(self): 298 """Enable automatic DM server updates on policy change.""" 299 self.pol_manager.auto_updateDM = True 300 301 def disable_Fake_DM_autoupdate(self): 302 """Disable automatic DM server updates on policy change.""" 303 self.pol_manager.auto_updateDM = False 304 305 def remove_policy(self, 306 name, 307 policy_type, 308 extID=None, 309 verify_policies=True): 310 """ 311 Remove a policy from the currently configured policies. 312 313 @param name: str, the name of the policy 314 @param policy_type: str, the policy type. Must "extension" for 315 extension policies, other for user/device/suggested 316 @param ExtID: str, ID of extension to remove the policy from, if 317 policy_type is extension 318 @param verify_policies: bool, re-verify policies after removal of the 319 specified policy 320 321 """ 322 self.pol_manager.remove_policy(name, policy_type, extID) 323 self.reload_policies(verify_policies) 324 if verify_policies: 325 self.pol_manager.verify_policies() 326 327 def start_arc(self, use_clouddpc_test, setup_arc): 328 """ 329 Start ARC when creating the chrome object. 330 331 Specifically will create the ADB shell container for testing use. 332 333 We are NOT going to use the arc class initialize, it overwrites the 334 creation of chrome.chrome() in a way which cannot support the DM sever. 335 336 Instead we check for the android container, and run arc_setup if 337 needed. Note: To use the cloud dpc test, you MUST also run setup_arc 338 339 @param setup_arc: whether to run setup_arc in arc.Arctest. 340 @param use_clouddpc_test: bool, run_clouddpc_test() or not. 341 342 """ 343 _APP_FILENAME = 'autotest-deps-cloudpctest-0.4.apk' 344 _DEP_PACKAGE = 'CloudDPCTest-apks' 345 _PKG_NAME = 'com.google.android.apps.work.clouddpc.e2etests' 346 347 # By default on boot the container is alive, and will not close until 348 # a user with ARC disabled logs in. This wait accounts for that. 349 time.sleep(3) 350 351 if use_clouddpc_test and not setup_arc: 352 raise error.TestFail('For cloud DPC setup_arc cannot be disabled') 353 354 if is_android_container_alive(): 355 logging.info('Android Container is alive!') 356 else: 357 logging.error('Android Container is not alive!') 358 359 # Install the clouddpc test. 360 if use_clouddpc_test: 361 self.arc_setup(dep_packages=_DEP_PACKAGE, 362 apks=[_APP_FILENAME], 363 full_pkg_names=[_PKG_NAME]) 364 self.run_clouddpc_test() 365 else: 366 if setup_arc: 367 self.arc_setup() 368 369 self.arc_enabled = True 370 371 def run_clouddpc_test(self): 372 """ 373 Run clouddpc end-to-end test and fail this test if it fails. 374 375 Assumes start_arc() was run with use_clouddpc_test. 376 377 Determines the policy values to pass to the test from those set in 378 Chrome OS. 379 380 @raises error.TestFail if the test does not pass. 381 382 """ 383 policy_blob = self.pol_manager.getCloudDpc() 384 policy_blob_str = json.dumps(policy_blob, separators=(',', ':')) 385 cmd = ('am instrument -w -e policy "%s" ' 386 'com.google.android.apps.work.clouddpc.e2etests/' 387 '.ArcInstrumentationTestRunner') % policy_blob_str 388 389 # Run the command as a shell script so that its length is not capped. 390 temp_shell_script_path = '/sdcard/tmp.sh' 391 arc.write_android_file(temp_shell_script_path, cmd) 392 393 logging.info('Running clouddpc test with policy: %s', policy_blob_str) 394 results = arc.adb_shell('sh ' + temp_shell_script_path, 395 ignore_status=True).strip() 396 arc.remove_android_file(temp_shell_script_path) 397 if results.find('FAILURES!!!') >= 0: 398 logging.info('CloudDPC E2E Results:\n%s', results) 399 err_msg = results.splitlines()[-1] 400 raise error.TestFail('CloudDPC E2E failure: %s' % err_msg) 401 402 logging.debug(results) 403 logging.info('CloudDPC E2E test passed!') 404 405 def _get_policy_value_from_new_tab(self, policy): 406 """ 407 TO BE DEPRICATED DO NOT USE. 408 409 Gets the value of the policy. 410 411 """ 412 return self.pol_manager.get_policy_value_from_DUT(policy) 413 414 def add_policies(self, 415 user={}, 416 suggested_user={}, 417 device={}, 418 extension={}, 419 new=False): 420 """Add policies to the policy rules.""" 421 self.pol_manager.configure_policies(user=user, 422 suggested_user=suggested_user, 423 device=device, 424 extension=extension, 425 new=new) 426 self.reload_policies(True) 427 428 def update_policies(self, user_policies={}, suggested_user_policies={}, 429 device_policies={}, extension_policies={}): 430 """ 431 Change the policies to the ones provided. 432 433 NOTE: This will override any current policies configured. 434 435 @param user_policies: mandatory user policies -> values. 436 @param suggested user_policies: suggested user policies -> values. 437 @param device_policies: mandatory device policies -> values. 438 @param extension_policies: extension policies. 439 440 """ 441 self.add_policies(user_policies, suggested_user_policies, 442 device_policies, extension_policies, True) 443 444 def reload_policies(self, wait_for_new=False): 445 """ 446 Force a policy fetch. 447 448 @param wait_for_new: bool, wait up to 1 second for the policy values 449 from the API to update 450 451 """ 452 enterprise_policy_utils.refresh_policies(self.cr.autotest_ext, 453 wait_for_new) 454 455 def verify_extension_stats(self, extension_policies, sensitive_fields=[]): 456 """ 457 Verify the extension policies match what is on chrome://policy. 458 459 @param extension_policies: the dictionary of extension IDs mapping 460 to download_url and secure_hash. 461 @param sensitive_fields: list of fields that should have their value 462 censored. 463 @raises error.TestError: if the shown values do not match what we are 464 expecting. 465 466 """ 467 # Refetch the policies from the DUT 468 self.pol_manager.obtain_policies_from_device() 469 470 # For this method we are expecting the extension policies to be living 471 # within the chrome file system. 472 for ext_id in extension_policies.keys(): 473 filePath = extension_policies[ext_id]['download_url'] 474 actual_extension_policies = ( 475 self._get_extension_policies_from_file(filePath)) 476 477 # Obfuscate and strip the extra dict level 478 self._configure_extension_file_policies(actual_extension_policies, 479 sensitive_fields) 480 481 self.pol_manager.configure_extension_visual_policy( 482 {ext_id: actual_extension_policies}) 483 484 self.pol_manager.verify_policies() 485 486 def _configure_extension_file_policies(self, policies, sensitive_fields): 487 """ 488 Given policies, change sensitive_fields to "********". 489 490 @param policies: dict, the extension policies as loaded from the file. 491 @param sensitive_fields: list or set 492 493 """ 494 for policy_name, value in policies.items(): 495 if policy_name in sensitive_fields: 496 policies[policy_name] = '*' * 8 497 else: 498 if 'Value' in value: 499 policies[policy_name] = value['Value'] 500 501 def _get_extension_policies_from_file(self, download_url): 502 """ 503 Get the configured extension policies from file system. 504 505 Extension tests will store the "actual" extensions as a file within 506 ChromeOS. Open the file within the enterprise_dir, and return the 507 policies from the file. 508 509 @param download_url: URL of the download location 510 511 """ 512 policy_file = os.path.join(self.enterprise_dir, 513 download_url.split('/')[-1]) 514 515 with open(policy_file) as f: 516 policies = json.load(f) 517 return policies 518 519 def verify_policy_value(self, 520 policy_name, 521 expected_value, 522 extensionID=None): 523 """ 524 Verify the value of a single policy. 525 526 Note: This will not format the expected value. (Ie it will not 527 automatically change a password/sensitive field to ********) 528 529 @param policy_name: the policy we are checking. 530 @param expected_value: the expected value for policy_name. 531 532 @raises error.TestError if value does not match expected. 533 534 """ 535 self.pol_manager.verify_policy(policy_name, 536 expected_value, 537 extensionID) 538 539 def _initialize_chrome_extra_flags(self): 540 """ 541 Initialize flags used to create Chrome instance. 542 543 @returns: list of extra Chrome flags. 544 545 """ 546 # Construct DM Server URL flags if not using production server. 547 env_flag_list = [] 548 if self.env != 'prod': 549 if self.dms_is_fake: 550 # Use URL provided by the fake AutoTest DM server. 551 dmserver_str = (DMSERVER % self.fake_dm_server.server_url) 552 else: 553 # Use URL defined in the DMS URL dictionary. 554 dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env])) 555 if self.env == 'dm-test': 556 dmserver_str = (dmserver_str % self.dms_name) 557 558 # Merge with other flags needed by non-prod enviornment. 559 env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env]) 560 561 return env_flag_list 562 563 def _enterprise_enroll(self, extra_flags, extension_paths, auto_login): 564 """ 565 Enroll a device, configure self.cr, optionally login. 566 567 @param extra_chrome_flags: list of flags to add. 568 @param extension_paths: list of extensions to install. 569 @param auto_login: sign in to chromeos. 570 571 """ 572 self.cr = chrome.Chrome( 573 auto_login=False, 574 autotest_ext=True, 575 extra_browser_args=extra_flags, 576 extension_paths=extension_paths, 577 expect_policy_fetch=True) 578 579 if self.dms_is_fake: 580 if self._kiosk_mode: 581 enrollment.KioskEnrollment( 582 self.cr.browser, self.username, self.password, 583 self.gaia_id) 584 else: 585 enrollment.EnterpriseFakeEnrollment( 586 self.cr.browser, self.username, self.password, 587 self.gaia_id, auto_login=auto_login) 588 else: 589 enrollment.EnterpriseEnrollment( 590 self.cr.browser, self.username, self.password, 591 auto_login=auto_login) 592 593 def _create_chrome(self, 594 enroll=False, 595 auto_login=True, 596 arc_mode=None, 597 real_gaia=False, 598 init_network_controller=False, 599 disable_default_apps=True, 600 extension_paths=[], 601 extra_chrome_flags=[]): 602 """ 603 Create a Chrome object. Enroll and/or sign in. 604 605 Function results in self.cr set as the Chrome object. 606 607 @param enroll: enroll the device. 608 @param auto_login: sign in to chromeos. 609 @param arc_mode: enable arc mode. 610 @param real_gaia: to login with a real gaia account or not. 611 @param init_network_controller: whether to init network controller. 612 @param disable_default_apps: disables default apps or not 613 (e.g. the Files app). 614 @param extension_paths: list of extensions to install. 615 @param extra_chrome_flags: list of flags to add. 616 """ 617 extra_flags = (self._initialize_chrome_extra_flags() + 618 extra_chrome_flags) 619 620 logging.info('Chrome Browser Arguments:') 621 logging.info(' extra_browser_args: %s', extra_flags) 622 logging.info(' username: %s', self.username) 623 logging.info(' password: %s', self.password) 624 logging.info(' gaia_login: %s', not self.dms_is_fake) 625 logging.info(' enroll: %s', enroll) 626 627 if enroll: 628 self._enterprise_enroll(extra_flags, extension_paths, auto_login) 629 elif auto_login: 630 # gaia_login (aka real login) is true on arc_mode, or real_gaia. 631 # otherwise: gaia_login is False when a fake dm server is used. 632 gaia_login = (True if (arc_mode or real_gaia) 633 else not self.dms_is_fake) 634 # Do not disable_gaia_services, or disable_arc_opt_in 635 # if using a gaia_login. 636 disable_gaia_services = disable_arc_opt_in = not gaia_login 637 enterprise_arc_test = gaia_login 638 639 self.cr = chrome.Chrome( 640 extra_browser_args=extra_flags, 641 username=self.username, 642 password=self.password, 643 gaia_login=gaia_login, 644 arc_mode=arc_mode, 645 disable_arc_opt_in=disable_arc_opt_in, 646 disable_gaia_services=disable_gaia_services, 647 autotest_ext=True, 648 enterprise_arc_test=enterprise_arc_test, 649 init_network_controller=init_network_controller, 650 expect_policy_fetch=True, 651 extension_paths=extension_paths, 652 disable_default_apps=disable_default_apps) 653 654 else: 655 self.cr = chrome.Chrome( 656 auto_login=False, 657 extra_browser_args=extra_flags, 658 disable_gaia_services=self.dms_is_fake, 659 autotest_ext=True, 660 expect_policy_fetch=True) 661 662 self.ui = ui_utils.UI_Handler() 663 # Used by arc.py to determine the state of the chrome obj 664 self.initialized = True 665 if auto_login: 666 if not cryptohome.is_vault_mounted(user=self.username, 667 allow_fail=True): 668 raise error.TestError('Expected to find a mounted vault for %s.' 669 % self.username) 670 671 def navigate_to_url(self, url, tab=None): 672 """Navigate tab to the specified |url|. Create new tab if none given. 673 674 @param url: URL of web page to load. 675 @param tab: browser tab to load (if any). 676 @returns: browser tab loaded with web page. 677 @raises: telemetry TimeoutException if document ready state times out. 678 """ 679 logging.info('Navigating to URL: %r', url) 680 if not tab: 681 tab = self.cr.browser.tabs.New() 682 tab.Activate() 683 tab.Navigate(url, timeout=8) 684 tab.WaitForDocumentReadyStateToBeComplete() 685 return tab 686 687 def get_elements_from_page(self, tab, cmd): 688 """Get collection of page elements that match the |cmd| filter. 689 690 @param tab: tab containing the page to be scraped. 691 @param cmd: JavaScript command to evaluate on the page. 692 @returns object containing elements on page that match the cmd. 693 @raises: TestFail if matching elements are not found on the page. 694 """ 695 try: 696 elements = tab.EvaluateJavaScript(cmd) 697 except Exception as err: 698 raise error.TestFail('Unable to find matching elements on ' 699 'the test page: %s\n %r' % (tab.url, err)) 700 return elements 701 702 def log_out_via_keyboard(self): 703 """Log out of the device using the keyboard shortcut.""" 704 _keyboard = keyboard.Keyboard() 705 _keyboard.press_key('ctrl+shift+q') 706 _keyboard.press_key('ctrl+shift+q') 707 _keyboard.close() 708