1# Lint as: python2, python3 2# Copyright 2014 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6""" 7This module allows tests to interact with the Chrome Web Store (CWS) 8using ChromeDriver. They should inherit from the webstore_test class, 9and should override the run() method. 10""" 11 12from __future__ import absolute_import 13from __future__ import division 14from __future__ import print_function 15 16import logging 17import six 18from six.moves import range 19from six.moves import zip 20import time 21 22from autotest_lib.client.bin import test 23from autotest_lib.client.common_lib import error 24from autotest_lib.client.common_lib.cros import chromedriver 25from autotest_lib.client.common_lib.global_config import global_config 26from selenium.webdriver.common.by import By 27from selenium.webdriver.support import expected_conditions 28from selenium.webdriver.support.ui import WebDriverWait 29 30# How long to wait, in seconds, for an app to launch. This is larger 31# than it needs to be, because it might be slow on older Chromebooks 32_LAUNCH_DELAY = 4 33 34# How long to wait before entering the password when logging in to the CWS 35_ENTER_PASSWORD_DELAY = 2 36 37# How long to wait before entering payment info 38_PAYMENT_DELAY = 5 39 40def enum(*enumNames): 41 """ 42 Creates an enum. Returns an enum object with a value for each enum 43 name, as well as from_string and to_string mappings. 44 45 @param enumNames: The strings representing the values of the enum 46 """ 47 enums = dict(zip(enumNames, list(range(len(enumNames))))) 48 reverse = dict((value, key) for key, value in six.iteritems(enums)) 49 enums['from_string'] = enums 50 enums['to_string'] = reverse 51 return type('Enum', (), enums) 52 53# TODO: staging and PNL don't work in these tests (crbug/396660) 54TestEnv = enum('staging', 'pnl', 'prod', 'sandbox') 55 56ItemType = enum( 57 'hosted_app', 58 'packaged_app', 59 'chrome_app', 60 'extension', 61 'theme', 62) 63 64# NOTE: paid installs don't work right now 65InstallType = enum( 66 'free', 67 'free_trial', 68 'paid', 69) 70 71def _labeled_button(label): 72 """ 73 Returns a button with the class webstore-test-button-label and the 74 specified label 75 76 @param label: The label on the button 77 """ 78 return ('//div[contains(@class,"webstore-test-button-label") ' 79 'and text()="' + label + '"]') 80 81def _install_type_click_xpath(item_type, install_type): 82 """ 83 Returns the XPath of the button to install an item of the given type. 84 85 @param item_type: The type of the item to install 86 @param install_type: The type of installation being used 87 """ 88 if install_type == InstallType.free: 89 return _labeled_button('Free') 90 elif install_type == InstallType.free_trial: 91 # Both of these cases return buttons that say "Add to Chrome", 92 # but they are actually different buttons with only one being 93 # visible at a time. 94 if item_type == ItemType.hosted_app: 95 return ('//div[@id="cxdialog-install-paid-btn" and ' 96 '@aria-label="Add to Chrome"]') 97 else: 98 return _labeled_button('Add to Chrome') 99 else: 100 return ('//div[contains(@aria-label,"Buy for") ' 101 'and not(contains(@style,"display: none"))]') 102 103def _get_chrome_flags(test_env): 104 """ 105 Returns the Chrome flags for the given test environment. 106 """ 107 flags = ['--apps-gallery-install-auto-confirm-for-tests=accept'] 108 if test_env == TestEnv.prod: 109 return flags 110 111 url_middle = { 112 TestEnv.staging: 'staging.corp', 113 TestEnv.sandbox: 'staging.sandbox', 114 TestEnv.pnl: 'prod-not-live.corp' 115 }[test_env] 116 download_url_middle = { 117 TestEnv.staging: 'download-staging.corp', 118 TestEnv.sandbox: 'download-staging.sandbox', 119 TestEnv.pnl: 'omaha.sandbox' 120 }[test_env] 121 flags.append('--apps-gallery-url=https://webstore-' + url_middle + 122 '.google.com') 123 flags.append('--apps-gallery-update-url=https://' + download_url_middle + 124 '.google.com/service/update2/crx') 125 logging.info('Using flags %s', flags) 126 return flags 127 128 129class webstore_test(test.test): 130 """ 131 The base class for tests that interact with the web store. 132 133 Subclasses must define run(), but should not override run_once(). 134 Subclasses should use methods in this module such as install_item, 135 but they can also use the driver directly if they need to. 136 """ 137 138 def initialize(self, test_env=TestEnv.sandbox, 139 account='cwsbotdeveloper1@gmail.com'): 140 """ 141 Initialize the test. 142 143 @param test_env: The test environment to use 144 """ 145 super(webstore_test, self).initialize() 146 147 self.username = account 148 self.password = global_config.get_config_value( 149 'CLIENT', 'webstore_test_password', type=str) 150 151 self.test_env = test_env 152 self._chrome_flags = _get_chrome_flags(test_env) 153 self.webstore_url = { 154 TestEnv.staging: 155 'https://webstore-staging.corp.google.com', 156 TestEnv.sandbox: 157 'https://webstore-staging.sandbox.google.com/webstore', 158 TestEnv.pnl: 159 'https://webstore-prod-not-live.corp.google.com/webstore', 160 TestEnv.prod: 161 'https://chrome.google.com/webstore' 162 }[test_env] 163 164 165 def build_url(self, page): 166 """ 167 Builds a webstore URL for the specified page. 168 169 @param page: the page to build a URL for 170 """ 171 return self.webstore_url + page + "?gl=US" 172 173 174 def detail_page(self, item_id): 175 """ 176 Returns the URL of the detail page for the given item 177 178 @param item_id: The item ID 179 """ 180 return self.build_url("/detail/" + item_id) 181 182 183 def wait_for(self, xpath): 184 """ 185 Waits until the element specified by the given XPath is visible 186 187 @param xpath: The xpath of the element to wait for 188 """ 189 self._wait.until(expected_conditions.visibility_of_element_located( 190 (By.XPATH, xpath))) 191 192 193 def run_once(self, **kwargs): 194 with chromedriver.chromedriver( 195 username=self.username, 196 password=self.password, 197 extra_chrome_flags=self._chrome_flags) \ 198 as chromedriver_instance: 199 self.driver = chromedriver_instance.driver 200 self.driver.implicitly_wait(15) 201 self._wait = WebDriverWait(self.driver, 20) 202 logging.info('Running test on test environment %s', 203 TestEnv.to_string[self.test_env]) 204 self.run(**kwargs) 205 206 207 def run(self): 208 """ 209 Runs the test. Should be overridden by subclasses. 210 """ 211 raise error.TestError('The test needs to override run()') 212 213 214 def install_item(self, item_id, item_type, install_type): 215 """ 216 Installs an item from the CWS. 217 218 @param item_id: The ID of the item to install 219 (a 32-char string of letters) 220 @param item_type: The type of the item to install 221 @param install_type: The type of installation 222 (free, free trial, or paid) 223 """ 224 logging.info('Installing item %s of type %s with install_type %s', 225 item_id, ItemType.to_string[item_type], 226 InstallType.to_string[install_type]) 227 228 # We need to go to the CWS home page before going to the detail 229 # page due to a bug in the CWS 230 self.driver.get(self.webstore_url) 231 self.driver.get(self.detail_page(item_id)) 232 233 install_type_click_xpath = _install_type_click_xpath( 234 item_type, install_type) 235 if item_type == ItemType.extension or item_type == ItemType.theme: 236 post_install_xpath = ( 237 '//div[@aria-label="Added to Chrome" ' 238 ' and not(contains(@style,"display: none"))]') 239 else: 240 post_install_xpath = _labeled_button('Launch app') 241 242 # In this case we need to sign in again 243 if install_type != InstallType.free: 244 button_xpath = _labeled_button('Sign in to add') 245 logging.info('Clicking button %s', button_xpath) 246 self.driver.find_element_by_xpath(button_xpath).click() 247 time.sleep(_ENTER_PASSWORD_DELAY) 248 password_field = self.driver.find_element_by_xpath( 249 '//input[@id="Passwd"]') 250 password_field.send_keys(self.password) 251 self.driver.find_element_by_xpath('//input[@id="signIn"]').click() 252 253 logging.info('Clicking %s', install_type_click_xpath) 254 self.driver.find_element_by_xpath(install_type_click_xpath).click() 255 256 if install_type == InstallType.paid: 257 handle = self.driver.current_window_handle 258 iframe = self.driver.find_element_by_xpath( 259 '//iframe[contains(@src, "sandbox.google.com/checkout")]') 260 self.driver.switch_to_frame(iframe) 261 self.driver.find_element_by_id('purchaseButton').click() 262 time.sleep(_PAYMENT_DELAY) # Wait for animation to finish 263 self.driver.find_element_by_id('finishButton').click() 264 self.driver.switch_to_window(handle) 265 266 self.wait_for(post_install_xpath) 267 268 269 def launch_app(self, app_id): 270 """ 271 Launches an app. Verifies that it launched by verifying that 272 a new tab/window was opened. 273 274 @param app_id: The ID of the app to run 275 """ 276 logging.info('Launching app %s', app_id) 277 num_handles_before = len(self.driver.window_handles) 278 self.driver.get(self.webstore_url) 279 self.driver.get(self.detail_page(app_id)) 280 launch_button = self.driver.find_element_by_xpath( 281 _labeled_button('Launch app')) 282 launch_button.click(); 283 time.sleep(_LAUNCH_DELAY) # Wait for the app to launch 284 num_handles_after = len(self.driver.window_handles) 285 if num_handles_after <= num_handles_before: 286 raise error.TestError('App failed to launch') 287