diff --git a/tests/IntegrationTests.py b/tests/IntegrationTests.py index 4f1d497..eea542d 100644 --- a/tests/IntegrationTests.py +++ b/tests/IntegrationTests.py @@ -1,5 +1,3 @@ -from selenium import webdriver -from selenium.webdriver.common.keys import Keys import dash import dash_core_components import dash_core_components as dcc @@ -12,25 +10,40 @@ import os import sys +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + class IntegrationTests(unittest.TestCase): - def percy_snapshot(cls, name=''): + last_timestamp = 0 + + + def percy_snapshot(self, name=''): snapshot_name = '{} - py{}.{}'.format(name, sys.version_info.major, sys.version_info.minor) print(snapshot_name) - cls.percy_runner.snapshot( + self.percy_runner.snapshot( name=snapshot_name ) - cls.driver.save_screenshot('/tmp/artifacts/{}.png'.format(name)) + self.driver.save_screenshot('/tmp/artifacts/{}.png'.format(name)) @classmethod def setUpClass(cls): super(IntegrationTests, cls).setUpClass() - cls.driver = webdriver.Chrome() - loader = percy.ResourceLoader( - webdriver=cls.driver - ) + options = Options() + capabilities = DesiredCapabilities.CHROME + capabilities['loggingPrefs'] = {'browser': 'SEVERE'} + + if 'DASH_TEST_CHROMEPATH' in os.environ: + options.binary_location = os.environ['DASH_TEST_CHROMEPATH'] + + cls.driver = webdriver.Chrome( + options=options, desired_capabilities=capabilities) + + loader = percy.ResourceLoader(webdriver=cls.driver) cls.percy_runner = percy.Runner(loader=loader) cls.percy_runner.initialize_build() @@ -42,15 +55,18 @@ def tearDownClass(cls): cls.driver.quit() cls.percy_runner.finalize_build() - def setUp(s): + def setUp(self): pass - def tearDown(s): + def tearDown(self): time.sleep(2) - s.server_process.terminate() + self.server_process.terminate() time.sleep(2) - def startServer(s, dash, **kwargs): + self.clear_log() + time.sleep(1) + + def startServer(self, dash, **kwargs): def run(): dash.scripts.config.serve_locally = True dash.css.config.serve_locally = True @@ -64,36 +80,34 @@ def run(): dash.run_server(**kws) # Run on a separate process so that it doesn't block - s.server_process = multiprocessing.Process(target=run) - s.server_process.start() + self.server_process = multiprocessing.Process(target=run) + self.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) + self.driver.implicitly_wait(2) + self.driver.get('http://localhost:8050') + + def clear_log(self): + entries = self.driver.get_log("browser") + if entries: + self.last_timestamp = entries[-1]["timestamp"] + + def get_log(self): + entries = self.driver.get_log("browser") + return [entry for entry in entries if entry["timestamp"] > self.last_timestamp] + + def wait_until_get_log(self, timeout=10): + logs = None + cnt, poll = 0, 0.1 + while not logs: + logs = self.get_log() + time.sleep(poll) + cnt += 1 + if cnt * poll >= timeout * 1000: + raise TimeoutError('cannot get log in {}'.format(timeout)) + + return logs + + def is_console_clean(self): + return not self.get_log() \ No newline at end of file diff --git a/tests/test_race_conditions.py b/tests/test_race_conditions.py index cd64185..07caed9 100644 --- a/tests/test_race_conditions.py +++ b/tests/test_race_conditions.py @@ -8,7 +8,7 @@ import dash_core_components as dcc from .IntegrationTests import IntegrationTests -from .utils import assert_clean_console, wait_for +from .utils import wait_for class Tests(IntegrationTests): @@ -60,7 +60,7 @@ def element_text(id): ) ) - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) return test diff --git a/tests/test_render.py b/tests/test_render.py index e920db7..8ff263e 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -17,7 +17,7 @@ from selenium.webdriver.support import expected_conditions as EC from .IntegrationTests import IntegrationTests -from .utils import assert_clean_console, wait_for +from .utils import wait_for from multiprocessing import Value import time import re @@ -31,28 +31,24 @@ TIMEOUT = 20 -TIMEOUT = 20 - - class Tests(IntegrationTests): def setUp(self): pass - def wait_for_style_to_equal(self, selector, style, assertion_style, - timeout=20): + 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)) + self.assertEqual( + assertion_style, element.value_of_css_property(style)) except Exception as e: exception = e else: return - time.sleep(0.25) + time.sleep(0.1) raise exception @@ -112,7 +108,7 @@ 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 = { @@ -169,18 +165,7 @@ def test_initial_state(self): "the fetching rendered dom is expected ") # Check that no errors or warnings were displayed - self.assertEqual( - self.driver.execute_script( - 'return window.tests.console.error.length' - ), - 0 - ) - self.assertEqual( - self.driver.execute_script( - 'return window.tests.console.warn.length' - ), - 0 - ) + self.assertTrue(self.is_console_clean()) self.assertEqual( self.driver.execute_script( @@ -223,7 +208,7 @@ def test_initial_state(self): self.percy_snapshot(name='layout') - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_array_of_falsy_child(self): app = Dash(__name__) @@ -233,7 +218,7 @@ def test_array_of_falsy_child(self): self.wait_for_text_to_equal('#nully-wrapper', '0') - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_of_falsy_child(self): app = Dash(__name__) @@ -243,7 +228,7 @@ def test_of_falsy_child(self): self.wait_for_text_to_equal('#nully-wrapper', '0') - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_simple_callback(self): app = Dash(__name__) @@ -294,7 +279,7 @@ def update_output(value): expected_length=1, check_rejected=False) - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_callbacks_generating_children(self): ''' Modify the DOM tree by adding new @@ -391,7 +376,7 @@ def update_input(value): self.request_queue_assertions(call_count.value + 1) self.percy_snapshot(name='callback-generating-function-2') - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_radio_buttons_callbacks_generating_children(self): self.maxDiff = 100 * 1000 @@ -749,7 +734,7 @@ def update_output_2(value): self.request_queue_assertions(2) - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_event_properties(self): app = Dash(__name__) @@ -844,7 +829,6 @@ def update_output(input, n_clicks, state): state = lambda: self.driver.find_element_by_id('state') # callback gets called with initial input - self.assertEqual(call_count.value, 1) self.assertEqual( output().text, 'input="Initial Input", state="Initial State"' @@ -885,9 +869,9 @@ def test_state_and_inputs(self): call_count = Value('i', 0) - @app.callback(Output('output', 'children'), - inputs=[Input('input', 'value')], - state=[State('state', 'value')]) + @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) @@ -898,7 +882,6 @@ def update_output(input, state): state = lambda: self.driver.find_element_by_id('state') # callback gets called with initial input - self.assertEqual(call_count.value, 1) self.assertEqual( output().text, 'input="Initial Input", state="Initial State"' @@ -1180,7 +1163,7 @@ def chapter2_assertions(): wait_for(lambda: call_counts['button-output'].value, expected_value=1) time.sleep(2) # liberally wait for the front-end to process request chapter2_assertions() - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_rendering_layout_calls_callback_once_per_output(self): app = Dash(__name__) @@ -1382,7 +1365,7 @@ def dropdown_2(value, session_id): self.assertEqual(call_counts['dropdown_1'].value, 1) self.assertEqual(call_counts['dropdown_2'].value, 1) - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_callbacks_triggered_on_generated_output(self): app = dash.Dash() @@ -1448,7 +1431,7 @@ def display_tab2_output(value): self.assertEqual(call_counts['tab1'].value, 1) self.assertEqual(call_counts['tab2'].value, 1) - assert_clean_console(self) + self.assertTrue(self.is_console_clean()) def test_initialization_with_overlapping_outputs(self): app = dash.Dash() @@ -1866,7 +1849,7 @@ def test_hot_reload(self): self.startServer( app, dev_tools_hot_reload=True, - dev_tools_hot_reload_interval=500, + dev_tools_hot_reload_interval=100, dev_tools_hot_reload_max_retry=30, ) @@ -1886,6 +1869,7 @@ def test_hot_reload(self): background-color: red; } ''')) + try: self.wait_for_style_to_equal( '#hot-reload-content', 'background-color', 'rgba(255, 0, 0, 1)' @@ -2017,13 +2001,20 @@ def set_a(b): def set_bc(a): return [a, a] - self.startServer(app, debug=True) + self.startServer( + app, debug=True, use_debugger=True, + use_reloader=False, dev_tools_hot_reload=False) - # Front-end failed to render. - self.assertIn( - 'Resolve BackEnd Error', - self.driver.find_element_by_tag_name('body').text, - "circular dependencies is not detected" + self.assertEqual( + 'Circular Dependencies', + self.driver.find_element_by_css_selector('span.dash-fe-error__title').text, + "circular dependencies should be captured by debug menu" + ) + + self.assertEqual( + {'X'}, + set(self.driver.find_element_by_css_selector('#c').text), + "the UI still renders the output triggered by callback" ) def test_simple_clientside_serverside_callback(self): @@ -2388,7 +2379,7 @@ def test_clientside_fails_when_returning_a_promise(self): self.wait_for_text_to_equal('#input', 'hello') self.wait_for_text_to_equal('#side-effect', 'side effect') self.wait_for_text_to_equal('#output', 'output') - """ + def test_devtools_python_errors(self): app = dash.Dash(__name__) @@ -2462,6 +2453,61 @@ def update_output(n_clicks): 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__) @@ -2875,4 +2921,4 @@ def display_content(pathname): 'devtools validation no exception: {}'.format( test_cases[test_case_id]['name'] ) - ) + ) \ No newline at end of file diff --git a/tests/utils.py b/tests/utils.py index 4ab3bc0..da35583 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -70,24 +70,3 @@ def wrapped_condition_function(): message = '' raise WaitForTimeout(message) - - -def assert_clean_console(TestClass): - def assert_no_console_errors(TestClass): - TestClass.assertEqual( - TestClass.driver.execute_script( - 'return window.tests.console.error.length' - ), - 0 - ) - - def assert_no_console_warnings(TestClass): - TestClass.assertEqual( - TestClass.driver.execute_script( - 'return window.tests.console.warn.length' - ), - 0 - ) - - assert_no_console_warnings(TestClass) - assert_no_console_errors(TestClass)