Browse Source

feat(selenium): Switch to headless Chrome

This also moves us to using Trusty for Travis, which enables the Google Chrome addon.

Lastly, it disables Cassandra on Travis as it seems broken on trusty images.
David Cramer 7 years ago
parent
commit
24bba58060

+ 4 - 6
.travis.yml

@@ -1,6 +1,6 @@
 filter_secrets: false
 sudo: false
-dist: precise
+dist: trusty
 language: python
 rvm:
   - 2.2
@@ -19,6 +19,7 @@ addons:
     packages:
       - g++-4.8
       - libxmlsec1-dev
+  chrome: stable
 env:
   global:
     - NODE_ENV=production
@@ -29,6 +30,7 @@ env:
     - CXX=g++-4.8
     - SOUTH_TESTS_MIGRATE=1
 install:
+  - 'export PATH=$PATH:~/.bin'
   - nvm install $TRAVIS_NODE_VERSION
   - npm install -g yarn@1.3.2
   - make travis-install-$TEST_SUITE
@@ -63,17 +65,13 @@ matrix:
     - language: ruby
       rvm: 2.2
       env: TEST_SUITE=danger
-    # only the sqlite suite runs cassandra/riak tests
+    # only the sqlite suite runs riak tests
     - python: 2.7
       env: TEST_SUITE=sqlite DB=sqlite
       services:
         - memcached
         - riak
         - redis-server
-        - cassandra
-      before_script:
-        - make travis-setup-cassandra
-        - pip freeze
     - python: 2.7
       env: TEST_SUITE=postgres DB=postgres
       services:

+ 1 - 0
Brewfile

@@ -1,2 +1,3 @@
 brew 'libxmlsec1'
 brew 'openssl'
+brew 'chromedriver'

+ 6 - 3
Makefile

@@ -20,9 +20,6 @@ install-yarn:
 	@hash yarn 2> /dev/null || (echo 'Cannot continue with JavaScript dependencies. Please install yarn before proceeding. For more information refer to https://yarnpkg.com/lang/en/docs/install/'; echo 'If you are on a mac run:'; echo '  brew install yarn'; exit 1)
 	# Use NODE_ENV=development so that yarn installs both dependencies + devDependencies
 	NODE_ENV=development yarn install --pure-lockfile
-	# Fix phantomjs-prebuilt not installed via yarn
-	# See: https://github.com/karma-runner/karma-phantomjs-launcher/issues/120#issuecomment-262634703
-	node ./node_modules/phantomjs-prebuilt/install.js
 
 install-brew:
 	@hash brew 2> /dev/null && brew bundle || (echo '! Homebrew not found, skipping system dependencies.')
@@ -213,6 +210,12 @@ travis-install-mysql: travis-install-python
 	pip install mysqlclient
 	echo 'create database sentry;' | mysql -uroot
 travis-install-acceptance: install-yarn travis-install-postgres
+	wget -N http://chromedriver.storage.googleapis.com/2.33/chromedriver_linux64.zip -P ~/
+	unzip ~/chromedriver_linux64.zip -d ~/
+	rm ~/chromedriver_linux64.zip
+	chmod +x ~/chromedriver
+	mkdir -p ~/.bin
+	mv ~/chromedriver ~/.bin/
 travis-install-network: travis-install-postgres
 travis-install-js:
 	$(MAKE) travis-upgrade-pip

+ 1 - 2
package.json

