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