Don't preload rarely seen large images
[chromium-blink-merge.git] / components / proximity_auth / e2e_test / cros.py
blob4c10226c2cfd94ab24ca2b4cbe5791a12ff5cd9a
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 exceptions
21 from telemetry.core import util
22 from telemetry.core.platform import cros_interface
23 from telemetry.internal.browser import extension_to_load
25 logger = logging.getLogger('proximity_auth.%s' % __name__)
27 class AccountPickerScreen(object):
28 """ Wrapper for the ChromeOS account picker screen.
30 The account picker screen is the WebContents page used for both the lock
31 screen and signin screen.
33 Note: This class assumes the account picker screen only has one user. If there
34 are multiple user pods, the first one will be used.
35 """
37 class AuthType:
38 """ The authentication type expected for a user pod. """
39 OFFLINE_PASSWORD = 0
40 ONLINE_SIGN_IN = 1
41 NUMERIC_PIN = 2
42 USER_CLICK = 3
43 EXPAND_THEN_USER_CLICK = 4
44 FORCE_OFFLINE_PASSWORD = 5
46 class SmartLockState:
47 """ The state of the Smart Lock icon on a user pod.
48 """
49 NOT_SHOWN = 'not_shown'
50 AUTHENTICATED = 'authenticated'
51 LOCKED = 'locked'
52 HARD_LOCKED = 'hardlocked'
53 TO_BE_ACTIVATED = 'to_be_activated'
54 SPINNER = 'spinner'
56 # JavaScript expression for getting the user pod on the page
57 _GET_POD_JS = 'document.getElementById("pod-row").pods[0]'
59 def __init__(self, oobe, chromeos):
60 """
61 Args:
62 oobe: Inspector page of the OOBE WebContents.
63 chromeos: The parent Chrome wrapper.
64 """
65 self._oobe = oobe
66 self._chromeos = chromeos
68 @property
69 def is_lockscreen(self):
70 return self._oobe.EvaluateJavaScript(
71 '!document.getElementById("sign-out-user-item").hidden')
73 @property
74 def auth_type(self):
75 return self._oobe.EvaluateJavaScript('%s.authType' % self._GET_POD_JS)
77 @property
78 def smart_lock_state(self):
79 icon_shown = self._oobe.EvaluateJavaScript(
80 '!%s.customIconElement.hidden' % self._GET_POD_JS)
81 if not icon_shown:
82 return self.SmartLockState.NOT_SHOWN
83 class_list_dict = self._oobe.EvaluateJavaScript(
84 '%s.customIconElement.querySelector(".custom-icon")'
85 '.classList' % self._GET_POD_JS)
86 class_list = [v for k,v in class_list_dict.items() if k != 'length']
88 if 'custom-icon-unlocked' in class_list:
89 return self.SmartLockState.AUTHENTICATED
90 if 'custom-icon-locked' in class_list:
91 return self.SmartLockState.LOCKED
92 if 'custom-icon-hardlocked' in class_list:
93 return self.SmartLockState.HARD_LOCKED
94 if 'custom-icon-locked-to-be-activated' in class_list:
95 return self.SmartLockState.TO_BE_ACTIVATED
96 if 'custom-icon-spinner' in class_list:
97 return self.SmartLockState.SPINNER
99 def WaitForSmartLockState(self, state, wait_time_secs=60):
100 """ Waits for the Smart Lock icon to reach the given state.
102 Args:
103 state: A value in AccountPickerScreen.SmartLockState
104 wait_time_secs: The time to wait
105 Returns:
106 True if the state is reached within the wait time, else False.
108 try:
109 util.WaitFor(lambda: self.smart_lock_state == state, wait_time_secs)
110 return True
111 except exceptions.TimeoutException:
112 return False
114 def EnterPassword(self):
115 """ Enters the password to unlock or sign-in.
117 Raises:
118 TimeoutException: entering the password fails to enter/resume the user
119 session.
121 assert(self.auth_type == self.AuthType.OFFLINE_PASSWORD or
122 self.auth_type == self.AuthType.FORCE_OFFLINE_PASSWORD)
123 oobe = self._oobe
124 oobe.EvaluateJavaScript(
125 '%s.passwordElement.value = "%s"' % (
126 self._GET_POD_JS, self._chromeos.password))
127 oobe.EvaluateJavaScript(
128 '%s.activate()' % self._GET_POD_JS)
129 util.WaitFor(lambda: (self._chromeos.session_state ==
130 ChromeOS.SessionState.IN_SESSION),
133 def UnlockWithClick(self):
134 """ Clicks the user pod to unlock or sign-in. """
135 assert(self.auth_type == self.AuthType.USER_CLICK)
136 self._oobe.EvaluateJavaScript('%s.activate()' % self._GET_POD_JS)
139 class SmartLockSettings(object):
140 """ Wrapper for the Smart Lock settings in chromeos://settings.
142 def __init__(self, tab, chromeos):
144 Args:
145 tab: Inspector page of the chromeos://settings tag.
146 chromeos: The parent Chrome wrapper.
148 self._tab = tab
149 self._chromeos = chromeos
151 @property
152 def is_smart_lock_enabled(self):
153 ''' Returns true if the settings show that Smart Lock is enabled. '''
154 return self._tab.EvaluateJavaScript(
155 '!document.getElementById("easy-unlock-enabled").hidden')
157 def TurnOffSmartLock(self):
158 """ Turns off Smart Lock.
160 Smart Lock is turned off by clicking the turn-off button and navigating
161 through the resulting overlay.
163 Raises:
164 TimeoutException: Timed out waiting for Smart Lock to be turned off.
166 assert(self.is_smart_lock_enabled)
167 tab = self._tab
168 tab.EvaluateJavaScript(
169 'document.getElementById("easy-unlock-turn-off-button").click()')
170 util.WaitFor(lambda: tab.EvaluateJavaScript(
171 '!document.getElementById("easy-unlock-turn-off-overlay").hidden && '
172 'document.getElementById("easy-unlock-turn-off-confirm") != null'),
174 tab.EvaluateJavaScript(
175 'document.getElementById("easy-unlock-turn-off-confirm").click()')
176 util.WaitFor(lambda: tab.EvaluateJavaScript(
177 '!document.getElementById("easy-unlock-disabled").hidden'), 15)
179 def StartSetup(self):
180 """ Starts the Smart Lock setup flow by clicking the button.
182 assert(not self.is_smart_lock_enabled)
183 self._tab.EvaluateJavaScript(
184 'document.getElementById("easy-unlock-setup-button").click()')
186 def StartSetupAndReturnApp(self):
187 """ Runs the setup and returns the wrapper to the setup app.
189 After clicking the setup button in the settings page, enter the password to
190 reauthenticate the user before the app launches.
192 Returns:
193 A SmartLockApp object of the app that was launched.
195 Raises:
196 TimeoutException: Timed out waiting for app.
198 self.StartSetup()
199 util.WaitFor(lambda: (self._chromeos.session_state ==
200 ChromeOS.SessionState.LOCK_SCREEN),
202 lock_screen = self._chromeos.GetAccountPickerScreen()
203 lock_screen.EnterPassword()
204 util.WaitFor(lambda: self._chromeos.GetSmartLockApp() is not None, 10)
205 return self._chromeos.GetSmartLockApp()
208 class SmartLockApp(object):
209 """ Wrapper for the Smart Lock setup dialog.
211 Note: This does not include the app's background page.
214 class PairingState:
215 """ The current state of the setup flow. """
216 SCAN = 'scan'
217 PAIR = 'pair'
218 CLICK_FOR_TRIAL_RUN = 'click_for_trial_run'
219 TRIAL_RUN_COMPLETED = 'trial_run_completed'
221 def __init__(self, app_page, chromeos):
223 Args:
224 app_page: Inspector page of the app window.
225 chromeos: The parent Chrome wrapper.
227 self._app_page = app_page
228 self._chromeos = chromeos
230 @property
231 def pairing_state(self):
232 ''' Returns the state the app is currently in.
234 Raises:
235 ValueError: The current state is unknown.
237 state = self._app_page.EvaluateJavaScript(
238 'document.body.getAttribute("step")')
239 if state == 'scan':
240 return SmartLockApp.PairingState.SCAN
241 elif state == 'pair':
242 return SmartLockApp.PairingState.PAIR
243 elif state == 'complete':
244 button_text = self._app_page.EvaluateJavaScript(
245 'document.getElementById("pairing-button").textContent')
246 button_text = button_text.strip().lower()
247 if button_text == 'try it out':
248 return SmartLockApp.PairingState.CLICK_FOR_TRIAL_RUN
249 elif button_text == 'done':
250 return SmartLockApp.PairingState.TRIAL_RUN_COMPLETED
251 else:
252 raise ValueError('Unknown button text: %s', button_text)
253 else:
254 raise ValueError('Unknown pairing state: %s' % state)
256 def FindPhone(self, retries=3):
257 """ Starts the initial step to find nearby phones.
259 The app must be in the SCAN state.
261 Args:
262 retries: The number of times to retry if no phones are found.
263 Returns:
264 True if a phone is found, else False.
266 assert(self.pairing_state == self.PairingState.SCAN)
267 for _ in xrange(retries):
268 self._ClickPairingButton()
269 if self.pairing_state == self.PairingState.PAIR:
270 return True
271 # Wait a few seconds before retrying.
272 time.sleep(10)
273 return False
275 def PairPhone(self):
276 """ Starts the step of finding nearby phones.
278 The app must be in the PAIR state.
280 Returns:
281 True if pairing succeeded, else False.
283 assert(self.pairing_state == self.PairingState.PAIR)
284 self._ClickPairingButton()
285 return self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN
287 def StartTrialRun(self):
288 """ Starts the trial run.
290 The app must be in the CLICK_FOR_TRIAL_RUN state.
292 Raises:
293 TimeoutException: Timed out starting the trial run.
295 assert(self.pairing_state == self.PairingState.CLICK_FOR_TRIAL_RUN)
296 self._app_page.EvaluateJavaScript(
297 'document.getElementById("pairing-button").click()')
298 util.WaitFor(lambda: (self._chromeos.session_state ==
299 ChromeOS.SessionState.LOCK_SCREEN),
302 def DismissApp(self):
303 """ Dismisses the app after setup is completed.
305 The app must be in the TRIAL_RUN_COMPLETED state.
307 assert(self.pairing_state == self.PairingState.TRIAL_RUN_COMPLETED)
308 self._app_page.EvaluateJavaScript(
309 'document.getElementById("pairing-button").click()')
311 def _ClickPairingButton(self):
312 self._app_page.EvaluateJavaScript(
313 'document.getElementById("pairing-button").click()')
314 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
315 '!document.getElementById("pairing-button").disabled'), 60)
316 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
317 '!document.getElementById("pairing-button-title")'
318 '.classList.contains("animated-fade-out")'), 5)
319 util.WaitFor(lambda: self._app_page.EvaluateJavaScript(
320 '!document.getElementById("pairing-button-title")'
321 '.classList.contains("animated-fade-in")'), 5)
324 class ChromeOS(object):
325 """ Wrapper for a remote ChromeOS device.
327 Operations performed through this wrapper are sent through the network to
328 Chrome using the Chrome DevTools API. Therefore, any function may throw an
329 exception if the communication to the remote device is severed.
332 class SessionState:
333 """ The state of the user session.
335 SIGNIN_SCREEN = 'signin_screen'
336 IN_SESSION = 'in_session'
337 LOCK_SCREEN = 'lock_screen'
339 _SMART_LOCK_SETTINGS_URL = 'chrome://settings/search#Smart%20Lock'
341 def __init__(self, remote_address, username, password, ssh_port=None):
343 Args:
344 remote_address: The remote address of the cros device.
345 username: The username of the account to test.
346 password: The password of the account to test.
347 ssh_port: The ssh port to connect to.
349 self._remote_address = remote_address
350 self._username = username
351 self._password = password
352 self._ssh_port = ssh_port
353 self._browser = None
354 self._cros_interface = None
355 self._background_page = None
356 self._processes = []
358 @property
359 def username(self):
360 ''' Returns the username of the user to login. '''
361 return self._username
363 @property
364 def password(self):
365 ''' Returns the password of the user to login. '''
366 return self._password
368 @property
369 def session_state(self):
370 ''' Returns the state of the user session. '''
371 assert(self._browser is not None)
372 if self._browser.oobe_exists:
373 if self._cros_interface.IsCryptohomeMounted(self.username, False):
374 return self.SessionState.LOCK_SCREEN
375 else:
376 return self.SessionState.SIGNIN_SCREEN
377 else:
378 return self.SessionState.IN_SESSION;
380 @property
381 def cryptauth_access_token(self):
382 try:
383 util.WaitFor(lambda: self._background_page.EvaluateJavaScript(
384 'var __token = __token || null; '
385 'chrome.identity.getAuthToken(function(token) {'
386 ' __token = token;'
387 '}); '
388 '__token != null'), 5)
389 return self._background_page.EvaluateJavaScript('__token');
390 except exceptions.TimeoutException:
391 logger.error('Failed to get access token.');
392 return ''
394 def __enter__(self):
395 return self
397 def __exit__(self, *args):
398 if self._browser is not None:
399 self._browser.Close()
400 if self._cros_interface is not None:
401 self._cros_interface.CloseConnection()
402 for process in self._processes:
403 process.terminate()
405 def Start(self, local_app_path=None):
406 """ Connects to the ChromeOS device and logs in.
407 Args:
408 local_app_path: A path on the local device containing the Smart Lock app
409 to use instead of the app on the ChromeOS device.
410 Return:
411 |self| for using in a "with" statement.
413 assert(self._browser is None)
415 finder_opts = browser_options.BrowserFinderOptions('cros-chrome')
416 finder_opts.CreateParser().parse_args(args=[])
417 finder_opts.cros_remote = self._remote_address
418 if self._ssh_port is not None:
419 finder_opts.cros_remote_ssh_port = self._ssh_port
420 finder_opts.verbosity = 1
422 browser_opts = finder_opts.browser_options
423 browser_opts.create_browser_with_oobe = True
424 browser_opts.disable_component_extensions_with_background_pages = False
425 browser_opts.gaia_login = True
426 browser_opts.username = self._username
427 browser_opts.password = self._password
428 browser_opts.auto_login = True
430 self._cros_interface = cros_interface.CrOSInterface(
431 finder_opts.cros_remote,
432 finder_opts.cros_remote_ssh_port,
433 finder_opts.cros_ssh_identity)
435 browser_opts.disable_default_apps = local_app_path is not None
436 if local_app_path is not None:
437 easy_unlock_app = extension_to_load.ExtensionToLoad(
438 path=local_app_path,
439 browser_type='cros-chrome',
440 is_component=True)
441 finder_opts.extensions_to_load.append(easy_unlock_app)
443 retries = 3
444 while self._browser is not None or retries > 0:
445 try:
446 browser_to_create = browser_finder.FindBrowser(finder_opts)
447 self._browser = browser_to_create.Create(finder_opts);
448 break;
449 except (exceptions.LoginException) as e:
450 logger.error('Timed out logging in: %s' % e);
451 if retries == 1:
452 raise
454 bg_page_path = '/_generated_background_page.html'
455 util.WaitFor(
456 lambda: self._FindSmartLockAppPage(bg_page_path) is not None,
457 10);
458 self._background_page = self._FindSmartLockAppPage(bg_page_path)
459 return self
461 def GetAccountPickerScreen(self):
462 """ Returns the wrapper for the lock screen or sign-in screen.
464 Return:
465 An instance of AccountPickerScreen.
466 Raises:
467 TimeoutException: Timed out waiting for account picker screen to load.
469 assert(self._browser is not None)
470 assert(self.session_state == self.SessionState.LOCK_SCREEN or
471 self.session_state == self.SessionState.SIGNIN_SCREEN)
472 oobe = self._browser.oobe
473 def IsLockScreenResponsive():
474 return (oobe.EvaluateJavaScript("typeof Oobe == 'function'") and
475 oobe.EvaluateJavaScript(
476 "typeof Oobe.authenticateForTesting == 'function'"))
477 util.WaitFor(IsLockScreenResponsive, 10)
478 util.WaitFor(lambda: oobe.EvaluateJavaScript(
479 'document.getElementById("pod-row") && '
480 'document.getElementById("pod-row").pods && '
481 'document.getElementById("pod-row").pods.length > 0'), 10)
482 return AccountPickerScreen(oobe, self)
484 def GetSmartLockSettings(self):
485 """ Returns the wrapper for the Smart Lock settings.
486 A tab will be navigated to chrome://settings if it does not exist.
488 Return:
489 An instance of SmartLockSettings.
490 Raises:
491 TimeoutException: Timed out waiting for settings page.
493 if not len(self._browser.tabs):
494 self._browser.New()
495 tab = self._browser.tabs[0]
496 url = tab.EvaluateJavaScript('document.location.href')
497 if url != self._SMART_LOCK_SETTINGS_URL:
498 tab.Navigate(self._SMART_LOCK_SETTINGS_URL)
500 # Wait for settings page to be responsive.
501 util.WaitFor(lambda: tab.EvaluateJavaScript(
502 'document.getElementById("easy-unlock-disabled") && '
503 'document.getElementById("easy-unlock-enabled") && '
504 '(!document.getElementById("easy-unlock-disabled").hidden || '
505 ' !document.getElementById("easy-unlock-enabled").hidden)'), 10)
506 settings = SmartLockSettings(tab, self)
507 logger.info('Started Smart Lock settings: enabled=%s' %
508 settings.is_smart_lock_enabled)
509 return settings
511 def GetSmartLockApp(self):
512 """ Returns the wrapper for the Smart Lock setup app.
514 Return:
515 An instance of SmartLockApp or None if the app window does not exist.
517 app_page = self._FindSmartLockAppPage('/pairing.html')
518 if app_page is not None:
519 # Wait for app window to be responsive.
520 util.WaitFor(lambda: app_page.EvaluateJavaScript(
521 'document.getElementById("pairing-button") != null'), 10)
522 return SmartLockApp(app_page, self)
523 return None
525 def SetCryptAuthStaging(self, cryptauth_staging_url):
526 logger.info('Setting CryptAuth to Staging')
527 try:
528 self._background_page.ExecuteJavaScript(
529 'var key = app.CryptAuthClient.GOOGLE_API_URL_OVERRIDE_;'
530 'var __complete = false;'
531 'chrome.storage.local.set({key: "%s"}, function() {'
532 ' __complete = true;'
533 '});' % cryptauth_staging_url)
534 util.WaitFor(lambda: self._background_page.EvaluateJavaScript(
535 '__complete == true'), 10)
536 except exceptions.TimeoutException:
537 logger.error('Failed to override CryptAuth to staging url.')
539 def RunBtmon(self):
540 """ Runs the btmon command.
541 Return:
542 A subprocess.Popen object of the btmon process.
544 assert(self._cros_interface)
545 cmd = self._cros_interface.FormSSHCommandLine(['btmon'])
546 process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE,
547 stderr=subprocess.PIPE)
548 self._processes.append(process)
549 return process
551 def _FindSmartLockAppPage(self, page_name):
552 try:
553 extensions = self._browser.extensions.GetByExtensionId(
554 'mkaemigholebcgchlkbankmihknojeak')
555 except KeyError:
556 return None
557 for extension_page in extensions:
558 pathname = extension_page.EvaluateJavaScript('document.location.pathname')
559 if pathname == page_name:
560 return extension_page
561 return None