Repository: plotly/dash

Issue:
merge pytest fixtures
the scope of the issue is to merge and restructure the pytest fixtures in https://github.com/plotly/pytest-dash into dash. It might include few integration tests rewrite  with the new fixtures and python state `assert`, but all tests rewrite is out of the scope of this issue. 

Note: all fixtures will be available for pytest as plugin thanks to the `pytest11` entry point by `pip install dash`. 


Discussion:

Resolving patch:
diff --git a/dash-renderer/version.py b/dash-renderer/version.py
--- a/dash-renderer/version.py
+++ b/dash-renderer/version.py
@@ -1 +1 @@
-__version__ = '0.23.0'
+__version__ = '0.24.0'
diff --git a/setup.py b/setup.py
--- 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=[


Test patch:
diff --git a/tests/__init__.py b/dash/testing/__init__.py
similarity index 100%
rename from tests/__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
--- /dev/null
+++ b/dash/testing/application_runners.py
@@ -0,0 +1,216 @@
+from __future__ import print_function
+
+import sys
+import uuid
+import shlex
+import threading
+import subprocess
+import logging
+
+import runpy
+import six
+import flask
+import requests
+
+from dash.testing.errors import (
+    NoAppFoundError,
+    TestingTimeoutError,
+    ServerCloseError,
+)
+import dash.testing.wait as wait
+
+
+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:
+        logger.exception("the app name cannot be found")
+        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 TestingTimeoutError:
+                raise ServerCloseError(
+                    "Cannot stop server within {}s 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=3):
+        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
+            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
+            logger.exception("threaded server failed to start")
+            self.started = False
+
+        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)
+
+
+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=3):
+        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 server with waitress-serve in process flavor """
+        entrypoint = "{}:{}.server".format(app_module, application_name)
+        self.port = port
+
+        args = shlex.split(
+            "waitress-serve --listen=0.0.0.0:{} {}".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):
+            logger.exception("process server has encountered an error")
+            self.started = False
+            return
+
+        self.started = True
+
+    def stop(self):
+        if self.proc:
+            try:
+                self.proc.terminate()
+                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
+                    self.proc.communicate()
+            except _except:
+                logger.exception(
+                    "subprocess terminate not success, trying to kill "
+                    "the subprocess in a safe manner"
+                )
+                self.proc.kill()
+                self.proc.communicate()
diff --git a/dash/testing/browser.py b/dash/testing/browser.py
new file mode 100644
--- /dev/null
+++ b/dash/testing/browser.py
@@ -0,0 +1,266 @@
+# pylint: disable=missing-docstring
+import os
+import sys
+import logging
+import warnings
+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.keys import Keys
+from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
+from selenium.webdriver.common.action_chains import ActionChains
+
+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
+from dash.testing.errors import DashAppLoadingError
+
+
+logger = logging.getLogger(__name__)
+
+
+class Browser(DashPageMixin):
+    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(self.driver, wait_timeout)
+        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 => %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)
+
+    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 = (
+            self._wd_wait
+            if timeout is None
+            else WebDriverWait(self.driver, timeout)
+        )
+        logger.debug(
+            "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):
+        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):
+        return self._wait_for(
+            EC.presence_of_element_located,
+            ((By.CSS_SELECTOR, selector),),
+            timeout,
+            "timeout {} => waiting for selector {}".format(timeout, 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,
+            args=(selector, text),
+            timeout=timeout,
+            msg="text -> {} not found within {}s".format(text, timeout),
+        )
+
+    def wait_for_page(self, url=None, timeout=10):
+
+        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
+            )
+        except TimeoutException:
+            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((str(log) for log in 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()
+                ),
+            )
+        )
+
+    @staticmethod
+    def _get_chrome():
+        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
+
+    @staticmethod
+    def _get_firefox():
+
+        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 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)
+
+        return webdriver.Firefox(fp, capabilities=capabilities)
+
+    @staticmethod
+    def _is_windows():
+        return sys.platform == "win32"
+
+    def multiple_click(self, css_selector, clicks):
+        for _ in range(clicks):
+            self.find_element(css_selector).click()
+
+    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":
+            return [
+                entry
+                for entry in self.driver.get_log("browser")
+                if entry["timestamp"] > self._last_ts
+            ]
+        warnings.warn(
+            "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.lower() == "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 if the server is ready
+        for selenium testing
+        """
+        self._url = value
+        self.wait_for_page()
diff --git a/dash/testing/composite.py b/dash/testing/composite.py
new file mode 100644
--- /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_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
diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py
new file mode 100644
--- /dev/null
+++ b/dash/testing/dash_page.py
@@ -0,0 +1,37 @@
+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"
+
+    @property
+    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(
+            "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/errors.py b/dash/testing/errors.py
new file mode 100644
--- /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/plugin.py b/dash/testing/plugin.py
new file mode 100644
--- /dev/null
+++ b/dash/testing/plugin.py
@@ -0,0 +1,76 @@
+# pylint: disable=missing-docstring,redefined-outer-name
+import pytest
+
+from selenium import webdriver
+
+from dash.testing.application_runners import ThreadedRunner, ProcessRunner
+from dash.testing.browser import Browser
+from dash.testing.composite import DashComposite
+
+WEBDRIVERS = {
+    "Chrome": webdriver.Chrome,
+    "Firefox": webdriver.Firefox,
+    "Remote": webdriver.Remote,
+}
+
+
+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(
+        "--webdriver",
+        choices=tuple(WEBDRIVERS.keys()),
+        default="Chrome",
+        help="Name of the selenium driver to use",
+    )
+
+
+@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
+###############################################################################
+
+
+@pytest.fixture
+def dash_thread_server():
+    """Start a local dash server in a new thread"""
+    with ThreadedRunner() as starter:
+        yield starter
+
+
+@pytest.fixture
+def dash_process_server():
+    """Start a Dash server with subprocess.Popen and waitress-serve"""
+    with ProcessRunner() as starter:
+        yield starter
+
+
+@pytest.fixture
+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/dash/testing/wait.py b/dash/testing/wait.py
new file mode 100644
--- /dev/null
+++ b/dash/testing/wait.py
@@ -0,0 +1,91 @@
+# pylint: disable=too-few-public-methods
+"""Utils methods for pytest-dash such wait_for wrappers"""
+import time
+import logging
+from selenium.common.exceptions import WebDriverException
+from dash.testing.errors import TestingTimeoutError
+
+
+logger = logging.getLogger(__name__)
+
+
+def until(
+    wait_cond,
+    timeout,
+    poll=0.1,
+    msg="expected condition not met within timeout",
+):  # noqa: C0330
+    res = None
+    logger.debug(
+        "start wait.until with method, timeout, poll => %s %s %s",
+        wait_cond,
+        timeout,
+        poll,
+    )
+    end_time = time.time() + timeout
+    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_not method, timeout, poll => %s %s %s",
+        wait_cond,
+        timeout,
+        poll,
+    )
+    end_time = time.time() + timeout
+    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):
+    def __init__(self, selector, text):
+        self.selector = selector
+        self.text = text
+
+    def __call__(self, driver):
+        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 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
diff --git a/tests/assets/simple_app.py b/tests/assets/simple_app.py
new file mode 100644
--- /dev/null
+++ b/tests/assets/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/integration/IntegrationTests.py b/tests/integration/IntegrationTests.py
--- 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/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/callbacks/test_basic_callback.py
@@ -0,0 +1,142 @@
+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_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_server(app)
+
+    assert call_count.value == 1, "called once at initial stage"
+
+    pad_input, pad_div = dash_duo.dash_innerhtml_dom.select_one(
+        "#output > div"
+    ).contents
+
+    assert (
+        pad_input.attrs["value"] == "sub input initial value"
+        and pad_input.attrs["id"] == "sub-input-1"
+    )
+    assert pad_input.name == "input"
+
+    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"
+
+    dash_duo.percy_snapshot(name="callback-generating-function-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
+    dash_duo.find_element("#sub-input-1").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"
+    )
+
+    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.percy_snapshot(name="callback-generating-function-2")
+    assert dash_duo.get_logs() == [], "console is clean"
diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py
new file mode 100644
--- /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_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/dash_assets/test_assets.py b/tests/integration/dash_assets/test_assets.py
deleted file mode 100644
--- a/tests/integration/dash_assets/test_assets.py
+++ /dev/null
@@ -1,150 +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 tests.integration.IntegrationTests import IntegrationTests
-from tests.integration.utils import wait_for, invincible
-
-
-class TestAssets(IntegrationTests):
-
-    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 = '''
-        <!DOCTYPE html>
-        <html>
-            <head>
-                {%metas%}
-                <title>{%title%}</title>
-                {%css%}
-            </head>
-            <body>
-                <div id="tested"></div>
-                {%app_entry%}
-                <footer>
-                    {%config%}
-                    {%scripts%}
-                    {%renderer%}
-                </footer>
-            </body>
-        </html>
-        '''
-
-        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 = '''
-        <!DOCTYPE html>
-        <html>
-            <head>
-                {%metas%}
-                <title>{%title%}</title>
-                {%css%}
-            </head>
-            <body>
-                <div id="tested"></div>
-                <div id="ramda-test"></div>
-                <button type="button" id="btn">Btn</button>
-                {%app_entry%}
-                <footer>
-                    {%config%}
-                    {%scripts%}
-                    {%renderer%}
-                </footer>
-            </body>
-        </html>
-        '''
-
-        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
--- /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 = """
+    <!DOCTYPE html>
+    <html>
+        <head>
+            {%metas%}
+            <title>{%title%}</title>
+            {%css%}
+        </head>
+        <body>
+            <div id="tested"></div>
+            {%app_entry%}
+            <footer>
+                {%config%}
+                {%scripts%}
+                {%renderer%}
+            </footer>
+        </body>
+    </html>
+    """
+
+    app.layout = html.Div(
+        [html.Div("Content", id="content"), dcc.Input(id="test")], id="layout"
+    )
+
+    dash_duo.start_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 = """
+    <!DOCTYPE html>
+    <html>
+        <head>
+            {%metas%}
+            <title>{%title%}</title>
+            {%css%}
+        </head>
+        <body>
+            <div id="tested"></div>
+            <div id="ramda-test"></div>
+            <button type="button" id="btn">Btn</button>
+            {%app_entry%}
+            <footer>
+                {%config%}
+                {%scripts%}
+                {%renderer%}
+            </footer>
+        </body>
+    </html>
+    """
+
+    app.layout = html.Div()
+
+    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]
+
+    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
new file mode 100644
--- /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_dveh001_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_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(dash_duo.devtools_error_count_locator, "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(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_dveh002_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_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(dash_duo.devtools_error_count_locator, "1")
+    dash_duo.percy_snapshot(
+        "devtools - prevent update - only a single exception"
+    )
+
+
+def test_dveh003_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_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(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_dveh004_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_server(
+        app,
+        debug=True,
+        use_reloader=False,
+        use_debugger=True,
+        dev_tools_hot_reload=False,
+    )
+
+    dash_duo.wait_for_element("#button").click()
+    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_dveh005_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_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(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()
+    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
--- /dev/null
+++ b/tests/integration/devtools/test_devtools_ui.py
@@ -0,0 +1,66 @@
+import dash_core_components as dcc
+import dash_html_components as html
+import dash
+import dash.testing.wait as wait
+
+
+def test_dvui001_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_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_dvui002_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_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")
+    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(
+        ".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_hot_reload.py b/tests/integration/devtools/test_hot_reload.py
new file mode 100644
--- /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_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
new file mode 100644
--- /dev/null
+++ b/tests/integration/devtools/test_props_check.py
@@ -0,0 +1,227 @@
+import dash_core_components as dcc
+import dash_html_components as html
+import dash
+from dash.dependencies import Input, Output
+
+
+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"}]},
+    },
+}
+
+
+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")])
+
+    @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_server(
+        app,
+        debug=True,
+        use_reloader=False,
+        use_debugger=True,
+        dev_tools_hot_reload=False,
+    )
+
+    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[tc]["name"]
+                )
+            )
+        else:
+            dash_duo.wait_for_element("#new-component")
+            dash_duo.percy_snapshot(
+                "devtools validation no exception: {}".format(
+                    test_cases[tc]["name"]
+                )
+            )
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
--- /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_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")]
+    )
+
+    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_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
--- /dev/null
+++ b/tests/integration/renderer/test_due_diligence.py
@@ -0,0 +1,109 @@
+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_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 = dash_duo.dash_outerhtml_dom
+
+    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
--- /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_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_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_devtools.py b/tests/integration/test_devtools.py
deleted file mode 100644
--- a/tests/integration/test_devtools.py
+++ /dev/null
@@ -1,689 +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
-
-
-@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']
-                    )
-                )
diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py
--- 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
diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py
--- a/tests/integration/test_render.py
+++ b/tests/integration/test_render.py
@@ -1,10 +1,7 @@
 # -*- coding: UTF-8 -*-
