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