From 5440e41fbe4c3031d31be5fc9e6344b19e3062f6 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 22 May 2019 23:49:44 -0400 Subject: [PATCH 01/46] :hocho: test.sh is already in circleci config --- test.sh | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100755 test.sh diff --git a/test.sh b/test.sh deleted file mode 100755 index bc00cf94e7..0000000000 --- a/test.sh +++ /dev/null @@ -1,20 +0,0 @@ -EXIT_STATE=0 - -pylint dash setup.py --rcfile=$PYLINTRC || EXIT_STATE=$? -pylint tests -d all -e C0410,C0411,C0412,C0413,W0109 || EXIT_STATE=$? -flake8 dash setup.py || EXIT_STATE=$? -flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests || EXIT_STATE=$? - -python -m unittest tests.development.test_base_component || EXIT_STATE=$? -python -m unittest tests.development.test_component_loader || EXIT_STATE=$? -python -m unittest tests.test_integration || EXIT_STATE=$? -python -m unittest tests.test_resources || EXIT_STATE=$? -python -m unittest tests.test_configs || EXIT_STATE=$? - -if [ $EXIT_STATE -ne 0 ]; then - echo "One or more tests failed" -else - echo "All tests passed!" -fi - -exit $EXIT_STATE From 48c48262b7bfd5a94c9e659677c238e50001155b Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 24 May 2019 17:42:45 -0400 Subject: [PATCH 02/46] :tada: basic set up for dash.testing --- .../requirements/dev-requirements-py37.txt | 3 +- .circleci/requirements/dev-requirements.txt | 1 + .pylintrc | 3 +- .pylintrc37 | 3 +- dash/exceptions.py | 20 ++ __init__.py => dash/testing/__init__.py | 0 dash/testing/application_runners.py | 212 ++++++++++++++++++ dash/testing/plugin.py | 72 ++++++ dash/testing/wait.py | 25 +++ pytest.ini | 4 + setup.py | 11 +- 11 files changed, 348 insertions(+), 6 deletions(-) rename __init__.py => dash/testing/__init__.py (100%) create mode 100644 dash/testing/application_runners.py create mode 100644 dash/testing/plugin.py create mode 100644 dash/testing/wait.py diff --git a/.circleci/requirements/dev-requirements-py37.txt b/.circleci/requirements/dev-requirements-py37.txt index 127f48d5ec..723f0cac14 100644 --- a/.circleci/requirements/dev-requirements-py37.txt +++ b/.circleci/requirements/dev-requirements-py37.txt @@ -14,4 +14,5 @@ requests beautifulsoup4 pytest pytest-sugar -pytest-mock \ No newline at end of file +pytest-mock +waitress \ No newline at end of file diff --git a/.circleci/requirements/dev-requirements.txt b/.circleci/requirements/dev-requirements.txt index c151ccc7d3..6533e7f5bc 100644 --- a/.circleci/requirements/dev-requirements.txt +++ b/.circleci/requirements/dev-requirements.txt @@ -14,3 +14,4 @@ pytest-mock lxml requests beautifulsoup4 +waitress diff --git a/.pylintrc b/.pylintrc index a9688e4e7a..c6b7ec6d9e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -59,7 +59,8 @@ disable=fixme, invalid-name, too-many-lines, old-style-class, - superfluous-parens + superfluous-parens, + bad-continuation, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/.pylintrc37 b/.pylintrc37 index 57c45836cd..533bbade91 100644 --- a/.pylintrc37 +++ b/.pylintrc37 @@ -147,7 +147,8 @@ disable=invalid-name, useless-object-inheritance, possibly-unused-variable, too-many-lines, - too-many-statements + too-many-statements, + bad-continuation # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/dash/exceptions.py b/dash/exceptions.py index 0b02966209..477eb7d151 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -94,3 +94,23 @@ class SameInputOutputException(CallbackException): class MissingCallbackContextException(CallbackException): pass + + +class DashTestingError(Exception): + """Base error for pytest-dash.""" + + +class InvalidDriverError(DashTestingError): + """An invalid selenium driver was specified.""" + + +class NoAppFoundError(DashTestingError): + """No `app` was found in the file.""" + + +class DashAppLoadingError(DashTestingError): + """The dash app failed to load""" + + +class ServerCloseError(DashTestingError): + """The server cannot be closed""" diff --git a/__init__.py b/dash/testing/__init__.py similarity index 100% rename from __init__.py rename to dash/testing/__init__.py diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py new file mode 100644 index 0000000000..527b7065c0 --- /dev/null +++ b/dash/testing/application_runners.py @@ -0,0 +1,212 @@ +from __future__ import print_function + +import sys +import uuid +import shlex +import threading +import subprocess + +import six +import runpy +import flask +import requests + +from dash.exceptions import ( + NoAppFoundError, + # DashAppLoadingError, + ServerCloseError, +) +import dash.testing.wait as wait + +import logging + +logger = logging.getLogger(__name__) + + +def import_app(app_file, application_name="app"): + """ + Import a dash application from a module. + The import path is in dot notation to the module. + The variable named app will be returned. + + :Example: + + >>> app = import_app('my_app.app') + + Will import the application in module `app` of the package `my_app`. + + :param app_file: Path to the app (dot-separated). + :type app_file: str + :param application_name: The name of the dash application instance. + :raise: dash_tests.errors.NoAppFoundError + :return: App from module. + :rtype: dash.Dash + """ + try: + app_module = runpy.run_module(app_file) + app = app_module[application_name] + except KeyError: + raise NoAppFoundError( + "No dash `app` instance was found in {}".format(app_file) + ) + return app + + +class BaseDashRunner(object): + """Base context manager class for running applications.""" + + def __init__(self, keep_open, stop_timeout): + self.port = 8050 + self.started = None + self.keep_open = keep_open + self.stop_timeout = stop_timeout + + def start(self, *args, **kwargs): + raise NotImplementedError # pragma: no cover + + def stop(self): + raise NotImplementedError # pragma: no cover + + def __call__(self, *args, **kwargs): + return self.start(*args, **kwargs) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, traceback): + if self.started and not self.keep_open: + try: + logger.info("killing the app runner") + self.stop() + except TimeoutError: + raise ServerCloseError( + "Cannot stop server within {} timeout".format( + self.stop_timeout + ) + ) + + @property + def url(self): + """the default server url""" + return "http://localhost:{}".format(self.port) + + +class ThreadedRunner(BaseDashRunner): + """Runs a dash application in a thread + + this is the default flavor to use in dash integration tests + """ + + def __init__(self, keep_open=False, stop_timeout=1): + super(ThreadedRunner, self).__init__( + keep_open=keep_open, stop_timeout=stop_timeout + ) + self.stop_route = "/_stop-{}".format(uuid.uuid4().hex) + self.thread = None + + @staticmethod + def _stop_server(): + # https://werkzeug.palletsprojects.com/en/0.15.x/serving/#shutting-down-the-server + stopper = flask.request.environ.get("werkzeug.server.shutdown") + if stopper is None: + raise RuntimeError("Not running with the Werkzeug Server") + stopper() + return "Flask server is shutting down" + + # pylint: disable=arguments-differ,C0330 + def start(self, app, **kwargs): + """Start the app server in threading flavor""" + app.server.add_url_rule( + self.stop_route, self.stop_route, self._stop_server + ) + + def _handle_error(): + self._stop_server() + + app.server.errorhandler(500)(_handle_error) + + def run(): + app.scripts.config.serve_locally = True + app.css.config.serve_locally = True + if "port" not in kwargs: + kwargs["port"] = self.port + app.run_server(threaded=True, **kwargs) + + self.thread = threading.Thread(target=run) + self.thread.daemon = True + + try: + self.thread.start() + except RuntimeError: # multiple call on same thread + self.started = False + + self.started = self.thread.is_alive() + + def stop(self): + requests.get("{}{}".format(self.url, self.stop_route)) + wait.until_not(self.thread.is_alive, self.stop_timeout) + + +class ProcessRunner(BaseDashRunner): + """Runs a dash application in a waitress-serve subprocess + + this flavor is closer to production environment but slower + """ + + def __init__(self, keep_open=False, stop_timeout=1): + super(ProcessRunner, self).__init__( + keep_open=keep_open, stop_timeout=stop_timeout + ) + self.proc = None + + # pylint: disable=arguments-differ + def start(self, app_module, application_name="app", port=8050): + """ + Start the waitress-serve process. + + .. seealso:: :py:func:`~.plugin.dash_subprocess` + + :param app_module: Dot notation path to the app file. + :type app_module: str + :param application_name: Variable name of the dash instance. + :type application_name: str + :param port: Port to serve the application. + :type port: int + :return: + """ + entrypoint = "{}:{}.server".format(app_module, application_name) + self.port = port + + args = shlex.split( + "waitress-serve --listen=127.0.0.1:{} {}".format(port, entrypoint), + posix=sys.platform != "win32", + ) + logger.debug("start dash process with %s", args) + try: + self.proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except (OSError, ValueError): + self.started = False + + self.started = True + + def stop(self): + self.proc.terminate() + try: + if six.PY3: + _except = subprocess.TimeoutExpired + return self.proc.communicate(timeout=self.stop_timeout) + else: + _except = OSError + return self.proc.communicate() + + except _except: + logger.warning( + "subprocess terminate timeout %s reached, trying to kill " + "the subprocess in a safe manner", self.stop_timeout + ) + self.proc.kill() + return self.proc.communicate() diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py new file mode 100644 index 0000000000..269a4c0e50 --- /dev/null +++ b/dash/testing/plugin.py @@ -0,0 +1,72 @@ +import pytest + +from selenium import webdriver + +from dash.testing.application_runners import ThreadedRunner, ProcessRunner + +WEBDRIVERS = { + "Chrome": webdriver.Chrome, + "Firefox": webdriver.Firefox, + "Remote": webdriver.Remote, +} + + +def _get_config(config, key, default=None): + opt = config.getoption(key) + ini = config.getini(key) + return opt or ini or default + + +############################################################################### +# Plugin hooks. +############################################################################### + +# pylint: disable=missing-docstring +def pytest_addoption(parser): + # Add options to the pytest parser, either on the commandline or ini + # TODO add more options for the selenium driver. + dash = parser.getgroup("Dash", "Dash Integration Tests") + help_msg = "Name of the selenium driver to use" + dash.addoption( + "--webdriver", choices=tuple(WEBDRIVERS.keys()), help=help_msg + ) + parser.addini("webdriver", help=help_msg) + + +############################################################################### +# Fixtures +############################################################################### + + +@pytest.fixture +def thread_server(): + """ + Start a local dash server in a new thread. Stop the server in teardown. + :Example: + .. code-block:: python + import dash + import dash_html_components as html + def test_application(dash_threaded): + app = dash.Dash(__name__) + app.layout = html.Div('My app) + dash_threaded(app) + .. seealso:: :py:class:`pytest_dash.application_runners.DashThreaded` + """ + + with ThreadedRunner() as starter: + yield starter + + +@pytest.fixture +def process_server(): + """ + Start a Dash server with subprocess.Popen and waitress-serve. + :Example: + .. code-block:: python + def test_application(dash_subprocess): + # consider the application file is named `app.py` + dash_subprocess('app') + .. seealso:: :py:class:`pytest_dash.application_runners.DashSubprocess` + """ + with ProcessRunner() as starter: + yield starter diff --git a/dash/testing/wait.py b/dash/testing/wait.py new file mode 100644 index 0000000000..aa02617369 --- /dev/null +++ b/dash/testing/wait.py @@ -0,0 +1,25 @@ +"""Utils methods for pytest-dash such wait_for wrappers""" +import time + + +def until( + wait_cond, + timeout, + poll=0.1, + msg="expected condition not met within timeout", +): # noqa: C0330 + end_time = time.time() + timeout + while wait_cond(): + time.sleep(poll) + if time.time() > end_time: + raise TimeoutError(msg) + + +def until_not( + wait_cond, timeout, poll=0.1, msg="expected condition met within timeout" +): # noqa: C0330 + end_time = time.time() + timeout + while not wait_cond(): + time.sleep(poll) + if time.time() > end_time: + raise TimeoutError(msg) diff --git a/pytest.ini b/pytest.ini index 52c3d7b0e6..bfada58fd9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,9 @@ [pytest] +testpaths = tests/ addopts = -rsxX -vv +log_cli=true +log_cli_level = DEBUG +webdriver = Chrome diff --git a/setup.py b/setup.py index 64cda7e1ea..3d8070c86f 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,10 @@ packages=find_packages(exclude=['tests*']), include_package_data=True, license='MIT', - description=('A Python framework for building reactive web-apps. ' - 'Developed by Plotly.'), + description=( + 'A Python framework for building reactive web-apps. ' + 'Developed by Plotly.' + ), long_description=io.open('README.md', encoding='utf-8').read(), long_description_content_type='text/markdown', install_requires=[ @@ -29,7 +31,10 @@ 'console_scripts': [ 'dash-generate-components =' ' dash.development.component_generator:cli' - ] + ], + 'pytest11': [ + 'dash = dash.testing.plugin' + ], }, url='https://plot.ly/dash', classifiers=[ From d67e3e547865fa824f59681524e6aae53e04ef43 Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 24 May 2019 18:13:46 -0400 Subject: [PATCH 03/46] :white_check_mark: add dash smoke test with fixtures --- dash/testing/application_runners.py | 7 ++--- tests/unit/dash/app_assets/__init__.py | 0 tests/unit/dash/app_assets/simple_app.py | 38 ++++++++++++++++++++++++ tests/unit/dash/test_app_runners.py | 29 ++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/unit/dash/app_assets/__init__.py create mode 100644 tests/unit/dash/app_assets/simple_app.py create mode 100644 tests/unit/dash/test_app_runners.py diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 527b7065c0..ec0304c378 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -184,9 +184,7 @@ def start(self, app_module, application_name="app", port=8050): logger.debug("start dash process with %s", args) try: self.proc = subprocess.Popen( - args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) except (OSError, ValueError): self.started = False @@ -206,7 +204,8 @@ def stop(self): except _except: logger.warning( "subprocess terminate timeout %s reached, trying to kill " - "the subprocess in a safe manner", self.stop_timeout + "the subprocess in a safe manner", + self.stop_timeout, ) self.proc.kill() return self.proc.communicate() diff --git a/tests/unit/dash/app_assets/__init__.py b/tests/unit/dash/app_assets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/dash/app_assets/simple_app.py b/tests/unit/dash/app_assets/simple_app.py new file mode 100644 index 0000000000..2e7d0c34b0 --- /dev/null +++ b/tests/unit/dash/app_assets/simple_app.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-docstring +import dash +from dash.dependencies import Output, Input +from dash.exceptions import PreventUpdate + +import dash_html_components as html +import dash_core_components as dcc + +app = dash.Dash(__name__) + +app.layout = html.Div([ + dcc.Input(id='value', placeholder='my-value'), + html.Div(['You entered: ', html.Span(id='out')]), + html.Button('style-btn', id='style-btn'), + html.Div('style-container', id='style-output'), +]) + + +@app.callback(Output('out', 'children'), [Input('value', 'value')]) +def on_value(value): + if value is None: + raise PreventUpdate + + return value + + +@app.callback( + Output('style-output', 'style'), [Input('style-btn', 'n_clicks')] +) +def on_style(value): + if value is None: + raise PreventUpdate + + return {'padding': '10px'} + + +if __name__ == '__main__': + app.run_server(debug=True, port=10850) \ No newline at end of file diff --git a/tests/unit/dash/test_app_runners.py b/tests/unit/dash/test_app_runners.py new file mode 100644 index 0000000000..4aa8bdb707 --- /dev/null +++ b/tests/unit/dash/test_app_runners.py @@ -0,0 +1,29 @@ +import time +import requests + +import dash +import dash_html_components as html + + +def test_threaded_server_smoke(thread_server): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button("click me", id="clicker"), + html.Div(id="output", children="hello thread"), + ] + ) + thread_server(app, debug=True, use_reloader=False, use_debugger=True) + time.sleep(0.2) + r = requests.get(thread_server.url) + assert r.status_code == 200, "the threaded server is reachable" + assert 'id="react-entry-point"' in r.text, "the entrypoint is present" + + +def test_process_server_smoke(process_server): + process_server("tests.unit.dash.app_assets.simple_app") + time.sleep(2.5) + r = requests.get(process_server.url) + assert r.status_code == 200, "the server is reachable" + assert 'id="react-entry-point"' in r.text, "the entrypoint is present" From 0832a183cc383f27fa5823460695c1fe1cc12401 Mon Sep 17 00:00:00 2001 From: byron Date: Sat, 25 May 2019 23:13:16 -0400 Subject: [PATCH 04/46] :construction: fixing issues in python2 --- .pylintrc | 1 + .pylintrc37 | 3 +- dash/exceptions.py | 4 + dash/testing/application_runners.py | 16 ++-- dash/testing/plugin.py | 1 + dash/testing/wait.py | 5 +- .../dash/app_assets => dash_apps}/__init__.py | 0 tests/dash_apps/simple_app.py | 38 +++++++++ tests/unit/dash/app_assets/simple_app.py | 38 --------- tests/unit/dash/test_app_runners.py | 4 +- tests/unit/dash/test_resources.py | 77 +++++++++---------- 11 files changed, 95 insertions(+), 92 deletions(-) rename tests/{unit/dash/app_assets => dash_apps}/__init__.py (100%) create mode 100644 tests/dash_apps/simple_app.py delete mode 100644 tests/unit/dash/app_assets/simple_app.py diff --git a/.pylintrc b/.pylintrc index c6b7ec6d9e..bde8d00a15 100644 --- a/.pylintrc +++ b/.pylintrc @@ -61,6 +61,7 @@ disable=fixme, old-style-class, superfluous-parens, bad-continuation, + unexpected-keyword-arg # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/.pylintrc37 b/.pylintrc37 index 533bbade91..30691595f4 100644 --- a/.pylintrc37 +++ b/.pylintrc37 @@ -148,7 +148,8 @@ disable=invalid-name, possibly-unused-variable, too-many-lines, too-many-statements, - bad-continuation + bad-continuation, + unexpected-keyword-arg # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/dash/exceptions.py b/dash/exceptions.py index 477eb7d151..9af4a35f24 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -114,3 +114,7 @@ class DashAppLoadingError(DashTestingError): class ServerCloseError(DashTestingError): """The server cannot be closed""" + + +class TestingTimeoutError(DashTestingError): + """"all timeout error about dash testing""" diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index ec0304c378..1ef060abfd 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -5,20 +5,20 @@ import shlex import threading import subprocess +import logging -import six import runpy +import six import flask import requests from dash.exceptions import ( NoAppFoundError, - # DashAppLoadingError, + TestingTimeoutError, ServerCloseError, ) import dash.testing.wait as wait -import logging logger = logging.getLogger(__name__) @@ -78,7 +78,7 @@ def __exit__(self, exc_type, exc_val, traceback): try: logger.info("killing the app runner") self.stop() - except TimeoutError: + except TestingTimeoutError: raise ServerCloseError( "Cannot stop server within {} timeout".format( self.stop_timeout @@ -195,11 +195,11 @@ def stop(self): self.proc.terminate() try: if six.PY3: - _except = subprocess.TimeoutExpired + _except = subprocess.TimeoutExpired # pylint:disable=no-member return self.proc.communicate(timeout=self.stop_timeout) - else: - _except = OSError - return self.proc.communicate() + + _except = OSError + return self.proc.communicate() except _except: logger.warning( diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 269a4c0e50..eadbcbb69f 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-docstring import pytest from selenium import webdriver diff --git a/dash/testing/wait.py b/dash/testing/wait.py index aa02617369..6306d2a2f8 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -1,5 +1,6 @@ """Utils methods for pytest-dash such wait_for wrappers""" import time +from dash.exceptions import TestingTimeoutError def until( @@ -12,7 +13,7 @@ def until( while wait_cond(): time.sleep(poll) if time.time() > end_time: - raise TimeoutError(msg) + raise TestingTimeoutError(msg) def until_not( @@ -22,4 +23,4 @@ def until_not( while not wait_cond(): time.sleep(poll) if time.time() > end_time: - raise TimeoutError(msg) + raise TestingTimeoutError(msg) diff --git a/tests/unit/dash/app_assets/__init__.py b/tests/dash_apps/__init__.py similarity index 100% rename from tests/unit/dash/app_assets/__init__.py rename to tests/dash_apps/__init__.py diff --git a/tests/dash_apps/simple_app.py b/tests/dash_apps/simple_app.py new file mode 100644 index 0000000000..3e485c0890 --- /dev/null +++ b/tests/dash_apps/simple_app.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-docstring +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Output, Input +from dash.exceptions import PreventUpdate + + +app = dash.Dash(__name__) + +app.layout = html.Div( + [ + dcc.Input(id="value", placeholder="my-value"), + html.Div(["You entered: ", html.Span(id="out")]), + html.Button("style-btn", id="style-btn"), + html.Div("style-container", id="style-output"), + ] +) + + +@app.callback(Output("out", "children"), [Input("value", "value")]) +def on_value(value): + if value is None: + raise PreventUpdate + + return value + + +@app.callback(Output("style-output", "style"), [Input("style-btn", "n_clicks")]) +def on_style(value): + if value is None: + raise PreventUpdate + + return {"padding": "10px"} + + +if __name__ == "__main__": + app.run_server(debug=True, port=10850) diff --git a/tests/unit/dash/app_assets/simple_app.py b/tests/unit/dash/app_assets/simple_app.py deleted file mode 100644 index 2e7d0c34b0..0000000000 --- a/tests/unit/dash/app_assets/simple_app.py +++ /dev/null @@ -1,38 +0,0 @@ -# pylint: disable=missing-docstring -import dash -from dash.dependencies import Output, Input -from dash.exceptions import PreventUpdate - -import dash_html_components as html -import dash_core_components as dcc - -app = dash.Dash(__name__) - -app.layout = html.Div([ - dcc.Input(id='value', placeholder='my-value'), - html.Div(['You entered: ', html.Span(id='out')]), - html.Button('style-btn', id='style-btn'), - html.Div('style-container', id='style-output'), -]) - - -@app.callback(Output('out', 'children'), [Input('value', 'value')]) -def on_value(value): - if value is None: - raise PreventUpdate - - return value - - -@app.callback( - Output('style-output', 'style'), [Input('style-btn', 'n_clicks')] -) -def on_style(value): - if value is None: - raise PreventUpdate - - return {'padding': '10px'} - - -if __name__ == '__main__': - app.run_server(debug=True, port=10850) \ No newline at end of file diff --git a/tests/unit/dash/test_app_runners.py b/tests/unit/dash/test_app_runners.py index 4aa8bdb707..c245cdb2dd 100644 --- a/tests/unit/dash/test_app_runners.py +++ b/tests/unit/dash/test_app_runners.py @@ -1,8 +1,8 @@ import time import requests -import dash import dash_html_components as html +import dash def test_threaded_server_smoke(thread_server): @@ -22,7 +22,7 @@ def test_threaded_server_smoke(thread_server): def test_process_server_smoke(process_server): - process_server("tests.unit.dash.app_assets.simple_app") + process_server("tests.dash_apps.simple_app") time.sleep(2.5) r = requests.get(process_server.url) assert r.status_code == 200, "the server is reachable" diff --git a/tests/unit/dash/test_resources.py b/tests/unit/dash/test_resources.py index 4a32f54878..398682b6ae 100644 --- a/tests/unit/dash/test_resources.py +++ b/tests/unit/dash/test_resources.py @@ -5,28 +5,28 @@ _monkey_patched_js_dist = [ { - 'external_url': 'https://external_javascript.js', - 'relative_package_path': 'external_javascript.js', - 'namespace': 'dash_core_components' + "external_url": "https://external_javascript.js", + "relative_package_path": "external_javascript.js", + "namespace": "dash_core_components", }, { - 'external_url': 'https://external_css.css', - 'relative_package_path': 'external_css.css', - 'namespace': 'dash_core_components' + "external_url": "https://external_css.css", + "relative_package_path": "external_css.css", + "namespace": "dash_core_components", }, { - 'relative_package_path': 'fake_dcc.js', - 'dev_package_path': 'fake_dcc.dev.js', - 'external_url': 'https://component_library.bundle.js', - 'namespace': 'dash_core_components' + "relative_package_path": "fake_dcc.js", + "dev_package_path": "fake_dcc.dev.js", + "external_url": "https://component_library.bundle.js", + "namespace": "dash_core_components", }, { - 'relative_package_path': 'fake_dcc.min.js.map', - 'dev_package_path': 'fake_dcc.dev.js.map', - 'external_url': 'https://component_library.bundle.js.map', - 'namespace': 'dash_core_components', - 'dynamic': True - } + "relative_package_path": "fake_dcc.min.js.map", + "dev_package_path": "fake_dcc.dev.js.map", + "external_url": "https://component_library.bundle.js.map", + "namespace": "dash_core_components", + "dynamic": True, + }, ] @@ -35,61 +35,56 @@ class StatMock(object): def test_external(mocker): - mocker.patch('dash_core_components._js_dist') + mocker.patch("dash_core_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, dcc.__version__ = 1 app = dash.Dash( - __name__, - assets_folder='tests/assets', - assets_ignore='load_after.+.js' + __name__, assets_folder="tests/assets", assets_ignore="load_after.+.js" ) app.layout = dcc.Markdown() app.scripts.config.serve_locally = False - with mock.patch('dash.dash.os.stat', return_value=StatMock()): + with mock.patch("dash.dash.os.stat", return_value=StatMock()): resource = app._collect_and_register_resources( app.scripts.get_all_scripts() ) assert resource == [ - 'https://external_javascript.js', - 'https://external_css.css', - 'https://component_library.bundle.js' + "https://external_javascript.js", + "https://external_css.css", + "https://component_library.bundle.js", ] def test_internal(mocker): - mocker.patch('dash_core_components._js_dist') + mocker.patch("dash_core_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, dcc.__version__ = 1 app = dash.Dash( - __name__, - assets_folder='tests/assets', - assets_ignore='load_after.+.js' + __name__, assets_folder="tests/assets", assets_ignore="load_after.+.js" ) app.layout = dcc.Markdown() assert app.scripts.config.serve_locally and app.css.config.serve_locally - with mock.patch('dash.dash.os.stat', return_value=StatMock()): - with mock.patch('dash.dash.importlib.import_module', - return_value=dcc): + with mock.patch("dash.dash.os.stat", return_value=StatMock()): + with mock.patch("dash.dash.importlib.import_module", return_value=dcc): resource = app._collect_and_register_resources( app.scripts.get_all_scripts() ) assert resource == [ - '/_dash-component-suites/' - 'dash_core_components/external_javascript.js?v=1&m=1', - '/_dash-component-suites/' - 'dash_core_components/external_css.css?v=1&m=1', - '/_dash-component-suites/' - 'dash_core_components/fake_dcc.js?v=1&m=1', + "/_dash-component-suites/" + "dash_core_components/external_javascript.js?v=1&m=1", + "/_dash-component-suites/" + "dash_core_components/external_css.css?v=1&m=1", + "/_dash-component-suites/" "dash_core_components/fake_dcc.js?v=1&m=1", ] - assert 'fake_dcc.min.js.map' in app.registered_paths['dash_core_components'], \ - 'Dynamic resource not available in registered path {}'.format( - app.registered_paths['dash_core_components'] - ) + assert ( + "fake_dcc.min.js.map" in app.registered_paths["dash_core_components"] + ), "Dynamic resource not available in registered path {}".format( + app.registered_paths["dash_core_components"] + ) From 13f8b979431d685421a5fad4c441dbe48d726672 Mon Sep 17 00:00:00 2001 From: byron Date: Sat, 25 May 2019 23:40:14 -0400 Subject: [PATCH 05/46] :construction: debug with circleci cli --- tests/unit/dash/test_app_runners.py | 2 +- tests/unit/dash/test_resources.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/dash/test_app_runners.py b/tests/unit/dash/test_app_runners.py index c245cdb2dd..e64131b54d 100644 --- a/tests/unit/dash/test_app_runners.py +++ b/tests/unit/dash/test_app_runners.py @@ -23,7 +23,7 @@ def test_threaded_server_smoke(thread_server): def test_process_server_smoke(process_server): process_server("tests.dash_apps.simple_app") - time.sleep(2.5) + time.sleep(4) r = requests.get(process_server.url) assert r.status_code == 200, "the server is reachable" assert 'id="react-entry-point"' in r.text, "the entrypoint is present" diff --git a/tests/unit/dash/test_resources.py b/tests/unit/dash/test_resources.py index 398682b6ae..fa2054e701 100644 --- a/tests/unit/dash/test_resources.py +++ b/tests/unit/dash/test_resources.py @@ -50,11 +50,11 @@ def test_external(mocker): app.scripts.get_all_scripts() ) - assert resource == [ + assert set(resource) == { "https://external_javascript.js", "https://external_css.css", "https://component_library.bundle.js", - ] + } def test_internal(mocker): @@ -75,13 +75,13 @@ def test_internal(mocker): app.scripts.get_all_scripts() ) - assert resource == [ + assert set(resource) == { "/_dash-component-suites/" "dash_core_components/external_javascript.js?v=1&m=1", "/_dash-component-suites/" "dash_core_components/external_css.css?v=1&m=1", "/_dash-component-suites/" "dash_core_components/fake_dcc.js?v=1&m=1", - ] + } assert ( "fake_dcc.min.js.map" in app.registered_paths["dash_core_components"] From fb72a183a21233ab31e3d163c697fb45cb4f3e9c Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 28 May 2019 22:02:40 -0400 Subject: [PATCH 06/46] :alembic: dbg --- .circleci/config.yml | 25 ++++----- dash/testing/application_runners.py | 54 ++++++++----------- tests/simple_app.py | 38 +++++++++++++ tests/unit/dash/development/__init__.py | 0 tests/unit/dash/test_app_runners.py | 29 ---------- .../development/TestReactComponent.react.js | 0 .../TestReactComponentRequired.react.js | 0 tests/unit/{dash => development}/__init__.py | 0 .../development/flow_metadata_test.json | 0 .../development/metadata_required_test.json | 0 .../{dash => }/development/metadata_test.json | 0 .../{dash => }/development/metadata_test.py | 0 .../development/test_base_component.py | 3 +- .../development/test_component_loader.py | 0 tests/unit/simple_app.py | 38 +++++++++++++ tests/unit/{dash => }/test_configs.py | 0 tests/unit/test_import.py | 14 +++++ tests/unit/{dash => }/test_resources.py | 0 18 files changed, 128 insertions(+), 73 deletions(-) create mode 100644 tests/simple_app.py delete mode 100644 tests/unit/dash/development/__init__.py delete mode 100644 tests/unit/dash/test_app_runners.py rename tests/unit/{dash => }/development/TestReactComponent.react.js (100%) rename tests/unit/{dash => }/development/TestReactComponentRequired.react.js (100%) rename tests/unit/{dash => development}/__init__.py (100%) rename tests/unit/{dash => }/development/flow_metadata_test.json (100%) rename tests/unit/{dash => }/development/metadata_required_test.json (100%) rename tests/unit/{dash => }/development/metadata_test.json (100%) rename tests/unit/{dash => }/development/metadata_test.py (100%) rename tests/unit/{dash => }/development/test_base_component.py (99%) rename tests/unit/{dash => }/development/test_component_loader.py (100%) create mode 100644 tests/unit/simple_app.py rename tests/unit/{dash => }/test_configs.py (100%) create mode 100644 tests/unit/test_import.py rename tests/unit/{dash => }/test_resources.py (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 27eb110792..3700726468 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,6 +2,7 @@ version: 2 jobs: "python-2.7": &test-template + working_directory: ~/dash docker: - image: circleci/python:2.7-stretch-node-browsers environment: @@ -38,24 +39,24 @@ jobs: paths: - "venv" - - run: - name: 🌸 linting - command: | - . venv/bin/activate - pip install -e . --quiet - pip list | grep dash - flake8 dash setup.py - flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests - pylint dash setup.py --rcfile=$PYLINTRC - pylint tests/unit -d all -e C0410,C0411,C0412,C0413,W0109 - cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test + # - run: + # name: 🌸 linting + # command: | + # . venv/bin/activate + # pip install -e . --quiet + # pip list | grep dash + # flake8 dash setup.py + # flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests + # pylint dash setup.py --rcfile=$PYLINTRC + # pylint tests/unit -d all -e C0410,C0411,C0412,C0413,W0109 + # cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test - run: name: ⛑ Run unit tests command: | . venv/bin/activate mkdir test-reports - pytest --junitxml=test-reports/junit.xml tests/unit + PYTHONPATH=~/dash pytest --junitxml=test-reports/junit.xml tests/unit - store_test_results: path: test-reports - store_artifacts: diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 1ef060abfd..bd9275eb36 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -161,51 +161,43 @@ def __init__(self, keep_open=False, stop_timeout=1): # pylint: disable=arguments-differ def start(self, app_module, application_name="app", port=8050): - """ - Start the waitress-serve process. - - .. seealso:: :py:func:`~.plugin.dash_subprocess` - - :param app_module: Dot notation path to the app file. - :type app_module: str - :param application_name: Variable name of the dash instance. - :type application_name: str - :param port: Port to serve the application. - :type port: int - :return: - """ + """Start the server with waitress-serve in process flavor """ entrypoint = "{}:{}.server".format(app_module, application_name) self.port = port args = shlex.split( - "waitress-serve --listen=127.0.0.1:{} {}".format(port, entrypoint), + "waitress-serve --listen=0.0.0.0:{} {}".format(port, entrypoint), posix=sys.platform != "win32", ) logger.debug("start dash process with %s", args) try: + # print('start ......') self.proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) except (OSError, ValueError): + logger.exception("subprocess error") self.started = False self.started = True def stop(self): - self.proc.terminate() - try: - if six.PY3: - _except = subprocess.TimeoutExpired # pylint:disable=no-member - return self.proc.communicate(timeout=self.stop_timeout) - - _except = OSError - return self.proc.communicate() - - except _except: - logger.warning( - "subprocess terminate timeout %s reached, trying to kill " - "the subprocess in a safe manner", - self.stop_timeout, - ) - self.proc.kill() - return self.proc.communicate() + if self.proc: + self.proc.terminate() + try: + if six.PY3: + # pylint:disable=no-member + _except = subprocess.TimeoutExpired + return self.proc.communicate(timeout=self.stop_timeout) + + _except = OSError + return self.proc.communicate() + + except _except: + logger.warning( + "subprocess terminate timeout %s reached, trying to kill " + "the subprocess in a safe manner", + self.stop_timeout, + ) + self.proc.kill() + return self.proc.communicate() diff --git a/tests/simple_app.py b/tests/simple_app.py new file mode 100644 index 0000000000..3e485c0890 --- /dev/null +++ b/tests/simple_app.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-docstring +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Output, Input +from dash.exceptions import PreventUpdate + + +app = dash.Dash(__name__) + +app.layout = html.Div( + [ + dcc.Input(id="value", placeholder="my-value"), + html.Div(["You entered: ", html.Span(id="out")]), + html.Button("style-btn", id="style-btn"), + html.Div("style-container", id="style-output"), + ] +) + + +@app.callback(Output("out", "children"), [Input("value", "value")]) +def on_value(value): + if value is None: + raise PreventUpdate + + return value + + +@app.callback(Output("style-output", "style"), [Input("style-btn", "n_clicks")]) +def on_style(value): + if value is None: + raise PreventUpdate + + return {"padding": "10px"} + + +if __name__ == "__main__": + app.run_server(debug=True, port=10850) diff --git a/tests/unit/dash/development/__init__.py b/tests/unit/dash/development/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/dash/test_app_runners.py b/tests/unit/dash/test_app_runners.py deleted file mode 100644 index e64131b54d..0000000000 --- a/tests/unit/dash/test_app_runners.py +++ /dev/null @@ -1,29 +0,0 @@ -import time -import requests - -import dash_html_components as html -import dash - - -def test_threaded_server_smoke(thread_server): - app = dash.Dash(__name__) - - app.layout = html.Div( - [ - html.Button("click me", id="clicker"), - html.Div(id="output", children="hello thread"), - ] - ) - thread_server(app, debug=True, use_reloader=False, use_debugger=True) - time.sleep(0.2) - r = requests.get(thread_server.url) - assert r.status_code == 200, "the threaded server is reachable" - assert 'id="react-entry-point"' in r.text, "the entrypoint is present" - - -def test_process_server_smoke(process_server): - process_server("tests.dash_apps.simple_app") - time.sleep(4) - r = requests.get(process_server.url) - assert r.status_code == 200, "the server is reachable" - assert 'id="react-entry-point"' in r.text, "the entrypoint is present" diff --git a/tests/unit/dash/development/TestReactComponent.react.js b/tests/unit/development/TestReactComponent.react.js similarity index 100% rename from tests/unit/dash/development/TestReactComponent.react.js rename to tests/unit/development/TestReactComponent.react.js diff --git a/tests/unit/dash/development/TestReactComponentRequired.react.js b/tests/unit/development/TestReactComponentRequired.react.js similarity index 100% rename from tests/unit/dash/development/TestReactComponentRequired.react.js rename to tests/unit/development/TestReactComponentRequired.react.js diff --git a/tests/unit/dash/__init__.py b/tests/unit/development/__init__.py similarity index 100% rename from tests/unit/dash/__init__.py rename to tests/unit/development/__init__.py diff --git a/tests/unit/dash/development/flow_metadata_test.json b/tests/unit/development/flow_metadata_test.json similarity index 100% rename from tests/unit/dash/development/flow_metadata_test.json rename to tests/unit/development/flow_metadata_test.json diff --git a/tests/unit/dash/development/metadata_required_test.json b/tests/unit/development/metadata_required_test.json similarity index 100% rename from tests/unit/dash/development/metadata_required_test.json rename to tests/unit/development/metadata_required_test.json diff --git a/tests/unit/dash/development/metadata_test.json b/tests/unit/development/metadata_test.json similarity index 100% rename from tests/unit/dash/development/metadata_test.json rename to tests/unit/development/metadata_test.json diff --git a/tests/unit/dash/development/metadata_test.py b/tests/unit/development/metadata_test.py similarity index 100% rename from tests/unit/dash/development/metadata_test.py rename to tests/unit/development/metadata_test.py diff --git a/tests/unit/dash/development/test_base_component.py b/tests/unit/development/test_base_component.py similarity index 99% rename from tests/unit/dash/development/test_base_component.py rename to tests/unit/development/test_base_component.py index d901189ec4..9d0242df04 100644 --- a/tests/unit/dash/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -6,7 +6,7 @@ import shutil import unittest import plotly - +import pytest from dash.development.base_component import Component from dash.development._py_components_generation import generate_class_string, generate_class_file, generate_class, \ create_docstring, prohibit_events, js_to_py_type @@ -48,6 +48,7 @@ def nested_tree(): return c, c1, c2, c3, c4, c5 +@pytest.mark.skip(reason='') class TestComponent(unittest.TestCase): def test_init(self): Component(a=3) diff --git a/tests/unit/dash/development/test_component_loader.py b/tests/unit/development/test_component_loader.py similarity index 100% rename from tests/unit/dash/development/test_component_loader.py rename to tests/unit/development/test_component_loader.py diff --git a/tests/unit/simple_app.py b/tests/unit/simple_app.py new file mode 100644 index 0000000000..3e485c0890 --- /dev/null +++ b/tests/unit/simple_app.py @@ -0,0 +1,38 @@ +# pylint: disable=missing-docstring +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Output, Input +from dash.exceptions import PreventUpdate + + +app = dash.Dash(__name__) + +app.layout = html.Div( + [ + dcc.Input(id="value", placeholder="my-value"), + html.Div(["You entered: ", html.Span(id="out")]), + html.Button("style-btn", id="style-btn"), + html.Div("style-container", id="style-output"), + ] +) + + +@app.callback(Output("out", "children"), [Input("value", "value")]) +def on_value(value): + if value is None: + raise PreventUpdate + + return value + + +@app.callback(Output("style-output", "style"), [Input("style-btn", "n_clicks")]) +def on_style(value): + if value is None: + raise PreventUpdate + + return {"padding": "10px"} + + +if __name__ == "__main__": + app.run_server(debug=True, port=10850) diff --git a/tests/unit/dash/test_configs.py b/tests/unit/test_configs.py similarity index 100% rename from tests/unit/dash/test_configs.py rename to tests/unit/test_configs.py diff --git a/tests/unit/test_import.py b/tests/unit/test_import.py new file mode 100644 index 0000000000..d6e6acebb5 --- /dev/null +++ b/tests/unit/test_import.py @@ -0,0 +1,14 @@ +import importlib +import types + + +def test_dash_import_is_correct(): + imported = importlib.import_module("dash") + assert isinstance(imported, types.ModuleType), "dash can be imported" + + with open("./dash/version.py") as fp: + assert imported.__version__ in fp.read(), "version is consistent" + + assert ( + getattr(imported, "Dash").__name__ == "Dash" + ), "access to main Dash class is valid" \ No newline at end of file diff --git a/tests/unit/dash/test_resources.py b/tests/unit/test_resources.py similarity index 100% rename from tests/unit/dash/test_resources.py rename to tests/unit/test_resources.py From 73b4dbc1e426958ca6b6e4c4b9ab70bf04fe0d7d Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 28 May 2019 22:08:44 -0400 Subject: [PATCH 07/46] :alembic: add missing test file --- tests/unit/test_app_runners.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unit/test_app_runners.py diff --git a/tests/unit/test_app_runners.py b/tests/unit/test_app_runners.py new file mode 100644 index 0000000000..8fa71e8dd9 --- /dev/null +++ b/tests/unit/test_app_runners.py @@ -0,0 +1,30 @@ +import time +import requests + +import dash_html_components as html +import dash + + +def test_threaded_server_smoke(thread_server): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button("click me", id="clicker"), + html.Div(id="output", children="hello thread"), + ] + ) + thread_server(app, debug=True, use_reloader=False, use_debugger=True) + time.sleep(0.2) + r = requests.get(thread_server.url) + assert r.status_code == 200, "the threaded server is reachable" + assert 'id="react-entry-point"' in r.text, "the entrypoint is present" + + +def test_process_server_smoke(process_server): + process_server('tests.simple_app') + time.sleep(2.5) + r = requests.get(process_server.url) + assert r.status_code == 200, "the server is reachable" + assert 'id="react-entry-point"' in r.text, "the entrypoint is present" + # os.unsetenv('PYTHONPATH') From 791f03d581119c32db9b353a075fa2f937f14d83 Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 28 May 2019 22:32:22 -0400 Subject: [PATCH 08/46] :fire: :construction: add pythonpath --- .circleci/config.yml | 24 +++---- tests/__init__.py | 0 tests/{dash_apps => assets}/simple_app.py | 0 tests/dash_apps/__init__.py | 0 tests/package.json | 11 --- tests/simple_app.py | 38 ---------- tests/unit/development/test_base_component.py | 13 ++-- tests/unit/simple_app.py | 38 ---------- tests/unit/test_app_runners.py | 2 +- tests/utils.py | 72 ------------------- 10 files changed, 19 insertions(+), 179 deletions(-) delete mode 100644 tests/__init__.py rename tests/{dash_apps => assets}/simple_app.py (100%) delete mode 100644 tests/dash_apps/__init__.py delete mode 100644 tests/package.json delete mode 100644 tests/simple_app.py delete mode 100644 tests/unit/simple_app.py delete mode 100644 tests/utils.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 3700726468..e92e89e060 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,24 +39,24 @@ jobs: paths: - "venv" - # - run: - # name: 🌸 linting - # command: | - # . venv/bin/activate - # pip install -e . --quiet - # pip list | grep dash - # flake8 dash setup.py - # flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests - # pylint dash setup.py --rcfile=$PYLINTRC - # pylint tests/unit -d all -e C0410,C0411,C0412,C0413,W0109 - # cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test + - run: + name: 🌸 linting + command: | + . venv/bin/activate + pip install -e . --quiet + pip list | grep dash + flake8 dash setup.py + flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests + pylint dash setup.py --rcfile=$PYLINTRC + pylint tests/unit -d all -e C0410,C0411,C0412,C0413,W0109 + cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test - run: name: ⛑ Run unit tests command: | . venv/bin/activate mkdir test-reports - PYTHONPATH=~/dash pytest --junitxml=test-reports/junit.xml tests/unit + PYTHONPATH=~/dash/tests/assets pytest --junitxml=test-reports/junit.xml tests/unit - store_test_results: path: test-reports - store_artifacts: diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/dash_apps/simple_app.py b/tests/assets/simple_app.py similarity index 100% rename from tests/dash_apps/simple_app.py rename to tests/assets/simple_app.py diff --git a/tests/dash_apps/__init__.py b/tests/dash_apps/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/package.json b/tests/package.json deleted file mode 100644 index 48a500deba..0000000000 --- a/tests/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "dash_tests", - "version": "1.0.0", - "description": "Utilities to help with dash tests", - "main": "na", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "chris@plot.ly", - "license": "ISC" -} diff --git a/tests/simple_app.py b/tests/simple_app.py deleted file mode 100644 index 3e485c0890..0000000000 --- a/tests/simple_app.py +++ /dev/null @@ -1,38 +0,0 @@ -# pylint: disable=missing-docstring -import dash_core_components as dcc -import dash_html_components as html -import dash -from dash.dependencies import Output, Input -from dash.exceptions import PreventUpdate - - -app = dash.Dash(__name__) - -app.layout = html.Div( - [ - dcc.Input(id="value", placeholder="my-value"), - html.Div(["You entered: ", html.Span(id="out")]), - html.Button("style-btn", id="style-btn"), - html.Div("style-container", id="style-output"), - ] -) - - -@app.callback(Output("out", "children"), [Input("value", "value")]) -def on_value(value): - if value is None: - raise PreventUpdate - - return value - - -@app.callback(Output("style-output", "style"), [Input("style-btn", "n_clicks")]) -def on_style(value): - if value is None: - raise PreventUpdate - - return {"padding": "10px"} - - -if __name__ == "__main__": - app.run_server(debug=True, port=10850) diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py index 9d0242df04..1405947841 100644 --- a/tests/unit/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -48,7 +48,6 @@ def nested_tree(): return c, c1, c2, c3, c4, c5 -@pytest.mark.skip(reason='') class TestComponent(unittest.TestCase): def test_init(self): Component(a=3) @@ -499,7 +498,7 @@ def test_pop(self): class TestGenerateClassFile(unittest.TestCase): def setUp(self): json_path = os.path.join( - 'tests', 'unit', 'dash', 'development', 'metadata_test.json') + 'tests', 'unit', 'development', 'metadata_test.json') with open(json_path) as data_file: json_string = data_file.read() data = json\ @@ -539,7 +538,7 @@ def setUp(self): # The expected result for both class string and class file generation expected_string_path = os.path.join( - 'tests', 'unit', 'dash', 'development', 'metadata_test.py' + 'tests', 'unit', 'development', 'metadata_test.py' ) with open(expected_string_path, 'r') as f: self.expected_class_string = f.read() @@ -569,7 +568,7 @@ def test_class_file(self): class TestGenerateClass(unittest.TestCase): def setUp(self): path = os.path.join( - 'tests', 'unit', 'dash', 'development', 'metadata_test.json') + 'tests', 'unit', 'development', 'metadata_test.json') with open(path) as data_file: json_string = data_file.read() data = json\ @@ -585,7 +584,7 @@ def setUp(self): ) path = os.path.join( - 'tests', 'unit', 'dash', 'development', + 'tests', 'unit', 'development', 'metadata_required_test.json' ) with open(path) as data_file: @@ -758,7 +757,7 @@ def test_required_props(self): class TestMetaDataConversions(unittest.TestCase): def setUp(self): path = os.path.join( - 'tests', 'unit', 'dash', 'development', 'metadata_test.json') + 'tests', 'unit', 'development', 'metadata_test.json') with open(path) as data_file: json_string = data_file.read() data = json\ @@ -942,7 +941,7 @@ def assert_docstring(assertEqual, docstring): class TestFlowMetaDataConversions(unittest.TestCase): def setUp(self): path = os.path.join( - 'tests', 'unit', 'dash', 'development', 'flow_metadata_test.json') + 'tests', 'unit', 'development', 'flow_metadata_test.json') with open(path) as data_file: json_string = data_file.read() data = json\ diff --git a/tests/unit/simple_app.py b/tests/unit/simple_app.py deleted file mode 100644 index 3e485c0890..0000000000 --- a/tests/unit/simple_app.py +++ /dev/null @@ -1,38 +0,0 @@ -# pylint: disable=missing-docstring -import dash_core_components as dcc -import dash_html_components as html -import dash -from dash.dependencies import Output, Input -from dash.exceptions import PreventUpdate - - -app = dash.Dash(__name__) - -app.layout = html.Div( - [ - dcc.Input(id="value", placeholder="my-value"), - html.Div(["You entered: ", html.Span(id="out")]), - html.Button("style-btn", id="style-btn"), - html.Div("style-container", id="style-output"), - ] -) - - -@app.callback(Output("out", "children"), [Input("value", "value")]) -def on_value(value): - if value is None: - raise PreventUpdate - - return value - - -@app.callback(Output("style-output", "style"), [Input("style-btn", "n_clicks")]) -def on_style(value): - if value is None: - raise PreventUpdate - - return {"padding": "10px"} - - -if __name__ == "__main__": - app.run_server(debug=True, port=10850) diff --git a/tests/unit/test_app_runners.py b/tests/unit/test_app_runners.py index 8fa71e8dd9..54e7bdf588 100644 --- a/tests/unit/test_app_runners.py +++ b/tests/unit/test_app_runners.py @@ -22,7 +22,7 @@ def test_threaded_server_smoke(thread_server): def test_process_server_smoke(process_server): - process_server('tests.simple_app') + process_server('simple_app') time.sleep(2.5) r = requests.get(process_server.url) assert r.status_code == 200, "the server is reachable" diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index da35583492..0000000000 --- a/tests/utils.py +++ /dev/null @@ -1,72 +0,0 @@ -import time - - -TIMEOUT = 5 # 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=None, expected_value=None, - timeout=TIMEOUT, *args, **kwargs): - """ - Waits for condition_function to return truthy or raises WaitForTimeout. - :param (function) condition_function: Should return truthy or - expected_value on success. - :param (function) get_message: Optional failure message function - :param expected_value: Optional return value to wait for. If omitted, - success is any truthy value. - :param (float) timeout: max seconds to wait. Defaults to 5 - :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() - - start_time = time.time() - while time.time() < start_time + timeout: - condition_val = wrapped_condition_function() - if expected_value is None: - if condition_val: - return True - elif condition_val == expected_value: - return True - time.sleep(0.5) - - if get_message: - message = get_message() - elif expected_value: - message = 'Final value: {}'.format(condition_val) - else: - message = '' - - raise WaitForTimeout(message) From fc0d0ff6876b4a834be85f8a77d34f2896e82a5e Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 28 May 2019 22:37:23 -0400 Subject: [PATCH 09/46] :art: fix lint --- tests/unit/development/test_base_component.py | 1 - tests/unit/test_import.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py index 1405947841..6ac4cdefd6 100644 --- a/tests/unit/development/test_base_component.py +++ b/tests/unit/development/test_base_component.py @@ -6,7 +6,6 @@ import shutil import unittest import plotly -import pytest from dash.development.base_component import Component from dash.development._py_components_generation import generate_class_string, generate_class_file, generate_class, \ create_docstring, prohibit_events, js_to_py_type diff --git a/tests/unit/test_import.py b/tests/unit/test_import.py index d6e6acebb5..5448882471 100644 --- a/tests/unit/test_import.py +++ b/tests/unit/test_import.py @@ -11,4 +11,4 @@ def test_dash_import_is_correct(): assert ( getattr(imported, "Dash").__name__ == "Dash" - ), "access to main Dash class is valid" \ No newline at end of file + ), "access to main Dash class is valid" From 12b8c3906d77999176558719d0a4942565f75e81 Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 28 May 2019 22:56:21 -0400 Subject: [PATCH 10/46] :art: lint --- dash/testing/application_runners.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index bd9275eb36..b57960e071 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -188,10 +188,10 @@ def stop(self): if six.PY3: # pylint:disable=no-member _except = subprocess.TimeoutExpired - return self.proc.communicate(timeout=self.stop_timeout) + self.proc.communicate(timeout=self.stop_timeout) _except = OSError - return self.proc.communicate() + self.proc.communicate() except _except: logger.warning( @@ -200,4 +200,4 @@ def stop(self): self.stop_timeout, ) self.proc.kill() - return self.proc.communicate() + self.proc.communicate() From 3bd185b50a2514a50f4e5398a3d07ba4969ebd85 Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 28 May 2019 23:28:55 -0400 Subject: [PATCH 11/46] :bug: fix one logic error, skip unit to check intg --- dash/testing/application_runners.py | 9 ++++----- tests/unit/test_resources.py | 16 +++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index b57960e071..c3e57f7b09 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -183,16 +183,15 @@ def start(self, app_module, application_name="app", port=8050): def stop(self): if self.proc: - self.proc.terminate() try: + self.proc.terminate() if six.PY3: # pylint:disable=no-member _except = subprocess.TimeoutExpired self.proc.communicate(timeout=self.stop_timeout) - - _except = OSError - self.proc.communicate() - + else: + _except = OSError + self.proc.communicate() except _except: logger.warning( "subprocess terminate timeout %s reached, trying to kill " diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index fa2054e701..7fb92beaaa 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,6 +1,6 @@ import mock +import pytest import dash_core_components as dcc - import dash _monkey_patched_js_dist = [ @@ -34,9 +34,10 @@ class StatMock(object): st_mtime = 1 +@pytest.mark.skip("tmp") def test_external(mocker): mocker.patch("dash_core_components._js_dist") - dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, + dcc._js_dist = _monkey_patched_js_dist # noqa: W0212 dcc.__version__ = 1 app = dash.Dash( @@ -50,13 +51,14 @@ def test_external(mocker): app.scripts.get_all_scripts() ) - assert set(resource) == { + assert resource == [ "https://external_javascript.js", "https://external_css.css", "https://component_library.bundle.js", - } + ] +@pytest.mark.skip("tmp") def test_internal(mocker): mocker.patch("dash_core_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, @@ -70,18 +72,18 @@ def test_internal(mocker): assert app.scripts.config.serve_locally and app.css.config.serve_locally with mock.patch("dash.dash.os.stat", return_value=StatMock()): - with mock.patch("dash.dash.importlib.import_module", return_value=dcc): + with mock.patch("dash.importlib.import_module", return_value=dcc): resource = app._collect_and_register_resources( app.scripts.get_all_scripts() ) - assert set(resource) == { + assert resource == [ "/_dash-component-suites/" "dash_core_components/external_javascript.js?v=1&m=1", "/_dash-component-suites/" "dash_core_components/external_css.css?v=1&m=1", "/_dash-component-suites/" "dash_core_components/fake_dcc.js?v=1&m=1", - } + ] assert ( "fake_dcc.min.js.map" in app.registered_paths["dash_core_components"] From 5361cc56681e92e3e22ec7838903b33a347de3d2 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 29 May 2019 10:51:39 -0400 Subject: [PATCH 12/46] :bug: mock also html --- tests/unit/test_resources.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py index 7fb92beaaa..1aaf496eb8 100644 --- a/tests/unit/test_resources.py +++ b/tests/unit/test_resources.py @@ -1,5 +1,4 @@ import mock -import pytest import dash_core_components as dcc import dash @@ -34,9 +33,9 @@ class StatMock(object): st_mtime = 1 -@pytest.mark.skip("tmp") def test_external(mocker): mocker.patch("dash_core_components._js_dist") + mocker.patch("dash_html_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212 dcc.__version__ = 1 @@ -46,10 +45,9 @@ def test_external(mocker): app.layout = dcc.Markdown() app.scripts.config.serve_locally = False - with mock.patch("dash.dash.os.stat", return_value=StatMock()): - resource = app._collect_and_register_resources( - app.scripts.get_all_scripts() - ) + resource = app._collect_and_register_resources( + app.scripts.get_all_scripts() + ) assert resource == [ "https://external_javascript.js", @@ -58,9 +56,9 @@ def test_external(mocker): ] -@pytest.mark.skip("tmp") def test_internal(mocker): mocker.patch("dash_core_components._js_dist") + mocker.patch("dash_html_components._js_dist") dcc._js_dist = _monkey_patched_js_dist # noqa: W0212, dcc.__version__ = 1 @@ -72,7 +70,7 @@ def test_internal(mocker): assert app.scripts.config.serve_locally and app.css.config.serve_locally with mock.patch("dash.dash.os.stat", return_value=StatMock()): - with mock.patch("dash.importlib.import_module", return_value=dcc): + with mock.patch("dash.dash.importlib.import_module", return_value=dcc): resource = app._collect_and_register_resources( app.scripts.get_all_scripts() ) From 6efa443fb541fcdf4d291cb57c470365ca3f1492 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 29 May 2019 10:53:54 -0400 Subject: [PATCH 13/46] :construction: skip test_assets until fixture for selenium is ready --- tests/integration/dash_assets/test_assets.py | 22 +++++++++++--------- tests/integration/test_integration.py | 1 - 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/integration/dash_assets/test_assets.py b/tests/integration/dash_assets/test_assets.py index 894c7d1395..dcb4aaa321 100644 --- a/tests/integration/dash_assets/test_assets.py +++ b/tests/integration/dash_assets/test_assets.py @@ -6,19 +6,21 @@ import dash_core_components as dcc from dash import Dash -from tests.integration.IntegrationTests import IntegrationTests -from tests.integration.utils import wait_for, invincible +# from IntegrationTests import IntegrationTests +# from integration.utils import wait_for, invincible +import pytest -class TestAssets(IntegrationTests): +@pytest.mark.skip("rewrite with fixture can solve the import issue") +class TestAssets(): - def setUp(self): - 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 setUp(self): + # 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 test_assets(self): app = Dash(__name__, assets_ignore='.*ignored.*') diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index bfcc1e5b2e..4e6f57bb8d 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1,4 +1,3 @@ -import json from multiprocessing import Value import datetime import itertools From ae8fc18afeb8f33ecc7c522fc6aa5d8238761017 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 29 May 2019 11:13:13 -0400 Subject: [PATCH 14/46] :lipstick: polish the app_runner --- dash/testing/application_runners.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index c3e57f7b09..041554d7a6 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -46,6 +46,7 @@ def import_app(app_file, application_name="app"): app_module = runpy.run_module(app_file) app = app_module[application_name] except KeyError: + logger.exception("the app name cannot be found") raise NoAppFoundError( "No dash `app` instance was found in {}".format(app_file) ) @@ -138,6 +139,7 @@ def run(): try: self.thread.start() except RuntimeError: # multiple call on same thread + logger.exception("threaded server failed to start") self.started = False self.started = self.thread.is_alive() @@ -170,13 +172,13 @@ def start(self, app_module, application_name="app", port=8050): posix=sys.platform != "win32", ) logger.debug("start dash process with %s", args) + try: - # print('start ......') self.proc = subprocess.Popen( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) except (OSError, ValueError): - logger.exception("subprocess error") + logger.exception("process server has encountered an error") self.started = False self.started = True @@ -193,10 +195,9 @@ def stop(self): _except = OSError self.proc.communicate() except _except: - logger.warning( - "subprocess terminate timeout %s reached, trying to kill " - "the subprocess in a safe manner", - self.stop_timeout, + logger.exception( + "subprocess terminate not success, trying to kill " + "the subprocess in a safe manner" ) self.proc.kill() self.proc.communicate() From 8282fc5bc9feaa8b3fa03a4dc52fbccc1ec19f4a Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 29 May 2019 12:14:54 -0400 Subject: [PATCH 15/46] :alembic: improvement --- dash/testing/application_runners.py | 4 ++-- tests/assets/__init__.py | 0 tests/integration/test_render.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 tests/assets/__init__.py diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 041554d7a6..e34be900ef 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -98,7 +98,7 @@ class ThreadedRunner(BaseDashRunner): this is the default flavor to use in dash integration tests """ - def __init__(self, keep_open=False, stop_timeout=1): + def __init__(self, keep_open=False, stop_timeout=3): super(ThreadedRunner, self).__init__( keep_open=keep_open, stop_timeout=stop_timeout ) @@ -155,7 +155,7 @@ class ProcessRunner(BaseDashRunner): this flavor is closer to production environment but slower """ - def __init__(self, keep_open=False, stop_timeout=1): + def __init__(self, keep_open=False, stop_timeout=3): super(ProcessRunner, self).__init__( keep_open=keep_open, stop_timeout=stop_timeout ) diff --git a/tests/assets/__init__.py b/tests/assets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index d087ed12cd..21e188d1ea 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -2,9 +2,10 @@ import os import textwrap +import pytest import dash from dash import Dash -from dash.dependencies import Input, Output, State, ClientsideFunction +from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate from dash.development.base_component import Component import dash_html_components as html @@ -22,8 +23,6 @@ from .utils import wait_for from multiprocessing import Value import time -import re -import itertools import json import string import plotly @@ -933,6 +932,7 @@ def update_output(input, n_clicks, state): output().text, 'input="Initial Inputx", state="Initial Statex"') + @pytest.mark.flakey def test_state_and_inputs(self): app = Dash(__name__) app.layout = html.Div([ @@ -956,6 +956,7 @@ def update_output(input, state): state = lambda: self.driver.find_element_by_id('state') # callback gets called with initial input + time.sleep(0.5) self.assertEqual( output().text, 'input="Initial Input", state="Initial State"' From 30dc9433332efd677cd7ffa763c9cdca162fd385 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 29 May 2019 12:34:00 -0400 Subject: [PATCH 16/46] :alembic: more delete --- dash/testing/application_runners.py | 1 + tests/assets/__init__.py | 0 tests/unit/__init__.py | 0 3 files changed, 1 insertion(+) delete mode 100644 tests/assets/__init__.py delete mode 100644 tests/unit/__init__.py diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index e34be900ef..62547cfc2a 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -180,6 +180,7 @@ def start(self, app_module, application_name="app", port=8050): except (OSError, ValueError): logger.exception("process server has encountered an error") self.started = False + return self.started = True diff --git a/tests/assets/__init__.py b/tests/assets/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 31a9f9369352267eac681bc10a9feff1cfd06c29 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 29 May 2019 14:14:23 -0400 Subject: [PATCH 17/46] :fire: skip the process test for python2 now --- tests/unit/test_app_runners.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_app_runners.py b/tests/unit/test_app_runners.py index 54e7bdf588..64fb069047 100644 --- a/tests/unit/test_app_runners.py +++ b/tests/unit/test_app_runners.py @@ -1,5 +1,7 @@ import time +import sys import requests +import pytest import dash_html_components as html import dash @@ -21,10 +23,12 @@ def test_threaded_server_smoke(thread_server): assert 'id="react-entry-point"' in r.text, "the entrypoint is present" +@pytest.mark.skipif( + sys.version_info < (3,), reason="requires python3 for process testing" +) def test_process_server_smoke(process_server): - process_server('simple_app') + process_server("simple_app") time.sleep(2.5) r = requests.get(process_server.url) assert r.status_code == 200, "the server is reachable" assert 'id="react-entry-point"' in r.text, "the entrypoint is present" - # os.unsetenv('PYTHONPATH') From 98249cf92d623d657d7d72a2be18db4f9d4cecb0 Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 31 May 2019 14:54:25 -0400 Subject: [PATCH 18/46] :tada: browser class for wd fixtures --- dash/testing/browser.py | 194 +++++++++++++++++++++++++++++ dash/testing/plugin.py | 52 +++----- dash/testing/wait.py | 14 +++ pytest.ini | 7 +- tests/integration/test_devtools.py | 6 + 5 files changed, 232 insertions(+), 41 deletions(-) create mode 100644 dash/testing/browser.py diff --git a/dash/testing/browser.py b/dash/testing/browser.py new file mode 100644 index 0000000000..44ba17ccc8 --- /dev/null +++ b/dash/testing/browser.py @@ -0,0 +1,194 @@ +import os +import sys +import logging +import percy + +from selenium import webdriver +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.common.exceptions import WebDriverException, TimeoutException + +from dash.testing.wait import text_to_equal +from dash.exceptions import DashAppLoadingError + +logger = logging.getLogger(__name__) + + +class Browser: + def __init__(self, browser, remote=None, wait_timeout=10): + self._browser = browser.lower() + self._wait_timeout = wait_timeout + + self._driver = self.get_webdriver(remote) + self._driver.implicitly_wait(2) + + self._wd_wait = WebDriverWait( + driver=self.driver, timeout=wait_timeout, poll_frequency=0.2 + ) + self._last_ts = 0 + self._url = None + + self.percy_runner = percy.Runner( + loader=percy.ResourceLoader( + webdriver=self.driver, + base_url="/assets", + root_dir="tests/assets", + ) + ) + self.percy_runner.initialize_build() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, traceback): + try: + self.driver.quit() + self.percy_runner.finalize_build() + except WebDriverException: + logger.exception("webdriver quit was not successfully") + except percy.errors.Error: + logger.exception("percy runner failed to finalize properly") + + def percy_snapshot(self, name=""): + snapshot_name = "{} - py{}.{}".format( + name, sys.version_info.major, sys.version_info.minor + ) + logger.info("taking snapshot name => {}".format(snapshot_name)) + self.percy_runner.snapshot(name=snapshot_name) + + def _wait_for(self, method, args, timeout, msg): + """abstract generic pattern for explicit webdriver wait""" + _wait = ( + self._wd_wait + if timeout is None + else WebDriverWait(self.driver, timeout) + ) + return _wait.until(method(*args), msg) + + # keep these two wait_for API for easy migration + def wait_for_element_by_css_selector(self, selector, timeout=None): + return self._wait_for( + EC.presence_of_element_located, + ((By.CSS_SELECTOR, selector),), + timeout, + "cannot find_element using the css selector", + ) + + def wait_for_text_to_equal(self, selector, text, timeout=None): + return self._wait_for( + text_to_equal, + (selector, text), + timeout, + "cannot wait until element contains expected text {}".format(text), + ) + + def wait_until_server_is_ready(self, timeout=10): + + self.driver.get(self.server_url) + try: + self.wait_for_element_by_css_selector( + "#react-entry-point", timeout=timeout + ) + except TimeoutException: + logger.exception( + "dash server is not loaded within {} seconds".format(timeout) + ) + raise DashAppLoadingError( + "the expected Dash react entry point cannot be loaded" + " in browser\n HTML => {}\n Console Logs => {}\n".format( + self.driver.find_element_by_tag_name("body").get_property( + "innerHTML" + ), + "\n".join(self.get_logs()), + ) + ) + + def get_webdriver(self, remote): + return ( + getattr(self, "_get_{}".format(self._browser))() + if remote is None + else webdriver.Remote( + command_executor=remote, + desired_capabilities=getattr( + DesiredCapabilities, self._browser.upper() + ), + ) + ) + + def _get_chrome(self): + options = Options() + options.add_argument("--no-sandbox") + + capabilities = DesiredCapabilities.CHROME + capabilities["loggingPrefs"] = {"browser": "SEVERE"} + + if "DASH_TEST_CHROMEPATH" in os.environ: + options.binary_location = os.environ["DASH_TEST_CHROMEPATH"] + + chrome = webdriver.Chrome( + options=options, desired_capabilities=capabilities + ) + chrome.set_window_position(0, 0) + return chrome + + def _get_firefox(self): + + capabilities = DesiredCapabilities.FIREFOX + capabilities["loggingPrefs"] = {"browser": "SEVERE"} + capabilities["marionette"] = True + + # https://developer.mozilla.org/en-US/docs/Download_Manager_preferences + fp = webdriver.FirefoxProfile() + + # this will be useful if we wanna test download csv or other data + # files with selenium + # TODO this can be fed as a tmpdir fixture from pytest + fp.set_preference("browser.download.dir", "/tmp") + fp.set_preference("browser.download.folderList", 2) + fp.set_preference("browser.download.manager.showWhenStarting", False) + + return webdriver.Firefox(fp, capabilities=capabilities) + + def get_logs(self): + """get_logs works only with chrome webdriver""" + return ( + [ + entry + for entry in self.driver.get_log("browser") + if entry["timestamp"] > self._last_ts + ] + if self.driver.name == "chrome" + else [] + ) + + def reset_log_timestamp(self): + '''reset_log_timestamp only work with chrome webdrier''' + if self.driver.name == "chrome": + entries = self.driver.get_log("browser") + if entries: + self._last_ts = entries[-1]["timestamp"] + + @property + def driver(self): + return self._driver + + @property + def session_id(self): + return self.driver.session_id + + @property + def server_url(self): + return self._url + + @server_url.setter + def server_url(self, value): + """property setter for server_url + + Note: set server_url will implicitly check the server is ready + for selenium testing + """ + self._url = value + self.wait_until_server_is_ready() diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index eadbcbb69f..e881e9f160 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -4,6 +4,7 @@ from selenium import webdriver from dash.testing.application_runners import ThreadedRunner, ProcessRunner +from dash.testing.browser import Browser WEBDRIVERS = { "Chrome": webdriver.Chrome, @@ -12,27 +13,20 @@ } -def _get_config(config, key, default=None): - opt = config.getoption(key) - ini = config.getini(key) - return opt or ini or default - - -############################################################################### -# Plugin hooks. -############################################################################### - # pylint: disable=missing-docstring def pytest_addoption(parser): + # Add options to the pytest parser, either on the commandline or ini # TODO add more options for the selenium driver. dash = parser.getgroup("Dash", "Dash Integration Tests") - help_msg = "Name of the selenium driver to use" + dash.addoption( - "--webdriver", choices=tuple(WEBDRIVERS.keys()), help=help_msg + "-w", + "--webdriver", + choices=tuple(WEBDRIVERS.keys()), + default="Chrome", + help="Name of the selenium driver to use", ) - parser.addini("webdriver", help=help_msg) - ############################################################################### # Fixtures @@ -41,33 +35,19 @@ def pytest_addoption(parser): @pytest.fixture def thread_server(): - """ - Start a local dash server in a new thread. Stop the server in teardown. - :Example: - .. code-block:: python - import dash - import dash_html_components as html - def test_application(dash_threaded): - app = dash.Dash(__name__) - app.layout = html.Div('My app) - dash_threaded(app) - .. seealso:: :py:class:`pytest_dash.application_runners.DashThreaded` - """ - + """Start a local dash server in a new thread""" with ThreadedRunner() as starter: yield starter @pytest.fixture def process_server(): - """ - Start a Dash server with subprocess.Popen and waitress-serve. - :Example: - .. code-block:: python - def test_application(dash_subprocess): - # consider the application file is named `app.py` - dash_subprocess('app') - .. seealso:: :py:class:`pytest_dash.application_runners.DashSubprocess` - """ + """Start a Dash server with subprocess.Popen and waitress-serve""" with ProcessRunner() as starter: yield starter + + +@pytest.fixture +def br(request): + with Browser(request.config.getoption("webdriver")) as browser: + yield browser diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 6306d2a2f8..df7d87184c 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -24,3 +24,17 @@ def until_not( time.sleep(poll) if time.time() > end_time: raise TestingTimeoutError(msg) + + +class text_to_equal(object): + def __init__(self, selector, text): + self.selector = selector + self.text = text + + def __call__(self, driver): + elem = driver.find_element_by_css_selector(self.selector) + return ( + str(elem.text) == self.text + or str(elem.get_attribute("value")) == self.text + ) + diff --git a/pytest.ini b/pytest.ini index bfada58fd9..c26b36e040 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,8 +2,5 @@ testpaths = tests/ addopts = -rsxX -vv log_cli=true -log_cli_level = DEBUG -webdriver = Chrome - - - +log_format = %(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s +log_cli_level = ERROR diff --git a/tests/integration/test_devtools.py b/tests/integration/test_devtools.py index bc49127d66..a9c9ce4e9f 100644 --- a/tests/integration/test_devtools.py +++ b/tests/integration/test_devtools.py @@ -34,6 +34,12 @@ TIMEOUT = 20 +def test_wdr001_simple_br_dash_docs(br): + br.server_url = 'https://dash.plot.ly/' + br.wait_for_element_by_css_selector('#wait-for-layout') + assert not br.get_logs(), "no console errors" + + @pytest.mark.skip( reason="flakey with circleci, will readdressing after pytest fixture") class Tests(IntegrationTests): From e17a0c0b2655a5a6d2feb13dae6843c81c4588c6 Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 31 May 2019 14:59:15 -0400 Subject: [PATCH 19/46] :art: fix lint --- dash/testing/wait.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dash/testing/wait.py b/dash/testing/wait.py index df7d87184c..bf91917ccf 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -37,4 +37,3 @@ def __call__(self, driver): str(elem.text) == self.text or str(elem.get_attribute("value")) == self.text ) - From 7ffd6ef433becd052dee1a189ca12d02f30f6fbc Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 31 May 2019 14:59:15 -0400 Subject: [PATCH 20/46] :art: fix lint --- dash/testing/browser.py | 13 ++++++++----- dash/testing/plugin.py | 3 --- dash/testing/wait.py | 3 +-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 44ba17ccc8..8aac1b0c91 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -1,3 +1,4 @@ +# pylint: disable=missing-docstring import os import sys import logging @@ -56,7 +57,7 @@ def percy_snapshot(self, name=""): snapshot_name = "{} - py{}.{}".format( name, sys.version_info.major, sys.version_info.minor ) - logger.info("taking snapshot name => {}".format(snapshot_name)) + logger.info("taking snapshot name => %s", snapshot_name) self.percy_runner.snapshot(name=snapshot_name) def _wait_for(self, method, args, timeout, msg): @@ -94,7 +95,7 @@ def wait_until_server_is_ready(self, timeout=10): ) except TimeoutException: logger.exception( - "dash server is not loaded within {} seconds".format(timeout) + "dash server is not loaded within %s seconds", timeout ) raise DashAppLoadingError( "the expected Dash react entry point cannot be loaded" @@ -118,7 +119,8 @@ def get_webdriver(self, remote): ) ) - def _get_chrome(self): + @staticmethod + def _get_chrome(): options = Options() options.add_argument("--no-sandbox") @@ -134,7 +136,8 @@ def _get_chrome(self): chrome.set_window_position(0, 0) return chrome - def _get_firefox(self): + @staticmethod + def _get_firefox(): capabilities = DesiredCapabilities.FIREFOX capabilities["loggingPrefs"] = {"browser": "SEVERE"} @@ -165,7 +168,7 @@ def get_logs(self): ) def reset_log_timestamp(self): - '''reset_log_timestamp only work with chrome webdrier''' + """reset_log_timestamp only work with chrome webdrier""" if self.driver.name == "chrome": entries = self.driver.get_log("browser") if entries: diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index e881e9f160..060435c495 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -13,15 +13,12 @@ } -# pylint: disable=missing-docstring def pytest_addoption(parser): - # Add options to the pytest parser, either on the commandline or ini # TODO add more options for the selenium driver. dash = parser.getgroup("Dash", "Dash Integration Tests") dash.addoption( - "-w", "--webdriver", choices=tuple(WEBDRIVERS.keys()), default="Chrome", diff --git a/dash/testing/wait.py b/dash/testing/wait.py index df7d87184c..1c6da05bce 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -26,7 +26,7 @@ def until_not( raise TestingTimeoutError(msg) -class text_to_equal(object): +class text_to_equal(object): # pylint: disable=too-few-public-methods def __init__(self, selector, text): self.selector = selector self.text = text @@ -37,4 +37,3 @@ def __call__(self, driver): str(elem.text) == self.text or str(elem.get_attribute("value")) == self.text ) - From 4d5e7f00c57971ad3c334a39b295032b87edb5ed Mon Sep 17 00:00:00 2001 From: byron Date: Sun, 2 Jun 2019 23:29:34 -0400 Subject: [PATCH 21/46] :ok_hand: based on feedbacks --- .pylintrc | 2 +- .pylintrc37 | 3 +-- dash/exceptions.py | 24 ----------------------- dash/testing/application_runners.py | 3 ++- dash/testing/browser.py | 30 +++++++++++++++++------------ dash/testing/errors.py | 22 +++++++++++++++++++++ dash/testing/wait.py | 6 +++--- 7 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 dash/testing/errors.py diff --git a/.pylintrc b/.pylintrc index bde8d00a15..766a5f6d1c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -61,7 +61,7 @@ disable=fixme, old-style-class, superfluous-parens, bad-continuation, - unexpected-keyword-arg + # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/.pylintrc37 b/.pylintrc37 index 30691595f4..533bbade91 100644 --- a/.pylintrc37 +++ b/.pylintrc37 @@ -148,8 +148,7 @@ disable=invalid-name, possibly-unused-variable, too-many-lines, too-many-statements, - bad-continuation, - unexpected-keyword-arg + bad-continuation # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/dash/exceptions.py b/dash/exceptions.py index 9af4a35f24..0b02966209 100644 --- a/dash/exceptions.py +++ b/dash/exceptions.py @@ -94,27 +94,3 @@ class SameInputOutputException(CallbackException): class MissingCallbackContextException(CallbackException): pass - - -class DashTestingError(Exception): - """Base error for pytest-dash.""" - - -class InvalidDriverError(DashTestingError): - """An invalid selenium driver was specified.""" - - -class NoAppFoundError(DashTestingError): - """No `app` was found in the file.""" - - -class DashAppLoadingError(DashTestingError): - """The dash app failed to load""" - - -class ServerCloseError(DashTestingError): - """The server cannot be closed""" - - -class TestingTimeoutError(DashTestingError): - """"all timeout error about dash testing""" diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 62547cfc2a..bd7a022e1e 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -12,7 +12,7 @@ import flask import requests -from dash.exceptions import ( +from dash.testing.errors import ( NoAppFoundError, TestingTimeoutError, ServerCloseError, @@ -191,6 +191,7 @@ def stop(self): if six.PY3: # pylint:disable=no-member _except = subprocess.TimeoutExpired + # pylint: disable=unexpected-keyword-arg self.proc.communicate(timeout=self.stop_timeout) else: _except = OSError diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 8aac1b0c91..08c73feb09 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -2,6 +2,7 @@ import os import sys import logging +import warnings import percy from selenium import webdriver @@ -13,7 +14,7 @@ from selenium.common.exceptions import WebDriverException, TimeoutException from dash.testing.wait import text_to_equal -from dash.exceptions import DashAppLoadingError +from dash.testing.errors import DashAppLoadingError logger = logging.getLogger(__name__) @@ -69,6 +70,9 @@ def _wait_for(self, method, args, timeout, msg): ) return _wait.until(method(*args), msg) + def wait_for_element(self, css_selector, timeout=None): + self.wait_for_element_by_css_selector(css_selector, timeout) + # keep these two wait_for API for easy migration def wait_for_element_by_css_selector(self, selector, timeout=None): return self._wait_for( @@ -86,7 +90,7 @@ def wait_for_text_to_equal(self, selector, text, timeout=None): "cannot wait until element contains expected text {}".format(text), ) - def wait_until_server_is_ready(self, timeout=10): + def wait_for_page(self, timeout=10): self.driver.get(self.server_url) try: @@ -157,15 +161,17 @@ def _get_firefox(): def get_logs(self): """get_logs works only with chrome webdriver""" - return ( - [ - entry - for entry in self.driver.get_log("browser") - if entry["timestamp"] > self._last_ts - ] - if self.driver.name == "chrome" - else [] - ) + if self.driver.name == 'Chrome': + return ( + [ + entry + for entry in self.driver.get_log("browser") + if entry["timestamp"] > self._last_ts + ] + ) + else: + warnings.warn("get_logs always return None with your webdriver") + return None def reset_log_timestamp(self): """reset_log_timestamp only work with chrome webdrier""" @@ -194,4 +200,4 @@ def server_url(self, value): for selenium testing """ self._url = value - self.wait_until_server_is_ready() + self.wait_for_page() diff --git a/dash/testing/errors.py b/dash/testing/errors.py new file mode 100644 index 0000000000..9de48e30eb --- /dev/null +++ b/dash/testing/errors.py @@ -0,0 +1,22 @@ +class DashTestingError(Exception): + """Base error for pytest-dash.""" + + +class InvalidDriverError(DashTestingError): + """An invalid selenium driver was specified.""" + + +class NoAppFoundError(DashTestingError): + """No `app` was found in the file.""" + + +class DashAppLoadingError(DashTestingError): + """The dash app failed to load""" + + +class ServerCloseError(DashTestingError): + """The server cannot be closed""" + + +class TestingTimeoutError(DashTestingError): + """"all timeout error about dash testing""" diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 1c6da05bce..6aae737c4f 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -1,6 +1,6 @@ """Utils methods for pytest-dash such wait_for wrappers""" import time -from dash.exceptions import TestingTimeoutError +from dash.testing.errors import TestingTimeoutError def until( @@ -11,9 +11,9 @@ def until( ): # noqa: C0330 end_time = time.time() + timeout while wait_cond(): - time.sleep(poll) if time.time() > end_time: raise TestingTimeoutError(msg) + time.sleep(poll) def until_not( @@ -21,9 +21,9 @@ def until_not( ): # noqa: C0330 end_time = time.time() + timeout while not wait_cond(): - time.sleep(poll) if time.time() > end_time: raise TestingTimeoutError(msg) + time.sleep(poll) class text_to_equal(object): # pylint: disable=too-few-public-methods From b89219c873e61e8c95f81102c06b1567f250d901 Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 00:03:30 -0400 Subject: [PATCH 22/46] :art: fix lint --- dash/testing/browser.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 08c73feb09..d24225f271 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -169,9 +169,11 @@ def get_logs(self): if entry["timestamp"] > self._last_ts ] ) - else: - warnings.warn("get_logs always return None with your webdriver") - return None + warnings.warn( + "get_logs always return None with your webdriver {}".format( + self.driver.name + )) + return None def reset_log_timestamp(self): """reset_log_timestamp only work with chrome webdrier""" From 545a02ca23b954a8ecf22007634ec7a39239199f Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 14:59:12 -0400 Subject: [PATCH 23/46] :bug: fix logic error in wait --- dash/testing/wait.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 6aae737c4f..0a0d9a2593 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -1,8 +1,12 @@ """Utils methods for pytest-dash such wait_for wrappers""" import time +import logging from dash.testing.errors import TestingTimeoutError +logger = logging.getLogger(__name__) + + def until( wait_cond, timeout, @@ -10,7 +14,7 @@ def until( msg="expected condition not met within timeout", ): # noqa: C0330 end_time = time.time() + timeout - while wait_cond(): + while not wait_cond(): if time.time() > end_time: raise TestingTimeoutError(msg) time.sleep(poll) @@ -20,7 +24,7 @@ def until_not( wait_cond, timeout, poll=0.1, msg="expected condition met within timeout" ): # noqa: C0330 end_time = time.time() + timeout - while not wait_cond(): + while wait_cond(): if time.time() > end_time: raise TestingTimeoutError(msg) time.sleep(poll) From e6c545ff597e58e1bd111403395f80e0f8aaa7d8 Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 15:01:16 -0400 Subject: [PATCH 24/46] :sparkles: improve apis --- dash/testing/application_runners.py | 13 ++++++++++- dash/testing/browser.py | 36 +++++++++++++++++------------ dash/testing/composite.py | 17 ++++++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 dash/testing/composite.py diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index bd7a022e1e..9085c8ec3d 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -131,11 +131,12 @@ def run(): app.css.config.serve_locally = True if "port" not in kwargs: kwargs["port"] = self.port + else: + self.port = kwargs["port"] app.run_server(threaded=True, **kwargs) self.thread = threading.Thread(target=run) self.thread.daemon = True - try: self.thread.start() except RuntimeError: # multiple call on same thread @@ -144,6 +145,16 @@ def run(): self.started = self.thread.is_alive() + def accessible(): + try: + requests.get(self.url) + except requests.exceptions.RequestException: + return False + return True + + # wait until server is able to answer http request + wait.until(accessible, timeout=1) + def stop(self): requests.get("{}{}".format(self.url, self.stop_route)) wait.until_not(self.thread.is_alive, self.stop_timeout) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index d24225f271..b31d059cb4 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) -class Browser: +class Browser(object): def __init__(self, browser, remote=None, wait_timeout=10): self._browser = browser.lower() self._wait_timeout = wait_timeout @@ -70,8 +70,16 @@ def _wait_for(self, method, args, timeout, msg): ) return _wait.until(method(*args), msg) + def find_element(self, css_selector): + """wrapper for find_element_by_css_selector from driver""" + return self.driver.find_element_by_css_selector(css_selector) + + def find_elements(self, css_selector): + """wrapper for find_elements_by_css_selector from driver""" + return self.driver.find_elements_by_css_selector(css_selector) + def wait_for_element(self, css_selector, timeout=None): - self.wait_for_element_by_css_selector(css_selector, timeout) + return self.wait_for_element_by_css_selector(css_selector, timeout) # keep these two wait_for API for easy migration def wait_for_element_by_css_selector(self, selector, timeout=None): @@ -101,13 +109,14 @@ def wait_for_page(self, timeout=10): logger.exception( "dash server is not loaded within %s seconds", timeout ) + logger.debug(self.get_logs()) raise DashAppLoadingError( "the expected Dash react entry point cannot be loaded" " in browser\n HTML => {}\n Console Logs => {}\n".format( self.driver.find_element_by_tag_name("body").get_property( "innerHTML" ), - "\n".join(self.get_logs()), + "\n".join([]), ) ) @@ -161,23 +170,20 @@ def _get_firefox(): def get_logs(self): """get_logs works only with chrome webdriver""" - if self.driver.name == 'Chrome': - return ( - [ - entry - for entry in self.driver.get_log("browser") - if entry["timestamp"] > self._last_ts - ] - ) + if self.driver.name.lower() == "chrome": + return [ + entry + for entry in self.driver.get_log("browser") + if entry["timestamp"] > self._last_ts + ] warnings.warn( - "get_logs always return None with your webdriver {}".format( - self.driver.name - )) + "get_logs always return None with webdrivers other than Chrome" + ) return None def reset_log_timestamp(self): """reset_log_timestamp only work with chrome webdrier""" - if self.driver.name == "chrome": + if self.driver.name.lower() == "chrome": entries = self.driver.get_log("browser") if entries: self._last_ts = entries[-1]["timestamp"] diff --git a/dash/testing/composite.py b/dash/testing/composite.py new file mode 100644 index 0000000000..1e3f781d34 --- /dev/null +++ b/dash/testing/composite.py @@ -0,0 +1,17 @@ +from dash.testing.browser import Browser + + +class DashComposite(Browser): + + def __init__(self, server, browser, remote=None, wait_timeout=10): + super(DashComposite, self).__init__(browser, remote, wait_timeout) + self.server = server + + def start_app_server(self, app, **kwargs): + '''start the local server with app''' + + # start server with app and pass Dash arguments + self.server(app, **kwargs) + + # set the default server_url, it implicitly call wait_for_page + self.server_url = self.server.url From 0d6b1e263b4aacf6f4719b10fc3cff694e4b4abc Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 15:02:23 -0400 Subject: [PATCH 25/46] :truck: make all fixtures with dash_ prefix, so it has less chance to have name collision --- dash/testing/plugin.py | 16 +++++++++++++--- tests/unit/test_app_runners.py | 13 ++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 060435c495..7e7c003995 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -5,6 +5,7 @@ from dash.testing.application_runners import ThreadedRunner, ProcessRunner from dash.testing.browser import Browser +from dash.testing.composite import DashComposite WEBDRIVERS = { "Chrome": webdriver.Chrome, @@ -25,26 +26,35 @@ def pytest_addoption(parser): help="Name of the selenium driver to use", ) + ############################################################################### # Fixtures ############################################################################### @pytest.fixture -def thread_server(): +def dash_thread_server(): """Start a local dash server in a new thread""" with ThreadedRunner() as starter: yield starter @pytest.fixture -def process_server(): +def dash_process_server(): """Start a Dash server with subprocess.Popen and waitress-serve""" with ProcessRunner() as starter: yield starter @pytest.fixture -def br(request): +def dash_br(request): with Browser(request.config.getoption("webdriver")) as browser: yield browser + + +@pytest.fixture +def dash_duo(request, dash_thread_server): + with DashComposite( + dash_thread_server, request.config.getoption("webdriver") + ) as dc: + yield dc diff --git a/tests/unit/test_app_runners.py b/tests/unit/test_app_runners.py index 64fb069047..4bd4ed43d1 100644 --- a/tests/unit/test_app_runners.py +++ b/tests/unit/test_app_runners.py @@ -7,7 +7,7 @@ import dash -def test_threaded_server_smoke(thread_server): +def test_threaded_server_smoke(dash_thread_server): app = dash.Dash(__name__) app.layout = html.Div( @@ -16,9 +16,8 @@ def test_threaded_server_smoke(thread_server): html.Div(id="output", children="hello thread"), ] ) - thread_server(app, debug=True, use_reloader=False, use_debugger=True) - time.sleep(0.2) - r = requests.get(thread_server.url) + dash_thread_server(app, debug=True, use_reloader=False, use_debugger=True) + r = requests.get(dash_thread_server.url) assert r.status_code == 200, "the threaded server is reachable" assert 'id="react-entry-point"' in r.text, "the entrypoint is present" @@ -26,9 +25,9 @@ def test_threaded_server_smoke(thread_server): @pytest.mark.skipif( sys.version_info < (3,), reason="requires python3 for process testing" ) -def test_process_server_smoke(process_server): - process_server("simple_app") +def test_process_server_smoke(dash_process_server): + dash_process_server("simple_app") time.sleep(2.5) - r = requests.get(process_server.url) + r = requests.get(dash_process_server.url) assert r.status_code == 200, "the server is reachable" assert 'id="react-entry-point"' in r.text, "the entrypoint is present" From 4005368bbc21beaa07a6f2c369437747831b49c7 Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 15:04:36 -0400 Subject: [PATCH 26/46] :white_check_mark: refactoring on devtools with new fixtures --- .circleci/config.yml | 2 +- .../devtools/test_devtools_error_handling.py | 202 +++++ .../integration/devtools/test_devtools_ui.py | 64 ++ .../integration/devtools/test_props_check.py | 243 ++++++ tests/integration/test_devtools.py | 695 ------------------ 5 files changed, 510 insertions(+), 696 deletions(-) create mode 100644 tests/integration/devtools/test_devtools_error_handling.py create mode 100644 tests/integration/devtools/test_devtools_ui.py create mode 100644 tests/integration/devtools/test_props_check.py delete mode 100644 tests/integration/test_devtools.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 9bcd58559d..029fc206fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,7 +49,7 @@ jobs: flake8 dash setup.py flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests pylint dash setup.py --rcfile=$PYLINTRC - pylint tests/unit -d all -e C0410,C0411,C0412,C0413,W0109 + pylint tests/unit tests/integration/devtools -d all -e C0410,C0411,C0412,C0413,W0109 cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test - run: diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py new file mode 100644 index 0000000000..206767ae5b --- /dev/null +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -0,0 +1,202 @@ +# -*- coding: UTF-8 -*- +import dash_html_components as html +import dash_core_components as dcc + +import dash +from dash.dependencies import Input, Output +from dash.exceptions import PreventUpdate + + +def test_dev001_python_errors(dash_duo): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button(id="python", children="Python exception", n_clicks=0), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), [Input("python", "n_clicks")]) + def update_output(n_clicks): + if n_clicks == 1: + 1 / 0 + elif n_clicks == 2: + raise Exception("Special 2 clicks exception") + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + dash_duo.percy_snapshot("devtools - python exception - start") + + dash_duo.find_element("#python").click() + dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.percy_snapshot("devtools - python exception - closed") + + dash_duo.find_element(".test-devtools-error-toggle").click() + dash_duo.percy_snapshot("devtools - python exception - open") + + dash_duo.find_element(".test-devtools-error-toggle").click() + dash_duo.find_element("#python").click() + dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "2") + dash_duo.percy_snapshot("devtools - python exception - 2 errors") + + dash_duo.find_element(".test-devtools-error-toggle").click() + dash_duo.percy_snapshot("devtools - python exception - 2 errors open") + + +def test_dev002_prevent_update_not_in_error_msg(dash_duo): + # raising PreventUpdate shouldn't display the error message + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button(id="python", children="Prevent update", n_clicks=0), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), [Input("python", "n_clicks")]) + def update_output(n_clicks): + if n_clicks == 1: + raise PreventUpdate + if n_clicks == 2: + raise Exception("An actual python exception") + + return "button clicks: {}".format(n_clicks) + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + for _ in range(3): + dash_duo.find_element("#python").click() + + assert ( + dash_duo.find_element("#output").text == "button clicks: 3" + ), "the click counts correctly in output" + + # two exceptions fired, but only a single exception appeared in the UI: + # the prevent default was not displayed + dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.percy_snapshot( + "devtools - prevent update - only a single exception" + ) + + +def test_dev003_validation_errors_in_place(dash_duo): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button(id="button", children="update-graph", n_clicks=0), + dcc.Graph(id="output", figure={"data": [{"y": [3, 1, 2]}]}), + ] + ) + + # animate is a bool property + @app.callback(Output("output", "animate"), [Input("button", "n_clicks")]) + def update_output(n_clicks): + if n_clicks == 1: + return n_clicks + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + dash_duo.find_element("#button").click() + dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.percy_snapshot("devtools - validation exception - closed") + + dash_duo.find_element(".test-devtools-error-toggle").click() + dash_duo.percy_snapshot("devtools - validation exception - open") + + +def test_dev004_validation_errors_creation(dash_duo): + app = dash.Dash(__name__) + + app.layout = html.Div( + [ + html.Button(id="button", children="update-graph", n_clicks=0), + html.Div(id="output"), + ] + ) + + # animate is a bool property + @app.callback(Output("output", "children"), [Input("button", "n_clicks")]) + def update_output(n_clicks): + if n_clicks == 1: + return dcc.Graph( + id="output", animate=0, figure={"data": [{"y": [3, 1, 2]}]} + ) + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + dash_duo.find_element("#button").click() + dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.percy_snapshot("devtools - validation creation exception - closed") + + dash_duo.find_element(".test-devtools-error-toggle").click() + dash_duo.percy_snapshot("devtools - validation creation exception - open") + + +def test_dev005_multiple_outputs(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + html.Button( + id="multi-output", + children="trigger multi output update", + n_clicks=0, + ), + html.Div(id="multi-1"), + html.Div(id="multi-2"), + ] + ) + + @app.callback( + [Output("multi-1", "children"), Output("multi-2", "children")], + [Input("multi-output", "n_clicks")], + ) + def update_outputs(n_clicks): + if n_clicks == 0: + return [ + "Output 1 - {} Clicks".format(n_clicks), + "Output 2 - {} Clicks".format(n_clicks), + ] + else: + n_clicks / 0 + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + dash_duo.find_element("#multi-output").click() + dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.percy_snapshot("devtools - multi output python exception - closed") + + dash_duo.find_element(".test-devtools-error-toggle").click() + dash_duo.percy_snapshot("devtools - multi output python exception - open") diff --git a/tests/integration/devtools/test_devtools_ui.py b/tests/integration/devtools/test_devtools_ui.py new file mode 100644 index 0000000000..293762ca8a --- /dev/null +++ b/tests/integration/devtools/test_devtools_ui.py @@ -0,0 +1,64 @@ +import dash_core_components as dcc +import dash_html_components as html +import dash + + +def test_dev020_disable_props_check_config(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + html.P(id="tcid", children="Hello Props Check"), + dcc.Graph(id="broken", animate=3), # error ignored by disable + ] + ) + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + dev_tools_props_check=False, + ) + + dash_duo.wait_for_text_to_equal("#tcid", "Hello Props Check") + assert dash_duo.find_elements( + "#broken svg.main-svg" + ), "graph should be rendered" + + assert dash_duo.find_elements( + ".dash-debug-menu" + ), "the debug menu icon should show up" + + dash_duo.percy_snapshot( + "devtools - disable props check - Graph should render" + ) + + +def test_dev021_disable_ui_config(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + html.P(id="tcid", children="Hello Disable UI"), + dcc.Graph(id="broken", animate=3), # error ignored by disable + ] + ) + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + dev_tools_ui=False, + ) + + dash_duo.wait_for_text_to_equal("#tcid", "Hello Disable UI") + assert "Invalid argument `animate` passed into Graph" in str( + dash_duo.get_logs() + ), "the error should present in the console without DEV tools UI" + + assert not dash_duo.find_elements( + ".dash-debug-menu" + ), "the debug menu icon should NOT show up" + dash_duo.percy_snapshot("devtools - disable dev tools UI - no debug menu") diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py new file mode 100644 index 0000000000..97ca81d1c7 --- /dev/null +++ b/tests/integration/devtools/test_props_check.py @@ -0,0 +1,243 @@ +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Input, Output + + +def test_dev100_prop_check_errors_with_path(dash_duo): + app = dash.Dash(__name__) + + test_cases = { + "not-boolean": { + "fail": True, + "name": 'simple "not a boolean" check', + "component": dcc.Graph, + "props": {"animate": 0}, + }, + "missing-required-nested-prop": { + "fail": True, + "name": 'missing required "value" inside options', + "component": dcc.Checklist, + "props": {"options": [{"label": "hello"}], "values": ["test"]}, + }, + "invalid-nested-prop": { + "fail": True, + "name": "invalid nested prop", + "component": dcc.Checklist, + "props": { + "options": [{"label": "hello", "value": True}], + "values": ["test"], + }, + }, + "invalid-arrayOf": { + "fail": True, + "name": "invalid arrayOf", + "component": dcc.Checklist, + "props": {"options": "test", "values": []}, + }, + "invalid-oneOf": { + "fail": True, + "name": "invalid oneOf", + "component": dcc.Input, + "props": {"type": "test"}, + }, + "invalid-oneOfType": { + "fail": True, + "name": "invalid oneOfType", + "component": dcc.Input, + "props": {"max": True}, + }, + "invalid-shape-1": { + "fail": True, + "name": "invalid key within nested object", + "component": dcc.Graph, + "props": {"config": {"asdf": "that"}}, + }, + "invalid-shape-2": { + "fail": True, + "name": "nested object with bad value", + "component": dcc.Graph, + "props": {"config": {"edits": {"legendPosition": "asdf"}}}, + }, + "invalid-shape-3": { + "fail": True, + "name": "invalid oneOf within nested object", + "component": dcc.Graph, + "props": {"config": {"toImageButtonOptions": {"format": "asdf"}}}, + }, + "invalid-shape-4": { + "fail": True, + "name": "invalid key within deeply nested object", + "component": dcc.Graph, + "props": {"config": {"toImageButtonOptions": {"asdf": "test"}}}, + }, + "invalid-shape-5": { + "fail": True, + "name": "invalid not required key", + "component": dcc.Dropdown, + "props": { + "options": [ + {"label": "new york", "value": "ny", "typo": "asdf"} + ] + }, + }, + "string-not-list": { + "fail": True, + "name": "string-not-a-list", + "component": dcc.Checklist, + "props": { + "options": [{"label": "hello", "value": "test"}], + "values": "test", + }, + }, + "no-properties": { + "fail": False, + "name": "no properties", + "component": dcc.Graph, + "props": {}, + }, + "nested-children": { + "fail": True, + "name": "nested children", + "component": html.Div, + "props": {"children": [[1]]}, + }, + "deeply-nested-children": { + "fail": True, + "name": "deeply nested children", + "component": html.Div, + "props": {"children": html.Div([html.Div([3, html.Div([[10]])])])}, + }, + "dict": { + "fail": True, + "name": "returning a dictionary", + "component": html.Div, + "props": {"children": {"hello": "world"}}, + }, + "nested-prop-failure": { + "fail": True, + "name": "nested string instead of number/null", + "component": dcc.Graph, + "props": { + "figure": {"data": [{}]}, + "config": { + "toImageButtonOptions": {"width": None, "height": "test"} + }, + }, + }, + "allow-null": { + "fail": False, + "name": "nested null", + "component": dcc.Graph, + "props": { + "figure": {"data": [{}]}, + "config": { + "toImageButtonOptions": {"width": None, "height": None} + }, + }, + }, + "allow-null-2": { + "fail": False, + "name": "allow null as value", + "component": dcc.Dropdown, + "props": {"value": None}, + }, + "allow-null-3": { + "fail": False, + "name": "allow null in properties", + "component": dcc.Input, + "props": {"value": None}, + }, + "allow-null-4": { + "fail": False, + "name": "allow null in oneOfType", + "component": dcc.Store, + "props": {"id": "store", "data": None}, + }, + "long-property-string": { + "fail": True, + "name": "long property string with id", + "component": html.Div, + "props": {"id": "pink div", "style": "color: hotpink; " * 1000}, + }, + "multiple-wrong-values": { + "fail": True, + "name": "multiple wrong props", + "component": dcc.Dropdown, + "props": {"id": "dropdown", "value": 10, "options": "asdf"}, + }, + "boolean-html-properties": { + "fail": True, + "name": "dont allow booleans for dom props", + "component": html.Div, + "props": {"contentEditable": True}, + }, + "allow-exact-with-optional-and-required-1": { + "fail": False, + "name": "allow exact with optional and required keys", + "component": dcc.Dropdown, + "props": { + "options": [ + {"label": "new york", "value": "ny", "disabled": False} + ] + }, + }, + "allow-exact-with-optional-and-required-2": { + "fail": False, + "name": "allow exact with optional and required keys 2", + "component": dcc.Dropdown, + "props": {"options": [{"label": "new york", "value": "ny"}]}, + }, + } + + app.layout = html.Div( + [html.Div(id="content"), dcc.Location(id="location")]) + + @app.callback( + Output("content", "children"), [Input("location", "pathname")] + ) + def display_content(pathname): + if pathname is None or pathname == "/": + return "Initial state" + test_case = test_cases[pathname.strip("/")] + return html.Div( + id="new-component", + children=test_case["component"](**test_case["props"]), + ) + + dash_duo.start_app_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + + for tcid in test_cases: + dash_duo.driver.get("{}/{}".format(dash_duo.server_url, tcid)) + if test_cases[tcid]["fail"]: + try: + dash_duo.wait_for_element( + ".test-devtools-error-toggle" + ).click() + except Exception as e: + raise Exception( + "Error popup not shown for {}".format(tcid) + ) + dash_duo.percy_snapshot( + "devtools validation exception: {}".format( + test_cases[tcid]["name"] + ) + ) + else: + try: + dash_duo.wait_for_element("#new-component") + except Exception as e: + raise Exception( + "Component not rendered in {}".format(tcid) + ) + dash_duo.percy_snapshot( + "devtools validation no exception: {}".format( + test_cases[tcid]["name"] + ) + ) diff --git a/tests/integration/test_devtools.py b/tests/integration/test_devtools.py deleted file mode 100644 index a9c9ce4e9f..0000000000 --- a/tests/integration/test_devtools.py +++ /dev/null @@ -1,695 +0,0 @@ -# -*- coding: UTF-8 -*- -import os -import textwrap - -import dash -from dash import Dash -from dash.dependencies import Input, Output, State, ClientsideFunction -from dash.exceptions import PreventUpdate -from dash.development.base_component import Component -import dash_html_components as html -import dash_core_components as dcc -import dash_renderer_test_components - -from bs4 import BeautifulSoup -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from .IntegrationTests import IntegrationTests -from .utils import wait_for -from multiprocessing import Value -import time -import re -import itertools -import json -import string -import plotly -import requests -import pytest - - -TIMEOUT = 20 - - -def test_wdr001_simple_br_dash_docs(br): - br.server_url = 'https://dash.plot.ly/' - br.wait_for_element_by_css_selector('#wait-for-layout') - assert not br.get_logs(), "no console errors" - - -@pytest.mark.skip( - reason="flakey with circleci, will readdressing after pytest fixture") -class Tests(IntegrationTests): - def setUp(self): - pass - - def wait_for_style_to_equal(self, selector, style, assertion_style, timeout=TIMEOUT): - start = time.time() - exception = Exception('Time ran out, {} on {} not found'.format( - assertion_style, selector)) - while time.time() < start + timeout: - element = self.wait_for_element_by_css_selector(selector) - try: - self.assertEqual( - assertion_style, element.value_of_css_property(style)) - except Exception as e: - exception = e - else: - return - time.sleep(0.1) - - raise exception - - def wait_for_element_by_css_selector(self, selector, timeout=TIMEOUT): - return WebDriverWait(self.driver, timeout).until( - EC.presence_of_element_located((By.CSS_SELECTOR, selector)), - 'Could not find element with selector "{}"'.format(selector) - ) - - def wait_for_text_to_equal(self, selector, assertion_text, timeout=TIMEOUT): - self.wait_for_element_by_css_selector(selector) - WebDriverWait(self.driver, timeout).until( - lambda *args: ( - (str(self.wait_for_element_by_css_selector(selector).text) - == assertion_text) or - (str(self.wait_for_element_by_css_selector( - selector).get_attribute('value')) == assertion_text) - ), - "Element '{}' text expects to equal '{}' but it didn't".format( - selector, - assertion_text - ) - ) - - def clear_input(self, input_element): - ( - ActionChains(self.driver) - .click(input_element) - .send_keys(Keys.HOME) - .key_down(Keys.SHIFT) - .send_keys(Keys.END) - .key_up(Keys.SHIFT) - .send_keys(Keys.DELETE) - ).perform() - - def request_queue_assertions( - self, check_rejected=True, expected_length=None): - request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' - ) - self.assertTrue( - all([ - (r['status'] == 200) - for r in request_queue - ]) - ) - - if check_rejected: - self.assertTrue( - all([ - (r['rejected'] is False) - for r in request_queue - ]) - ) - - if expected_length is not None: - self.assertEqual(len(request_queue), expected_length) - - def test_devtools_python_errors(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='python', children='Python exception', n_clicks=0), - html.Div(id='output') - ]) - - @app.callback( - Output('output', 'children'), - [Input('python', 'n_clicks')]) - def update_output(n_clicks): - if n_clicks == 1: - 1 / 0 - elif n_clicks == 2: - raise Exception('Special 2 clicks exception') - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - ) - - self.percy_snapshot('devtools - python exception - start') - - self.wait_for_element_by_css_selector('#python').click() - self.wait_for_text_to_equal('.test-devtools-error-count', '1') - self.percy_snapshot('devtools - python exception - closed') - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - self.percy_snapshot('devtools - python exception - open') - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - - self.wait_for_element_by_css_selector('#python').click() - self.wait_for_text_to_equal('.test-devtools-error-count', '2') - self.percy_snapshot('devtools - python exception - 2 errors') - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - self.percy_snapshot('devtools - python exception - 2 errors open') - - def test_devtools_prevent_update(self): - # raising PreventUpdate shouldn't display the error message - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='python', children='Prevent update', n_clicks=0), - html.Div(id='output') - ]) - - @app.callback( - Output('output', 'children'), - [Input('python', 'n_clicks')]) - def update_output(n_clicks): - if n_clicks == 1: - raise PreventUpdate - if n_clicks == 2: - raise Exception('An actual python exception') - - return 'button clicks: {}'.format(n_clicks) - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - ) - - self.wait_for_element_by_css_selector('#python').click() - self.wait_for_element_by_css_selector('#python').click() - self.wait_for_element_by_css_selector('#python').click() - self.wait_for_text_to_equal('#output', 'button clicks: 3') - - # two exceptions fired, but only a single exception appeared in the UI: - # the prevent default was not displayed - self.wait_for_text_to_equal('.test-devtools-error-count', '1') - self.percy_snapshot('devtools - prevent update - only a single exception') - - def test_devtools_validation_errors_in_place(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='button', children='update-graph', n_clicks=0), - dcc.Graph(id='output', figure={'data': [{'y': [3, 1, 2]}]}) - ]) - - # animate is a bool property - @app.callback( - Output('output', 'animate'), - [Input('button', 'n_clicks')]) - def update_output(n_clicks): - if n_clicks == 1: - return n_clicks - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - ) - - self.wait_for_element_by_css_selector('#button').click() - self.wait_for_text_to_equal('.test-devtools-error-count', '1') - self.percy_snapshot('devtools - validation exception - closed') - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - self.percy_snapshot('devtools - validation exception - open') - - def test_dev_tools_disable_props_check_config(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.P(id='tcid', children='Hello Props Check'), - dcc.Graph(id='broken', animate=3), # error ignored by disable - ]) - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - dev_tools_props_check=False - ) - - self.wait_for_text_to_equal('#tcid', "Hello Props Check") - self.assertTrue( - self.driver.find_elements_by_css_selector('#broken svg.main-svg'), - "graph should be rendered") - self.assertTrue( - self.driver.find_elements_by_css_selector('.dash-debug-menu'), - "the debug menu icon should show up") - - self.percy_snapshot('devtools - disable props check - Graph should render') - - def test_dev_tools_disable_ui_config(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.P(id='tcid', children='Hello Disable UI'), - dcc.Graph(id='broken', animate=3), # error ignored by disable - ]) - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - dev_tools_ui=False - ) - - self.wait_for_text_to_equal('#tcid', "Hello Disable UI") - logs = self.wait_until_get_log() - self.assertIn( - 'Invalid argument `animate` passed into Graph', str(logs), - "the error should present in the console without DEV tools UI") - - self.assertFalse( - self.driver.find_elements_by_css_selector('.dash-debug-menu'), - "the debug menu icon should NOT show up") - - self.percy_snapshot('devtools - disable dev tools UI - no debug menu') - - def test_devtools_validation_errors_creation(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='button', children='update-graph', n_clicks=0), - html.Div(id='output') - ]) - - # animate is a bool property - @app.callback( - Output('output', 'children'), - [Input('button', 'n_clicks')]) - def update_output(n_clicks): - if n_clicks == 1: - return dcc.Graph( - id='output', - animate=0, - figure={'data': [{'y': [3, 1, 2]}]} - ) - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - ) - - self.wait_for_element_by_css_selector('#button').click() - self.wait_for_text_to_equal('.test-devtools-error-count', '1') - self.percy_snapshot('devtools - validation creation exception - closed') - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - self.percy_snapshot('devtools - validation creation exception - open') - - def test_devtools_multiple_outputs(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button( - id='multi-output', - children='trigger multi output update', - n_clicks=0 - ), - html.Div(id='multi-1'), - html.Div(id='multi-2'), - ]) - - @app.callback( - [Output('multi-1', 'children'), Output('multi-2', 'children')], - [Input('multi-output', 'n_clicks')]) - def update_outputs(n_clicks): - if n_clicks == 0: - return [ - 'Output 1 - {} Clicks'.format(n_clicks), - 'Output 2 - {} Clicks'.format(n_clicks), - ] - else: - n_clicks / 0 - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - ) - - self.wait_for_element_by_css_selector('#multi-output').click() - self.wait_for_text_to_equal('.test-devtools-error-count', '1') - self.percy_snapshot('devtools - multi output python exception - closed') - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - self.percy_snapshot('devtools - multi output python exception - open') - - def test_devtools_validation_errors(self): - app = dash.Dash(__name__) - - test_cases = { - 'not-boolean': { - 'fail': True, - 'name': 'simple "not a boolean" check', - 'component': dcc.Graph, - 'props': { - 'animate': 0 - } - }, - - 'missing-required-nested-prop': { - 'fail': True, - 'name': 'missing required "value" inside options', - 'component': dcc.Checklist, - 'props': { - 'options': [{ - 'label': 'hello' - }], - 'values': ['test'] - } - }, - - 'invalid-nested-prop': { - 'fail': True, - 'name': 'invalid nested prop', - 'component': dcc.Checklist, - 'props': { - 'options': [{ - 'label': 'hello', - 'value': True - }], - 'values': ['test'] - } - }, - - 'invalid-arrayOf': { - 'fail': True, - 'name': 'invalid arrayOf', - 'component': dcc.Checklist, - 'props': { - 'options': 'test', - 'values': [] - } - }, - - 'invalid-oneOf': { - 'fail': True, - 'name': 'invalid oneOf', - 'component': dcc.Input, - 'props': { - 'type': 'test', - } - }, - - 'invalid-oneOfType': { - 'fail': True, - 'name': 'invalid oneOfType', - 'component': dcc.Input, - 'props': { - 'max': True, - } - }, - - 'invalid-shape-1': { - 'fail': True, - 'name': 'invalid key within nested object', - 'component': dcc.Graph, - 'props': { - 'config': { - 'asdf': 'that' - } - } - }, - - 'invalid-shape-2': { - 'fail': True, - 'name': 'nested object with bad value', - 'component': dcc.Graph, - 'props': { - 'config': { - 'edits': { - 'legendPosition': 'asdf' - } - } - } - }, - - 'invalid-shape-3': { - 'fail': True, - 'name': 'invalid oneOf within nested object', - 'component': dcc.Graph, - 'props': { - 'config': { - 'toImageButtonOptions': { - 'format': 'asdf' - } - } - } - }, - - 'invalid-shape-4': { - 'fail': True, - 'name': 'invalid key within deeply nested object', - 'component': dcc.Graph, - 'props': { - 'config': { - 'toImageButtonOptions': { - 'asdf': 'test' - } - } - } - }, - - 'invalid-shape-5': { - 'fail': True, - 'name': 'invalid not required key', - 'component': dcc.Dropdown, - 'props': { - 'options': [ - { - 'label': 'new york', - 'value': 'ny', - 'typo': 'asdf' - } - ] - } - }, - - 'string-not-list': { - 'fail': True, - 'name': 'string-not-a-list', - 'component': dcc.Checklist, - 'props': { - 'options': [{ - 'label': 'hello', - 'value': 'test' - }], - 'values': 'test' - } - }, - - 'no-properties': { - 'fail': False, - 'name': 'no properties', - 'component': dcc.Graph, - 'props': {} - }, - - 'nested-children': { - 'fail': True, - 'name': 'nested children', - 'component': html.Div, - 'props': {'children': [[1]]} - }, - - 'deeply-nested-children': { - 'fail': True, - 'name': 'deeply nested children', - 'component': html.Div, - 'props': {'children': html.Div([ - html.Div([ - 3, - html.Div([[10]]) - ]) - ])} - }, - - 'dict': { - 'fail': True, - 'name': 'returning a dictionary', - 'component': html.Div, - 'props': { - 'children': {'hello': 'world'} - } - }, - - 'nested-prop-failure': { - 'fail': True, - 'name': 'nested string instead of number/null', - 'component': dcc.Graph, - 'props': { - 'figure': {'data': [{}]}, - 'config': { - 'toImageButtonOptions': { - 'width': None, - 'height': 'test' - } - } - } - }, - - 'allow-null': { - 'fail': False, - 'name': 'nested null', - 'component': dcc.Graph, - 'props': { - 'figure': {'data': [{}]}, - 'config': { - 'toImageButtonOptions': { - 'width': None, - 'height': None - } - } - } - }, - - 'allow-null-2': { - 'fail': False, - 'name': 'allow null as value', - 'component': dcc.Dropdown, - 'props': { - 'value': None - } - }, - - 'allow-null-3': { - 'fail': False, - 'name': 'allow null in properties', - 'component': dcc.Input, - 'props': { - 'value': None - } - }, - - 'allow-null-4': { - 'fail': False, - 'name': 'allow null in oneOfType', - 'component': dcc.Store, - 'props': { - 'id': 'store', - 'data': None - } - }, - - 'long-property-string': { - 'fail': True, - 'name': 'long property string with id', - 'component': html.Div, - 'props': { - 'id': 'pink div', - 'style': 'color: hotpink; ' * 1000 - } - }, - - 'multiple-wrong-values': { - 'fail': True, - 'name': 'multiple wrong props', - 'component': dcc.Dropdown, - 'props': { - 'id': 'dropdown', - 'value': 10, - 'options': 'asdf', - } - }, - - 'boolean-html-properties': { - 'fail': True, - 'name': 'dont allow booleans for dom props', - 'component': html.Div, - 'props': { - 'contentEditable': True - } - }, - - 'allow-exact-with-optional-and-required-1': { - 'fail': False, - 'name': 'allow exact with optional and required keys', - 'component': dcc.Dropdown, - 'props': { - 'options': [{ - 'label': 'new york', - 'value': 'ny', - 'disabled': False - }] - } - }, - - 'allow-exact-with-optional-and-required-2': { - 'fail': False, - 'name': 'allow exact with optional and required keys 2', - 'component': dcc.Dropdown, - 'props': { - 'options': [{ - 'label': 'new york', - 'value': 'ny' - }] - } - } - - } - - app.layout = html.Div([ - html.Div(id='content'), - dcc.Location(id='location'), - ]) - - @app.callback( - Output('content', 'children'), - [Input('location', 'pathname')]) - def display_content(pathname): - if pathname is None or pathname == '/': - return 'Initial state' - test_case = test_cases[pathname.strip('/')] - return html.Div( - id='new-component', - children=test_case['component'](**test_case['props']) - ) - - self.startServer( - app, - debug=True, - use_reloader=False, - use_debugger=True, - dev_tools_hot_reload=False, - ) - - for test_case_id in test_cases: - self.driver.get('http://localhost:8050/{}'.format(test_case_id)) - if test_cases[test_case_id]['fail']: - try: - self.wait_for_element_by_css_selector('.test-devtools-error-toggle').click() - except Exception as e: - raise Exception('Error popup not shown for {}'.format(test_case_id)) - self.percy_snapshot( - 'devtools validation exception: {}'.format( - test_cases[test_case_id]['name'] - ) - ) - else: - try: - self.wait_for_element_by_css_selector('#new-component') - except Exception as e: - raise Exception('Component not rendered in {}'.format(test_case_id)) - self.percy_snapshot( - 'devtools validation no exception: {}'.format( - test_cases[test_case_id]['name'] - ) - ) From 0c29ee8be7ba2ccb9dd16c98065b56f1c6e4c92e Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 15:07:03 -0400 Subject: [PATCH 27/46] :art: fix lint --- dash/testing/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 7e7c003995..e3bbc10683 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -1,4 +1,4 @@ -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring,redefined-outer-name import pytest from selenium import webdriver From e5f2e9311d023777399bdd90aadffd94a2c2f5b9 Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 15:08:22 -0400 Subject: [PATCH 28/46] :wrench: remove --vv from pytest.ini --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index c26b36e040..2c27f386fb 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] testpaths = tests/ -addopts = -rsxX -vv +addopts = -rsxX log_cli=true log_format = %(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s log_cli_level = ERROR From 049f717426773354521f03f0b2a2b7a1133c91fe Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 15:16:34 -0400 Subject: [PATCH 29/46] :pencil2: adding back the logs --- dash/testing/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index b31d059cb4..5011b90db9 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -116,7 +116,7 @@ def wait_for_page(self, timeout=10): self.driver.find_element_by_tag_name("body").get_property( "innerHTML" ), - "\n".join([]), + "\n".join((str(log) for log in self.get_logs())), ) ) From 25ace9a770141e7962b5533ecb608d6e6e15ba09 Mon Sep 17 00:00:00 2001 From: byron Date: Mon, 3 Jun 2019 16:58:19 -0400 Subject: [PATCH 30/46] :alembic: tuning the tests with docker failure --- dash/testing/browser.py | 1 + dash/testing/wait.py | 29 +++++++++++++++++-- .../devtools/test_devtools_error_handling.py | 4 +-- .../integration/devtools/test_devtools_ui.py | 7 +++-- .../integration/devtools/test_props_check.py | 21 ++++---------- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 5011b90db9..8be8538b27 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -16,6 +16,7 @@ from dash.testing.wait import text_to_equal from dash.testing.errors import DashAppLoadingError + logger = logging.getLogger(__name__) diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 0a0d9a2593..f006862590 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -1,3 +1,4 @@ +# pylint: disable=too-few-public-methods """Utils methods for pytest-dash such wait_for wrappers""" import time import logging @@ -13,24 +14,46 @@ def until( poll=0.1, msg="expected condition not met within timeout", ): # noqa: C0330 + res = None + logger.debug( + "start wait.until with %s, timeout[%s], poll[%s]", + wait_cond, + timeout, + poll, + ) end_time = time.time() + timeout - while not wait_cond(): + while not res: if time.time() > end_time: raise TestingTimeoutError(msg) time.sleep(poll) + res = wait_cond() + logger.debug("poll => %s", time.time()) + + return res def until_not( wait_cond, timeout, poll=0.1, msg="expected condition met within timeout" ): # noqa: C0330 + res = True + logger.debug( + "start wait.until with %s, timeout[%s], poll[%s]", + wait_cond, + timeout, + poll, + ) end_time = time.time() + timeout - while wait_cond(): + while res: if time.time() > end_time: raise TestingTimeoutError(msg) time.sleep(poll) + res = wait_cond() + logger.debug("poll => %s", time.time()) + + return res -class text_to_equal(object): # pylint: disable=too-few-public-methods +class text_to_equal(object): def __init__(self, selector, text): self.selector = selector self.text = text diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index 206767ae5b..b9743a5ed3 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -117,7 +117,7 @@ def update_output(n_clicks): dev_tools_hot_reload=False, ) - dash_duo.find_element("#button").click() + dash_duo.wait_for_element("#button").click() dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") dash_duo.percy_snapshot("devtools - validation exception - closed") @@ -151,7 +151,7 @@ def update_output(n_clicks): dev_tools_hot_reload=False, ) - dash_duo.find_element("#button").click() + dash_duo.wait_for_element("#button").click() dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") dash_duo.percy_snapshot("devtools - validation creation exception - closed") diff --git a/tests/integration/devtools/test_devtools_ui.py b/tests/integration/devtools/test_devtools_ui.py index 293762ca8a..b65ca3ecba 100644 --- a/tests/integration/devtools/test_devtools_ui.py +++ b/tests/integration/devtools/test_devtools_ui.py @@ -2,6 +2,8 @@ import dash_html_components as html import dash +import dash.testing.wait as wait + def test_dev020_disable_props_check_config(dash_duo): app = dash.Dash(__name__) @@ -54,8 +56,9 @@ def test_dev021_disable_ui_config(dash_duo): ) dash_duo.wait_for_text_to_equal("#tcid", "Hello Disable UI") - assert "Invalid argument `animate` passed into Graph" in str( - dash_duo.get_logs() + logs = str(wait.until(dash_duo.get_logs, timeout=1)) + assert ( + "Invalid argument `animate` passed into Graph" in logs ), "the error should present in the console without DEV tools UI" assert not dash_duo.find_elements( diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index 97ca81d1c7..57e075e2bb 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -3,6 +3,8 @@ import dash from dash.dependencies import Input, Output +from selenium.common.exceptions import TimeoutException + def test_dev100_prop_check_errors_with_path(dash_duo): app = dash.Dash(__name__) @@ -190,8 +192,7 @@ def test_dev100_prop_check_errors_with_path(dash_duo): }, } - app.layout = html.Div( - [html.Div(id="content"), dcc.Location(id="location")]) + app.layout = html.Div([html.Div(id="content"), dcc.Location(id="location")]) @app.callback( Output("content", "children"), [Input("location", "pathname")] @@ -216,26 +217,14 @@ def display_content(pathname): for tcid in test_cases: dash_duo.driver.get("{}/{}".format(dash_duo.server_url, tcid)) if test_cases[tcid]["fail"]: - try: - dash_duo.wait_for_element( - ".test-devtools-error-toggle" - ).click() - except Exception as e: - raise Exception( - "Error popup not shown for {}".format(tcid) - ) + dash_duo.wait_for_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot( "devtools validation exception: {}".format( test_cases[tcid]["name"] ) ) else: - try: - dash_duo.wait_for_element("#new-component") - except Exception as e: - raise Exception( - "Component not rendered in {}".format(tcid) - ) + dash_duo.wait_for_element("#new-component") dash_duo.percy_snapshot( "devtools validation no exception: {}".format( test_cases[tcid]["name"] From 12fa1b14f946b5c0c82ff757f3d98c23ff17154c Mon Sep 17 00:00:00 2001 From: byron Date: Tue, 4 Jun 2019 00:15:50 -0400 Subject: [PATCH 31/46] :alembic: dbg --- dash/testing/browser.py | 28 +++++++++++-------- dash/testing/locators.py | 7 +++++ dash/testing/wait.py | 18 ++++++++---- .../devtools/test_devtools_error_handling.py | 3 +- 4 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 dash/testing/locators.py diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 8be8538b27..3ca1396e4e 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -14,13 +14,14 @@ from selenium.common.exceptions import WebDriverException, TimeoutException from dash.testing.wait import text_to_equal +from dash.testing.locators import DashLocatorsMixin from dash.testing.errors import DashAppLoadingError logger = logging.getLogger(__name__) -class Browser(object): +class Browser(DashLocatorsMixin): def __init__(self, browser, remote=None, wait_timeout=10): self._browser = browser.lower() self._wait_timeout = wait_timeout @@ -28,9 +29,7 @@ def __init__(self, browser, remote=None, wait_timeout=10): self._driver = self.get_webdriver(remote) self._driver.implicitly_wait(2) - self._wd_wait = WebDriverWait( - driver=self.driver, timeout=wait_timeout, poll_frequency=0.2 - ) + self._wd_wait = WebDriverWait(self.driver, wait_timeout, 0.2) self._last_ts = 0 self._url = None @@ -62,6 +61,14 @@ def percy_snapshot(self, name=""): logger.info("taking snapshot name => %s", snapshot_name) self.percy_runner.snapshot(name=snapshot_name) + def find_element(self, css_selector): + """wrapper for find_element_by_css_selector from driver""" + return self.driver.find_element_by_css_selector(css_selector) + + def find_elements(self, css_selector): + """wrapper for find_elements_by_css_selector from driver""" + return self.driver.find_elements_by_css_selector(css_selector) + def _wait_for(self, method, args, timeout, msg): """abstract generic pattern for explicit webdriver wait""" _wait = ( @@ -69,16 +76,13 @@ def _wait_for(self, method, args, timeout, msg): if timeout is None else WebDriverWait(self.driver, timeout) ) + logger.debug( + "WebdriverWait timeout => %s, poll => %s", + _wait._timeout, + _wait._poll, + ) return _wait.until(method(*args), msg) - def find_element(self, css_selector): - """wrapper for find_element_by_css_selector from driver""" - return self.driver.find_element_by_css_selector(css_selector) - - def find_elements(self, css_selector): - """wrapper for find_elements_by_css_selector from driver""" - return self.driver.find_elements_by_css_selector(css_selector) - def wait_for_element(self, css_selector, timeout=None): return self.wait_for_element_by_css_selector(css_selector, timeout) diff --git a/dash/testing/locators.py b/dash/testing/locators.py new file mode 100644 index 0000000000..92701e8c3a --- /dev/null +++ b/dash/testing/locators.py @@ -0,0 +1,7 @@ +class DashLocatorsMixin(object): + def dev_tools_error_counts(self): + return int( + self.driver.find_element_by_css_selector( + ".test-devtools-error-count" + ).text + ) diff --git a/dash/testing/wait.py b/dash/testing/wait.py index f006862590..882af22b90 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -2,6 +2,7 @@ """Utils methods for pytest-dash such wait_for wrappers""" import time import logging +from selenium.common.exceptions import StaleElementReferenceException from dash.testing.errors import TestingTimeoutError @@ -59,8 +60,15 @@ def __init__(self, selector, text): self.text = text def __call__(self, driver): - elem = driver.find_element_by_css_selector(self.selector) - return ( - str(elem.text) == self.text - or str(elem.get_attribute("value")) == self.text - ) + try: + elem = driver.find_element_by_css_selector(self.selector) + logger.debug( + "text to equal {%s} => expected %s", elem.text, self.text + ) + return ( + str(elem.text) == self.text + or str(elem.get_attribute("value")) == self.text + ) + except StaleElementReferenceException: + logger.warning("text_to_equal, element is still stale") + return False diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index b9743a5ed3..70ded14dd8 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -117,7 +117,8 @@ def update_output(n_clicks): dev_tools_hot_reload=False, ) - dash_duo.wait_for_element("#button").click() + dash_duo.find_element("#button").click() + # assert dash_duo.dev_tools_error_counts == 1, dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") dash_duo.percy_snapshot("devtools - validation exception - closed") From 688385efb8afd023e629740e1c4bc5cad39e5b29 Mon Sep 17 00:00:00 2001 From: byron Date: Wed, 5 Jun 2019 12:58:19 -0400 Subject: [PATCH 32/46] :construction: improve for dbg experience --- .circleci/config.yml | 5 +- dash-renderer/version.py | 2 +- dash/testing/browser.py | 61 +++++++++++++++---- dash/testing/locators.py | 1 + dash/testing/plugin.py | 16 +++++ dash/testing/wait.py | 6 +- pytest.ini | 1 - .../devtools/test_devtools_error_handling.py | 2 - 8 files changed, 74 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 029fc206fe..5e0efef9cf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -89,11 +89,12 @@ jobs: command: | . venv/bin/activate pytest --junitxml=test-reports/junit_intg.xml tests/integration/ - + - store_artifacts: + path: test-reports - store_test_results: path: test-reports - store_artifacts: - path: test-reports + path: /tmp/dash_artifacts "python-3.6": diff --git a/dash-renderer/version.py b/dash-renderer/version.py index 08a9dbff61..f8ab8c2e1f 100644 --- a/dash-renderer/version.py +++ b/dash-renderer/version.py @@ -1 +1 @@ -__version__ = '0.23.0' +__version__ = '0.24.0' diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 3ca1396e4e..7435bae9ee 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -11,7 +11,13 @@ from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.desired_capabilities import DesiredCapabilities -from selenium.common.exceptions import WebDriverException, TimeoutException +from selenium.webdriver.common.action_chains import ActionChains + +from selenium.common.exceptions import ( + WebDriverException, + TimeoutException, + NoSuchElementException, +) from dash.testing.wait import text_to_equal from dash.testing.locators import DashLocatorsMixin @@ -29,7 +35,7 @@ def __init__(self, browser, remote=None, wait_timeout=10): self._driver = self.get_webdriver(remote) self._driver.implicitly_wait(2) - self._wd_wait = WebDriverWait(self.driver, wait_timeout, 0.2) + self._wd_wait = WebDriverWait(self.driver, wait_timeout) self._last_ts = 0 self._url = None @@ -61,6 +67,23 @@ def percy_snapshot(self, name=""): logger.info("taking snapshot name => %s", snapshot_name) self.percy_runner.snapshot(name=snapshot_name) + def take_snapshot(self, name): + """method used by hook to take snapshot while selenium test fails""" + target = ( + "/tmp/dash_artifacts" + if not self._is_windows() + else os.getenv("TEMP") + ) + if not os.path.exists(target): + try: + os.mkdir(target) + except OSError: + logger.exception("cannot make artifacts") + + self.driver.save_screenshot( + "{}/{}_{}.png".format(target, name, self.session_id) + ) + def find_element(self, css_selector): """wrapper for find_element_by_css_selector from driver""" return self.driver.find_element_by_css_selector(css_selector) @@ -77,10 +100,12 @@ def _wait_for(self, method, args, timeout, msg): else WebDriverWait(self.driver, timeout) ) logger.debug( - "WebdriverWait timeout => %s, poll => %s", - _wait._timeout, - _wait._poll, + "method, timeout, poll => %s %s %s", + method, + _wait.timeout, # pylint: disable=protected-access + _wait._poll, # pylint: disable=protected-access ) + return _wait.until(method(*args), msg) def wait_for_element(self, css_selector, timeout=None): @@ -97,10 +122,10 @@ def wait_for_element_by_css_selector(self, selector, timeout=None): def wait_for_text_to_equal(self, selector, text, timeout=None): return self._wait_for( - text_to_equal, - (selector, text), - timeout, - "cannot wait until element contains expected text {}".format(text), + method=text_to_equal, + args=(selector, text), + timeout=timeout, + msg="text -> {} not found within {}s".format(text, timeout), ) def wait_for_page(self, timeout=10): @@ -173,6 +198,21 @@ def _get_firefox(): return webdriver.Firefox(fp, capabilities=capabilities) + @staticmethod + def _is_windows(): + return sys.platform == "win32" + + def js_click(self, elem): + """click in native javascript way + note: this is NOT the recommended way to click""" + self.driver.execute_script("arguments[0].click();", elem) + + def mouse_click(self, elem): + try: + ActionChains(self.driver).click(elem).perform() + except NoSuchElementException: + logger.exception("mouse_click on wrong element") + def get_logs(self): """get_logs works only with chrome webdriver""" if self.driver.name.lower() == "chrome": @@ -208,8 +248,7 @@ def server_url(self): @server_url.setter def server_url(self, value): """property setter for server_url - - Note: set server_url will implicitly check the server is ready + Note: set server_url will implicitly check if the server is ready for selenium testing """ self._url = value diff --git a/dash/testing/locators.py b/dash/testing/locators.py index 92701e8c3a..56b9ed5a8f 100644 --- a/dash/testing/locators.py +++ b/dash/testing/locators.py @@ -1,3 +1,4 @@ +# pylint: disable=too-few-public-methods class DashLocatorsMixin(object): def dev_tools_error_counts(self): return int( diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index e3bbc10683..01b2d074d1 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -27,6 +27,22 @@ def pytest_addoption(parser): ) +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument + # execute all other hooks to obtain the report object + outcome = yield + rep = outcome.get_result() + + # we only look at actual failing test calls, not setup/teardown + if rep.when == "call" and rep.failed: + for name, fixture in item.funcargs.items(): + try: + if name in {"dash_duo", "dash_br"}: + fixture.take_snapshot(item.name) + except Exception as e: # pylint: disable=broad-except + print(e) + + ############################################################################### # Fixtures ############################################################################### diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 882af22b90..808671e629 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -2,7 +2,7 @@ """Utils methods for pytest-dash such wait_for wrappers""" import time import logging -from selenium.common.exceptions import StaleElementReferenceException +from selenium.common.exceptions import WebDriverException from dash.testing.errors import TestingTimeoutError @@ -69,6 +69,6 @@ def __call__(self, driver): str(elem.text) == self.text or str(elem.get_attribute("value")) == self.text ) - except StaleElementReferenceException: - logger.warning("text_to_equal, element is still stale") + except WebDriverException: + logger.exception("text_to_equal encountered an exception") return False diff --git a/pytest.ini b/pytest.ini index 2c27f386fb..d2dc22fc6a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,5 @@ [pytest] testpaths = tests/ addopts = -rsxX -log_cli=true log_format = %(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s log_cli_level = ERROR diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index 70ded14dd8..18d92282b2 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -1,7 +1,6 @@ # -*- coding: UTF-8 -*- import dash_html_components as html import dash_core_components as dcc - import dash from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate @@ -118,7 +117,6 @@ def update_output(n_clicks): ) dash_duo.find_element("#button").click() - # assert dash_duo.dev_tools_error_counts == 1, dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") dash_duo.percy_snapshot("devtools - validation exception - closed") From 8bb8312a6f7f940da5b2701d82d0556baf65fb6a Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 01:37:25 -0400 Subject: [PATCH 33/46] :alien: add and improve APIs --- dash/testing/browser.py | 28 +++++++++++++++++++++++++--- dash/testing/locators.py | 13 +++++++------ dash/testing/wait.py | 21 +++++++++++++++++++-- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 7435bae9ee..cf192a0797 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -19,7 +19,7 @@ NoSuchElementException, ) -from dash.testing.wait import text_to_equal +from dash.testing.wait import text_to_equal, style_to_equal from dash.testing.locators import DashLocatorsMixin from dash.testing.errors import DashAppLoadingError @@ -102,7 +102,7 @@ def _wait_for(self, method, args, timeout, msg): logger.debug( "method, timeout, poll => %s %s %s", method, - _wait.timeout, # pylint: disable=protected-access + _wait._timeout, # pylint: disable=protected-access _wait._poll, # pylint: disable=protected-access ) @@ -120,6 +120,16 @@ def wait_for_element_by_css_selector(self, selector, timeout=None): "cannot find_element using the css selector", ) + def wait_for_style_to_equal(self, selector, style, val, timeout=None): + return self._wait_for( + method=style_to_equal, + args=(selector, style, val), + timeout=timeout, + msg="style val => {} {} not found within {}s".format( + style, val, timeout + ), + ) + def wait_for_text_to_equal(self, selector, text, timeout=None): return self._wait_for( method=text_to_equal, @@ -133,7 +143,7 @@ def wait_for_page(self, timeout=10): self.driver.get(self.server_url) try: self.wait_for_element_by_css_selector( - "#react-entry-point", timeout=timeout + self.dash_entry_locator, timeout=timeout ) except TimeoutException: logger.exception( @@ -253,3 +263,15 @@ def server_url(self, value): """ self._url = value self.wait_for_page() + + @property + def redux_state_paths(self): + return self.driver.execute_script( + "return window.store.getState().paths" + ) + + @property + def redux_state_rqs(self): + return self.driver.execute_script( + "return window.store.getState().requestQueue" + ) diff --git a/dash/testing/locators.py b/dash/testing/locators.py index 56b9ed5a8f..177d188f68 100644 --- a/dash/testing/locators.py +++ b/dash/testing/locators.py @@ -1,8 +1,9 @@ # pylint: disable=too-few-public-methods class DashLocatorsMixin(object): - def dev_tools_error_counts(self): - return int( - self.driver.find_element_by_css_selector( - ".test-devtools-error-count" - ).text - ) + @property + def devtools_error_count_locator(self): + return ".test-devtools-error-count" + + @property + def dash_entry_locator(self): + return "#react-entry-point" diff --git a/dash/testing/wait.py b/dash/testing/wait.py index 808671e629..5d62d93c99 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -17,7 +17,7 @@ def until( ): # noqa: C0330 res = None logger.debug( - "start wait.until with %s, timeout[%s], poll[%s]", + "start wait.until with method, timeout, poll => %s %s %s", wait_cond, timeout, poll, @@ -38,7 +38,7 @@ def until_not( ): # noqa: C0330 res = True logger.debug( - "start wait.until with %s, timeout[%s], poll[%s]", + "start wait.until_not method, timeout, poll => %s %s %s", wait_cond, timeout, poll, @@ -72,3 +72,20 @@ def __call__(self, driver): except WebDriverException: logger.exception("text_to_equal encountered an exception") return False + + +class style_to_equal(object): + def __init__(self, selector, style, val): + self.selector = selector + self.style = style + self.val = val + + def __call__(self, driver): + try: + elem = driver.find_element_by_css_selector(self.selector) + val = elem.value_of_css_property(self.style) + logger.debug("style to equal {%s} => expected %s", val, self.val) + return val == self.val + except WebDriverException: + logger.exception("style_to_equal encountered an exception") + return False From 4849ace42425be1af2081c8202b1d818920ec4f4 Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 01:39:32 -0400 Subject: [PATCH 34/46] :art: :recycle: adapt / translate tests --- .circleci/config.yml | 2 +- tests/integration/dash_assets/test_assets.py | 152 ------- .../dash_assets/test_dash_assets.py | 135 ++++++ .../hr_assets}/hot_reload.css | 0 .../devtools/test_devtools_error_handling.py | 23 +- .../integration/devtools/test_devtools_ui.py | 5 +- tests/integration/devtools/test_hot_reload.py | 47 ++ .../integration/devtools/test_props_check.py | 4 +- .../initial_state_dash_app_content.html | 0 .../integration/renderer/test_dependencies.py | 47 ++ .../renderer/test_due_diligence.py | 114 +++++ .../renderer/test_state_and_input.py | 113 +++++ tests/integration/test_render.py | 412 +----------------- 13 files changed, 473 insertions(+), 581 deletions(-) delete mode 100644 tests/integration/dash_assets/test_assets.py create mode 100644 tests/integration/dash_assets/test_dash_assets.py rename tests/integration/{test_assets => devtools/hr_assets}/hot_reload.css (100%) create mode 100644 tests/integration/devtools/test_hot_reload.py rename tests/integration/{test_assets => renderer}/initial_state_dash_app_content.html (100%) create mode 100644 tests/integration/renderer/test_dependencies.py create mode 100644 tests/integration/renderer/test_due_diligence.py create mode 100644 tests/integration/renderer/test_state_and_input.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e0efef9cf..da1d988276 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,7 +49,7 @@ jobs: flake8 dash setup.py flake8 --ignore=E123,E126,E501,E722,E731,F401,F841,W503,W504 --exclude=metadata_test.py tests pylint dash setup.py --rcfile=$PYLINTRC - pylint tests/unit tests/integration/devtools -d all -e C0410,C0411,C0412,C0413,W0109 + pylint tests/unit tests/integration/devtools tests/integration/renderer tests/integration/dash_assets -d all -e C0410,C0411,C0412,C0413,W0109 cd dash-renderer && npm install --ignore-scripts && npm run lint:test && npm run format:test - run: diff --git a/tests/integration/dash_assets/test_assets.py b/tests/integration/dash_assets/test_assets.py deleted file mode 100644 index dcb4aaa321..0000000000 --- a/tests/integration/dash_assets/test_assets.py +++ /dev/null @@ -1,152 +0,0 @@ -import json -import time -import itertools - -import dash_html_components as html -import dash_core_components as dcc - -from dash import Dash -# from IntegrationTests import IntegrationTests -# from integration.utils import wait_for, invincible -import pytest - - -@pytest.mark.skip("rewrite with fixture can solve the import issue") -class TestAssets(): - - # def setUp(self): - # 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 test_assets(self): - app = Dash(__name__, assets_ignore='.*ignored.*') - app.index_string = ''' - - - - {%metas%} - {%title%} - {%css%} - - -
- {%app_entry%} -
- {%config%} - {%scripts%} - {%renderer%} -
- - - ''' - - app.layout = html.Div([ - html.Div('Content', id='content'), - dcc.Input(id='test') - ], id='layout') - - self.startServer(app) - - # time.sleep(3600) - - body = self.driver.find_element_by_tag_name('body') - - body_margin = body.value_of_css_property('margin') - self.assertEqual('0px', body_margin) - - content = self.wait_for_element_by_id('content') - content_padding = content.value_of_css_property('padding') - self.assertEqual('8px', content_padding) - - tested = self.wait_for_element_by_id('tested') - tested = json.loads(tested.text) - - order = ( - 'load_first', 'load_after', 'load_after1', 'load_after10', - 'load_after11', 'load_after2', 'load_after3', 'load_after4', - ) - - self.assertEqual(len(order), len(tested)) - - for idx, _ in enumerate(tested): - self.assertEqual(order[idx], tested[idx]) - - self.percy_snapshot('test assets includes') - - def test_external_files_init(self): - js_files = [ - 'https://www.google-analytics.com/analytics.js', - {'src': 'https://cdn.polyfill.io/v2/polyfill.min.js'}, - { - 'src': 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js', - 'integrity': 'sha256-43x9r7YRdZpZqTjDT5E0Vfrxn1ajIZLyYWtfAXsargA=', - 'crossorigin': 'anonymous' - }, - { - 'src': 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js', - 'integrity': 'sha256-7/yoZS3548fXSRXqc/xYzjsmuW3sFKzuvOCHd06Pmps=', - 'crossorigin': 'anonymous' - } - ] - - css_files = [ - 'https://codepen.io/chriddyp/pen/bWLwgP.css', - { - 'href': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css', - 'rel': 'stylesheet', - 'integrity': 'sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO', - 'crossorigin': 'anonymous' - } - ] - - app = Dash( - __name__, external_scripts=js_files, external_stylesheets=css_files) - - app.index_string = ''' - - - - {%metas%} - {%title%} - {%css%} - - -
-
- - {%app_entry%} -
- {%config%} - {%scripts%} - {%renderer%} -
- - - ''' - - app.layout = html.Div() - - self.startServer(app) - time.sleep(0.5) - - js_urls = [x['src'] if isinstance(x, dict) else x for x in js_files] - css_urls = [x['href'] if isinstance(x, dict) else x for x in css_files] - - for fmt, url in itertools.chain( - (("//script[@src='{}']", x) for x in js_urls), - (("//link[@href='{}']", x) for x in css_urls)): - self.driver.find_element_by_xpath(fmt.format(url)) - - # Ensure the button style was overloaded by reset (set to 38px in codepen) - btn = self.driver.find_element_by_id('btn') - btn_height = btn.value_of_css_property('height') - - self.assertEqual('18px', btn_height) - - # ensure ramda was loaded before the assets so they can use it. - lo_test = self.driver.find_element_by_id('ramda-test') - self.assertEqual('Hello World', lo_test.text) diff --git a/tests/integration/dash_assets/test_dash_assets.py b/tests/integration/dash_assets/test_dash_assets.py new file mode 100644 index 0000000000..53857e8c83 --- /dev/null +++ b/tests/integration/dash_assets/test_dash_assets.py @@ -0,0 +1,135 @@ +import json +import time +import itertools + +import dash_html_components as html +import dash_core_components as dcc + +from dash import Dash + + +def test_dada001_assets(dash_duo): + app = Dash(__name__, assets_ignore=".*ignored.*") + app.index_string = """ + + + + {%metas%} + {%title%} + {%css%} + + +
+ {%app_entry%} +
+ {%config%} + {%scripts%} + {%renderer%} +
+ + + """ + + app.layout = html.Div( + [html.Div("Content", id="content"), dcc.Input(id="test")], id="layout" + ) + + dash_duo.start_app_server(app) + + assert ( + dash_duo.find_element("body").value_of_css_property("margin") == "0px" + ), "margin is overloaded by assets css resource" + + assert ( + dash_duo.find_element("#content").value_of_css_property("padding") + == "8px" + ), "padding is overloaded by assets" + + tested = json.loads(dash_duo.wait_for_element("#tested").text) + + order = [ + u"load_first", + u"load_after", + u"load_after1", + u"load_after10", + u"load_after11", + u"load_after2", + u"load_after3", + u"load_after4", + ] + + assert order == tested, "the content and order is expected" + dash_duo.percy_snapshot("test assets includes") + + +def test_dada002_external_files_init(dash_duo): + js_files = [ + "https://www.google-analytics.com/analytics.js", + {"src": "https://cdn.polyfill.io/v2/polyfill.min.js"}, + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js", + "integrity": "sha256-43x9r7YRdZpZqTjDT5E0Vfrxn1ajIZLyYWtfAXsargA=", + "crossorigin": "anonymous", + }, + { + "src": "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js", + "integrity": "sha256-7/yoZS3548fXSRXqc/xYzjsmuW3sFKzuvOCHd06Pmps=", + "crossorigin": "anonymous", + }, + ] + + css_files = [ + "https://codepen.io/chriddyp/pen/bWLwgP.css", + { + "href": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css", + "rel": "stylesheet", + "integrity": "sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO", + "crossorigin": "anonymous", + }, + ] + + app = Dash( + __name__, external_scripts=js_files, external_stylesheets=css_files + ) + + app.index_string = """ + + + + {%metas%} + {%title%} + {%css%} + + +
+
+ + {%app_entry%} +
+ {%config%} + {%scripts%} + {%renderer%} +
+ + + """ + + app.layout = html.Div() + + dash_duo.start_app_server(app) + + js_urls = [x["src"] if isinstance(x, dict) else x for x in js_files] + css_urls = [x["href"] if isinstance(x, dict) else x for x in css_files] + + for fmt, url in itertools.chain( + (("//script[@src='{}']", x) for x in js_urls), + (("//link[@href='{}']", x) for x in css_urls), + ): + dash_duo.driver.find_element_by_xpath(fmt.format(url)) + + assert ( + dash_duo.find_element("#btn").value_of_css_property("height") == "18px" + ), "Ensure the button style was overloaded by reset (set to 38px in codepen)" + + # ensure ramda was loaded before the assets so they can use it. + assert dash_duo.find_element("#ramda-test").text == "Hello World" diff --git a/tests/integration/test_assets/hot_reload.css b/tests/integration/devtools/hr_assets/hot_reload.css similarity index 100% rename from tests/integration/test_assets/hot_reload.css rename to tests/integration/devtools/hr_assets/hot_reload.css diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index 18d92282b2..7dc22d38e2 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -6,7 +6,7 @@ from dash.exceptions import PreventUpdate -def test_dev001_python_errors(dash_duo): +def test_dveh001_python_errors(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( @@ -34,7 +34,7 @@ def update_output(n_clicks): dash_duo.percy_snapshot("devtools - python exception - start") dash_duo.find_element("#python").click() - dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.percy_snapshot("devtools - python exception - closed") dash_duo.find_element(".test-devtools-error-toggle").click() @@ -42,14 +42,15 @@ def update_output(n_clicks): dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.find_element("#python").click() - dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "2") + + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") dash_duo.percy_snapshot("devtools - python exception - 2 errors") dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - python exception - 2 errors open") -def test_dev002_prevent_update_not_in_error_msg(dash_duo): +def test_dveh002_prevent_update_not_in_error_msg(dash_duo): # raising PreventUpdate shouldn't display the error message app = dash.Dash(__name__) @@ -86,13 +87,13 @@ def update_output(n_clicks): # two exceptions fired, but only a single exception appeared in the UI: # the prevent default was not displayed - dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.percy_snapshot( "devtools - prevent update - only a single exception" ) -def test_dev003_validation_errors_in_place(dash_duo): +def test_dveh003_validation_errors_in_place(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( @@ -117,14 +118,14 @@ def update_output(n_clicks): ) dash_duo.find_element("#button").click() - dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.percy_snapshot("devtools - validation exception - closed") dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - validation exception - open") -def test_dev004_validation_errors_creation(dash_duo): +def test_dveh004_validation_errors_creation(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( @@ -151,14 +152,14 @@ def update_output(n_clicks): ) dash_duo.wait_for_element("#button").click() - dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.percy_snapshot("devtools - validation creation exception - closed") dash_duo.find_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot("devtools - validation creation exception - open") -def test_dev005_multiple_outputs(dash_duo): +def test_dveh005_multiple_outputs(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( [ @@ -194,7 +195,7 @@ def update_outputs(n_clicks): ) dash_duo.find_element("#multi-output").click() - dash_duo.wait_for_text_to_equal(".test-devtools-error-count", "1") + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "1") dash_duo.percy_snapshot("devtools - multi output python exception - closed") dash_duo.find_element(".test-devtools-error-toggle").click() diff --git a/tests/integration/devtools/test_devtools_ui.py b/tests/integration/devtools/test_devtools_ui.py index b65ca3ecba..d9589b7133 100644 --- a/tests/integration/devtools/test_devtools_ui.py +++ b/tests/integration/devtools/test_devtools_ui.py @@ -1,11 +1,10 @@ import dash_core_components as dcc import dash_html_components as html import dash - import dash.testing.wait as wait -def test_dev020_disable_props_check_config(dash_duo): +def test_dvui001_disable_props_check_config(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( [ @@ -37,7 +36,7 @@ def test_dev020_disable_props_check_config(dash_duo): ) -def test_dev021_disable_ui_config(dash_duo): +def test_dvui002_disable_ui_config(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( [ diff --git a/tests/integration/devtools/test_hot_reload.py b/tests/integration/devtools/test_hot_reload.py new file mode 100644 index 0000000000..c1119e6069 --- /dev/null +++ b/tests/integration/devtools/test_hot_reload.py @@ -0,0 +1,47 @@ +import os +import textwrap +import dash_html_components as html +import dash + + +def test_dvhr001_hot_reload(dash_duo): + app = dash.Dash(__name__, assets_folder="hr_assets") + app.layout = html.Div([html.H3("Hot reload")], id="hot-reload-content") + + dash_duo.start_app_server( + app, + dev_tools_hot_reload=True, + dev_tools_hot_reload_interval=100, + dev_tools_hot_reload_max_retry=30, + ) + + # default overload color is blue + dash_duo.wait_for_style_to_equal( + "#hot-reload-content", "background-color", "rgba(0, 0, 255, 1)" + ) + + hot_reload_file = os.path.join( + os.path.dirname(__file__), "hr_assets", "hot_reload.css" + ) + with open(hot_reload_file, "r+") as fp: + old_content = fp.read() + fp.truncate(0) + fp.seek(0) + fp.write( + textwrap.dedent( + """ + #hot-reload-content { + background-color: red; + } + """ + ) + ) + + try: + # red is live changed during the test execution + dash_duo.wait_for_style_to_equal( + "#hot-reload-content", "background-color", "rgba(255, 0, 0, 1)" + ) + finally: + with open(hot_reload_file, "w") as f: + f.write(old_content) diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index 57e075e2bb..a47054df35 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -3,10 +3,8 @@ import dash from dash.dependencies import Input, Output -from selenium.common.exceptions import TimeoutException - -def test_dev100_prop_check_errors_with_path(dash_duo): +def test_dvpc001_prop_check_errors_with_path(dash_duo): app = dash.Dash(__name__) test_cases = { diff --git a/tests/integration/test_assets/initial_state_dash_app_content.html b/tests/integration/renderer/initial_state_dash_app_content.html similarity index 100% rename from tests/integration/test_assets/initial_state_dash_app_content.html rename to tests/integration/renderer/initial_state_dash_app_content.html diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py new file mode 100644 index 0000000000..5b4a8e12e9 --- /dev/null +++ b/tests/integration/renderer/test_dependencies.py @@ -0,0 +1,47 @@ +from multiprocessing import Value + +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Input, Output + + +def test_rdd001_dependencies_on_components_that_dont_exist(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output-1")] + ) + + output_1_call_count = Value("i", 0) + + @app.callback(Output("output-1", "children"), [Input("input", "value")]) + def update_output(value): + output_1_call_count.value += 1 + return value + + # callback for component that doesn't yet exist in the dom + # in practice, it might get added by some other callback + app.config.supress_callback_exceptions = True + output_2_call_count = Value("i", 0) + + @app.callback(Output("output-2", "children"), [Input("input", "value")]) + def update_output_2(value): + output_2_call_count.value += 1 + return value + + dash_duo.start_app_server(app) + + assert dash_duo.find_element("#output-1").text == "initial value" + assert output_1_call_count.value == 1 and output_2_call_count.value == 0 + dash_duo.percy_snapshot(name="dependencies") + + dash_duo.find_element("#input").send_keys("a") + assert dash_duo.find_element("#output-1").text == "initial valuea" + + assert output_1_call_count.value == 2 and output_2_call_count.value == 0 + + rqs = dash_duo.redux_state_rqs + assert len(rqs) == 1 + assert rqs[0]["controllerId"] == "output-1.children" and not rqs[0]['rejected'] + + assert dash_duo.get_logs() == [] diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py new file mode 100644 index 0000000000..ec830ed64d --- /dev/null +++ b/tests/integration/renderer/test_due_diligence.py @@ -0,0 +1,114 @@ +import json +import os +import string + +from bs4 import BeautifulSoup +import requests + +import plotly +import dash_html_components as html +import dash + + +def test_rddd001_initial_state(dash_duo): + app = dash.Dash(__name__) + my_class_attrs = { + "id": "p.c.4", + "className": "my-class", + "title": "tooltip", + "style": {"color": "red", "fontSize": 30}, + } + # fmt:off + app.layout = html.Div([ + 'Basic string', + 3.14, + True, + None, + html.Div('Child div with basic string', **my_class_attrs), + html.Div(id='p.c.5'), + html.Div([ + html.Div('Grandchild div', id='p.c.6.p.c.0'), + html.Div([ + html.Div('Great grandchild', id='p.c.6.p.c.1.p.c.0'), + 3.14159, + 'another basic string' + ], id='p.c.6.p.c.1'), + html.Div([ + html.Div( + html.Div([ + html.Div([ + html.Div( + id='p.c.6.p.c.2.p.c.0.p.c.p.c.0.p.c.0' + ), + '', + html.Div( + id='p.c.6.p.c.2.p.c.0.p.c.p.c.0.p.c.2' + ) + ], id='p.c.6.p.c.2.p.c.0.p.c.p.c.0') + ], id='p.c.6.p.c.2.p.c.0.p.c'), + id='p.c.6.p.c.2.p.c.0' + ) + ], id='p.c.6.p.c.2') + ], id='p.c.6') + ]) + # fmt:on + + dash_duo.start_app_server(app) + + # Note: this .html file shows there's no undo/redo button by default + with open( + os.path.join( + os.path.dirname(__file__), "initial_state_dash_app_content.html" + ) + ) as fp: + expected_dom = BeautifulSoup(fp.read().strip(), "lxml") + + fetched_dom = BeautifulSoup( + dash_duo.find_element(dash_duo.dash_entry_locator).get_attribute( + "outerHTML" + ), + "lxml", + ) + + assert ( + fetched_dom.decode() == expected_dom.decode() + ), "the fetching rendered dom is expected" + + assert ( + dash_duo.get_logs() == [] + ), "Check that no errors or warnings were displayed" + + assert dash_duo.driver.execute_script( + "return JSON.parse(JSON.stringify(" + "window.store.getState().layout" + "))" + ) == json.loads( + json.dumps(app.layout, cls=plotly.utils.PlotlyJSONEncoder) + ), "the state layout is identical to app.layout" + + r = requests.get("{}/_dash-dependencies".format(dash_duo.server_url)) + assert r.status_code == 200 + assert ( + r.json() == [] + ), "no dependencies present in app as no callbacks are defined" + + assert dash_duo.redux_state_paths == { + abbr: [ + int(token) + if token in string.digits + else token.replace("p", "props").replace("c", "children") + for token in abbr.split(".") + ] + for abbr in ( + child.get("id") + for child in fetched_dom.find(id="react-entry-point").findChildren( + id=True + ) + ) + }, "paths should reflect to the component hierarchy" + + rqs = dash_duo.redux_state_rqs + assert not rqs, "no callback => no requestQueue" + + dash_duo.percy_snapshot(name="layout") + assert dash_duo.get_logs() == [], "console has no errors" diff --git a/tests/integration/renderer/test_state_and_input.py b/tests/integration/renderer/test_state_and_input.py new file mode 100644 index 0000000000..e422602aee --- /dev/null +++ b/tests/integration/renderer/test_state_and_input.py @@ -0,0 +1,113 @@ +from multiprocessing import Value +import time +import dash_html_components as html +import dash_core_components as dcc +import dash +from dash.dependencies import Input, Output, State +import dash.testing.wait as wait + + +def test_rdsi001_state_and_inputs(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(value="Initial Input", id="input"), + dcc.Input(value="Initial State", id="state"), + html.Div(id="output"), + ] + ) + + call_count = Value("i", 0) + + @app.callback( + Output("output", "children"), + [Input("input", "value")], + [State("state", "value")], + ) + def update_output(input, state): + call_count.value += 1 + return 'input="{}", state="{}"'.format(input, state) + + dash_duo.start_app_server(app) + + input_ = lambda: dash_duo.find_element("#input") + output_ = lambda: dash_duo.find_element("#output") + + assert ( + output_().text == 'input="Initial Input", state="Initial State"' + ), "callback gets called with initial input" + + input_().send_keys("x") + wait.until(lambda: call_count.value == 2, timeout=1) + assert ( + output_().text == 'input="Initial Inputx", state="Initial State"' + ), "output get updated with key `x`" + + dash_duo.find_element("#state").send_keys("z") + time.sleep(0.5) + assert call_count.value == 2, "state not trigger callback with 0.5 wait" + assert ( + output_().text == 'input="Initial Inputx", state="Initial State"' + ), "output remains the same as last step" + + input_().send_keys("y") + wait.until(lambda: call_count.value == 3, timeout=1) + assert ( + output_().text == 'input="Initial Inputxy", state="Initial Statez"' + ), "both input and state value get updated by input callback" + + +def test_rdsi002_event_properties_state_and_inputs(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + html.Button("Click Me", id="button"), + dcc.Input(value="Initial Input", id="input"), + dcc.Input(value="Initial State", id="state"), + html.Div(id="output"), + ] + ) + + call_count = Value("i", 0) + + @app.callback( + Output("output", "children"), + [Input("input", "value"), Input("button", "n_clicks")], + [State("state", "value")], + ) + def update_output(input, n_clicks, state): + call_count.value += 1 + return 'input="{}", state="{}"'.format(input, state) + + dash_duo.start_app_server(app) + + btn = lambda: dash_duo.find_element("#button") + output = lambda: dash_duo.find_element("#output") + + assert ( + output().text == 'input="Initial Input", state="Initial State"' + ), "callback gets called with initial input" + + btn().click() + wait.until(lambda: call_count.value == 2, timeout=1) + assert ( + output().text == 'input="Initial Input", state="Initial State"' + ), "button click doesn't count on output" + + dash_duo.find_element("#input").send_keys("x") + wait.until(lambda: call_count.value == 3, timeout=1) + + assert ( + output().text == 'input="Initial Inputx", state="Initial State"' + ), "output get updated with key `x`" + + dash_duo.find_element("#state").send_keys("z") + time.sleep(0.5) + assert call_count.value == 3, "state not trigger callback with 0.5 wait" + assert ( + output().text == 'input="Initial Inputx", state="Initial State"' + ), "output remains the same as last step" + + btn().click() + wait.until(lambda: call_count.value == 4, timeout=1) + assert output().text == 'input="Initial Inputx", state="Initial Statez"' diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index 21e188d1ea..618ef97cf8 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -1,11 +1,7 @@ # -*- coding: UTF-8 -*- -import os -import textwrap - -import pytest import dash from dash import Dash -from dash.dependencies import Input, Output, State +from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate from dash.development.base_component import Component import dash_html_components as html @@ -36,23 +32,6 @@ class Tests(IntegrationTests): def setUp(self): pass - def wait_for_style_to_equal(self, selector, style, assertion_style, timeout=TIMEOUT): - start = time.time() - exception = Exception('Time ran out, {} on {} not found'.format( - assertion_style, selector)) - while time.time() < start + timeout: - element = self.wait_for_element_by_css_selector(selector) - try: - self.assertEqual( - assertion_style, element.value_of_css_property(style)) - except Exception as e: - exception = e - else: - return - time.sleep(0.1) - - raise exception - def wait_for_element_by_css_selector(self, selector, timeout=TIMEOUT): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, selector)), @@ -108,109 +87,6 @@ def request_queue_assertions( if expected_length is not None: self.assertEqual(len(request_queue), expected_length) - def test_initial_state(self): - app = Dash(__name__) - my_class_attrs = { - 'id': 'p.c.4', - 'className': 'my-class', - 'title': 'tooltip', - 'style': {'color': 'red', 'fontSize': 30}, - } - app.layout = html.Div([ - 'Basic string', - 3.14, - True, - None, - html.Div('Child div with basic string', **my_class_attrs), - html.Div(id='p.c.5'), - html.Div([ - html.Div('Grandchild div', id='p.c.6.p.c.0'), - html.Div([ - html.Div('Great grandchild', id='p.c.6.p.c.1.p.c.0'), - 3.14159, - 'another basic string' - ], id='p.c.6.p.c.1'), - html.Div([ - html.Div( - html.Div([ - html.Div([ - html.Div( - id='p.c.6.p.c.2.p.c.0.p.c.p.c.0.p.c.0' - ), - '', - html.Div( - id='p.c.6.p.c.2.p.c.0.p.c.p.c.0.p.c.2' - ) - ], id='p.c.6.p.c.2.p.c.0.p.c.p.c.0') - ], id='p.c.6.p.c.2.p.c.0.p.c'), - id='p.c.6.p.c.2.p.c.0' - ) - ], id='p.c.6.p.c.2') - ], id='p.c.6') - ]) - - self.startServer(app) - el = self.wait_for_element_by_css_selector('#react-entry-point') - - # Note: this .html file shows there's no undo/redo button by default - _dash_app_content_html = os.path.join( - os.path.dirname(__file__), - 'test_assets', 'initial_state_dash_app_content.html') - with open(_dash_app_content_html) as fp: - rendered_dom = BeautifulSoup(fp.read().strip(), 'lxml') - fetched_dom = BeautifulSoup(el.get_attribute('outerHTML'), 'lxml') - - self.assertEqual( - fetched_dom.decode(), rendered_dom.decode(), - "the fetching rendered dom is expected ") - - # Check that no errors or warnings were displayed - self.assertTrue(self.is_console_clean()) - - self.assertEqual( - self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().layout' - '))' - ), - json.loads( - json.dumps(app.layout, cls=plotly.utils.PlotlyJSONEncoder)), - "the state layout is identical to app.layout" - ) - - r = requests.get('http://localhost:8050/_dash-dependencies') - self.assertEqual(r.status_code, 200) - self.assertEqual( - r.json(), [], - "no dependencies present in app as no callbacks are defined" - - ) - - self.assertEqual( - self.driver.execute_script( - 'return window.store.getState().paths' - ), - { - abbr: [ - int(token) if token in string.digits - else token.replace('p', 'props').replace('c', 'children') - for token in abbr.split('.') - ] - for abbr in ( - child.get('id') - for child in fetched_dom.find( - id='react-entry-point').findChildren(id=True) - ) - }, - "paths should refect to the component hierarchy" - ) - - self.request_queue_assertions(0) - - self.percy_snapshot(name='layout') - - self.assertTrue(self.is_console_clean()) - def click_undo(self): undo_selector = '._dash-undo-redo span:first-child div:last-child' undo = self.wait_for_element_by_css_selector(undo_selector) @@ -761,54 +637,6 @@ def chapter3_assertions(): chapter1_assertions() self.percy_snapshot(name='chapter-1-again') - def test_dependencies_on_components_that_dont_exist(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='input', value='initial value'), - html.Div(id='output-1') - ]) - - # standard callback - output_1_call_count = Value('i', 0) - - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - output_1_call_count.value += 1 - return value - - # callback for component that doesn't yet exist in the dom - # in practice, it might get added by some other callback - app.config.supress_callback_exceptions = True - output_2_call_count = Value('i', 0) - - @app.callback( - Output('output-2', 'children'), - [Input('input', 'value')] - ) - def update_output_2(value): - output_2_call_count.value += 1 - return value - - self.startServer(app) - - self.wait_for_text_to_equal('#output-1', 'initial value') - self.percy_snapshot(name='dependencies') - time.sleep(1.0) - self.assertEqual(output_1_call_count.value, 1) - self.assertEqual(output_2_call_count.value, 0) - - input = self.driver.find_element_by_id('input') - - input.send_keys('a') - self.wait_for_text_to_equal('#output-1', 'initial valuea') - time.sleep(1.0) - self.assertEqual(output_1_call_count.value, 2) - self.assertEqual(output_2_call_count.value, 0) - - self.request_queue_assertions(2) - - self.assertTrue(self.is_console_clean()) - def test_event_properties(self): app = Dash(__name__) app.layout = html.Div([ @@ -836,205 +664,6 @@ def update_output(n_clicks): wait_for(lambda: output().text == 'Click') self.assertEqual(call_count.value, 1) - def test_event_properties_and_state(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - dcc.Input(value='Initial State', id='state'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - [Input('button', 'n_clicks')], - [State('state', 'value')]) - def update_output(n_clicks, value): - if(not n_clicks): - raise PreventUpdate - call_count.value += 1 - return value - - self.startServer(app) - btn = self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') - - self.assertEqual(call_count.value, 0) - self.assertEqual(output().text, '') - - btn.click() - wait_for(lambda: output().text == 'Initial State') - self.assertEqual(call_count.value, 1) - - # Changing state shouldn't fire the callback - state = self.driver.find_element_by_id('state') - state.send_keys('x') - time.sleep(0.75) - self.assertEqual(output().text, 'Initial State') - self.assertEqual(call_count.value, 1) - - btn.click() - wait_for(lambda: output().text == 'Initial Statex') - self.assertEqual(call_count.value, 2) - - def test_event_properties_state_and_inputs(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - dcc.Input(value='Initial Input', id='input'), - dcc.Input(value='Initial State', id='state'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - [Input('input', 'value'), Input('button', 'n_clicks')], - [State('state', 'value')]) - def update_output(input, n_clicks, state): - call_count.value += 1 - return 'input="{}", state="{}"'.format(input, state) - - self.startServer(app) - btn = lambda: self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') - input = lambda: self.driver.find_element_by_id('input') - state = lambda: self.driver.find_element_by_id('state') - - # callback gets called with initial input - self.assertEqual( - output().text, - 'input="Initial Input", state="Initial State"' - ) - - btn().click() - wait_for(lambda: call_count.value == 2) - self.assertEqual( - output().text, - 'input="Initial Input", state="Initial State"') - - input().send_keys('x') - wait_for(lambda: call_count.value == 3) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - state().send_keys('x') - time.sleep(0.75) - self.assertEqual(call_count.value, 3) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - btn().click() - wait_for(lambda: call_count.value == 4) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial Statex"') - - @pytest.mark.flakey - def test_state_and_inputs(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(value='Initial Input', id='input'), - dcc.Input(value='Initial State', id='state'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback( - Output('output', 'children'), [Input('input', 'value')], - [State('state', 'value')]) - def update_output(input, state): - call_count.value += 1 - return 'input="{}", state="{}"'.format(input, state) - - self.startServer(app) - output = lambda: self.driver.find_element_by_id('output') - input = lambda: self.driver.find_element_by_id('input') - state = lambda: self.driver.find_element_by_id('state') - - # callback gets called with initial input - time.sleep(0.5) - self.assertEqual( - output().text, - 'input="Initial Input", state="Initial State"' - ) - - input().send_keys('x') - wait_for(lambda: call_count.value == 2) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - state().send_keys('x') - time.sleep(0.75) - self.assertEqual(call_count.value, 2) - self.assertEqual( - output().text, - 'input="Initial Inputx", state="Initial State"') - - input().send_keys('y') - wait_for(lambda: call_count.value == 3) - self.assertEqual( - output().text, - 'input="Initial Inputxy", state="Initial Statex"') - - def test_event_properties_creating_inputs(self): - app = Dash(__name__) - - ids = { - k: k for k in ['button', 'button-output', 'input', 'input-output'] - } - app.layout = html.Div([ - html.Button(id=ids['button']), - html.Div(id=ids['button-output']) - ]) - for script in dcc._js_dist: - script['namespace'] = 'dash_core_components' - app.scripts.append_script(script) - - app.config.supress_callback_exceptions = True - call_counts = { - ids['input-output']: Value('i', 0), - ids['button-output']: Value('i', 0) - } - - @app.callback( - Output(ids['button-output'], 'children'), - [Input(ids['button'], 'n_clicks')]) - def display(n_clicks): - if(not n_clicks): - raise PreventUpdate - call_counts['button-output'].value += 1 - return html.Div([ - dcc.Input(id=ids['input'], value='initial state'), - html.Div(id=ids['input-output']) - ]) - - @app.callback( - Output(ids['input-output'], 'children'), - [Input(ids['input'], 'value')]) - def update_input(value): - call_counts['input-output'].value += 1 - return 'Input is equal to "{}"'.format(value) - - self.startServer(app) - time.sleep(1) - self.assertEqual(call_counts[ids['button-output']].value, 0) - self.assertEqual(call_counts[ids['input-output']].value, 0) - - btn = lambda: self.driver.find_element_by_id(ids['button']) - output = lambda: self.driver.find_element_by_id(ids['input-output']) - with self.assertRaises(Exception): - output() - - btn().click() - wait_for(lambda: call_counts[ids['input-output']].value == 1) - self.assertEqual(call_counts[ids['button-output']].value, 1) - self.assertEqual(output().text, 'Input is equal to "initial state"') - def test_chained_dependencies_direct_lineage(self): app = Dash(__name__) app.layout = html.Div([ @@ -1914,45 +1543,6 @@ def render_content(tab): self.wait_for_text_to_equal('#graph2_info', json.dumps(graph_2_expected_clickdata)) - def test_hot_reload(self): - app = dash.Dash(__name__, assets_folder='test_assets') - - app.layout = html.Div([ - html.H3('Hot reload') - ], id='hot-reload-content') - - self.startServer( - app, - dev_tools_hot_reload=True, - dev_tools_hot_reload_interval=100, - dev_tools_hot_reload_max_retry=30, - ) - - hot_reload_file = os.path.join( - os.path.dirname(__file__), 'test_assets', 'hot_reload.css') - - self.wait_for_style_to_equal( - '#hot-reload-content', 'background-color', 'rgba(0, 0, 255, 1)' - ) - - with open(hot_reload_file, 'r+') as f: - old_content = f.read() - f.truncate(0) - f.seek(0) - f.write(textwrap.dedent(''' - #hot-reload-content { - background-color: red; - } - ''')) - - try: - self.wait_for_style_to_equal( - '#hot-reload-content', 'background-color', 'rgba(255, 0, 0, 1)' - ) - finally: - with open(hot_reload_file, 'w') as f: - f.write(old_content) - def test_single_input_multi_outputs_on_multiple_components(self): call_count = Value('i') From 6690ccf12bceee18e9f6286900a9fb0714f16179 Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 11:44:48 -0400 Subject: [PATCH 35/46] :recycle: API polish --- dash/testing/browser.py | 16 ++-------------- dash/testing/dash_page.py | 20 ++++++++++++++++++++ dash/testing/locators.py | 9 --------- 3 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 dash/testing/dash_page.py delete mode 100644 dash/testing/locators.py diff --git a/dash/testing/browser.py b/dash/testing/browser.py index cf192a0797..7565f5ae78 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -20,14 +20,14 @@ ) from dash.testing.wait import text_to_equal, style_to_equal -from dash.testing.locators import DashLocatorsMixin +from dash.testing.dash_page import DashPageMixin from dash.testing.errors import DashAppLoadingError logger = logging.getLogger(__name__) -class Browser(DashLocatorsMixin): +class Browser(DashPageMixin): def __init__(self, browser, remote=None, wait_timeout=10): self._browser = browser.lower() self._wait_timeout = wait_timeout @@ -263,15 +263,3 @@ def server_url(self, value): """ self._url = value self.wait_for_page() - - @property - def redux_state_paths(self): - return self.driver.execute_script( - "return window.store.getState().paths" - ) - - @property - def redux_state_rqs(self): - return self.driver.execute_script( - "return window.store.getState().requestQueue" - ) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py new file mode 100644 index 0000000000..8037d2aff3 --- /dev/null +++ b/dash/testing/dash_page.py @@ -0,0 +1,20 @@ +class DashPageMixin(object): + @property + def devtools_error_count_locator(self): + return ".test-devtools-error-count" + + @property + def dash_entry_locator(self): + return "#react-entry-point" + + @property + def redux_state_paths(self): + return self.driver.execute_script( + "return window.store.getState().paths" + ) + + @property + def redux_state_rqs(self): + return self.driver.execute_script( + "return window.store.getState().requestQueue" + ) diff --git a/dash/testing/locators.py b/dash/testing/locators.py deleted file mode 100644 index 177d188f68..0000000000 --- a/dash/testing/locators.py +++ /dev/null @@ -1,9 +0,0 @@ -# pylint: disable=too-few-public-methods -class DashLocatorsMixin(object): - @property - def devtools_error_count_locator(self): - return ".test-devtools-error-count" - - @property - def dash_entry_locator(self): - return "#react-entry-point" From 43596f5aead9275270145cea4b9f45677e159ab0 Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 11:45:02 -0400 Subject: [PATCH 36/46] minor update tcid --- tests/integration/renderer/test_dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index 5b4a8e12e9..15771026d7 100644 --- a/tests/integration/renderer/test_dependencies.py +++ b/tests/integration/renderer/test_dependencies.py @@ -6,7 +6,7 @@ from dash.dependencies import Input, Output -def test_rdd001_dependencies_on_components_that_dont_exist(dash_duo): +def test_rddp001_dependencies_on_components_that_dont_exist(dash_duo): app = dash.Dash(__name__) app.layout = html.Div( [dcc.Input(id="input", value="initial value"), html.Div(id="output-1")] From bccbf2974c7f1ed44d1c6a7f005d919d4277e80d Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 22:42:49 -0400 Subject: [PATCH 37/46] :recycle: polish API --- dash/testing/browser.py | 22 +++++++++++++++++++--- dash/testing/dash_page.py | 17 +++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 7565f5ae78..8511d330fb 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -10,6 +10,7 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.action_chains import ActionChains @@ -117,7 +118,7 @@ def wait_for_element_by_css_selector(self, selector, timeout=None): EC.presence_of_element_located, ((By.CSS_SELECTOR, selector),), timeout, - "cannot find_element using the css selector", + "timeout {} => waiting for selector {}".format(timeout, selector), ) def wait_for_style_to_equal(self, selector, style, val, timeout=None): @@ -138,9 +139,9 @@ def wait_for_text_to_equal(self, selector, text, timeout=None): msg="text -> {} not found within {}s".format(text, timeout), ) - def wait_for_page(self, timeout=10): + def wait_for_page(self, url=None, timeout=10): - self.driver.get(self.server_url) + self.driver.get(self.server_url if url is None else url) try: self.wait_for_element_by_css_selector( self.dash_entry_locator, timeout=timeout @@ -212,6 +213,10 @@ def _get_firefox(): def _is_windows(): return sys.platform == "win32" + def multiple_click(self, css_selector, clicks): + for _ in range(clicks): + self.driver.find_element(css_selector).click() + def js_click(self, elem): """click in native javascript way note: this is NOT the recommended way to click""" @@ -223,6 +228,17 @@ def mouse_click(self, elem): except NoSuchElementException: logger.exception("mouse_click on wrong element") + def clear_input(self, elem): + ( + ActionChains(self.driver) + .click(elem) + .send_keys(Keys.HOME) + .key_down(Keys.SHIFT) + .send_keys(Keys.END) + .key_up(Keys.SHIFT) + .send_keys(Keys.DELETE) + ).perform() + def get_logs(self): """get_logs works only with chrome webdriver""" if self.driver.name.lower() == "chrome": diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 8037d2aff3..3ba62af000 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -1,4 +1,13 @@ +from bs4 import BeautifulSoup + + class DashPageMixin(object): + def _get_dash_dom_by_attribute(self, attr): + return BeautifulSoup( + self.find_element(self.dash_entry_locator).get_attribute(attr), + "lxml", + ) + @property def devtools_error_count_locator(self): return ".test-devtools-error-count" @@ -7,6 +16,14 @@ def devtools_error_count_locator(self): def dash_entry_locator(self): return "#react-entry-point" + @property + def dash_outerhtml_dom(self): + return self._get_dash_dom_by_attribute('outerHTML') + + @property + def dash_innerhtml_dom(self): + return self._get_dash_dom_by_attribute('innerHTML') + @property def redux_state_paths(self): return self.driver.execute_script( From 6782fe7ad7da19383e1c2292f66f010c16d6a706 Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 22:43:45 -0400 Subject: [PATCH 38/46] :hocho: move out --- tests/integration/IntegrationTests.py | 2 +- tests/integration/test_render.py | 195 -------------------------- 2 files changed, 1 insertion(+), 196 deletions(-) diff --git a/tests/integration/IntegrationTests.py b/tests/integration/IntegrationTests.py index a92bc927cd..0db03a6b81 100644 --- a/tests/integration/IntegrationTests.py +++ b/tests/integration/IntegrationTests.py @@ -37,7 +37,7 @@ def setUpClass(cls): options.binary_location = os.environ['DASH_TEST_CHROMEPATH'] cls.driver = webdriver.Chrome( - chrome_options=options, desired_capabilities=capabilities, + options=options, desired_capabilities=capabilities, service_args=["--verbose", "--log-path=chrome.log"] ) diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index 618ef97cf8..88775a9ebd 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -53,17 +53,6 @@ def wait_for_text_to_equal(self, selector, assertion_text, timeout=TIMEOUT): ) ) - def clear_input(self, input_element): - ( - ActionChains(self.driver) - .click(input_element) - .send_keys(Keys.HOME) - .key_down(Keys.SHIFT) - .send_keys(Keys.END) - .key_up(Keys.SHIFT) - .send_keys(Keys.DELETE) - ).perform() - def request_queue_assertions( self, check_rejected=True, expected_length=None): request_queue = self.driver.execute_script( @@ -180,153 +169,6 @@ def test_of_falsy_child(self): self.assertTrue(self.is_console_clean()) - def test_simple_callback(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div( - html.Div([ - 1.5, - None, - 'string', - html.Div(id='output-1') - ]) - ) - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - call_count.value = call_count.value + 1 - return value - - self.startServer(app) - - self.wait_for_text_to_equal('#output-1', 'initial value') - self.percy_snapshot(name='simple-callback-1') - - input1 = self.wait_for_element_by_css_selector('#input') - self.clear_input(input1) - - input1.send_keys('hello world') - - self.wait_for_text_to_equal('#output-1', 'hello world') - self.percy_snapshot(name='simple-callback-2') - - self.assertEqual( - call_count.value, - # an initial call to retrieve the first value + clear is now one - 2 + - # one for each hello world character - len('hello world') - ) - - self.request_queue_assertions( - expected_length=1, - check_rejected=False) - - self.assertTrue(self.is_console_clean()) - - def test_callbacks_generating_children(self): - ''' Modify the DOM tree by adding new - components in the callbacks - ''' - - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div(id='output') - ]) - - @app.callback(Output('output', 'children'), [Input('input', 'value')]) - def pad_output(input): - return html.Div([ - dcc.Input( - id='sub-input-1', - value='sub input initial value' - ), - html.Div(id='sub-output-1') - ]) - - call_count = Value('i', 0) - - # these components don't exist in the initial render - app.config.supress_callback_exceptions = True - - @app.callback( - Output('sub-output-1', 'children'), - [Input('sub-input-1', 'value')] - ) - def update_input(value): - call_count.value = call_count.value + 1 - return value - - self.startServer(app) - - wait_for(lambda: call_count.value == 1) - - pad_input, pad_div = BeautifulSoup( - self.driver.find_element_by_css_selector( - '#react-entry-point').get_attribute('innerHTML'), - 'lxml').select_one('#output > div').contents - - self.assertEqual(pad_input.attrs['value'], 'sub input initial value') - self.assertEqual(pad_input.attrs['id'], 'sub-input-1') - self.assertEqual(pad_input.name, 'input') - - self.assertTrue( - pad_div.text == pad_input.attrs['value'] and - pad_div.get('id') == 'sub-output-1', - "the sub-output-1 content reflects to sub-input-1 value" - ) - - self.percy_snapshot(name='callback-generating-function-1') - - # the paths should include these new output IDs - self.assertEqual( - self.driver.execute_script('return window.store.getState().paths'), - { - 'input': [ - 'props', 'children', 0 - ], - 'output': ['props', 'children', 1], - 'sub-input-1': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 0 - ], - 'sub-output-1': [ - 'props', 'children', 1, - 'props', 'children', - 'props', 'children', 1 - ] - } - ) - - # editing the input should modify the sub output - sub_input = self.driver.find_element_by_id('sub-input-1') - - sub_input.send_keys('deadbeef') - self.wait_for_text_to_equal( - '#sub-output-1', - pad_input.attrs['value'] + 'deadbeef') - - self.assertEqual( - call_count.value, len('deadbeef') + 1, - "the total updates is initial one + the text input changes") - - self.request_queue_assertions(call_count.value + 1) - self.percy_snapshot(name='callback-generating-function-2') - - self.assertTrue(self.is_console_clean()) - def test_radio_buttons_callbacks_generating_children(self): self.maxDiff = 100 * 1000 app = Dash(__name__) @@ -953,43 +795,6 @@ def dynamic_output(*args): self.assertEqual(call_count.value, 1) - def test_callbacks_called_multiple_times_and_out_of_order(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button(id='input', n_clicks=0), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback( - Output('output', 'children'), - [Input('input', 'n_clicks')]) - def update_output(n_clicks): - call_count.value = call_count.value + 1 - if n_clicks == 1: - time.sleep(4) - return n_clicks - - self.startServer(app) - button = self.wait_for_element_by_css_selector('#input') - button.click() - button.click() - time.sleep(8) - self.percy_snapshot( - name='test_callbacks_called_multiple_times_and_out_of_order' - ) - self.assertEqual(call_count.value, 3) - self.assertEqual( - self.driver.find_element_by_id('output').text, - '2' - ) - request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' - ) - self.assertFalse(request_queue[0]['rejected']) - self.assertEqual(len(request_queue), 1) - def test_callbacks_called_multiple_times_and_out_of_order_multi_output(self): app = Dash(__name__) app.layout = html.Div([ From 5e65b843476369f164ef3fa20bd02e5e3082b87f Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 22:46:25 -0400 Subject: [PATCH 39/46] :white_check_mark: callbacks and other minor changes --- .../callbacks/test_basic_callback.py | 151 +++++++ .../callbacks/test_multiple_callbacks.py | 39 ++ .../integration/devtools/test_props_check.py | 373 +++++++++--------- .../renderer/test_due_diligence.py | 7 +- 4 files changed, 376 insertions(+), 194 deletions(-) create mode 100644 tests/integration/callbacks/test_basic_callback.py create mode 100644 tests/integration/callbacks/test_multiple_callbacks.py diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py new file mode 100644 index 0000000000..9c064b5391 --- /dev/null +++ b/tests/integration/callbacks/test_basic_callback.py @@ -0,0 +1,151 @@ +from multiprocessing import Value + +from bs4 import BeautifulSoup + +import dash_core_components as dcc +import dash_html_components as html +import dash +from dash.dependencies import Input, Output + + +def test_cbsc001_simple_callback(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + dcc.Input(id="input", value="initial value"), + html.Div(html.Div([1.5, None, "string", html.Div(id="output-1")])), + ] + ) + call_count = Value("i", 0) + + @app.callback(Output("output-1", "children"), [Input("input", "value")]) + def update_output(value): + call_count.value = call_count.value + 1 + return value + + dash_duo.start_app_server(app) + + assert dash_duo.find_element("#output-1").text == "initial value" + dash_duo.percy_snapshot(name="simple-callback-initial") + + input_ = dash_duo.find_element("#input") + dash_duo.clear_input(input_) + + input_.send_keys("hello world") + + assert dash_duo.find_element("#output-1").text == "hello world" + dash_duo.percy_snapshot(name="simple-callback-hello-world") + + assert call_count.value == 2 + len( + "hello world" + ), "initial count + each key stroke" + + rqs = dash_duo.redux_state_rqs + assert len(rqs) == 1 + + assert dash_duo.get_logs() == [] + + +def test_cbsc002_callbacks_generating_children(dash_duo): + """ Modify the DOM tree by adding new components in the callbacks""" + + app = dash.Dash(__name__) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) + + @app.callback(Output("output", "children"), [Input("input", "value")]) + def pad_output(input): + return html.Div( + [ + dcc.Input(id="sub-input-1", value="sub input initial value"), + html.Div(id="sub-output-1"), + ] + ) + + call_count = Value("i", 0) + + # these components don't exist in the initial render + app.config.supress_callback_exceptions = True + + @app.callback( + Output("sub-output-1", "children"), [Input("sub-input-1", "value")] + ) + def update_input(value): + call_count.value = call_count.value + 1 + return value + + dash_duo.start_app_server(app) + + assert call_count.value == 1, "called once at initial stage" + + pad_input, pad_div = ( + BeautifulSoup( + dash_duo.driver.find_element_by_css_selector( + "#react-entry-point" + ).get_attribute("innerHTML"), + "lxml", + ) + .select_one("#output > div") + .contents + ) + + dash_duo.assertEqual(pad_input.attrs["value"], "sub input initial value") + dash_duo.assertEqual(pad_input.attrs["id"], "sub-input-1") + dash_duo.assertEqual(pad_input.name, "input") + + dash_duo.assertTrue( + pad_div.text == pad_input.attrs["value"] + and pad_div.get("id") == "sub-output-1", + "the sub-output-1 content reflects to sub-input-1 value", + ) + + dash_duo.percy_snapshot(name="callback-generating-function-1") + + # the paths should include these new output IDs + dash_duo.assertEqual( + dash_duo.driver.execute_script("return window.store.getState().paths"), + { + "input": ["props", "children", 0], + "output": ["props", "children", 1], + "sub-input-1": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 0, + ], + "sub-output-1": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 1, + ], + }, + ) + + # editing the input should modify the sub output + sub_input = dash_duo.driver.find_element_by_id("sub-input-1") + + sub_input.send_keys("deadbeef") + dash_duo.wait_for_text_to_equal( + "#sub-output-1", pad_input.attrs["value"] + "deadbeef" + ) + + dash_duo.assertEqual( + call_count.value, + len("deadbeef") + 1, + "the total updates is initial one + the text input changes", + ) + + dash_duo.request_queue_assertions(call_count.value + 1) + dash_duo.percy_snapshot(name="callback-generating-function-2") + + dash_duo.assertTrue(dash_duo.is_console_clean()) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py new file mode 100644 index 0000000000..1c04a0f62c --- /dev/null +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -0,0 +1,39 @@ +import time +from multiprocessing import Value + +import dash_html_components as html +import dash +from dash.dependencies import Input, Output + + +def test_cbmt001_called_multiple_times_and_out_of_order(dash_duo): + app = dash.Dash(__name__) + app.layout = html.Div( + [html.Button(id="input", n_clicks=0), html.Div(id="output")] + ) + + call_count = Value("i", 0) + + @app.callback(Output("output", "children"), [Input("input", "n_clicks")]) + def update_output(n_clicks): + call_count.value = call_count.value + 1 + if n_clicks == 1: + time.sleep(1) + return n_clicks + + dash_duo.start_app_server(app) + dash_duo.multiple_click("#input", clicks=3) + + time.sleep(3) + + assert call_count.value == 4, "get called 4 times" + assert ( + dash_duo.find_element("#output").text == "3" + ), "clicked button 3 times" + + rqs = dash_duo.redux_state_rqs + assert len(rqs) == 1 and not rqs[0]["rejected"] + + dash_duo.percy_snapshot( + name="test_callbacks_called_multiple_times_and_out_of_order" + ) diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index a47054df35..f5b5e17568 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -4,191 +4,186 @@ from dash.dependencies import Input, Output -def test_dvpc001_prop_check_errors_with_path(dash_duo): - app = dash.Dash(__name__) - - test_cases = { - "not-boolean": { - "fail": True, - "name": 'simple "not a boolean" check', - "component": dcc.Graph, - "props": {"animate": 0}, - }, - "missing-required-nested-prop": { - "fail": True, - "name": 'missing required "value" inside options', - "component": dcc.Checklist, - "props": {"options": [{"label": "hello"}], "values": ["test"]}, - }, - "invalid-nested-prop": { - "fail": True, - "name": "invalid nested prop", - "component": dcc.Checklist, - "props": { - "options": [{"label": "hello", "value": True}], - "values": ["test"], - }, - }, - "invalid-arrayOf": { - "fail": True, - "name": "invalid arrayOf", - "component": dcc.Checklist, - "props": {"options": "test", "values": []}, - }, - "invalid-oneOf": { - "fail": True, - "name": "invalid oneOf", - "component": dcc.Input, - "props": {"type": "test"}, - }, - "invalid-oneOfType": { - "fail": True, - "name": "invalid oneOfType", - "component": dcc.Input, - "props": {"max": True}, - }, - "invalid-shape-1": { - "fail": True, - "name": "invalid key within nested object", - "component": dcc.Graph, - "props": {"config": {"asdf": "that"}}, - }, - "invalid-shape-2": { - "fail": True, - "name": "nested object with bad value", - "component": dcc.Graph, - "props": {"config": {"edits": {"legendPosition": "asdf"}}}, - }, - "invalid-shape-3": { - "fail": True, - "name": "invalid oneOf within nested object", - "component": dcc.Graph, - "props": {"config": {"toImageButtonOptions": {"format": "asdf"}}}, - }, - "invalid-shape-4": { - "fail": True, - "name": "invalid key within deeply nested object", - "component": dcc.Graph, - "props": {"config": {"toImageButtonOptions": {"asdf": "test"}}}, - }, - "invalid-shape-5": { - "fail": True, - "name": "invalid not required key", - "component": dcc.Dropdown, - "props": { - "options": [ - {"label": "new york", "value": "ny", "typo": "asdf"} - ] - }, - }, - "string-not-list": { - "fail": True, - "name": "string-not-a-list", - "component": dcc.Checklist, - "props": { - "options": [{"label": "hello", "value": "test"}], - "values": "test", - }, - }, - "no-properties": { - "fail": False, - "name": "no properties", - "component": dcc.Graph, - "props": {}, - }, - "nested-children": { - "fail": True, - "name": "nested children", - "component": html.Div, - "props": {"children": [[1]]}, - }, - "deeply-nested-children": { - "fail": True, - "name": "deeply nested children", - "component": html.Div, - "props": {"children": html.Div([html.Div([3, html.Div([[10]])])])}, - }, - "dict": { - "fail": True, - "name": "returning a dictionary", - "component": html.Div, - "props": {"children": {"hello": "world"}}, - }, - "nested-prop-failure": { - "fail": True, - "name": "nested string instead of number/null", - "component": dcc.Graph, - "props": { - "figure": {"data": [{}]}, - "config": { - "toImageButtonOptions": {"width": None, "height": "test"} - }, - }, - }, - "allow-null": { - "fail": False, - "name": "nested null", - "component": dcc.Graph, - "props": { - "figure": {"data": [{}]}, - "config": { - "toImageButtonOptions": {"width": None, "height": None} - }, +test_cases = { + "not-boolean": { + "fail": True, + "name": 'simple "not a boolean" check', + "component": dcc.Graph, + "props": {"animate": 0}, + }, + "missing-required-nested-prop": { + "fail": True, + "name": 'missing required "value" inside options', + "component": dcc.Checklist, + "props": {"options": [{"label": "hello"}], "values": ["test"]}, + }, + "invalid-nested-prop": { + "fail": True, + "name": "invalid nested prop", + "component": dcc.Checklist, + "props": { + "options": [{"label": "hello", "value": True}], + "values": ["test"], + }, + }, + "invalid-arrayOf": { + "fail": True, + "name": "invalid arrayOf", + "component": dcc.Checklist, + "props": {"options": "test", "values": []}, + }, + "invalid-oneOf": { + "fail": True, + "name": "invalid oneOf", + "component": dcc.Input, + "props": {"type": "test"}, + }, + "invalid-oneOfType": { + "fail": True, + "name": "invalid oneOfType", + "component": dcc.Input, + "props": {"max": True}, + }, + "invalid-shape-1": { + "fail": True, + "name": "invalid key within nested object", + "component": dcc.Graph, + "props": {"config": {"asdf": "that"}}, + }, + "invalid-shape-2": { + "fail": True, + "name": "nested object with bad value", + "component": dcc.Graph, + "props": {"config": {"edits": {"legendPosition": "asdf"}}}, + }, + "invalid-shape-3": { + "fail": True, + "name": "invalid oneOf within nested object", + "component": dcc.Graph, + "props": {"config": {"toImageButtonOptions": {"format": "asdf"}}}, + }, + "invalid-shape-4": { + "fail": True, + "name": "invalid key within deeply nested object", + "component": dcc.Graph, + "props": {"config": {"toImageButtonOptions": {"asdf": "test"}}}, + }, + "invalid-shape-5": { + "fail": True, + "name": "invalid not required key", + "component": dcc.Dropdown, + "props": { + "options": [{"label": "new york", "value": "ny", "typo": "asdf"}] + }, + }, + "string-not-list": { + "fail": True, + "name": "string-not-a-list", + "component": dcc.Checklist, + "props": { + "options": [{"label": "hello", "value": "test"}], + "values": "test", + }, + }, + "no-properties": { + "fail": False, + "name": "no properties", + "component": dcc.Graph, + "props": {}, + }, + "nested-children": { + "fail": True, + "name": "nested children", + "component": html.Div, + "props": {"children": [[1]]}, + }, + "deeply-nested-children": { + "fail": True, + "name": "deeply nested children", + "component": html.Div, + "props": {"children": html.Div([html.Div([3, html.Div([[10]])])])}, + }, + "dict": { + "fail": True, + "name": "returning a dictionary", + "component": html.Div, + "props": {"children": {"hello": "world"}}, + }, + "nested-prop-failure": { + "fail": True, + "name": "nested string instead of number/null", + "component": dcc.Graph, + "props": { + "figure": {"data": [{}]}, + "config": { + "toImageButtonOptions": {"width": None, "height": "test"} }, }, - "allow-null-2": { - "fail": False, - "name": "allow null as value", - "component": dcc.Dropdown, - "props": {"value": None}, - }, - "allow-null-3": { - "fail": False, - "name": "allow null in properties", - "component": dcc.Input, - "props": {"value": None}, - }, - "allow-null-4": { - "fail": False, - "name": "allow null in oneOfType", - "component": dcc.Store, - "props": {"id": "store", "data": None}, - }, - "long-property-string": { - "fail": True, - "name": "long property string with id", - "component": html.Div, - "props": {"id": "pink div", "style": "color: hotpink; " * 1000}, - }, - "multiple-wrong-values": { - "fail": True, - "name": "multiple wrong props", - "component": dcc.Dropdown, - "props": {"id": "dropdown", "value": 10, "options": "asdf"}, - }, - "boolean-html-properties": { - "fail": True, - "name": "dont allow booleans for dom props", - "component": html.Div, - "props": {"contentEditable": True}, - }, - "allow-exact-with-optional-and-required-1": { - "fail": False, - "name": "allow exact with optional and required keys", - "component": dcc.Dropdown, - "props": { - "options": [ - {"label": "new york", "value": "ny", "disabled": False} - ] - }, - }, - "allow-exact-with-optional-and-required-2": { - "fail": False, - "name": "allow exact with optional and required keys 2", - "component": dcc.Dropdown, - "props": {"options": [{"label": "new york", "value": "ny"}]}, - }, - } + }, + "allow-null": { + "fail": False, + "name": "nested null", + "component": dcc.Graph, + "props": { + "figure": {"data": [{}]}, + "config": {"toImageButtonOptions": {"width": None, "height": None}}, + }, + }, + "allow-null-2": { + "fail": False, + "name": "allow null as value", + "component": dcc.Dropdown, + "props": {"value": None}, + }, + "allow-null-3": { + "fail": False, + "name": "allow null in properties", + "component": dcc.Input, + "props": {"value": None}, + }, + "allow-null-4": { + "fail": False, + "name": "allow null in oneOfType", + "component": dcc.Store, + "props": {"id": "store", "data": None}, + }, + "long-property-string": { + "fail": True, + "name": "long property string with id", + "component": html.Div, + "props": {"id": "pink div", "style": "color: hotpink; " * 1000}, + }, + "multiple-wrong-values": { + "fail": True, + "name": "multiple wrong props", + "component": dcc.Dropdown, + "props": {"id": "dropdown", "value": 10, "options": "asdf"}, + }, + "boolean-html-properties": { + "fail": True, + "name": "dont allow booleans for dom props", + "component": html.Div, + "props": {"contentEditable": True}, + }, + "allow-exact-with-optional-and-required-1": { + "fail": False, + "name": "allow exact with optional and required keys", + "component": dcc.Dropdown, + "props": { + "options": [{"label": "new york", "value": "ny", "disabled": False}] + }, + }, + "allow-exact-with-optional-and-required-2": { + "fail": False, + "name": "allow exact with optional and required keys 2", + "component": dcc.Dropdown, + "props": {"options": [{"label": "new york", "value": "ny"}]}, + }, +} + + +def test_dvpc001_prop_check_errors_with_path(dash_duo): + app = dash.Dash(__name__) app.layout = html.Div([html.Div(id="content"), dcc.Location(id="location")]) @@ -212,19 +207,21 @@ def display_content(pathname): dev_tools_hot_reload=False, ) - for tcid in test_cases: - dash_duo.driver.get("{}/{}".format(dash_duo.server_url, tcid)) - if test_cases[tcid]["fail"]: + for tc in test_cases: + route_url = "{}/{}".format(dash_duo.server_url, tc) + dash_duo.wait_for_page(url=route_url) + + if test_cases[tc]["fail"]: dash_duo.wait_for_element(".test-devtools-error-toggle").click() dash_duo.percy_snapshot( "devtools validation exception: {}".format( - test_cases[tcid]["name"] + test_cases[tc]["name"] ) ) else: dash_duo.wait_for_element("#new-component") dash_duo.percy_snapshot( "devtools validation no exception: {}".format( - test_cases[tcid]["name"] + test_cases[tc]["name"] ) ) diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py index ec830ed64d..02a7c69d6a 100644 --- a/tests/integration/renderer/test_due_diligence.py +++ b/tests/integration/renderer/test_due_diligence.py @@ -63,12 +63,7 @@ def test_rddd001_initial_state(dash_duo): ) as fp: expected_dom = BeautifulSoup(fp.read().strip(), "lxml") - fetched_dom = BeautifulSoup( - dash_duo.find_element(dash_duo.dash_entry_locator).get_attribute( - "outerHTML" - ), - "lxml", - ) + fetched_dom = dash_duo.dash_outerhtml_dom assert ( fetched_dom.decode() == expected_dom.decode() From 4303e6192edf4b4009796641085eface9cb6efeb Mon Sep 17 00:00:00 2001 From: byron Date: Thu, 6 Jun 2019 22:47:18 -0400 Subject: [PATCH 40/46] :boom: use a specific dcc commit for failed 4 cases, this needs to get fixed before release --- .circleci/config.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index da1d988276..09514ecfa1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -74,12 +74,12 @@ jobs: - run: name: 🚧 install dependencies from latest master commit command: | - git clone --depth 1 https://github.com/plotly/dash-core-components.git + git clone https://github.com/plotly/dash-core-components.git git clone --depth 1 https://github.com/plotly/dash-html-components.git git clone --depth 1 https://github.com/plotly/dash-table.git git clone --depth 1 https://github.com/plotly/dash-renderer-test-components . venv/bin/activate - cd dash-core-components && npm install --ignore-scripts && npm run build && pip install -e . && cd .. + cd dash-core-components && git checkout 2932409 && npm install --ignore-scripts && npm run build && pip install -e . && cd .. cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd .. cd dash-table && npm install --ignore-scripts && npm run build && pip install -e . && cd .. cd dash-renderer-test-components && npm install --ignore-scripts && npm run build:all && pip install -e . && cd .. @@ -88,7 +88,8 @@ jobs: name: ⚙️ run integration test command: | . venv/bin/activate - pytest --junitxml=test-reports/junit_intg.xml tests/integration/ + # pytest --junitxml=test-reports/junit_intg.xml tests/integration/ + pytest --log-cli-level DEBUG -k dvpc001 - store_artifacts: path: test-reports - store_test_results: From 42a32dcd5d1087156fb63aece127822619e70930 Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 7 Jun 2019 10:01:23 -0400 Subject: [PATCH 41/46] :wrench: html change also breaks one test --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 09514ecfa1..8da123eee3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,12 +75,12 @@ jobs: name: 🚧 install dependencies from latest master commit command: | git clone https://github.com/plotly/dash-core-components.git - git clone --depth 1 https://github.com/plotly/dash-html-components.git + git clone https://github.com/plotly/dash-html-components.git git clone --depth 1 https://github.com/plotly/dash-table.git git clone --depth 1 https://github.com/plotly/dash-renderer-test-components . venv/bin/activate cd dash-core-components && git checkout 2932409 && npm install --ignore-scripts && npm run build && pip install -e . && cd .. - cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd .. + cd dash-html-components && git checkout 446b114 && npm install --ignore-scripts && npm run build && pip install -e . && cd .. cd dash-table && npm install --ignore-scripts && npm run build && pip install -e . && cd .. cd dash-renderer-test-components && npm install --ignore-scripts && npm run build:all && pip install -e . && cd .. From 68e0ddaeabcbb0e70dff64254ee0d378390466c5 Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 7 Jun 2019 10:01:57 -0400 Subject: [PATCH 42/46] :wrench: all all integration test back --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8da123eee3..930c4eeb92 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -88,8 +88,7 @@ jobs: name: ⚙️ run integration test command: | . venv/bin/activate - # pytest --junitxml=test-reports/junit_intg.xml tests/integration/ - pytest --log-cli-level DEBUG -k dvpc001 + pytest --junitxml=test-reports/junit_intg.xml tests/integration/ - store_artifacts: path: test-reports - store_test_results: From 0b1adf2ba0f5a18c31996250c9233e0995bc9857 Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 7 Jun 2019 12:09:05 -0400 Subject: [PATCH 43/46] :bug: oh forget to migrate this one --- .../callbacks/test_basic_callback.py | 101 ++++++++---------- 1 file changed, 46 insertions(+), 55 deletions(-) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 9c064b5391..91c9635e43 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -79,73 +79,64 @@ def update_input(value): assert call_count.value == 1, "called once at initial stage" - pad_input, pad_div = ( - BeautifulSoup( - dash_duo.driver.find_element_by_css_selector( - "#react-entry-point" - ).get_attribute("innerHTML"), - "lxml", - ) - .select_one("#output > div") - .contents - ) + pad_input, pad_div = dash_duo.dash_innerhtml_dom.select_one( + "#output > div" + ).contents - dash_duo.assertEqual(pad_input.attrs["value"], "sub input initial value") - dash_duo.assertEqual(pad_input.attrs["id"], "sub-input-1") - dash_duo.assertEqual(pad_input.name, "input") + assert ( + pad_input.attrs["value"] == "sub input initial value" + and pad_input.attrs["id"] == "sub-input-1" + ) + assert pad_input.name == "input" - dash_duo.assertTrue( + assert ( pad_div.text == pad_input.attrs["value"] - and pad_div.get("id") == "sub-output-1", - "the sub-output-1 content reflects to sub-input-1 value", - ) + and pad_div.get("id") == "sub-output-1" + ), "the sub-output-1 content reflects to sub-input-1 value" dash_duo.percy_snapshot(name="callback-generating-function-1") - # the paths should include these new output IDs - dash_duo.assertEqual( - dash_duo.driver.execute_script("return window.store.getState().paths"), - { - "input": ["props", "children", 0], - "output": ["props", "children", 1], - "sub-input-1": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 0, - ], - "sub-output-1": [ - "props", - "children", - 1, - "props", - "children", - "props", - "children", - 1, - ], - }, - ) + assert dash_duo.redux_state_paths == { + "input": ["props", "children", 0], + "output": ["props", "children", 1], + "sub-input-1": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 0, + ], + "sub-output-1": [ + "props", + "children", + 1, + "props", + "children", + "props", + "children", + 1, + ], + }, "the paths should include these new output IDs" # editing the input should modify the sub output - sub_input = dash_duo.driver.find_element_by_id("sub-input-1") + dash_duo.find_element("#sub-input-1").send_keys("deadbeef") - sub_input.send_keys("deadbeef") + assert ( + dash_duo.find_element("#sub-output-1").text + == pad_input.attrs["value"] + "deadbeef" + ), "deadbeef is added" + + # the total updates is initial one + the text input changes dash_duo.wait_for_text_to_equal( "#sub-output-1", pad_input.attrs["value"] + "deadbeef" ) - dash_duo.assertEqual( - call_count.value, - len("deadbeef") + 1, - "the total updates is initial one + the text input changes", - ) + rqs = dash_duo.redux_state_rqs + assert rqs, "request queue is not empty" + assert all((rq["status"] == 200 and not rq["rejected"] for rq in rqs)) - dash_duo.request_queue_assertions(call_count.value + 1) dash_duo.percy_snapshot(name="callback-generating-function-2") - - dash_duo.assertTrue(dash_duo.is_console_clean()) + assert dash_duo.get_logs() == [], "console is clean" From eee8429d500645c62ff3cc0b70e182d6f73cf22d Mon Sep 17 00:00:00 2001 From: byron Date: Fri, 7 Jun 2019 12:15:59 -0400 Subject: [PATCH 44/46] :bug: fix multiple_clicks --- dash/testing/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 8511d330fb..16d65a4a3b 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -215,7 +215,7 @@ def _is_windows(): def multiple_click(self, css_selector, clicks): for _ in range(clicks): - self.driver.find_element(css_selector).click() + self.find_element(css_selector).click() def js_click(self, elem): """click in native javascript way From 3bf9a9e7332e8da8c33f5e9a99bd65ef22ed7e43 Mon Sep 17 00:00:00 2001 From: byron Date: Sun, 9 Jun 2019 22:30:52 -0400 Subject: [PATCH 45/46] :ok_hand: remove other clicks and add seconds --- dash/testing/application_runners.py | 2 +- dash/testing/browser.py | 19 ++----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 9085c8ec3d..5dc2d3fa8f 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -81,7 +81,7 @@ def __exit__(self, exc_type, exc_val, traceback): self.stop() except TestingTimeoutError: raise ServerCloseError( - "Cannot stop server within {} timeout".format( + "Cannot stop server within {}s timeout".format( self.stop_timeout ) ) diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 16d65a4a3b..99e552c509 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -14,11 +14,7 @@ from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from selenium.webdriver.common.action_chains import ActionChains -from selenium.common.exceptions import ( - WebDriverException, - TimeoutException, - NoSuchElementException, -) +from selenium.common.exceptions import WebDriverException, TimeoutException from dash.testing.wait import text_to_equal, style_to_equal from dash.testing.dash_page import DashPageMixin @@ -202,7 +198,7 @@ def _get_firefox(): # this will be useful if we wanna test download csv or other data # files with selenium - # TODO this can be fed as a tmpdir fixture from pytest + # TODO this could be replaced with a tmpfixture from pytest too fp.set_preference("browser.download.dir", "/tmp") fp.set_preference("browser.download.folderList", 2) fp.set_preference("browser.download.manager.showWhenStarting", False) @@ -217,17 +213,6 @@ def multiple_click(self, css_selector, clicks): for _ in range(clicks): self.find_element(css_selector).click() - def js_click(self, elem): - """click in native javascript way - note: this is NOT the recommended way to click""" - self.driver.execute_script("arguments[0].click();", elem) - - def mouse_click(self, elem): - try: - ActionChains(self.driver).click(elem).perform() - except NoSuchElementException: - logger.exception("mouse_click on wrong element") - def clear_input(self, elem): ( ActionChains(self.driver) From 69371f4b31605148dd4c5d85fc8a1aa41db74b38 Mon Sep 17 00:00:00 2001 From: byron Date: Sun, 9 Jun 2019 22:31:29 -0400 Subject: [PATCH 46/46] :boom: shorten the api name from start_app_server to start_server --- dash/testing/composite.py | 2 +- tests/integration/callbacks/test_basic_callback.py | 4 ++-- tests/integration/callbacks/test_multiple_callbacks.py | 2 +- tests/integration/dash_assets/test_dash_assets.py | 4 ++-- .../devtools/test_devtools_error_handling.py | 10 +++++----- tests/integration/devtools/test_devtools_ui.py | 4 ++-- tests/integration/devtools/test_hot_reload.py | 2 +- tests/integration/devtools/test_props_check.py | 2 +- tests/integration/renderer/test_dependencies.py | 2 +- tests/integration/renderer/test_due_diligence.py | 2 +- tests/integration/renderer/test_state_and_input.py | 4 ++-- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dash/testing/composite.py b/dash/testing/composite.py index 1e3f781d34..485faacf08 100644 --- a/dash/testing/composite.py +++ b/dash/testing/composite.py @@ -7,7 +7,7 @@ def __init__(self, server, browser, remote=None, wait_timeout=10): super(DashComposite, self).__init__(browser, remote, wait_timeout) self.server = server - def start_app_server(self, app, **kwargs): + def start_server(self, app, **kwargs): '''start the local server with app''' # start server with app and pass Dash arguments diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 91c9635e43..f12c589b0b 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -23,7 +23,7 @@ def update_output(value): call_count.value = call_count.value + 1 return value - dash_duo.start_app_server(app) + dash_duo.start_server(app) assert dash_duo.find_element("#output-1").text == "initial value" dash_duo.percy_snapshot(name="simple-callback-initial") @@ -75,7 +75,7 @@ def update_input(value): call_count.value = call_count.value + 1 return value - dash_duo.start_app_server(app) + dash_duo.start_server(app) assert call_count.value == 1, "called once at initial stage" diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 1c04a0f62c..7be877dd1f 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -21,7 +21,7 @@ def update_output(n_clicks): time.sleep(1) return n_clicks - dash_duo.start_app_server(app) + dash_duo.start_server(app) dash_duo.multiple_click("#input", clicks=3) time.sleep(3) diff --git a/tests/integration/dash_assets/test_dash_assets.py b/tests/integration/dash_assets/test_dash_assets.py index 53857e8c83..578d726fa9 100644 --- a/tests/integration/dash_assets/test_dash_assets.py +++ b/tests/integration/dash_assets/test_dash_assets.py @@ -34,7 +34,7 @@ def test_dada001_assets(dash_duo): [html.Div("Content", id="content"), dcc.Input(id="test")], id="layout" ) - dash_duo.start_app_server(app) + dash_duo.start_server(app) assert ( dash_duo.find_element("body").value_of_css_property("margin") == "0px" @@ -116,7 +116,7 @@ def test_dada002_external_files_init(dash_duo): app.layout = html.Div() - dash_duo.start_app_server(app) + dash_duo.start_server(app) js_urls = [x["src"] if isinstance(x, dict) else x for x in js_files] css_urls = [x["href"] if isinstance(x, dict) else x for x in css_files] diff --git a/tests/integration/devtools/test_devtools_error_handling.py b/tests/integration/devtools/test_devtools_error_handling.py index 7dc22d38e2..398578cc76 100644 --- a/tests/integration/devtools/test_devtools_error_handling.py +++ b/tests/integration/devtools/test_devtools_error_handling.py @@ -23,7 +23,7 @@ def update_output(n_clicks): elif n_clicks == 2: raise Exception("Special 2 clicks exception") - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, @@ -70,7 +70,7 @@ def update_output(n_clicks): return "button clicks: {}".format(n_clicks) - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, @@ -109,7 +109,7 @@ def update_output(n_clicks): if n_clicks == 1: return n_clicks - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, @@ -143,7 +143,7 @@ def update_output(n_clicks): id="output", animate=0, figure={"data": [{"y": [3, 1, 2]}]} ) - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, @@ -186,7 +186,7 @@ def update_outputs(n_clicks): else: n_clicks / 0 - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, diff --git a/tests/integration/devtools/test_devtools_ui.py b/tests/integration/devtools/test_devtools_ui.py index d9589b7133..d0f958481c 100644 --- a/tests/integration/devtools/test_devtools_ui.py +++ b/tests/integration/devtools/test_devtools_ui.py @@ -13,7 +13,7 @@ def test_dvui001_disable_props_check_config(dash_duo): ] ) - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, @@ -45,7 +45,7 @@ def test_dvui002_disable_ui_config(dash_duo): ] ) - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, diff --git a/tests/integration/devtools/test_hot_reload.py b/tests/integration/devtools/test_hot_reload.py index c1119e6069..5c5845efde 100644 --- a/tests/integration/devtools/test_hot_reload.py +++ b/tests/integration/devtools/test_hot_reload.py @@ -8,7 +8,7 @@ def test_dvhr001_hot_reload(dash_duo): app = dash.Dash(__name__, assets_folder="hr_assets") app.layout = html.Div([html.H3("Hot reload")], id="hot-reload-content") - dash_duo.start_app_server( + dash_duo.start_server( app, dev_tools_hot_reload=True, dev_tools_hot_reload_interval=100, diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py index f5b5e17568..0f2fe200c8 100644 --- a/tests/integration/devtools/test_props_check.py +++ b/tests/integration/devtools/test_props_check.py @@ -199,7 +199,7 @@ def display_content(pathname): children=test_case["component"](**test_case["props"]), ) - dash_duo.start_app_server( + dash_duo.start_server( app, debug=True, use_reloader=False, diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index 15771026d7..d1160d9c45 100644 --- a/tests/integration/renderer/test_dependencies.py +++ b/tests/integration/renderer/test_dependencies.py @@ -29,7 +29,7 @@ def update_output_2(value): output_2_call_count.value += 1 return value - dash_duo.start_app_server(app) + dash_duo.start_server(app) assert dash_duo.find_element("#output-1").text == "initial value" assert output_1_call_count.value == 1 and output_2_call_count.value == 0 diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py index 02a7c69d6a..88ef5fbf29 100644 --- a/tests/integration/renderer/test_due_diligence.py +++ b/tests/integration/renderer/test_due_diligence.py @@ -53,7 +53,7 @@ def test_rddd001_initial_state(dash_duo): ]) # fmt:on - dash_duo.start_app_server(app) + dash_duo.start_server(app) # Note: this .html file shows there's no undo/redo button by default with open( diff --git a/tests/integration/renderer/test_state_and_input.py b/tests/integration/renderer/test_state_and_input.py index e422602aee..7ffe1ddbbe 100644 --- a/tests/integration/renderer/test_state_and_input.py +++ b/tests/integration/renderer/test_state_and_input.py @@ -28,7 +28,7 @@ def update_output(input, state): call_count.value += 1 return 'input="{}", state="{}"'.format(input, state) - dash_duo.start_app_server(app) + dash_duo.start_server(app) input_ = lambda: dash_duo.find_element("#input") output_ = lambda: dash_duo.find_element("#output") @@ -79,7 +79,7 @@ def update_output(input, n_clicks, state): call_count.value += 1 return 'input="{}", state="{}"'.format(input, state) - dash_duo.start_app_server(app) + dash_duo.start_server(app) btn = lambda: dash_duo.find_element("#button") output = lambda: dash_duo.find_element("#output")