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