Add basic Chrome Web Store tests.

These tests include: a basic sanity test, 5 tests that verify that free
items can be installed, and 3 tests that verify that free trial items
can be installed.

BUG=chromium:403498
TEST=These tests pass

Change-Id: Id7aa76b4e23e5059652dbdb84f5976dcd2b0a3f5
Reviewed-on: https://chromium-review.googlesource.com/211835
Reviewed-by: Dan Shi <[email protected]>
Tested-by: Nathan Stoddard <[email protected]>
Commit-Queue: Dan Shi <[email protected]>
diff --git a/client/common_lib/cros/chromedriver.py b/client/common_lib/cros/chromedriver.py
index 4c8872c..61e94de 100644
--- a/client/common_lib/cros/chromedriver.py
+++ b/client/common_lib/cros/chromedriver.py
@@ -28,7 +28,8 @@
     """Wrapper class, a context manager type, for tests to use Chrome Driver."""
 
     def __init__(self, extra_chrome_flags=[], subtract_extra_chrome_flags=[],
-                 extension_paths=[], is_component=True, *args, **kwargs):
+                 extension_paths=[], is_component=True, username=None,
+                 password=None, *args, **kwargs):
         """Initialize.
 
         @param extra_chrome_flags: Extra chrome flags to pass to chrome, if any.
@@ -37,12 +38,16 @@
         @param extension_paths: A list of paths to unzipped extensions. Note
                                 that paths to crx files won't work.
         @param is_component: True if the manifest.json has a key.
+        @param username: Log in using this username instead of the default.
+        @param username: Log in using this password instead of the default.
         """
         assert os.geteuid() == 0, 'Need superuser privileges'
 
         # Log in with telemetry
         self._chrome = chrome.Chrome(extension_paths=extension_paths,
                                      is_component=is_component,
+                                     username=username,
+                                     password=password,
                                      extra_browser_args=extra_chrome_flags)
         self._browser = self._chrome.browser
         # Close all tabs owned and opened by Telemetry, as these cannot be
