Repository: plotly/dash-html-components (language: python)

Issue:
Several components have limited or no functionality in current browsers
We recently prepared examples for `dashHtmlComponents` in advance of submission to CRAN.

As a result, several components were identified as non-functional or obsolete by those writing up the examples; this is most likely the result of browsers deprecating previously valid HTML elements.

A list of these follows below. :no_entry_sign: indicates a component which is suspected not to function anymore.

From @rpkyle:

- :no_entry_sign: `htmlBasefont`: this HTML5 element is deprecated, but the component does not suit the specifications as written and does not work.

From @CanerIrfanoglu:

- `htmlListing`: Deprecated. Example working.
- `htmlMarquee`: Obsolete. Example working.
- `htmlMeta`: Example added but injects `<meta>` tag to `<body>`. Not sure if this is acceptable.
- `htmlMultiCol`: Obsolete. Example added (requires `col` arg and Netscape 4 to work).
- :no_entry_sign: `htmlNextid`: Needs to be injected in the `<head>` and not supported by any browser.
- :no_entry_sign: `htmlObjectEl`: Example created but not working. `object` tag  has `data` attribute but `htmlObjectEl` does not. There is an [issue](https://github.com/plotly/dash-html-components/issues/66) created earlier.
- `htmlOptgroup`: Example working. However, labels are blank. Noted that `optgroup` should have `label` argument. Below is how it looks vs. how it is supposed to look:
<img width="224" alt="Screen Shot 2019-07-30 at 5 11 45 PM" src="https://user-images.githubusercontent.com/20918264/62165811-42401500-b2ed-11e9-8f17-a567438abe2e.png">                                                  <img width="120" alt="Screen Shot 2019-07-30 at 5 12 15 PM" src="https://user-images.githubusercontent.com/20918264/62165821-48ce8c80-b2ed-11e9-960f-726486234238.png">
- :no_entry_sign: `htmlOutput`: Need `oninput` attribute of `htmlForm` & `htmlInput`(does not exist) to create a working example.
- :no_entry_sign: `htmlParam`: Example added but not working. Depends on `data` attribute of  `htmlObjectEl`
- :no_entry_sign: `htmlPlaintext`: Obsolete. Example added but does not work as intended (should render rest of html as plain-text)

From @sacul-git:

- :no_entry_sign: `htmlScript`: I did not successfully create a minimal `htmlScript` that actually works.
- `htmlShadow`: This is [deprecated](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/shadow), and I'm not sure how to show a meaningful example in the context of a Dash app.
- `htmlSlot`: I'm not sure how to show a meaningful example in the context of a Dash app.
- `htmlSpacer`: [non-standard and obsolete](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/spacer)
- `htmlTitle`: Because an app's title is set through `Dash$new(name = "title")`, I'm not sure how to show a meaningful example of `htmlTitle`.
- `htmlTrack`: Not sure how to use this within a Dash app with audio or video, and if there is a way, I'm not sure where I should source a `vtt` file from.

@alexcjohnson 


Discussion / hints:
I motion to deprecate the following components in the next `dash-html-components` and `dashHtmlComponents` releases, as they are no longer functional/supported in modern browsers, and propose to eventually remove these altogether:

- `htmlBasefont`: only works with Microsoft browsers, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/basefont)
- `htmlBlink`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blink)
- `htmlCommand`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/command)
- `htmlElement`: does not appear to be supported by widely used modern browsers, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/element)
- `htmlIsindex`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/isindex)
- `htmlListing`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/listing)
- `htmlMarquee`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/marquee)
- `htmlMulticol`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/multicol)
- `htmlNextid`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nextid)
- `htmlShadow`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/shadow)
- `htmlSpacer`: not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/spacer)

@Marc-Andre-Rivet @alexcjohnson @wbrgss 
`htmlContent` should also be removed if `htmlShadow` is being removed. It is used inside of `Shadow DOM` and is also deprecated and not supported by any widely used modern browser, see [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/content).


Following a discussion offline, we've made the decision to retain the above components. Closing this issue.
I'd kindly ask for reconsidering here — I understand that the components are dynamically generated, but the fact that the _documentation_ is also dynamically generated makes it seem like these are fully functional bits and bobs to put into a dash.