-import os
-import textwrap
-
 import dash
 from dash import Dash
-from dash.dependencies import Input, Output, State, ClientsideFunction
+from dash.dependencies import Input, Output
 from dash.exceptions import PreventUpdate
 from dash.development.base_component import Component
 import dash_html_components as html
@@ -22,8 +19,6 @@
 from .utils import wait_for
 from multiprocessing import Value
 import time
-import re
-import itertools
 import json
 import string
 import plotly
@@ -37,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)),
@@ -75,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(
@@ -109,109 +76,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)
@@ -305,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__)
@@ -762,54 +479,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([
@@ -837,203 +506,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"')
-
-    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
-        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([
@@ -1323,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([
@@ -1913,45 +1348,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')
 
diff --git a/tests/package.json b/tests/package.json
deleted file mode 100644
--- 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/unit/dash/__init__.py b/tests/unit/dash/__init__.py
deleted file mode 100644
diff --git a/tests/unit/dash/development/__init__.py b/tests/unit/dash/development/__init__.py
deleted file mode 100644
diff --git a/tests/unit/dash/test_resources.py b/tests/unit/dash/test_resources.py
deleted file mode 100644
--- a/tests/unit/dash/test_resources.py
+++ /dev/null
@@ -1,95 +0,0 @@
-import mock
-import dash_core_components as dcc
-
-import dash
-
-_monkey_patched_js_dist = [
-    {
-        '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'
-    },
-    {
-        '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
-    }
-]
-
-
-class StatMock(object):
-    st_mtime = 1
-
-
-def test_external(mocker):
-    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'
-    )
-    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()
-        )
-
-    assert resource == [
-        'https://external_javascript.js',
-        'https://external_css.css',
-        'https://component_library.bundle.js'
-    ]
-
-
-def test_internal(mocker):
-    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'
-    )
-    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):
-            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',
-    ]
-
-    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']
-        )
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/__init__.py b/tests/unit/development/__init__.py
similarity index 100%
rename from tests/unit/__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
--- a/tests/unit/dash/development/test_base_component.py
+++ b/tests/unit/development/test_base_component.py
@@ -5,7 +5,6 @@
 import shutil
 import unittest
 import plotly
