Chromium Code Reviews| Index: components/proximity_auth/e2e_test/cros.py |
| diff --git a/components/proximity_auth/e2e_test/cros.py b/components/proximity_auth/e2e_test/cros.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..670e8d94f25c01ca49a942c56fd7fda2afc116b1 |
| --- /dev/null |
| +++ b/components/proximity_auth/e2e_test/cros.py |
| @@ -0,0 +1,494 @@ |
| +# Copyright 2015 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +import logging |
| +import os |
| +import subprocess |
| +import sys |
| +import time |
| + |
| +# Add the telemetry directory to Python's search paths. |
| +curr_dir = os.path.dirname(os.path.realpath(__file__)) |
|
Ilya Sherman
2015/03/24 01:25:13
nit: s/curr_dir/current_directory or at least curr
Tim Song
2015/03/25 23:08:43
Done.
|
| +telemetry_dir = os.path.realpath( |
| + os.path.join(curr_dir, '..', '..', '..', 'tools', 'telemetry')) |
| +if telemetry_dir not in sys.path: |
| + sys.path.append(telemetry_dir) |
| + |
| +from telemetry.core import browser_options |
| +from telemetry.core import browser_finder |
| +from telemetry.core import extension_to_load |
| +from telemetry.core import exceptions |
| +from telemetry.core import util |
| +from telemetry.core.platform import cros_interface |
| + |
| +logger = logging.getLogger('proximity_auth.%s' % __name__) |
| + |
| +class AccountPickerScreen(object): |
| + """ Wrapper for the ChromeOS account picker screen used on both the lock |
| + screen and signin screen. |
|
Ilya Sherman
2015/03/24 01:25:14
nit: The first line is supposed to fit on a single
Tim Song
2015/03/25 23:08:43
Done.
|
| + """ |
| + |
| + class AuthType: |
| + """ The authentication type expected for a user pod. |
| + """ |
|
Ilya Sherman
2015/03/24 01:25:13
nit: No need to wrap the closing """ (applies thro
Tim Song
2015/03/25 23:08:43
Done.
|
| + OFFLINE_PASSWORD = 0 |
| + ONLINE_SIGN_IN = 1 |
| + NUMERIC_PIN = 2 |
| + USER_CLICK = 3 |
| + EXPAND_THEN_USER_CLICK = 4 |
| + FORCE_OFFLINE_PASSWORD = 5 |
| + |
| + class SmartLockState: |
| + """ The state of the Smart Lock icon on a user pod. |
| + """ |
| + NOT_SHOWN = 'not_shown' |
| + AUTHENTICATED = 'authenticated' |
| + LOCKED = 'locked' |
| + HARD_LOCKED = 'hardlocked' |
| + TO_BE_ACTIVATED = 'to_be_activated' |
| + SPINNER = 'spinner' |
| + |
| + def __init__(self, oobe, chromeos): |
| + """ |
| + Args: |
| + oobe: Inspector page of the OOBE WebContents. |
| + chromeos: The parent Chrome wrapper. |
| + """ |
| + self._oobe = oobe |
| + self._chromeos = chromeos |
| + |
| + @property |
| + def is_lockscreen(self): |
| + return self._oobe.EvaluateJavaScript( |
|
Ilya Sherman
2015/03/24 01:25:13
nit: Extra space after "return"
Tim Song
2015/03/25 23:08:43
Done.
|
| + '!document.getElementById("sign-out-user-item").hidden') |
| + |
| + @property |
| + def auth_type(self): |
| + return self._oobe.EvaluateJavaScript( |
| + 'document.getElementById("pod-row").pods[0].authType') |
|
Ilya Sherman
2015/03/24 01:25:14
nit: Maybe define a string constant for "document.
Tim Song
2015/03/25 23:08:43
Done.
|
| + |
| + @property |
| + def smart_lock_state(self): |
| + icon_shown = self._oobe.EvaluateJavaScript( |
| + '!document.getElementById("pod-row").pods[0].customIconElement.hidden') |
| + if not icon_shown: |
| + return self.SmartLockState.NOT_SHOWN |
| + class_list_dict = self._oobe.EvaluateJavaScript( |
| + 'document.getElementById("pod-row").pods[0].customIconElement' |
| + '.querySelector(".custom-icon").classList') |
| + class_list = [v for k,v in class_list_dict.items() if k != 'length'] |
| + |
| + if 'custom-icon-unlocked' in class_list: |
| + return self.SmartLockState.AUTHENTICATED |
| + if 'custom-icon-locked' in class_list: |
| + return self.SmartLockState.LOCKED |
| + if 'custom-icon-hardlocked' in class_list: |
| + return self.SmartLockState.HARD_LOCKED |
| + if 'custom-icon-locked-to-be-activated' in class_list: |
| + return self.SmartLockState.TO_BE_ACTIVATED |
| + if 'custom-icon-spinner' in class_list: |
| + return self.SmartLockState.SPINNER |
| + |
| + def WaitForSmartLockState(self, state, wait_time_secs=60): |
| + """ Waits for the Smart Lock icon to reach the given state. |
|
Ilya Sherman
2015/03/24 01:25:13
nit: Please leave a blank line after this one. (Ap
Tim Song
2015/03/25 23:08:43
Done.
|
| + Note: The first user pod on the screen is used. |
|
Ilya Sherman
2015/03/24 01:25:13
nit: This note seems to apply to most of the metho
Tim Song
2015/03/25 23:08:44
Done.
|
| + |
| + Args: |
| + state: A value in AccountPickerScreen.SmartLockState |
| + wait_time_secs: The time to wait |
| + Returns: |
| + True if the state is reached within the wait time, else False. |
| + """ |
| + try: |
| + util.WaitFor(lambda: self.smart_lock_state == state, wait_time_secs) |
| + return True |
| + except exceptions.TimeoutException: |
| + return False |
| + |
| + def EnterPassword(self): |
| + """ Enters the password to unlock or sign-in. |
| + Note: The first user pod on the screen is used. |
|
Ilya Sherman
2015/03/24 01:25:14
nit: Please document that this can throw an except
Tim Song
2015/03/25 23:08:43
Done.
|
| + """ |
| + assert(self.auth_type == self.AuthType.OFFLINE_PASSWORD or |
| + self.auth_type == self.AuthType.FORCE_OFFLINE_PASSWORD) |
| + oobe = self._oobe |
| + oobe.EvaluateJavaScript( |
| + 'document.getElementById("pod-row").pods[0].passwordElement.value = ' |
| + '"%s"' % self._chromeos.password) |
| + oobe.EvaluateJavaScript( |
| + 'document.getElementById("pod-row").pods[0].activate()') |
| + util.WaitFor(lambda: self._chromeos.session_state == \ |
| + ChromeOS.SessionState.IN_SESSION, 5) |
| + |
| + def UnlockWithClick(self): |
| + """ Clicks the user pod to unlock or sign-in. |
| + Note: The first user pod on the screen is used. |
| + """ |
| + assert(self.auth_type == self.AuthType.USER_CLICK) |
| + self._oobe.EvaluateJavaScript( |
| + 'document.getElementById("pod-row").pods[0].activate()') |
| + |
|
Ilya Sherman
2015/03/24 01:25:14
nit: I believe that top-level definitions are supp
Tim Song
2015/03/25 23:08:43
Done.
|
| +class SmartLockSettings(object): |
| + """ Wrapper for the Smart Lock settings in chromeos://settings. |
| + """ |
| + def __init__(self, tab, chromeos): |
| + """ |
| + Args: |
| + tab: Inspector page of the chromeos://settings tag. |
| + chromeos: The parent Chrome wrapper. |
| + """ |
| + self._tab = tab |
| + self._chromeos = chromeos |
| + |
| + @property |
| + def smart_lock_enabled(self): |
|
Ilya Sherman
2015/03/24 01:25:13
nit: Doc string
Ilya Sherman
2015/03/24 01:25:13
nit: Maybe prepend "is_" to the method name?
Tim Song
2015/03/25 23:08:43
I have an assert in the test class to make sure th
Tim Song
2015/03/25 23:08:43
Done.
Ilya Sherman
2015/03/25 23:17:29
I just meant, "is_smart_lock_enabled" sounds more
Tim Song
2015/03/27 18:05:26
Done.
|
| + return self._tab.EvaluateJavaScript( |
| + '!document.getElementById("easy-unlock-enabled").hidden') |
| + |
| + def TurnOffSmartLock(self): |
| + """ Turns off Smart Lock by clicking the turn-off button and navigating |
| + through the overlay. |
| + """ |
|
Ilya Sherman
2015/03/24 01:25:14
nit: Please document that this can throw an except
Tim Song
2015/03/25 23:08:43
Done.
|
| + assert(self.smart_lock_enabled) |
| + tab = self._tab |
| + tab.EvaluateJavaScript( |
| + 'document.getElementById("easy-unlock-turn-off-button").click()') |
| + util.WaitFor(lambda: tab.EvaluateJavaScript( |
| + '!document.getElementById("easy-unlock-turn-off-overlay").hidden && ' |
| + 'document.getElementById("easy-unlock-turn-off-confirm") != null'), |
| + 10) |
| + tab.EvaluateJavaScript( |
| + 'document.getElementById("easy-unlock-turn-off-confirm").click()') |
| + util.WaitFor(lambda: tab.EvaluateJavaScript( |
| + '!document.getElementById("easy-unlock-disabled").hidden'), 15) |
| + |
| + def StartSetup(self): |
| + """ Starts the Smart Lock setup flow by clicking the button. |
| + """ |
| + assert(not self.smart_lock_enabled) |
| + self._tab.EvaluateJavaScript( |
| + 'document.getElementById("easy-unlock-setup-button").click()') |
| + |
| + def StartSetupAndReturnApp(self): |
| + """ Starts the Smart Lock setup flow and enters the password to |
| + reauthenticate the user before the app launches. |
| + |
| + Returns: |
| + A SmartLockApp object of the app that was launched. |
| + """ |
|
Ilya Sherman
2015/03/24 01:25:14
nit: Please document that this can throw an except
Tim Song
2015/03/25 23:08:44
Done.
|
| + self.StartSetup() |
| + util.WaitFor(lambda: self._chromeos.session_state == \ |
| + ChromeOS.SessionState.LOCK_SCREEN, 5) |
| + lock_screen = self._chromeos.GetAccountPickerScreen() |
| + lock_screen.EnterPassword() |
| + util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10) |
| + return self._chromeos.GetSmartLockApp() |
| + |
| +class SmartLockApp(object): |
| + """ Wrapper for the Smart Lock setup dialog. |
| + Note: This does not include the app's background page. |
| + """ |
| + |
| + class PairingState: |
| + """ The current state of the setup flow. |
| + """ |
| + SCAN = 'scan' |
| + PAIR = 'pair' |
| + CLICK_FOR_TRIAL_RUN = 'click_for_trial_run' |
| + TRIAL_RUN_COMPLETED = 'trial_run_completed' |
| + |
| + def __init__(self, app_page, chromeos): |
| + """ |
| + Args: |
| + app_page: Inspector page of the app window. |
| + chromeos: The parent Chrome wrapper. |
| + """ |
| + self._app_page = app_page |
| + self._chromeos = chromeos |
| + |
| + @property |
| + def pairing_state(self): |
|
Ilya Sherman
2015/03/24 01:25:14
nit: Doc string, including exception warning.
Tim Song
2015/03/25 23:08:44
Done.
|
| + state = self._app_page.EvaluateJavaScript( |
| + 'document.body.getAttribute("step")') |
| + if state == 'scan': |
| + return SmartLockApp.PairingState.SCAN |
| + elif state == 'pair': |
| + return SmartLockApp.PairingState.PAIR |
| + elif state == 'complete': |
| + button_text = self._app_page.EvaluateJavaScript( |
| + 'document.getElementById("pairing-button").textContent') |
| + button_text = button_text.strip().lower() |
| + if button_text == 'try it out': |
| + return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN |
| + elif button_text == 'done': |
| + return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED |
| + else: |
| + raise ValueError('Unknown button text: %s', button_text) |
| + else: |
| + raise ValueError('Unknown pairing state: %s' % state) |
| + |
| + def FindPhone(self, retries=3): |
| + """ Starts the initial step to find nearby phones. |
| + The app must be in the SCAN state. |
| + |
| + Args: |
| + retries: The number of times to retry if no phones are found. |
| + Returns: |
| + True if a phone is found, else False. |
| + """ |
| + assert(self.pairing_state == self.PairingState.SCAN) |
| + for _ in xrange(retries): |
| + self._ClickPairingButton() |
| + if self.pairing_state == self.PairingState.PAIR: |
| + return True |
| + # Wait a few seconds before retrying. |
| + time.sleep(10) |
|
Ilya Sherman
2015/03/24 01:25:14
Ick. What's the total runtime for the test?
Tim Song
2015/03/25 23:08:43
The test can take 30-60 seconds to run, depending
|
| + return False |
| + |
| + def PairPhone(self): |
| + """ Starts the step of finding nearby phones. |
| + The app must be in the PAIR state. |
| + |
| + Returns: |
| + True if pairing succeeded, else False. |
| + """ |
| + assert(self.pairing_state == self.PairingState.PAIR) |
| + self._ClickPairingButton() |
| + return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN |
| + |
| + def StartTrialRun(self): |
| + """ Starts the trial run. |
| + The app must be in the CLICK_FOR_TRIAL_RUN state. |
| + """ |
| + assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN) |
| + self._ClickPairingButton() |
| + util.WaitFor(lambda: self._chromeos.session_state == \ |
| + ChromeOS.SessionState.LOCK_SCREEN, 10) |
| + |
| + def DismissApp(self): |
| + """ Dismisses the app after setup is completed. |
| + The app must be in the TRIAL_RUN_COMPLETED state. |
| + """ |
| + assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED) |
| + self._app_page.EvaluateJavaScript( |
| + 'document.getElementById("pairing-button").click()') |
| + |
| + def _ClickPairingButton(self): |
| + self._app_page.EvaluateJavaScript( |
| + 'document.getElementById("pairing-button").click()') |
| + util.WaitFor(lambda: self._app_page.EvaluateJavaScript( |
| + '!document.getElementById("pairing-button").disabled'), 60) |
| + util.WaitFor(lambda: self._app_page.EvaluateJavaScript( |
| + '!document.getElementById("pairing-button-title")' |
| + '.classList.contains("animated-fade-out")'), 5) |
| + util.WaitFor(lambda: self._app_page.EvaluateJavaScript( |
| + '!document.getElementById("pairing-button-title")' |
| + '.classList.contains("animated-fade-in")'), 5) |
| + |
| +class ChromeOS(object): |
| + """ Wrapper for a remote ChromeOS device. |
| + """ |
| + |
| + class SessionState: |
| + """ The state of the user session. |
| + """ |
| + SIGNIN_SCREEN = 'signin_screen' |
| + IN_SESSION = 'in_session' |
| + LOCK_SCREEN = 'lock_screen' |
| + |
| + def __init__(self, remote_address, username, password): |
| + """ |
| + Args: |
| + remote_address: The remote address of the cros device. |
| + username: The username of the account to test. |
| + password: The password of the account to test. |
| + """ |
| + self._remote_address = remote_address; |
| + self._username = username |
| + self._password = password |
| + self._browser = None |
| + self._cros_interface = None |
| + self._background_page = None |
| + self._processes = [] |
| + |
| + @property |
| + def username(self): |
|
Ilya Sherman
2015/03/24 01:25:14
nit: Doc string. (Applies to all other undocument
Tim Song
2015/03/25 23:08:43
Done.
|
| + return self._username |
| + |
| + @property |
| + def password(self): |
| + return self._password |
| + |
| + @property |
| + def session_state(self): |
| + assert(self._browser is not None) |
| + if self._browser.oobe_exists: |
| + return self.SessionState.LOCK_SCREEN if \ |
| + self._cros_interface.IsCryptohomeMounted(self.username, False) else \ |
| + self.SessionState.SIGNIN_SCREEN |
|
Ilya Sherman
2015/03/24 01:25:13
nit: Please use a regular if/else since this spans
Tim Song
2015/03/25 23:08:44
Done.
|
| + else: |
| + return self.SessionState.IN_SESSION; |
| + |
| + @property |
| + def cryptauth_access_token(self): |
| + try: |
| + util.WaitFor(lambda: self._background_page.EvaluateJavaScript( |
| + 'var __token = __token || null; ' |
| + 'chrome.identity.getAuthToken(function(token) {' |
| + ' __token = token;' |
| + '}); ' |
| + '__token != null'), 5) |
| + return self._background_page.EvaluateJavaScript('__token'); |
| + except exceptions.TimeoutException: |
| + logger.error('Failed to get access token.'); |
| + return '' |
| + |
| + def __enter__(self): |
| + return self |
| + |
| + def __exit__(self, *args): |
| + if self._browser is not None: |
| + self._browser.Close() |
| + if self._cros_interface is not None: |
| + self._cros_interface.CloseConnection() |
| + for process in self._processes: |
| + process.terminate() |
| + |
| + def Start(self, local_app_path=None): |
| + """ Connects to the ChromeOS device and logs in. |
| + Args: |
| + local_app_path: A path on the local device containing the Smart Lock app |
| + to use instead of the app on the ChromeOS device. |
| + Return: |
| + |self| for using in a "with" statement. |
| + """ |
| + assert(self._browser is None) |
| + |
| + finder_opts = browser_options.BrowserFinderOptions('cros-chrome') |
| + finder_opts.CreateParser().parse_args(args=[]) |
| + finder_opts.cros_remote = self._remote_address |
| + finder_opts.verbosity = 1 |
| + |
| + browser_opts = finder_opts.browser_options |
| + browser_opts.create_browser_with_oobe = True |
| + browser_opts.disable_component_extensions_with_background_pages = False |
| + browser_opts.gaia_login = True |
| + browser_opts.username = self._username |
| + browser_opts.password = self._password |
| + browser_opts.auto_login = True |
| + |
| + self._cros_interface = cros_interface.CrOSInterface( |
| + finder_opts.cros_remote, |
| + finder_opts.cros_remote_ssh_port, |
| + finder_opts.cros_ssh_identity) |
| + |
| + browser_opts.disable_default_apps = local_app_path is not None |
| + if local_app_path is not None: |
| + easy_unlock_app = extension_to_load.ExtensionToLoad( |
| + path=local_app_path, |
| + browser_type='cros-chrome', |
| + is_component=True) |
| + finder_opts.extensions_to_load.append(easy_unlock_app) |
| + |
| + retries = 3 |
| + while self._browser is not None or retries > 0: |
| + try: |
| + browser_to_create = browser_finder.FindBrowser(finder_opts) |
| + self._browser = browser_to_create.Create(finder_opts); |
| + break; |
| + except (exceptions.LoginException) as e: |
| + logger.error('Timed out logging in: %s' % e); |
| + if retries == 1: |
| + raise |
| + |
| + bg_page_path = '/_generated_background_page.html' |
| + util.WaitFor( |
| + lambda: self._FindSmartLockAppPage(bg_page_path) is not None, |
| + 10); |
| + self._background_page = self._FindSmartLockAppPage(bg_page_path) |
| + return self |
| + |
| + def GetAccountPickerScreen(self): |
| + """ Returns the wrapper for the lock screen or sign-in screen. |
| + |
| + Return: |
| + An instance of AccountPickerScreen. |
| + An exception will be raised if the wrapper can't be created. |
| + """ |
| + assert(self._browser is not None) |
| + assert(self.session_state == self.SessionState.LOCK_SCREEN or |
| + self.session_state == self.SessionState.SIGNIN_SCREEN) |
| + oobe = self._browser.oobe |
| + def IsLockScreenResponsive(): |
| + return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and |
| + oobe.EvaluateJavaScript( |
| + "typeof Oobe.authenticateForTesting == 'function'")) |
| + util.WaitFor(IsLockScreenResponsive, 10) |
| + util.WaitFor(lambda: oobe.EvaluateJavaScript( |
| + 'document.getElementById("pod-row") && ' |
| + 'document.getElementById("pod-row").pods && ' |
| + 'document.getElementById("pod-row").pods.length > 0'), 10) |
| + return AccountPickerScreen(oobe, self) |
| + |
| + def GetSmartLockSettings(self): |
| + """ Returns the wrapper for the Smart Lock settings. |
| + A tab will be navigated to chrome://settings if it does not exist. |
| + |
| + Return: |
| + An instance of SmartLockSettings. |
| + An exception will be raised if the wrapper can't be created. |
| + """ |
| + if not len(self._browser.tabs): |
| + self._browser.New() |
| + tab = self._browser.tabs[0] |
| + url = tab.EvaluateJavaScript('document.location.href') |
| + smart_lock_url = 'chrome://settings/search#Smart%20Lock' |
|
Ilya Sherman
2015/03/24 01:25:13
nit: NAMED_CONSTANT
Tim Song
2015/03/25 23:08:43
Done.
|
| + if url != smart_lock_url: |
| + tab.Navigate(smart_lock_url) |
| + |
| + # Wait for settings page to be responsive. |
| + util.WaitFor(lambda: tab.EvaluateJavaScript( |
| + 'document.getElementById("easy-unlock-disabled") && ' |
| + 'document.getElementById("easy-unlock-enabled") && ' |
| + '(!document.getElementById("easy-unlock-disabled").hidden || ' |
| + ' !document.getElementById("easy-unlock-enabled").hidden)'), 10) |
| + settings = SmartLockSettings(tab, self) |
| + logger.info('Started Smart Lock settings: enabled=%s' % |
| + settings.smart_lock_enabled) |
| + return settings |
| + |
| + def GetSmartLockApp(self): |
| + """ Returns the wrapper for the Smart Lock setup app. |
| + |
| + Return: |
| + An instance of SmartLockApp or None if the app window does not exist. |
| + """ |
| + app_page = self._FindSmartLockAppPage('/pairing.html') |
| + if app_page is not None: |
| + # Wait for app window to be responsive. |
| + util.WaitFor(lambda: app_page.EvaluateJavaScript( |
| + 'document.getElementById("pairing-button") != null'), 10) |
| + return SmartLockApp(app_page, self) |
| + return None |
| + |
| + def RunBtmon(self): |
| + """ Runs the btmon command. |
| + Return: |
| + A subprocess.Popen object of the btmon process. |
| + """ |
| + assert(self._cros_interface) |
| + cmd = self._cros_interface.FormSSHCommandLine(['btmon']) |
| + process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, |
| + stderr=subprocess.PIPE) |
| + self._processes.append(process) |
| + return process |
| + |
| + def _FindSmartLockAppPage(self, page_name): |
| + extensions = self._browser.extensions.GetByExtensionId( |
| + 'mkaemigholebcgchlkbankmihknojeak') |
| + for extension_page in extensions: |
| + pathname = extension_page.EvaluateJavaScript('document.location.pathname') |
| + if pathname == page_name: |
| + return extension_page |
| + return None |