In particular, even though `<script>` is a _very_ functional HTML element `htmlScript` will _never work_ in a dash (as it currently stands) because browsers don't execute `<script>`s inserted in the way react does it (via innerHTML).  Including it in the documentation only leads folks down dead ends.  Either it should use a different insertion mechanism, or it should have a bright big warning in the docs, or it should be removed entirely.
I do agree we should keep these components around. `script` in particular has some other uses like storing data. Typically this is used for the back-end to pass data to the front end on page load, but one could imagine scenarios where this is useful when Dash is part of a larger app, to pass data between pieces of that app.

But @mbauman is right that we should document these limitations better. Reopening as a documentation issue.
_Possibly_ relevant to plotly/dashR#243, since we may want to document within the package also.

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 5c56769d..25b1440c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,6 +1,15 @@
 version: 2
 
 jobs:
+  percy-finalize:
+    docker:
+      - image: percyio/agent
+        auth:
+          username: dashautomation
+          password: $DASH_PAT_DOCKERHUB
+    steps:
+      - run: percy finalize --all
+
   python-2.7: &test-template
     docker:
       - image: circleci/python:2.7-stretch-node-browsers
@@ -9,7 +18,7 @@ jobs:
           password: $DASH_PAT_DOCKERHUB
         environment:
           PYTHON_VERSION: py27
-          PERCY_ENABLE: 1
+          PERCY_ENABLE: 0
 
     working_directory: ~/repo
 
@@ -76,7 +85,8 @@ jobs:
           password: $DASH_PAT_DOCKERHUB
         environment:
           PYTHON_VERSION: py37
-          PERCY_ENABLE: 0
+          PERCY_ENABLE: 1
+          PERCY_PARALLEL_TOTAL: -1
 
 workflows:
   version: 2
@@ -85,3 +95,6 @@ workflows:
       - python-2.7
       - python-3.6
       - python-3.7