diff --git a/client/cros/webstore_test.py b/client/cros/webstore_test.py
new file mode 100644
index 0000000..7edbe10
--- /dev/null
+++ b/client/cros/webstore_test.py
@@ -0,0 +1,278 @@
+# 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='[email protected]'):
+        """
+        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')
diff --git a/client/site_tests/webstore_InstallItem/control.chrome_app b/client/site_tests/webstore_InstallItem/control.chrome_app
new file mode 100644
index 0000000..46d2adf
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.chrome_app
@@ -0,0 +1,19 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallChromeApp"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a Chrome app from the CWS. Passes if the app can be installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='ihbbfdhfeoljcefdcjjcdaiiadglhphk',
+        item_type=ItemType.chrome_app, install_type=InstallType.free)
diff --git a/client/site_tests/webstore_InstallItem/control.extension b/client/site_tests/webstore_InstallItem/control.extension
new file mode 100644
index 0000000..4c4b95d
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.extension
@@ -0,0 +1,19 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallExtension"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs an extension from the CWS. Passes if the extension can be installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='plhgamimcbeefmbcllgknphaonjlkbdj',
+        item_type=ItemType.extension, install_type=InstallType.free)
diff --git a/client/site_tests/webstore_InstallItem/control.free_trial_chrome_app b/client/site_tests/webstore_InstallItem/control.free_trial_chrome_app
new file mode 100644
index 0000000..a93d9d3
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.free_trial_chrome_app
@@ -0,0 +1,20 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallFreeTrialChromeApp"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a free trial Chrome app from the CWS. Passes if the app
+can be installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='cpnbfoghjcnondaodkbkckggaeicddan',
+        item_type=ItemType.chrome_app, install_type=InstallType.free_trial)
diff --git a/client/site_tests/webstore_InstallItem/control.free_trial_extension b/client/site_tests/webstore_InstallItem/control.free_trial_extension
new file mode 100644
index 0000000..96ffcdc
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.free_trial_extension
@@ -0,0 +1,20 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallFreeTrialExtension"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a free trial extension from the CWS. Passes if the extension
+can be installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='kamegmikiiphdlcjlghnpocoapikpfhj',
+        item_type=ItemType.extension, install_type=InstallType.free_trial)
diff --git a/client/site_tests/webstore_InstallItem/control.free_trial_hosted_app b/client/site_tests/webstore_InstallItem/control.free_trial_hosted_app
new file mode 100644
index 0000000..bce5902
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.free_trial_hosted_app
@@ -0,0 +1,20 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallFreeTrialHostedApp"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a free trial hosted app from the CWS. Passes if the app
+can be installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='lihdchlmdeeijlokcnbinkaeaheodeij',
+        item_type=ItemType.hosted_app, install_type=InstallType.free_trial)
diff --git a/client/site_tests/webstore_InstallItem/control.hosted_app b/client/site_tests/webstore_InstallItem/control.hosted_app
new file mode 100644
index 0000000..9a22fc6
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.hosted_app
@@ -0,0 +1,20 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallHostedApp"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a hosted app from the CWS. Passes if the app can be
+installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='domgcacaimglefkngmjnjmabncaelmgp',
+        item_type=ItemType.hosted_app, install_type=InstallType.free)
diff --git a/client/site_tests/webstore_InstallItem/control.packaged_app b/client/site_tests/webstore_InstallItem/control.packaged_app
new file mode 100644
index 0000000..6515547
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.packaged_app
@@ -0,0 +1,20 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallPackagedApp"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a packaged app from the CWS. Passes if the app can be
+installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='ppaegeodkgonjciginjnpnhhiolamneg',
+        item_type=ItemType.packaged_app, install_type=InstallType.free)
diff --git a/client/site_tests/webstore_InstallItem/control.theme b/client/site_tests/webstore_InstallItem/control.theme
new file mode 100644
index 0000000..3348d94
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/control.theme
@@ -0,0 +1,20 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType, InstallType
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_InstallTheme"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+Installs a theme from the CWS. Passes if the theme can be
+installed successfully.
+"""
+
+job.run_test('webstore_InstallItem',
+        item_id='amapkohokabgjplbdgdmjenfgkjjpljp',
+        item_type=ItemType.theme, install_type=InstallType.free)
diff --git a/client/site_tests/webstore_InstallItem/webstore_InstallItem.py b/client/site_tests/webstore_InstallItem/webstore_InstallItem.py
new file mode 100644
index 0000000..c59242e
--- /dev/null
+++ b/client/site_tests/webstore_InstallItem/webstore_InstallItem.py
@@ -0,0 +1,21 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import ItemType
+from autotest_lib.client.cros.webstore_test import webstore_test
+
+class webstore_InstallItem(webstore_test):
+    """
+    Installs an item and tests that it installed correctly.
+
+    This is used by several tests, which pass the parameters item_id,
+    item_type, and install_type to the test. If it's an app, this
+    class verifies that the app can launch.
+    """
+    version = 1
+
+    def run(self, item_id, item_type, install_type):
+        self.install_item(item_id, item_type, install_type)
+        if item_type != ItemType.extension and item_type != ItemType.theme:
+            self.launch_app(item_id)
diff --git a/client/site_tests/webstore_SanityTest/control b/client/site_tests/webstore_SanityTest/control
new file mode 100644
index 0000000..7b913ee
--- /dev/null
+++ b/client/site_tests/webstore_SanityTest/control
@@ -0,0 +1,18 @@
+# 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.
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore_SanityTest"
+SUITE = "webstore"
+TIME = "SHORT"
+TEST_TYPE = "client"
+
+DOC = """
+A quick sanity test of the web store, to verify that the landing page
+is accessible and that it has the main components it's supposed to.
+This currently verifies that the 'featured' and 'more recommendations'
+sections appear, and that a marquee and at least one item appear.
+"""
+
+job.run_test('webstore_SanityTest')
diff --git a/client/site_tests/webstore_SanityTest/webstore_SanityTest.py b/client/site_tests/webstore_SanityTest/webstore_SanityTest.py
new file mode 100644
index 0000000..bacc345
--- /dev/null
+++ b/client/site_tests/webstore_SanityTest/webstore_SanityTest.py
@@ -0,0 +1,33 @@
+# 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.
+
+from autotest_lib.client.cros.webstore_test import webstore_test
+
+class webstore_SanityTest(webstore_test):
+    """
+    Verifies that the CWS landing page works properly.
+    """
+
+    version = 1
+
+    def section_header(self, name):
+        """
+        Returns the XPath of the section header for the given section.
+
+        @param name The name of the section
+        """
+        return '//div[contains(@class, "wall-structured-section-header")]' + \
+                '/div[text() = "%s"]' % name
+
+    sections = ['Featured', 'More recommendations']
+    wall_tile = '//div[contains(@class, "webstore-test-wall-tile")]'
+    marquee = '//div[contains(@class, "webstore-test-wall-marquee-slideshow")]'
+
+    def run(self):
+        self.driver.get(self.webstore_url)
+
+        for section in self.sections:
+            self.driver.find_element_by_xpath(self.section_header(section))
+        self.driver.find_element_by_xpath(self.wall_tile)
+        self.driver.find_element_by_xpath(self.marquee)
diff --git a/test_suites/control.webstore b/test_suites/control.webstore
new file mode 100644
index 0000000..a0ced34
--- /dev/null
+++ b/test_suites/control.webstore
@@ -0,0 +1,52 @@
+# Copyright 2013 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.
+
+AUTHOR = "Nathan Stoddard ([email protected])"
+NAME = "webstore"
+PURPOSE = "Tests the Chrome Web Store."
+CRITERIA = "All tests with SUITE=webstore must pass."
+
+TIME = "SHORT"
+TEST_CATEGORY = "General"
+TEST_CLASS = "suite"
+TEST_TYPE = "Server"
+
+DOC = """
+Tests the Chrome Web Store (CWS).
+
+@param build: The name of the image to test.
+Ex: x86-mario-release/R17-1412.33.0-a1-b29
+@param board: The board to test on. Ex: x86-mario
+@param pool: The pool of machines to utilize for scheduling. If pool=None
+board is used.
+@param check_hosts: require appropriate live hosts to exist in the lab.
+@param SKIP_IMAGE: (optional) If present and True, don't re-image devices.
+@param file_bugs: If True your suite will file bugs on failures.
+@param max_run_time: Amount of time each test shoud run in minutes.
+"""
+
+import common
+from autotest_lib.server.cros import provision
+from autotest_lib.server.cros.dynamic_suite import dynamic_suite
+
+# Values specified in this bug template will override default values when
+# filing bugs on tests that are a part of this suite. If left unspecified
+# the bug filer will fallback to it's defaults.
+_BUG_TEMPLATE = {
+    'labels': ['webstore'],
+    'owner': '',
+    'status': None,
+    'summary': None,
+    'title': None,
+    'ccs': ['[email protected]', '[email protected]']
+}
+
+dynamic_suite.reimage_and_run(
+    build=build, board=board, name='webstore', job=job, pool=pool,
+    check_hosts=check_hosts, add_experimental=True, num=num,
+    file_bugs=file_bugs, priority=priority, timeout_mins=timeout_mins,
+    max_runtime_mins=20, bug_template=_BUG_TEMPLATE,
+    devserver_url=devserver_url, version_prefix=provision.CROS_VERSION_PREFIX,
+    wait_for_results=wait_for_results, job_retry=job_retry,
+    skip_reimage=dynamic_suite.skip_reimage(globals()))
diff --git a/utils/unittest_suite.py b/utils/unittest_suite.py
index f9c53a2..7a84cd5 100755
--- a/utils/unittest_suite.py
+++ b/utils/unittest_suite.py
@@ -97,6 +97,7 @@
     'dev_server_test.py',
     'full_release_test.py',
     'scheduler_lib_unittest.py',
+    'webstore_test.py',
     ))
 
 LONG_TESTS = (REQUIRES_MYSQLDB |