# Copyright 2014 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ This module allows tests to interact with the Chrome Web Store (CWS) using ChromeDriver. They should inherit from the webstore_test class, and should override the run() method. """ import logging import time from autotest_lib.client.bin import test from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import chromedriver from autotest_lib.client.common_lib.global_config import global_config from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.ui import WebDriverWait # How long to wait, in seconds, for an app to launch. This is larger # than it needs to be, because it might be slow on older Chromebooks _LAUNCH_DELAY = 4 # How long to wait before entering the password when logging in to the CWS _ENTER_PASSWORD_DELAY = 2 # How long to wait before entering payment info _PAYMENT_DELAY = 5 def enum(*enumNames): """ Creates an enum. Returns an enum object with a value for each enum name, as well as from_string and to_string mappings. @param enumNames: The strings representing the values of the enum """ enums = dict(zip(enumNames, range(len(enumNames)))) reverse = dict((value, key) for key, value in enums.iteritems()) enums['from_string'] = enums enums['to_string'] = reverse return type('Enum', (), enums) # TODO: staging and PNL don't work in these tests (crbug/396660) TestEnv = enum('staging', 'pnl', 'prod', 'sandbox') ItemType = enum( 'hosted_app', 'packaged_app', 'chrome_app', 'extension', 'theme', ) # NOTE: paid installs don't work right now InstallType = enum( 'free', 'free_trial', 'paid', ) def _labeled_button(label): """ Returns a button with the class webstore-test-button-label and the specified label @param label: The label on the button """ return ('//div[contains(@class,"webstore-test-button-label") ' 'and text()="' + label + '"]') def _install_type_click_xpath(item_type, install_type): """ Returns the XPath of the button to install an item of the given type. @param item_type: The type of the item to install @param install_type: The type of installation being used """ if install_type == InstallType.free: return _labeled_button('Free') elif install_type == InstallType.free_trial: # Both of these cases return buttons that say "Add to Chrome", # but they are actually different buttons with only one being # visible at a time. if item_type == ItemType.hosted_app: return ('//div[@id="cxdialog-install-paid-btn" and ' '@aria-label="Add to Chrome"]') else: return _labeled_button('Add to Chrome') else: return ('//div[contains(@aria-label,"Buy for") ' 'and not(contains(@style,"display: none"))]') def _get_chrome_flags(test_env): """ Returns the Chrome flags for the given test environment. """ flags = ['--apps-gallery-install-auto-confirm-for-tests=accept'] if test_env == TestEnv.prod: return flags url_middle = { TestEnv.staging: 'staging.corp', TestEnv.sandbox: 'staging.sandbox', TestEnv.pnl: 'prod-not-live.corp' }[test_env] download_url_middle = { TestEnv.staging: 'download-staging.corp', TestEnv.sandbox: 'download-staging.sandbox', TestEnv.pnl: 'omaha.sandbox' }[test_env] flags.append('--apps-gallery-url=https://webstore-' + url_middle + '.google.com') flags.append('--apps-gallery-update-url=https://' + download_url_middle + '.google.com/service/update2/crx') logging.info('Using flags %s', flags) return flags class webstore_test(test.test): """ The base class for tests that interact with the web store. Subclasses must define run(), but should not override run_once(). Subclasses should use methods in this module such as install_item, but they can also use the driver directly if they need to. """ def initialize(self, test_env=TestEnv.sandbox, account='cwsbotdeveloper1@gmail.com'): """ Initialize the test. @param test_env: The test environment to use """ super(webstore_test, self).initialize() self.username = account self.password = global_config.get_config_value( 'CLIENT', 'webstore_test_password', type=str) self.test_env = test_env self._chrome_flags = _get_chrome_flags(test_env) self.webstore_url = { TestEnv.staging: 'https://webstore-staging.corp.google.com', TestEnv.sandbox: 'https://webstore-staging.sandbox.google.com/webstore', TestEnv.pnl: 'https://webstore-prod-not-live.corp.google.com/webstore', TestEnv.prod: 'https://chrome.google.com/webstore' }[test_env] def build_url(self, page): """ Builds a webstore URL for the specified page. @param page: the page to build a URL for """ return self.webstore_url + page + "?gl=US" def detail_page(self, item_id): """ Returns the URL of the detail page for the given item @param item_id: The item ID """ return self.build_url("/detail/" + item_id) def wait_for(self, xpath): """ Waits until the element specified by the given XPath is visible @param xpath: The xpath of the element to wait for """ self._wait.until(expected_conditions.visibility_of_element_located( (By.XPATH, xpath))) def run_once(self, **kwargs): with chromedriver.chromedriver( username=self.username, password=self.password, extra_chrome_flags=self._chrome_flags) \ as chromedriver_instance: self.driver = chromedriver_instance.driver self.driver.implicitly_wait(15) self._wait = WebDriverWait(self.driver, 20) logging.info('Running test on test environment %s', TestEnv.to_string[self.test_env]) self.run(**kwargs) def run(self): """ Runs the test. Should be overridden by subclasses. """ raise error.TestError('The test needs to override run()') def install_item(self, item_id, item_type, install_type): """ Installs an item from the CWS. @param item_id: The ID of the item to install (a 32-char string of letters) @param item_type: The type of the item to install @param install_type: The type of installation (free, free trial, or paid) """ logging.info('Installing item %s of type %s with install_type %s', item_id, ItemType.to_string[item_type], InstallType.to_string[install_type]) # We need to go to the CWS home page before going to the detail # page due to a bug in the CWS self.driver.get(self.webstore_url) self.driver.get(self.detail_page(item_id)) install_type_click_xpath = _install_type_click_xpath( item_type, install_type) if item_type == ItemType.extension or item_type == ItemType.theme: post_install_xpath = ( '//div[@aria-label="Added to Chrome" ' ' and not(contains(@style,"display: none"))]') else: post_install_xpath = _labeled_button('Launch app') # In this case we need to sign in again if install_type != InstallType.free: button_xpath = _labeled_button('Sign in to add') logging.info('Clicking button %s', button_xpath) self.driver.find_element_by_xpath(button_xpath).click() time.sleep(_ENTER_PASSWORD_DELAY) password_field = self.driver.find_element_by_xpath( '//input[@id="Passwd"]') password_field.send_keys(self.password) self.driver.find_element_by_xpath('//input[@id="signIn"]').click() logging.info('Clicking %s', install_type_click_xpath) self.driver.find_element_by_xpath(install_type_click_xpath).click() if install_type == InstallType.paid: handle = self.driver.current_window_handle iframe = self.driver.find_element_by_xpath( '//iframe[contains(@src, "sandbox.google.com/checkout")]') self.driver.switch_to_frame(iframe) self.driver.find_element_by_id('purchaseButton').click() time.sleep(_PAYMENT_DELAY) # Wait for animation to finish self.driver.find_element_by_id('finishButton').click() self.driver.switch_to_window(handle) self.wait_for(post_install_xpath) def launch_app(self, app_id): """ Launches an app. Verifies that it launched by verifying that a new tab/window was opened. @param app_id: The ID of the app to run """ logging.info('Launching app %s', app_id) num_handles_before = len(self.driver.window_handles) self.driver.get(self.webstore_url) self.driver.get(self.detail_page(app_id)) launch_button = self.driver.find_element_by_xpath( _labeled_button('Launch app')) launch_button.click(); time.sleep(_LAUNCH_DELAY) # Wait for the app to launch num_handles_after = len(self.driver.window_handles) if num_handles_after <= num_handles_before: raise error.TestError('App failed to launch')