-
 from dash.development.base_component import Component
 from dash.development.component_generator import reserved_words
 from dash.development._py_components_generation import (
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/test_app_runners.py b/tests/unit/test_app_runners.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/test_app_runners.py
@@ -0,0 +1,33 @@
+import time
+import sys
+import requests
+import pytest
+
+import dash_html_components as html
+import dash
+
+
+def test_threaded_server_smoke(dash_thread_server):
+    app = dash.Dash(__name__)
+
+    app.layout = html.Div(
+        [
+            html.Button("click me", id="clicker"),
+            html.Div(id="output", children="hello thread"),
+        ]
+    )
+    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"
+
+
+@pytest.mark.skipif(
+    sys.version_info < (3,), reason="requires python3 for process testing"
+)
+def test_process_server_smoke(dash_process_server):
+    dash_process_server("simple_app")
+    time.sleep(2.5)
+    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"
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
--- /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"
diff --git a/tests/unit/test_resources.py b/tests/unit/test_resources.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/test_resources.py
@@ -0,0 +1,90 @@
+import mock
+import dash_core_components as dcc
+import dash
+
+_monkey_patched_js_dist = [
+    {
+        "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",
+    },
+    {
+        "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,
+    },
+]
+
+
+class StatMock(object):
+    st_mtime = 1
+
+
+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
+
+    app = dash.Dash(
+        __name__, assets_folder="tests/assets", assets_ignore="load_after.+.js"
+    )
+    app.layout = dcc.Markdown()
+    app.scripts.config.serve_locally = False
+
+    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",
+    ]
+
+
+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
+
+    app = dash.Dash(
+        __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):
+            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",
+    ]
+
+    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"]
+    )
diff --git a/tests/utils.py b/tests/utils.py
deleted file mode 100644
--- 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)