+      - percy-finalize:
+          requires:
+            - python-3.7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d32256bf..9a250adc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## UNRELEASED
+### Fixed
+- [#178](https://github.com/plotly/dash-html-components/pull/178) - Fix [#161](https://github.com/plotly/dash-html-components/issues/161) <object> `data` property, and fix [#129](https://github.com/plotly/dash-html-components/issues/129) obsolete, deprecated, and discouraged elements. No elements were removed, but comments were added to the documentation about these elements detailing their limitations.
+
 ## [1.1.2] - 2021-01-19
 ### Fixed
 - [#169](https://github.com/plotly/dash-html-components/pull/169) - part of fixing dash import bug https://github.com/plotly/dash/issues/1143
diff --git a/package.json b/package.json
index 9bc338a0..76bb724c 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "postbuild": "es-check es5 dash_html_components/*.js",
     "build:watch": "watch 'npm run build' src",
     "test:import": "python -m unittest tests.test_dash_import",
-    "test:py": "python -m unittest tests.test_dash_html_components tests.test_integration",
+    "test:py": "pytest --nopercyfinalize --headless tests/test_dash_html_components.py tests/test_integration.py",
     "test": "run-s -c test:py test:import lint"
   },
   "author": "Chris Parmer <chris@plotly.com>",
diff --git a/scripts/extract-attributes.js b/scripts/extract-attributes.js
index ec4e1a85..997a34b8 100644
--- a/scripts/extract-attributes.js
+++ b/scripts/extract-attributes.js
@@ -10,14 +10,13 @@ const dataPath = './data/attributes.json';
 const htmlPath = './data/attributes.html';
 
 // From https://facebook.github.io/react/docs/tags-and-attributes.html#supported-attributes
-// less the `data` attribute,
-// the special `className` and `htmlFor` props,
+// less the special `className` and `htmlFor` props,
 // and `httpEquiv` + `acceptCharset` which are already correctly camelCased.
 const supportedAttributes = ['accept', 'accessKey', 'action',
 'allowFullScreen', 'allowTransparency', 'alt', 'async', 'autoComplete',
 'autoFocus', 'autoPlay', 'capture', 'cellPadding', 'cellSpacing', 'challenge',
 'charSet', 'checked', 'cite', 'classID', 'colSpan', 'cols', 'content',
-'contentEditable', 'contextMenu', 'controls', 'coords', 'crossOrigin',
+'contentEditable', 'contextMenu', 'controls', 'coords', 'crossOrigin', 'data',
 'dateTime', 'default', 'defer', 'dir', 'disabled', 'download', 'draggable',
 'encType', 'form', 'formAction', 'formEncType', 'formMethod', 'formNoValidate',
 'formTarget', 'frameBorder', 'headers', 'height', 'hidden', 'high', 'href',
@@ -82,7 +81,7 @@ function extractAttributes($) {
             .toString();
 
         // Skip `data-*` attributes
-        if (htmlAttribute.indexOf('data') === 0) {
+        if (htmlAttribute.indexOf('data-') === 0) {
             return;
         }
 
diff --git a/scripts/extract-elements.js b/scripts/extract-elements.js
index d7a5f4f8..9292f5a9 100644
--- a/scripts/extract-elements.js
+++ b/scripts/extract-elements.js
@@ -20,11 +20,18 @@ function extractElements($) {
         'svg', 'math',
         // obsolete, non-standard, or deprecated tags
         'image', 'dir', 'tt', 'applet', 'noembed', 'bgsound', 'menu', 'menuitem',
-        'noframes'
+        'noframes',
+        // experimental, don't add yet
+        'portal'
     ];
     // `<section>` is for some reason missing from the reference tables.
+    // `<command>` and `element` are obsolete and has been removed from the
+    // reference table, but we had them in the past so we should wait for a
+    // major to remove
     const addElements = [
         'base',
+        'command',
+        'element',
         'section',
         'h1',
         'h2',
@@ -64,6 +71,7 @@ request(refUrl, (error, response, html) => {
     if (elements.length !== expectedElCount) {
         throw new Error(
             'Unexpected number of elements extracted from ' + refUrl +
+            ' - Found ' + elements.length + ' but expected ' + expectedElCount +
             ' Check the output and edit expectedElCount if this is intended.'
         );
     }
diff --git a/scripts/generate-components.js b/scripts/generate-components.js
index d2e1a0da..e2f1a77e 100644
--- a/scripts/generate-components.js
+++ b/scripts/generate-components.js
@@ -198,16 +198,68 @@ function generatePropTypes(element, attributes) {
     'setProps': PropTypes.func`
 }
 
+const obsoleteDoc = element => `
+ * OBSOLETE: <${element}> is included for completeness, but should be avoided
+ * as it is not supported by any modern browsers.`;
+
+const customDocs = {
+    basefont: `
+ * OBSOLETE: <basefont> is included for completeness, but should be avoided
+ * as it is only supported by Internet Explorer.`,
+    blink: obsoleteDoc('blink'),
+    command: obsoleteDoc('command'),
+    element: obsoleteDoc('element'),
+    isindex: obsoleteDoc('isindex'),
+    keygen: `
+ * DEPRECATED: <keygen> is included for completeness, but should be avoided
+ * as it is not supported by all browsers and may be removed at any time from
+ * those that do support it.`,
+    listing: obsoleteDoc('listing') + ' Use <pre> or <code> instead.',
+    marquee: `
+ * DEPRECATED: <marquee> is included for completeness, but should be avoided
+ * as browsers may remove it at any time.`,
+    meta: `
+ * CAUTION: <meta> is included for completeness, but generally will not behave
+ * as expected since <meta> tags should be static HTML content in the <head> of
+ * the document. Dash components are dynamic <body> content.`,
+    multicol: obsoleteDoc('multicol'),
+    nextid: obsoleteDoc('nextid'),
+    output: `
+ * CAUTION: <output> is included for completeness, but its typical usage
+ * requires the oninput attribute of the enclosing <form> element, which
+ * is not accessible to Dash.`,
+    script: `
+ * CAUTION: <script> is included for completeness, but you cannot execute
+ * JavaScript code by providing it to a <script> element. Use a clientside
+ * callback for this purpose instead.`,
+    plaintext: `
+ * OBSOLETE: <plaintext> is included for completeness, but should be avoided
+ * as browsers may remove it at any time, and its behavior when added
+ * dynamically by Dash is not what it would be statically on page load.
+ * Use <pre> or <code> instead.`,
+    shadow: `
+ * DEPRECATED: <shadow> is included for completeness, but should be avoided
+ * as it is not supported by all browsers and may be removed at any time from
+ * those that do support it.`,
+    spacer: obsoleteDoc('spacer'),
+    title: `
+ * CAUTION: <title> is included for completeness, but is not expected to
+ * do anything outside of <head>. Dash components are always created in the
+ * <body>.`
+};
+
 function generateComponent(Component, element, attributes) {
     const propTypes = generatePropTypes(element, attributes);
 
+    const customDoc = customDocs[element] ? ('\n *' + customDocs[element] + '\n *') : '';
+
     return `
 import React from 'react';
 import PropTypes from 'prop-types';
 import {omit} from 'ramda';
 
 /**
- * ${Component} is a wrapper for the <${element}> HTML5 element.
+ * ${Component} is a wrapper for the <${element}> HTML5 element.${customDoc}
  * For detailed attribute info see:
  * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/${element}
  */


Test patch:
diff --git a/tests/IntegrationTests.py b/tests/IntegrationTests.py
deleted file mode 100644
index bbcbacf3..00000000
--- a/tests/IntegrationTests.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import multiprocessing
-import sys
-import time
-import unittest
-from selenium import webdriver
-import percy
-
-
-class IntegrationTests(unittest.TestCase):
-
-    def percy_snapshot(cls, name=''):
-        snapshot_name = '{} - py{}.{}'.format(name, sys.version_info.major, sys.version_info.minor)
-        print(snapshot_name)
-        cls.percy_runner.snapshot(
-            name=snapshot_name
-        )
-
-    @classmethod
-    def setUpClass(cls):
-        super(IntegrationTests, cls).setUpClass()
-        cls.driver = webdriver.Chrome()
-
-        loader = percy.ResourceLoader(
-            webdriver=cls.driver
-        )
-        cls.percy_runner = percy.Runner(loader=loader)
-
-        cls.percy_runner.initialize_build()
-
-    @classmethod
-    def tearDownClass(cls):
-        super(IntegrationTests, cls).tearDownClass()
-        cls.driver.quit()
-        cls.percy_runner.finalize_build()
-
-    def setUp(s):
-        pass
-
-    def tearDown(s):
-        time.sleep(2)
-        s.server_process.terminate()
-        time.sleep(2)
-
-    def startServer(s, dash):
-        def run():
-            dash.scripts.config.serve_locally = True
-            dash.run_server(
-                port=8050,
-                debug=False,
-                processes=4,
-                threaded=False
-            )
-
-        # Run on a separate process so that it doesn't block
-        s.server_process = multiprocessing.Process(target=run)
-        s.server_process.start()
-        time.sleep(0.5)
-
-        # Visit the Dash page
-        s.driver.get('http://localhost:8050')
-        time.sleep(0.5)
-
-        # Inject an error and warning logger
-        logger = '''
-        window.tests = {};
-        window.tests.console = {error: [], warn: [], log: []};
-
-        var _log = console.log;
-        var _warn = console.warn;
-        var _error = console.error;
-
-        console.log = function() {
-            window.tests.console.log.push({method: 'log', arguments: arguments});
-            return _log.apply(console, arguments);
-        };
-
-        console.warn = function() {
-            window.tests.console.warn.push({method: 'warn', arguments: arguments});
-            return _warn.apply(console, arguments);
-        };
-
-        console.error = function() {
-            window.tests.console.error.push({method: 'error', arguments: arguments});
-            return _error.apply(console, arguments);
-        };
-        '''
-        s.driver.execute_script(logger)
diff --git a/tests/test_dash_html_components.py b/tests/test_dash_html_components.py
index 8b276cfa..2bb0cfd1 100644
--- a/tests/test_dash_html_components.py
+++ b/tests/test_dash_html_components.py
@@ -1,43 +1,48 @@
-import unittest
-import dash_html_components
-
-
-class TestDashHtmlComponents(unittest.TestCase):
-    def test_imports(self):
-        with open('./scripts/data/elements.txt') as f:
-            elements = [
-                s[0].upper() + s[1:] for s in
-                f.read().split('\n')
-            ]
-            elements += ['MapEl', 'ObjectEl']
-            for s in ['Map', 'Object']:
-                elements.remove(s)
-
-        print(dir(dash_html_components))
-
-        self.assertEqual(
-            set([d for d in dir(dash_html_components) if d[0] != '_' and d[0] == d[0].capitalize()]),
-            set(elements)
-        )
-
-    def test_sample_items(self):
-        Div = dash_html_components.Div
-        Img = dash_html_components.Img
-
-        layout = Div(
-            Div(
-                Img(src='https://plotly.com/~chris/1638.png')
-            ), style={'color': 'red'}
-        )
-
-        self.assertEqual(
-            repr(layout),
-            ''.join([
-                "Div(children=Div(Img(src='https://plotly.com/~chris/1638.png')), "
-                "style={'color': 'red'})"
-            ])
-        )
-
-        self.assertEqual(
-            layout._namespace, 'dash_html_components'
-        )
+import pytest
+import dash_html_components as html
+
+
+def test_imports():
+    with open("./scripts/data/elements.txt") as f:
+        elements = [s[0].upper() + s[1:] for s in f.read().split("\n")]
+        elements += ["MapEl", "ObjectEl"]
+        for s in ["Map", "Object"]:
+            elements.remove(s)
+
+    dir_set = set(
+        [
+            d
+            for d in dir(html)
+            if d[0] != "_" and d[0] == d[0].capitalize()
+        ]
+    )
+    assert dir_set == set(elements)
+
+
+def test_sample_items():
+    layout = html.Div(
+        html.Div(html.Img(src="https://plotly.com/~chris/1638.png")),
+        style={"color": "red"}
+    )
+
+    expected = (
+        "Div(children=Div(Img(src='https://plotly.com/~chris/1638.png')), "
+        "style={'color': 'red'})"
+    )
+    assert repr(layout) == expected
+
+    assert layout._namespace == "dash_html_components"
+
+
+def test_objectEl():
+    layout = html.ObjectEl(data="something", **{"data-x": "else"})
+    assert repr(layout) == "ObjectEl(data='something', data-x='else')"
+
+    with pytest.raises(TypeError):
+        html.ObjectEl(datax="something")
+
+
+def test_customDocs():
+    assert "CAUTION" in html.Script.__doc__[:100]
+    assert "OBSOLETE" in html.Blink.__doc__[:100]
+    assert "DEPRECATED" in html.Marquee.__doc__[:100]
diff --git a/tests/test_dash_import.py b/tests/test_dash_import.py
index 20d61165..d82e6342 100644
--- a/tests/test_dash_import.py
+++ b/tests/test_dash_import.py
@@ -5,18 +5,18 @@
 class TestDashImport(unittest.TestCase):
     def setUp(self):
         with open('dash.py', 'w') as f:
-            pass        
-        
+            pass
+
     def tearDown(self):
         try:
             os.remove('dash.py')
             os.remove('dash.pyc')
         except OSError:
             pass
-        
+
     def test_dash_import(self):
         """Test that program exits if the wrong dash module was imported"""
-        
+
         with self.assertRaises(SystemExit) as cm:
             import dash_html_components
 
diff --git a/tests/test_integration.py b/tests/test_integration.py
index d259520d..313ad503 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,159 +1,99 @@
-import base64
-from datetime import datetime
-import io
-import itertools
 from multiprocessing import Value
-import os
-import pandas as pd
-from selenium import webdriver
-from selenium.webdriver.common.keys import Keys
-import sys
-from textwrap import dedent
 import time
-try:
-    from urlparse import urlparse
-except ImportError:
-    from urllib.parse import urlparse
 
 import dash
-from dash.dependencies import Input, Output, State
+from dash.dependencies import Input, Output
 import dash_html_components as html
-import dash_core_components as dcc
-
-from .IntegrationTests import IntegrationTests
-from .utils import assert_clean_console
-
-
-class Tests(IntegrationTests):
-    def setUp(self):
-        pass
-
-    def wait_for_element_by_css_selector(self, selector):
-        start_time = time.time()
-        error = None
-        while time.time() < start_time + 20:
-            try:
-                return self.driver.find_element_by_css_selector(selector)
-            except Exception as e:
-                error = e
-            self.driver.implicitly_wait(1)
-        raise error
-
-    def wait_for_text_to_equal(self, selector, assertion_text):
-        start_time = time.time()
-        error = None
-        while time.time() < start_time + 20:
-            el = self.wait_for_element_by_css_selector(selector)
-            try:
-                return self.assertEqual(el.text, assertion_text)
-            except Exception as e:
-                error = e
-            time.sleep(0.25)
-        raise error
-
-    def snapshot(self, name):
-        if 'PERCY_PROJECT' in os.environ and 'PERCY_TOKEN' in os.environ:
-            python_version = sys.version.split(' ')[0]
-            print('Percy Snapshot {}'.format(python_version))
-            self.percy_runner.snapshot(name=name)
-
-    def test_click(self):
-        call_count = Value('i', 0)
-
-        app = dash.Dash()
-        app.layout = html.Div([
-            html.Div(id='container'),
-            html.Button('Click', id='button', n_clicks=0)
-        ])
-
-        @app.callback(Output('container', 'children'), [Input('button', 'n_clicks')])
-        def update_output(n_clicks):
-            call_count.value += 1
-            return 'You have clicked the button {} times'.format(n_clicks)
-
-        self.startServer(app)
-
-        self.wait_for_element_by_css_selector('#container')
-
-        self.wait_for_text_to_equal(
-            '#container', 'You have clicked the button 0 times')
-        self.assertEqual(call_count.value, 1)
-        self.snapshot('button initialization')
-
-        self.driver.find_element_by_css_selector('#button').click()
-
-        self.wait_for_text_to_equal(
-            '#container', 'You have clicked the button 1 times')
-        self.assertEqual(call_count.value, 2)
-        self.snapshot('button click')
-
-
-    def test_click_prev(self):
-        call_count = Value('i', 0)
-        timestamp_1 = Value('d', -5)
-        timestamp_2 = Value('d', -5)
-
-        app = dash.Dash()
-        app.layout = html.Div([
-            html.Div(id='container'),
-            html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1),
-            html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1)
-        ])
-
-        @app.callback(
-            Output('container', 'children'),
-            [Input('button-1', 'n_clicks'),
-             Input('button-1', 'n_clicks_timestamp'),
-             Input('button-2', 'n_clicks'),
-             Input('button-2', 'n_clicks_timestamp')])
-        def update_output(*args):
-            print(args)
-            call_count.value += 1
-            timestamp_1.value = args[1]
-            timestamp_2.value = args[3]
-            return '{}, {}'.format(args[0], args[2])
-
-        self.startServer(app)
-
-        self.wait_for_element_by_css_selector('#container')
-        time.sleep(2)
-        self.wait_for_text_to_equal('#container', '0, 0')
-        self.assertEqual(timestamp_1.value, -1)
-        self.assertEqual(timestamp_2.value, -1)
-        self.assertEqual(call_count.value, 1)
-        self.snapshot('button initialization 1')
-
-        self.driver.find_element_by_css_selector('#button-1').click()
-        time.sleep(2)
-        self.wait_for_text_to_equal('#container', '1, 0')
-        print(timestamp_1.value)
-        print((time.time() - (24 * 60 * 60)) * 1000)
-        self.assertTrue(
-            timestamp_1.value >
-            ((time.time() - (24 * 60 * 60)) * 1000))
-        self.assertEqual(timestamp_2.value, -1)
-        self.assertEqual(call_count.value, 2)
-        self.snapshot('button-1 click')
-        prev_timestamp_1 = timestamp_1.value
-
-        self.driver.find_element_by_css_selector('#button-2').click()
-        time.sleep(2)
-        self.wait_for_text_to_equal('#container', '1, 1')
-        self.assertEqual(timestamp_1.value, prev_timestamp_1)
-        self.assertTrue(
-            timestamp_2.value >
-            ((time.time() - 24 * 60 * 60) * 1000))
-        self.assertEqual(call_count.value, 3)
-        self.snapshot('button-2 click')
-        prev_timestamp_2 = timestamp_2.value
-
-        self.driver.find_element_by_css_selector('#button-2').click()
-        time.sleep(2)
-        self.wait_for_text_to_equal('#container', '1, 2')
-        self.assertEqual(timestamp_1.value, prev_timestamp_1)
-        self.assertTrue(
-            timestamp_2.value >
-            prev_timestamp_2)
-        self.assertTrue(timestamp_2.value > timestamp_1.value)
-        self.assertEqual(call_count.value, 4)
-        self.snapshot('button-2 click again')
+
+
+def test_click_simple(dash_duo):
+    call_count = Value('i', 0)
+
+    app = dash.Dash(__name__)
+    app.layout = html.Div([
+        html.Div(id='container'),
+        html.Button('Click', id='button', n_clicks=0)
+    ])
+
+    @app.callback(Output('container', 'children'), Input('button', 'n_clicks'))
+    def update_output(n_clicks):
+        call_count.value += 1
+        return 'clicked {} times'.format(n_clicks)
+
+    dash_duo.start_server(app)
+
+    dash_duo.find_element('#container')
+
+    dash_duo.wait_for_text_to_equal(
+        '#container', 'clicked 0 times')
+    assert call_count.value == 1
+    dash_duo.percy_snapshot('button initialization')
+
+    dash_duo.find_element('#button').click()
+
+    dash_duo.wait_for_text_to_equal(
+        '#container', 'clicked 1 times')
+    assert call_count.value == 2
+    dash_duo.percy_snapshot('button click')
+
+    assert not dash_duo.get_logs()
+
+
+def test_click_prev(dash_duo):
+    call_count = Value('i', 0)
+    timestamp_1 = Value('d', -5)
+    timestamp_2 = Value('d', -5)
+
+    app = dash.Dash(__name__)
+    app.layout = html.Div([
+        html.Div(id='container'),
+        html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1),
+        html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1)
+    ])
+
+    @app.callback(
+        Output('container', 'children'),
+        [Input('button-1', 'n_clicks'),
+         Input('button-1', 'n_clicks_timestamp'),
+         Input('button-2', 'n_clicks'),
+         Input('button-2', 'n_clicks_timestamp')])
+    def update_output(*args):
+        print(args)
+        call_count.value += 1
+        timestamp_1.value = args[1]
+        timestamp_2.value = args[3]
+        return '{}, {}'.format(args[0], args[2])
+
+    dash_duo.start_server(app)
+
+    dash_duo.wait_for_text_to_equal('#container', '0, 0')
+    assert timestamp_1.value == -1
+    assert timestamp_2.value == -1
+    assert call_count.value == 1
+    dash_duo.percy_snapshot('button initialization 1')
+
+    dash_duo.find_element('#button-1').click()
+    dash_duo.wait_for_text_to_equal('#container', '1, 0')
+    assert timestamp_1.value > ((time.time() - (24 * 60 * 60)) * 1000)
+    assert timestamp_2.value == -1
+    assert call_count.value == 2
+    dash_duo.percy_snapshot('button-1 click')
+    prev_timestamp_1 = timestamp_1.value
+
+    dash_duo.find_element('#button-2').click()
+    dash_duo.wait_for_text_to_equal('#container', '1, 1')
+    assert timestamp_1.value == prev_timestamp_1
+    assert timestamp_2.value > ((time.time() - 24 * 60 * 60) * 1000)
+    assert call_count.value == 3
+    dash_duo.percy_snapshot('button-2 click')
+    prev_timestamp_2 = timestamp_2.value
+
+    dash_duo.find_element('#button-2').click()
+    dash_duo.wait_for_text_to_equal('#container', '1, 2')
+    assert timestamp_1.value == prev_timestamp_1
+    assert timestamp_2.value > prev_timestamp_2
+    assert timestamp_2.value > timestamp_1.value
+    assert call_count.value == 4
+    dash_duo.percy_snapshot('button-2 click again')
+
+    assert not dash_duo.get_logs()