@@ -99,7 +99,7 @@
     "moduleNameMapper": {
       "\\.(css|less)$": "<rootDir>/tests/js/helpers/importStyleMock.js",
       "jquery": "<rootDir>/src/sentry/static/sentry/__mocks__/jquery.jsx",
-      "integration-docs-platforms": "<rootDir>/tests/fixtures/_platforms.json"
+      "integration-docs-platforms": "<rootDir>/tests/fixtures/integration-docs/_platforms.json"
     },
     "modulePaths": [
       "<rootDir>/src/sentry/static/sentry"
@@ -133,7 +133,6 @@
     "eslint-plugin-import": "^2.7.0",
     "eslint-plugin-react": "7.4.0",
     "jest": "21.2.1",
-    "phantomjs-prebuilt": "2.1.14",
     "prettier": "1.7.4",
     "sinon": "1.17.2",
     "sinon-chai": "2.8.0",

+ 1 - 1
requirements-base.txt

@@ -39,7 +39,7 @@ PyYAML>=3.11,<3.12
 raven>=5.29.0,<6.0.0
 redis>=2.10.3,<2.10.6
 requests[security]>=2.18.4,<2.19.0
-selenium==3.4.3
+selenium==3.8.0
 simplejson>=3.2.0,<3.9.0
 six>=1.10.0,<1.11.0
 setproctitle>=1.1.7,<1.2.0

+ 1 - 2
setup.cfg

@@ -3,8 +3,7 @@ python_files = test*.py
 addopts = --tb=native -p no:doctest -p no:warnings
 norecursedirs = bin dist docs htmlcov script hooks node_modules .* {args}
 looponfailroots = src tests
-selenium_driver = phantomjs
-phantomjs_path = node_modules/phantomjs-prebuilt/bin/phantomjs
+selenium_driver = chrome
 
 [flake8]
 ignore = F999,E501,E128,E124,E402,W503,E731,C901

+ 1 - 1
src/sentry/receivers/superuser.py

@@ -11,7 +11,7 @@ def enable_superuser(request, user, **kwargs):
         if user.is_superuser:
             su.set_logged_in(user)
         else:
-            su.set_logged_out()
+            su._set_logged_out()
 
 
 def disable_superuser(request, user, **kwargs):

+ 2 - 1
src/sentry/utils/integrationdocs.py

@@ -12,7 +12,8 @@ import sentry
 BASE_URL = 'https://docs.sentry.io/_platforms/{}'
 
 # Also see INTEGRATION_DOC_FOLDER in setup.py
-DOC_FOLDER = os.path.abspath(os.path.join(os.path.dirname(sentry.__file__), 'integration-docs'))
+DOC_FOLDER = os.environ.get('INTEGRATION_DOC_FOLDER') or os.path.abspath(
+    os.path.join(os.path.dirname(sentry.__file__), 'integration-docs'))
 
 # We cannot leverage six here, so we need to vendor
 # bits that we need.

+ 85 - 21
src/sentry/utils/pytest/selenium.py

@@ -45,18 +45,22 @@ class Browser(object):
 
     def get(self, path, *args, **kwargs):
         self.driver.get(self.route(path), *args, **kwargs)
+        self._has_initialized_cookie_store = True
         return self
 
     def post(self, path, *args, **kwargs):
         self.driver.post(self.route(path), *args, **kwargs)
+        self._has_initialized_cookie_store = True
         return self
 
     def put(self, path, *args, **kwargs):
         self.driver.put(self.route(path), *args, **kwargs)
+        self._has_initialized_cookie_store = True
         return self
 
     def delete(self, path, *args, **kwargs):
         self.driver.delete(self.route(path), *args, **kwargs)
+        self._has_initialized_cookie_store = True
         return self
 
     def element(self, selector):
@@ -72,32 +76,59 @@ class Browser(object):
     def click(self, selector):
         self.element(selector).click()
 
-    def wait_until(self, selector, timeout=3):
+    def wait_until(self, selector=None, title=None, timeout=3):
         """
         Waits until ``selector`` is found in the browser, or until ``timeout``
         is hit, whichever happens first.
         """
         from selenium.webdriver.common.by import By
 
+        if selector:
+            condition = expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector))
+        elif title:
+            condition = expected_conditions.title_is(title)
+        else:
+            raise ValueError
+
         WebDriverWait(
             self.driver, timeout
-        ).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)))
+        ).until(condition)
 
         return self
 
-    def wait_until_not(self, selector, timeout=3):
+    def wait_until_not(self, selector=None, title=None, timeout=3):
         """
         Waits until ``selector`` is NOT found in the browser, or until
         ``timeout`` is hit, whichever happens first.
         """
         from selenium.webdriver.common.by import By
 
+        if selector:
+            condition = expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector))
+        elif title:
+            condition = expected_conditions.title_is(title)
+        else:
+            raise
+
         WebDriverWait(
             self.driver, timeout
-        ).until_not(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)))
+        ).until_not(condition)
 
         return self
 
