diff --git a/packages/dash-core-components/circle.yml b/packages/dash-core-components/circle.yml index 18308813ed..77698ec7ba 100644 --- a/packages/dash-core-components/circle.yml +++ b/packages/dash-core-components/circle.yml @@ -1,8 +1,21 @@ machine: - node: - version: 4.4.7 + node: + version: 6.9.2 + post: + - pyenv global 2.7.10 + environment: + TOX_PYTHON_27: python2.7 + +dependencies: + pre: + - npm install -g eslint + - pip install tox + - pip install -r dev-requirements.txt + - npm install + - node_modules/.bin/builder run build-dist + - node_modules/.bin/builder run copy-lib test: - post: - # Ensure steps leading up to publishing work. - - node_modules/.bin/builder run build-dist \ No newline at end of file + override: + - tox + - npm run test diff --git a/packages/dash-core-components/dev-requirements.txt b/packages/dash-core-components/dev-requirements.txt new file mode 100644 index 0000000000..3ab1828a57 --- /dev/null +++ b/packages/dash-core-components/dev-requirements.txt @@ -0,0 +1,12 @@ +dash_html_components +dash_renderer +dash +percy +selenium +mock +tox +tox-pyenv +six +plotly>=2.0.8 +requests[security] +ipdb diff --git a/packages/dash-core-components/package.json b/packages/dash-core-components/package.json index 5efcc08665..b6a32b7eec 100644 --- a/packages/dash-core-components/package.json +++ b/packages/dash-core-components/package.json @@ -26,7 +26,7 @@ "license": "MIT", "dependencies": { "builder": "2.10.1", - "dash-components-archetype": "^0.2.4", + "dash-components-archetype": "^0.2.11", "moment": "^2.18.1", "radium": "^0.17.1", "ramda": "^0.23.0", @@ -43,7 +43,7 @@ }, "devDependencies": { "component-playground": "^1.3.2", - "dash-components-archetype-dev": "^0.2.4", + "dash-components-archetype-dev": "^0.2.11", "enzyme": "^2.4.1" } } diff --git a/packages/dash-core-components/test/IntegrationTests.py b/packages/dash-core-components/test/IntegrationTests.py new file mode 100644 index 0000000000..85fb96fc38 --- /dev/null +++ b/packages/dash-core-components/test/IntegrationTests.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +import importlib +import multiprocessing +import percy +import time +import unittest + + +class IntegrationTests(unittest.TestCase): + + @classmethod + def setUpClass(cls): + super(IntegrationTests, cls).setUpClass() + cls.driver = webdriver.Chrome() + + loader = percy.ResourceLoader(webdriver=cls.driver) + cls.percy_runner = percy.Runner(loader=loader) + cls.percy_runner.initialize_build() + + @classmethod + def tearDownClass(cls): + super(IntegrationTests, cls).tearDownClass() + cls.driver.quit() + cls.percy_runner.finalize_build() + + def setUp(self): + pass + + def tearDown(self): + time.sleep(3) + self.server_process.terminate() + time.sleep(3) + + if hasattr(self, 'driver'): + self.driver.quit() + + + def startServer(self, app): + def run(): + app.scripts.config.serve_locally = True + app.run_server( + port=8050, + debug=False, + processes=4 + ) + + # Run on a separate process so that it doesn't block + self.server_process = multiprocessing.Process(target=run) + self.server_process.start() + time.sleep(0.5) + + # Visit the dash page + self.driver.get('http://localhost:8050') + time.sleep(0.5) + + # Inject an error and warning logger + logger = ''' + window.tests = {}; + window.tests.console = {error: [], warn: [], log: []}; + + var _log = console.log; + var _warn = console.warn; + var _error = console.error; + + console.log = function() { + window.tests.console.log.push({method: 'log', arguments: arguments}); + return _log.apply(console, arguments); + }; + + console.warn = function() { + window.tests.console.warn.push({method: 'warn', arguments: arguments}); + return _warn.apply(console, arguments); + }; + + console.error = function() { + window.tests.console.error.push({method: 'error', arguments: arguments}); + return _error.apply(console, arguments); + }; + ''' + self.driver.execute_script(logger) diff --git a/packages/dash-core-components/test/__init__.py b/packages/dash-core-components/test/__init__.py index 65248e6c3b..e69de29bb2 100644 --- a/packages/dash-core-components/test/__init__.py +++ b/packages/dash-core-components/test/__init__.py @@ -1,5 +0,0 @@ -from os.path import dirname, basename, isfile -import glob -modules = glob.glob(dirname(__file__)+"/*.py") -__all__ = [basename(f)[:-3] for f in modules if isfile(f)] - diff --git a/packages/dash-core-components/test/main.js b/packages/dash-core-components/test/main.js index 50180cd079..e69de29bb2 100644 --- a/packages/dash-core-components/test/main.js +++ b/packages/dash-core-components/test/main.js @@ -1,11 +0,0 @@ -/* eslint-disable import/default */ -/* global require:false */ -import karmaRunner from 'dash-components-archetype-dev/karma-runner'; - -karmaRunner.setupEnvironment(); - -// Use webpack to infer and `require` tests automatically. -var testsReq = require.context('../src', true, /\.test.js$/); -testsReq.keys().map(testsReq); - -karmaRunner.startKarma(); diff --git a/packages/dash-core-components/test/test_integration.py b/packages/dash-core-components/test/test_integration.py index a014bc0725..23feecb3cf 100644 --- a/packages/dash-core-components/test/test_integration.py +++ b/packages/dash-core-components/test/test_integration.py @@ -1,5 +1,7 @@ -import dash_core_components -from dash.react import Dash +# -*- coding: utf-8 -*- +import dash_core_components as dcc +import dash_html_components as html +import dash import importlib import percy from selenium import webdriver @@ -7,6 +9,10 @@ import time import multiprocessing import unittest +import os + +from .IntegrationTests import IntegrationTests +from .utils import assert_clean_console, invincible, wait_for # Download geckodriver: https://github.com/mozilla/geckodriver/releases # And add to path: @@ -17,59 +23,100 @@ # export PERCY_TOKEN=... -class IntegrationTests(unittest.TestCase): - - @classmethod - def setUpClass(cls): - super(IntegrationTests, cls).setUpClass() - cls.driver = webdriver.Firefox() - - loader = percy.ResourceLoader( - webdriver=cls.driver - ) - cls.percy_runner = percy.Runner(loader=loader) - - cls.percy_runner.initialize_build() - - @classmethod - def tearDownClass(cls): - super(IntegrationTests, cls).tearDownClass() - cls.driver.quit() - cls.percy_runner.finalize_build() - - def setUp(s): - pass - - def tearDown(s): - s.server_process.terminate() - - def startServer(s, dash): - def run(): - dash.run_server( - port=8050, - debug=False, - component_suites=[ - 'dash_core_components' +class Tests(IntegrationTests): + def setUp(self): + self.driver = webdriver.Chrome() + def wait_for_element_by_id(id): + wait_for(lambda: None is not invincible( + lambda: self.driver.find_element_by_id(id) + )) + return self.driver.find_element_by_id(id) + self.wait_for_element_by_id = wait_for_element_by_id + + def wait_for_element_by_css_selector(css_selector): + wait_for(lambda: None is not invincible( + lambda: self.driver.find_element_by_css_selector(css_selector) + )) + return self.driver.find_element_by_css_selector(css_selector) + self.wait_for_element_by_css_selector = wait_for_element_by_css_selector + + + def test_integration(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Div(id='waitfor'), + html.Label('Dropdown'), + dcc.Dropdown( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} + ], + value='MTL' + ), + + html.Label('Multi-Select Dropdown'), + dcc.Dropdown( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} ], - threaded=True + value=['MTL', 'SF'], + multi=True + ), + + html.Label('Radio Items'), + dcc.RadioItems( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} + ], + value='MTL' + ), + + html.Label('Checkboxes'), + dcc.Checklist( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'} + ], + values=['MTL', 'SF'] + ), + + html.Label('Text Input'), + dcc.Input(value='MTL', type='text'), + + html.Label('Slider'), + dcc.Slider( + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) for i in range(1, 6)}, + value=5, + ), + + html.Label('Graph'), + dcc.Graph( + id='graph', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [4, 1, 4] + }] + } ) - - # Run on a separate process so that it doesn't block - s.server_process = multiprocessing.Process(target=run) - s.server_process.start() - time.sleep(0.5) - - def test_integration(s): - dash = Dash(__name__) - - dash.layout = dash_core_components.Input( - id='hello-world' - ) - - s.startServer(dash) - s.driver.get('http://localhost:8050') - - el = s.driver.find_element_by_id('hello-world') - - # Take a screenshot with percy - s.percy_runner.snapshot(name='dash_core_components') + ]) + self.startServer(app) + + try: + self.wait_for_element_by_id('waitfor') + except Exception as e: + print(self.wait_for_element_by_id( + '_dash-app-content').get_attribute('innerHTML')) + raise e + + if 'PERCY_PROJECT' in os.environ and 'PERCY_TOKEN' in os.environ: + self.percy_runner.snapshot(name='dash_core_components') diff --git a/packages/dash-core-components/test/utils.py b/packages/dash-core-components/test/utils.py new file mode 100644 index 0000000000..2dbdd280c2 --- /dev/null +++ b/packages/dash-core-components/test/utils.py @@ -0,0 +1,84 @@ +import json +import time + + +TIMEOUT = 20 # Seconds + + +def invincible(func): + def wrap(): + try: + return func() + except: + pass + return wrap + + + +class WaitForTimeout(Exception): + """This should only be raised inside the `wait_for` function.""" + pass + + +def wait_for(condition_function, get_message=lambda: '', *args, **kwargs): + """ + Waits for condition_function to return True or raises WaitForTimeout. + :param (function) condition_function: Should return True on success. + :param args: Optional args to pass to condition_function. + :param kwargs: Optional kwargs to pass to condition_function. + if `timeout` is in kwargs, it will be used to override TIMEOUT + :raises: WaitForTimeout If condition_function doesn't return True in time. + Usage: + def get_element(selector): + # some code to get some element or return a `False`-y value. + selector = '.js-plotly-plot' + try: + wait_for(get_element, selector) + except WaitForTimeout: + self.fail('element never appeared...') + plot = get_element(selector) # we know it exists. + """ + def wrapped_condition_function(): + """We wrap this to alter the call base on the closure.""" + if args and kwargs: + return condition_function(*args, **kwargs) + if args: + return condition_function(*args) + if kwargs: + return condition_function(**kwargs) + return condition_function() + + if 'timeout' in kwargs: + timeout = kwargs['timeout'] + del kwargs['timeout'] + else: + timeout = TIMEOUT + + start_time = time.time() + while time.time() < start_time + timeout: + if wrapped_condition_function(): + return True + time.sleep(0.5) + + raise WaitForTimeout(get_message()) + + +def assert_clean_console(TestClass): + def assert_no_console_errors(TestClass): + TestClass.assertEqual( + TestClass.driver.execute_script( + 'return window.tests.console.error.length' + ), + 0 + ) + + def assert_no_console_warnings(TestClass): + TestClass.assertEqual( + TestClass.driver.execute_script( + 'return window.tests.console.warn.length' + ), + 0 + ) + + assert_no_console_warnings(TestClass) + assert_no_console_errors(TestClass) diff --git a/packages/dash-core-components/tox.ini b/packages/dash-core-components/tox.ini new file mode 100644 index 0000000000..be1001a998 --- /dev/null +++ b/packages/dash-core-components/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py27 + +[testenv] +deps = -rdev-requirements.txt +# Variables for chrome and selenium +# See https://bugs.launchpad.net/horizon/+bug/1684321 +passenv = + HOME + DISPLAY + +[testenv:py27] +basepython={env:TOX_PYTHON_27} +commands = + python --version + python -m unittest test.test_integration