Use multiline attribute to check for IA2_STATE_MULTILINE.
[chromium-blink-merge.git] / components / proximity_auth / e2e_test / cros.py
blobfcaf26e6cd3abadf8021610f0863a503cb410919
1 # Copyright 2015 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.
5 import logging
6 import os
7 import subprocess
8 import sys
9 import time
11 # Add the telemetry directory to Python's search paths.
12 current_directory = os.path.dirname(os.path.realpath(__file__))
13 telemetry_dir = os.path.realpath(
14 os.path.join(current_directory, '..', '..', '..', 'tools', 'telemetry'))
15 if telemetry_dir not in sys.path:
16 sys.path.append(telemetry_dir)
18 from telemetry.core import browser_options
19 from telemetry.core import browser_finder
20 from telemetry.core import extension_to_load
21 from telemetry.core import exceptions
22 from telemetry.core import util
23 from telemetry.core.platform import cros_interface
25 logger = logging.getLogger('proximity_auth.%s' % __name__)
28 class AccountPickerScreen(object):
29 """ Wrapper for the ChromeOS account picker screen.
31 The account picker screen is the WebContents page used for both the lock
32 screen and signin screen.
34 Note: This class assumes the account picker screen only has one user. If there
35 are multiple user pods, the first one will be used.
36 """
38 class AuthType:
39 """ The authentication type expected for a user pod. """
40 OFFLINE_PASSWORD = 0
41 ONLINE_SIGN_IN = 1
42 NUMERIC_PIN = 2
43 USER_CLICK = 3
44 EXPAND_THEN_USER_CLICK = 4
45 FORCE_OFFLINE_PASSWORD = 5
47 class SmartLockState:
48 """ The state of the Smart Lock icon on a user pod.
49 """
50 NOT_SHOWN = 'not_shown'
51 AUTHENTICATED = 'authenticated'
52 LOCKED = 'locked'
53 HARD_LOCKED = 'hardlocked'
54 TO_BE_ACTIVATED = 'to_be_activated'
55 SPINNER = 'spinner'
57 # JavaScript expression for getting the user pod on the page
58 _GET_POD_JS = 'document.getElementById("pod-row").pods[0]'
60 def __init__(self, oobe, chromeos):
61 """
62 Args:
63 oobe: Inspector page of the OOBE WebContents.
64 chromeos: The parent Chrome wrapper.
65 """
66 self._oobe = oobe
67 self._chromeos = chromeos
69 @property
70 def is_lockscreen(self):
71 return self._oobe.EvaluateJavaScript(
72 '!document.getElementById("sign-out-user-item").hidden')
74 @property
75 def auth_type(self):
76 return self._oobe.EvaluateJavaScript('%s.authType' % self._GET_POD_JS)
78 @property
79 def smart_lock_state(self):
80 icon_shown = self._oobe.EvaluateJavaScript(
81 '!%s.customIconElement.hidden' % self._GET_POD_JS)
82 if not icon_shown:
83 return self.SmartLockState.NOT_SHOWN
84 class_list_dict = self._oobe.EvaluateJavaScript(
85 '%s.customIconElement.querySelector(".custom-icon")'
86 '.classList' % self._GET_POD_JS)
87 class_list = [v for k,v in class_list_dict.items() if k != 'length']
89 if 'custom-icon-unlocked' in class_list:
90 return self.SmartLockState.AUTHENTICATED
91 if 'custom-icon-locked' in class_list:
92 return self.SmartLockState.LOCKED
93 if 'custom-icon-hardlocked' in class_list:
94 return self.SmartLockState.HARD_LOCKED
95 if 'custom-icon-locked-to-be-activated' in class_list:
96 return self.SmartLockState.TO_BE_ACTIVATED
97 if 'custom-icon-spinner' in class_list:
98 return self.SmartLockState.SPINNER
100 def WaitForSmartLockState(self, state, wait_time_secs=60):
101 """ Waits for the Smart Lock icon to reach the given state.
103 Args:
104 state: A value in AccountPickerScreen.SmartLockState
105 wait_time_secs: The time to wait
106 Returns:
107 True if the state is reached within the wait time, else False.
109 try:
110 util.WaitFor(lambda: self.smart_lock_state == state, wait_time_secs)
111 return True
112 except exceptions.TimeoutException:
113 return False
115 def EnterPassword(self):
116 """ Enters the password to unlock or sign-in.
118 Raises:
119 TimeoutException: entering the password fails to enter/resume the user
120 session.
122 assert(self.auth_type == self.AuthType.OFFLINE_PASSWORD or
123 self.auth_type == self.AuthType.FORCE_OFFLINE_PASSWORD)
124 oobe = self._oobe
125 oobe.EvaluateJavaScript(
126 '%s.passwordElement.value = "%s"' % (
127 self._GET_POD_JS, self._chromeos.password))
128 oobe.EvaluateJavaScript(
129 '%s.activate()' % self._GET_POD_JS)
130 util.WaitFor(lambda: (self._chromeos.session_state ==
131 ChromeOS.SessionState.IN_SESSION),
134 def UnlockWithClick(self):
135 """ Clicks the user pod to unlock or sign-in. """
136 assert(self.auth_type == self.AuthType.USER_CLICK)
137 self._oobe.EvaluateJavaScript('%s.activate()' % self._GET_POD_JS)
140 class SmartLockSettings(object):
141 """ Wrapper for the Smart Lock settings in chromeos://settings.
143 def __init__(self, tab, chromeos):
145 Args:
146 tab: Inspector page of the chromeos://settings tag.
147 chromeos: The parent Chrome wrapper.
149 self._tab = tab
150 self._chromeos = chromeos
152 @property
153 def is_smart_lock_enabled(self):
154 ''' Returns true if the settings show that Smart Lock is enabled. '''
155 return self._tab.EvaluateJavaScript(
156 '!document.getElementById("easy-unlock-enabled").hidden')
158 def TurnOffSmartLock(self):
159 """ Turns off Smart Lock.
161 Smart Lock is turned off by clicking the turn-off button and navigating
162 through the resulting overlay.
164 Raises:
165 TimeoutException: Timed out waiting for Smart Lock to be turned off.
167 assert(self.is_smart_lock_enabled)
168 tab = self._tab
169 tab.EvaluateJavaScript(
170 'document.getElementById("easy-unlock-turn-off-button").click()')
171 util.WaitFor(lambda: tab.EvaluateJavaScript(
172 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && '
173 'document.getElementById("easy-unlock-turn-off-confirm") != null'),
175 tab.EvaluateJavaScript(
176 'document.getElementById("easy-unlock-turn-off-confirm").click()')
177 util.WaitFor(lambda: tab.EvaluateJavaScript(
178 '!document.getElementById("easy-unlock-disabled").hidden'), 15)
180 def StartSetup(self):
181 """ Starts the Smart Lock setup flow by clicking the button.
183 assert(not self.is_smart_lock_enabled)
184 self._tab.EvaluateJavaScript(
185 'document.getElementById("easy-unlock-setup-button").click()')
187 def StartSetupAndReturnApp(self):
188 """ Runs the setup and returns the wrapper to the setup app.
190 After clicking the setup button in the settings page, enter the password to
191 reauthenticate the user before the app launches.
193 Returns:
194 A SmartLockApp object of the app that was launched.
196 Raises:
197 TimeoutException: Timed out waiting for app.
199 self.StartSetup()
200 util.WaitFor(lambda: (self._chromeos.session_state ==
201 ChromeOS.SessionState.LOCK_SCREEN),
203 lock_screen = self._chromeos.GetAccountPickerScreen()
204 lock_screen.EnterPassword()
205 util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10)
206 return self._chromeos.GetSmartLockApp()
209 class SmartLockApp(object):
210 """ Wrapper for the Smart Lock setup dialog.
212 Note: This does not include the app's background page.
215 class PairingState:
216 """ The current state of the setup flow. """
217 SCAN = 'scan'
218 PAIR = 'pair'
219 CLICK_FOR_TRIAL_RUN = 'click_for_trial_run'
220 TRIAL_RUN_COMPLETED = 'trial_run_completed'
222 def __init__(self, app_page, chromeos):
224 Args:
225 app_page: Inspector page of the app window.
226 chromeos: The parent Chrome wrapper.
228 self._app_page = app_page
229 self._chromeos = chromeos
231 @property
232 def pairing_state(self):
233 ''' Returns the state the app is currently in.
235 Raises:
236 ValueError: The current state is unknown.
238 state = self._app_page.EvaluateJavaScript(
239 'document.body.getAttribute("step")')
240 if state == 'scan':
241 return SmartLockApp.PairingState.SCAN
242 elif state == 'pair':
243 return SmartLockApp.PairingState.PAIR
244 elif state == 'complete':
245 button_text = self._app_page.EvaluateJavaScript(
246 'document.getElementById("pairing-button").textContent')
247 button_text = button_text.strip().lower()
248 if button_text == 'try it out':
249 return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN
250 elif button_text == 'done':
251 return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED
252 else:
253 raise ValueError('Unknown button text: %s', button_text)
254 else:
255 raise ValueError('Unknown pairing state: %s' % state)
257 def FindPhone(self, retries=3):
258 """ Starts the initial step to find nearby phones.
260 The app must be in the SCAN state.
262 Args:
263 retries: The number of times to retry if no phones are found.
264 Returns:
265 True if a phone is found, else False.
267 assert(self.pairing_state == self.PairingState.SCAN)
268 for _ in xrange(retries):
269 self._ClickPairingButton()
270 if self.pairing_state == self.PairingState.PAIR:
271 return True
272 # Wait a few seconds before retrying.
273 time.sleep(10)
274 return False
276 def PairPhone(self):
277 """ Starts the step of finding nearby phones.
279 The app must be in the PAIR state.
281 Returns:
282 True if pairing succeeded, else False.
284 assert(self.pairing_state == self.PairingState.PAIR)
285 self._ClickPairingButton()
286 return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN
288 def StartTrialRun(self):
289 """ Starts the trial run.
291 The app must be in the CLICK_FOR_TRIAL_RUN state.
293 Raises:
294 TimeoutException: Timed out starting the trial run.
296 assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN)
297 self._app_page.EvaluateJavaScript(
298 'document.getElementById("pairing-button").click()')
299 util.WaitFor(lambda: (self._chromeos.session_state ==
300 ChromeOS.SessionState.LOCK_SCREEN),
303 def DismissApp(self):
304 """ Dismisses the app after setup is completed.
306 The app must be in the TRIAL_RUN_COMPLETED state.
308 assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED)
309 self._app_page.EvaluateJavaScript(
310 'document.getElementById("pairing-button").click()')
312 def _ClickPairingButton(self):
313 self._app_page.EvaluateJavaScript(
314 'document.getElementById("pairing-button").click()')
315 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
316 '!document.getElementById("pairing-button").disabled'), 60)
317 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
318 '!document.getElementById("pairing-button-title")'
319 '.classList.contains("animated-fade-out")'), 5)
320 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
321 '!document.getElementById("pairing-button-title")'
322 '.classList.contains("animated-fade-in")'), 5)
325 class ChromeOS(object):
326 """ Wrapper for a remote ChromeOS device.
328 Operations performed through this wrapper are sent through the network to
329 Chrome using the Chrome DevTools API. Therefore, any function may throw an
330 exception if the communication to the remote device is severed.
333 class SessionState:
334 """ The state of the user session.
336 SIGNIN_SCREEN = 'signin_screen'
337 IN_SESSION = 'in_session'
338 LOCK_SCREEN = 'lock_screen'
340 _SMART_LOCK_SETTINGS_URL = 'chrome://settings/search#Smart%20Lock'
342 def __init__(self, remote_address, username, password, ssh_port=None):
344 Args:
345 remote_address: The remote address of the cros device.
346 username: The username of the account to test.
347 password: The password of the account to test.
348 ssh_port: The ssh port to connect to.
350 self._remote_address = remote_address;
351 self._username = username
352 self._password = password
353 self._ssh_port = ssh_port
354 self._browser = None
355 self._cros_interface = None
356 self._background_page = None
357 self._processes = []
359 @property
360 def username(self):
361 ''' Returns the username of the user to login. '''
362 return self._username
364 @property
365 def password(self):
366 ''' Returns the password of the user to login. '''
367 return self._password
369 @property
370 def session_state(self):
371 ''' Returns the state of the user session. '''
372 assert(self._browser is not None)
373 if self._browser.oobe_exists:
374 if self._cros_interface.IsCryptohomeMounted(self.username, False):
375 return self.SessionState.LOCK_SCREEN
376 else:
377 return self.SessionState.SIGNIN_SCREEN
378 else:
379 return self.SessionState.IN_SESSION;
381 @property
382 def cryptauth_access_token(self):
383 try:
384 util.WaitFor(lambda: self._background_page.EvaluateJavaScript(
385 'var __token = __token || null; '
386 'chrome.identity.getAuthToken(function(token) {'
387 ' __token = token;'
388 '}); '
389 '__token != null'), 5)
390 return self._background_page.EvaluateJavaScript('__token');
391 except exceptions.TimeoutException:
392 logger.error('Failed to get access token.');
393 return ''
395 def __enter__(self):
396 return self
398 def __exit__(self, *args):
399 if self._browser is not None:
400 self._browser.Close()
401 if self._cros_interface is not None:
402 self._cros_interface.CloseConnection()
403 for process in self._processes:
404 process.terminate()
406 def Start(self, local_app_path=None):
407 """ Connects to the ChromeOS device and logs in.
408 Args:
409 local_app_path: A path on the local device containing the Smart Lock app
410 to use instead of the app on the ChromeOS device.
411 Return:
412 |self| for using in a "with" statement.
414 assert(self._browser is None)
416 finder_opts = browser_options.BrowserFinderOptions('cros-chrome')
417 finder_opts.CreateParser().parse_args(args=[])
418 finder_opts.cros_remote = self._remote_address
419 if self._ssh_port is not None:
420 finder_opts.cros_remote_ssh_port = self._ssh_port
421 finder_opts.verbosity = 1
423 browser_opts = finder_opts.browser_options
424 browser_opts.create_browser_with_oobe = True
425 browser_opts.disable_component_extensions_with_background_pages = False
426 browser_opts.gaia_login = True
427 browser_opts.username = self._username
428 browser_opts.password = self._password
429 browser_opts.auto_login = True
431 self._cros_interface = cros_interface.CrOSInterface(
432 finder_opts.cros_remote,
433 finder_opts.cros_remote_ssh_port,
434 finder_opts.cros_ssh_identity)
436 browser_opts.disable_default_apps = local_app_path is not None
437 if local_app_path is not None:
438 easy_unlock_app = extension_to_load.ExtensionToLoad(
439 path=local_app_path,
440 browser_type='cros-chrome',
441 is_component=True)
442 finder_opts.extensions_to_load.append(easy_unlock_app)
444 retries = 3
445 while self._browser is not None or retries > 0:
446 try:
447 browser_to_create = browser_finder.FindBrowser(finder_opts)
448 self._browser = browser_to_create.Create(finder_opts);
449 break;
450 except (exceptions.LoginException) as e:
451 logger.error('Timed out logging in: %s' % e);
452 if retries == 1:
453 raise
455 bg_page_path = '/_generated_background_page.html'
456 util.WaitFor(
457 lambda: self._FindSmartLockAppPage(bg_page_path) is not None,
458 10);
459 self._background_page = self._FindSmartLockAppPage(bg_page_path)
460 return self
462 def GetAccountPickerScreen(self):
463 """ Returns the wrapper for the lock screen or sign-in screen.
465 Return:
466 An instance of AccountPickerScreen.
467 Raises:
468 TimeoutException: Timed out waiting for account picker screen to load.
470 assert(self._browser is not None)
471 assert(self.session_state == self.SessionState.LOCK_SCREEN or
472 self.session_state == self.SessionState.SIGNIN_SCREEN)
473 oobe = self._browser.oobe
474 def IsLockScreenResponsive():
475 return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and
476 oobe.EvaluateJavaScript(
477 "typeof Oobe.authenticateForTesting == 'function'"))
478 util.WaitFor(IsLockScreenResponsive, 10)
479 util.WaitFor(lambda: oobe.EvaluateJavaScript(
480 'document.getElementById("pod-row") && '
481 'document.getElementById("pod-row").pods && '
482 'document.getElementById("pod-row").pods.length > 0'), 10)
483 return AccountPickerScreen(oobe, self)
485 def GetSmartLockSettings(self):
486 """ Returns the wrapper for the Smart Lock settings.
487 A tab will be navigated to chrome://settings if it does not exist.
489 Return:
490 An instance of SmartLockSettings.
491 Raises:
492 TimeoutException: Timed out waiting for settings page.
494 if not len(self._browser.tabs):
495 self._browser.New()
496 tab = self._browser.tabs[0]
497 url = tab.EvaluateJavaScript('document.location.href')
498 if url != self._SMART_LOCK_SETTINGS_URL:
499 tab.Navigate(self._SMART_LOCK_SETTINGS_URL)
501 # Wait for settings page to be responsive.
502 util.WaitFor(lambda: tab.EvaluateJavaScript(
503 'document.getElementById("easy-unlock-disabled") && '
504 'document.getElementById("easy-unlock-enabled") && '
505 '(!document.getElementById("easy-unlock-disabled").hidden || '
506 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10)
507 settings = SmartLockSettings(tab, self)
508 logger.info('Started Smart Lock settings: enabled=%s' %
509 settings.is_smart_lock_enabled)
510 return settings
512 def GetSmartLockApp(self):
513 """ Returns the wrapper for the Smart Lock setup app.
515 Return:
516 An instance of SmartLockApp or None if the app window does not exist.
518 app_page = self._FindSmartLockAppPage('/pairing.html')
519 if app_page is not None:
520 # Wait for app window to be responsive.
521 util.WaitFor(lambda: app_page.EvaluateJavaScript(
522 'document.getElementById("pairing-button") != null'), 10)
523 return SmartLockApp(app_page, self)
524 return None
526 def RunBtmon(self):
527 """ Runs the btmon command.
528 Return:
529 A subprocess.Popen object of the btmon process.
531 assert(self._cros_interface)
532 cmd = self._cros_interface.FormSSHCommandLine(['btmon'])
533 process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE,
534 stderr=subprocess.PIPE)
535 self._processes.append(process)
536 return process
538 def _FindSmartLockAppPage(self, page_name):
539 try:
540 extensions = self._browser.extensions.GetByExtensionId(
541 'mkaemigholebcgchlkbankmihknojeak')
542 except KeyError:
543 return None
544 for extension_page in extensions:
545 pathname = extension_page.EvaluateJavaScript('document.location.pathname')
546 if pathname == page_name:
547 return extension_page
548 return None