+    @property
+    def switch_to(self):
+        return self.driver.switch_to
+
+    def implicitly_wait(self, duration):
+        """
+        An implicit wait tells WebDriver to poll the DOM for a certain amount of
+        time when trying to find any element (or elements) not immediately
+        available. The default setting is 0. Once set, the implicit wait is set
+        for the life of the WebDriver object.
+        """
+        self.driver.implicitly_wait(duration)
+
     def snapshot(self, name):
         """
         Capture a screenshot of the current state of the page. Screenshots
@@ -110,40 +141,51 @@ class Browser(object):
         return self
 
     def save_cookie(self, name, value, path='/', expires='Tue, 20 Jun 2025 19:07:44 GMT'):
-        # XXX(dcramer): "hit a url before trying to set cookies"
+        cookie = {
+            'name': name,
+            'value': value,
+            'expires': expires,
+            'path': path,
+            'domain': self.domain,
+        }
+
+        # XXX(dcramer): the cookie store must be initialized via a URL
         if not self._has_initialized_cookie_store:
             logger.info('selenium.initialize-cookies')
             self.get('/')
-            self._has_initialized_cookie_store = True
 
         # XXX(dcramer): PhantomJS does not let us add cookies with the native
         # selenium API because....
         # http://stackoverflow.com/questions/37103621/adding-cookies-working-with-firefox-webdriver-but-not-in-phantomjs
+
         # TODO(dcramer): this should be escaped, but idgaf
-        logger.info('selenium.set-cookies.{}'.format(name), extra={
+        logger.info('selenium.set-cookie.{}'.format(name), extra={
             'value': value,
         })
-
-        self.driver.execute_script(
-            "document.cookie = '{name}={value}; path={path}; domain={domain}; expires={expires}';\n".
-            format(
-                name=name,
-                value=value,
-                expires=expires,
-                path=path,
-                domain=self.domain,
+        if isinstance(self.driver, webdriver.PhantomJS):
+            self.driver.execute_script(
+                "document.cookie = '{name}={value}; path={path}; domain={domain}; expires={expires}';\n".format(
+                    **cookie)
             )
-        )
+        else:
+            self.driver.add_cookie(cookie)
 
 
 def pytest_addoption(parser):
-    parser.addini('selenium_driver', help='selenium driver (phantomjs or firefox)')
+    parser.addini('selenium_driver', help='selenium driver (chrome, phantomjs, or firefox)')
 
     group = parser.getgroup('selenium', 'selenium')
     group._addoption(
-        '--selenium-driver', dest='selenium_driver', help='selenium driver (phantomjs or firefox)'
+        '--selenium-driver', dest='selenium_driver', help='selenium driver (chrome, phantomjs, or firefox)'
     )
+    group._addoption(
+        '--window-size',
+        dest='window_size',
+        help='window size (WIDTHxHEIGHT)',
+        default='1280x800')
     group._addoption('--phantomjs-path', dest='phantomjs_path', help='path to phantomjs driver')
+    group._addoption('--chrome-path', dest='chrome_path', help='path to google-chrome')
+    group._addoption('--chromedriver-path', dest='chromedriver_path', help='path to chromedriver')
 
 
 def pytest_configure(config):
@@ -174,8 +216,29 @@ def percy(request):
 
 @pytest.fixture(scope='function')
 def browser(request, percy, live_server):
+    window_size = request.config.getoption('window_size')
+    window_width, window_height = list(map(int, window_size.split('x', 1)))
+
     driver_type = request.config.getoption('selenium_driver')
-    if driver_type == 'firefox':
+    if driver_type == 'chrome':
+        options = webdriver.ChromeOptions()
+        options.add_argument('headless')
+        options.add_argument('disable-gpu')
+        options.add_argument('window-size={}'.format(window_size))
+        chrome_path = request.config.getoption('chrome_path')
+        if chrome_path:
+            options.binary_location = chrome_path
+        chromedriver_path = request.config.getoption('chromedriver_path')
+        if chromedriver_path:
+            driver = webdriver.Chrome(
+                executable_path=chromedriver_path,
+                chrome_options=options,
+            )
+        else:
+            driver = webdriver.Chrome(
+                chrome_options=options,
+            )
+    elif driver_type == 'firefox':
         driver = webdriver.Firefox()
     elif driver_type == 'phantomjs':
         phantomjs_path = request.config.getoption('phantomjs_path')
@@ -187,10 +250,11 @@ def browser(request, percy, live_server):
                 'phantomjs',
             )
         driver = webdriver.PhantomJS(executable_path=phantomjs_path)
-        driver.set_window_size(1280, 800)
     else:
         raise pytest.UsageError('--driver must be specified')
 
+    driver.set_window_size(window_width, window_height)
+
     def fin():
         # Teardown Selenium.
         try:

+ 20 - 0
src/sentry/utils/pytest/sentry.py

@@ -5,6 +5,15 @@ import os
 
 from django.conf import settings
 
+TEST_ROOT = os.path.normpath(
+    os.path.join(
+        os.path.dirname(__file__),
+        os.pardir,
+        os.pardir,
+        os.pardir,
+        os.pardir,
+        'tests'))
+
 
 def pytest_configure(config):
     # HACK: Only needed for testing!
@@ -12,6 +21,17 @@ def pytest_configure(config):
 
     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sentry.conf.server')
 
+    # override docs which are typically synchronized from an upstream server
+    # to ensure tests are consistent
+    os.environ.setdefault(
+        'INTEGRATION_DOC_FOLDER',
+        os.path.join(
+            TEST_ROOT,
+            'fixtures',
+            'integration-docs'))
+    from sentry.utils import integrationdocs
+    integrationdocs.DOC_FOLDER = os.environ['INTEGRATION_DOC_FOLDER']
+
     if not settings.configured:
         # only configure the db if its not already done
         test_db = os.environ.get('DB', 'postgres')

Some files were not shown because too many files changed in this diff