diff --git a/.circleci/config.yml b/.circleci/config.yml index e7454f2a..72ef31f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,12 +26,12 @@ jobs: when: always - "python-3.6": + "python-3.6": &test-template docker: - image: circleci/python:3.6-stretch-browsers environment: - PERCY_ENABLED: False + PERCY_ENABLED: True steps: - checkout @@ -70,16 +70,31 @@ jobs: when: always - run: - name: Integration Tests + name: Integration Tests - Usage command: | . venv/bin/activate - python -m unittest tests.test_render + python -m unittest tests.test_usage when: always + - run: + name: Capture Percy Snapshots + command: | + . venv/bin/activate + python -m unittest tests.test_percy_snapshot + when: always + + "python-3.7": + <<: *test-template + docker: + - image: circleci/python:3.7-stretch-browsers + environment: + PERCY_ENABLE: False + workflows: version: 2 build: jobs: - "python-3.6" + - "python-3.7" - "node" diff --git a/.gitignore b/.gitignore index 6ab50f9b..4ab8bf3c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ # Gradle .idea/**/gradle.xml .idea/**/libraries +tests/screenshots/*.png # misc .DS_Store diff --git a/dash_cytoscape/package.json b/dash_cytoscape/package.json index 1ca08185..db759bd7 100644 --- a/dash_cytoscape/package.json +++ b/dash_cytoscape/package.json @@ -6,7 +6,6 @@ "scripts": { "start": "webpack-serve ./webpack.serve.config.js --open", "validate-init": "python _validate_init.py", - "prepublish": "npm run validate-init", "build:js-dev": "webpack --mode development", "build:js": "webpack --mode production", "build:py": "dash-generate-components ./src/lib/components dash_cytoscape", diff --git a/package.json b/package.json index 1ca08185..db759bd7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "scripts": { "start": "webpack-serve ./webpack.serve.config.js --open", "validate-init": "python _validate_init.py", - "prepublish": "npm run validate-init", "build:js-dev": "webpack --mode development", "build:js": "webpack --mode production", "build:py": "dash-generate-components ./src/lib/components dash_cytoscape", diff --git a/tests/IntegrationTests.py b/tests/IntegrationTests.py index 1487a395..e3237de8 100644 --- a/tests/IntegrationTests.py +++ b/tests/IntegrationTests.py @@ -20,6 +20,7 @@ class IntegrationTests(unittest.TestCase): def percy_snapshot(self, name=''): if os.environ.get('PERCY_ENABLED', False): snapshot_name = '{} - {}'.format(name, sys.version_info) + self.percy_runner.snapshot( name=snapshot_name ) @@ -32,13 +33,13 @@ def setUpClass(cls): if 'DASH_TEST_CHROMEPATH' in os.environ: options.binary_location = os.environ['DASH_TEST_CHROMEPATH'] - cls.driver = webdriver.Chrome(chrome_options=options) + cls.driver = webdriver.Chrome(options=options) + cls.driver.set_window_size(1280, 1000) if os.environ.get('PERCY_ENABLED', False): - loader = percy.ResourceLoader( - webdriver=cls.driver - ) - cls.percy_runner = percy.Runner(loader=loader) + loader = percy.ResourceLoader(webdriver=cls.driver) + percy_config = percy.Config(default_widths=[1280]) + cls.percy_runner = percy.Runner(loader=loader, config=percy_config) cls.percy_runner.initialize_build() @classmethod @@ -56,21 +57,22 @@ def tearDown(self): time.sleep(3) if platform.system() == 'Windows': requests.get('http://localhost:8050/stop') + sys.exit() else: self.server_process.terminate() time.sleep(3) - def startServer(self, app): + def startServer(self, app, port=8050): if 'DASH_TEST_PROCESSES' in os.environ: processes = int(os.environ['DASH_TEST_PROCESSES']) else: - processes = 4 + processes = 1 def run(): app.scripts.config.serve_locally = True app.css.config.serve_locally = True app.run_server( - port=8050, + port=port, debug=False, processes=processes ) @@ -86,9 +88,9 @@ def _stop_server_windows(): return 'stop' app.run_server( - port=8050, + port=port, debug=False, - threaded=True + threaded=False ) # Run on a separate process so that it doesn't block @@ -104,5 +106,5 @@ def _stop_server_windows(): time.sleep(5) # Visit the dash page - self.driver.get('http://localhost:8050') + self.driver.get('http://localhost:{}'.format(port)) time.sleep(0.5) diff --git a/tests/requirements.txt b/tests/requirements.txt index 6b543d12..7e492fb4 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -13,3 +13,4 @@ selenium flake8 pylint pytest-dash>=1.0.1 +colour==0.1.5 \ No newline at end of file diff --git a/tests/screenshots/readme.md b/tests/screenshots/readme.md new file mode 100644 index 00000000..4ab55142 --- /dev/null +++ b/tests/screenshots/readme.md @@ -0,0 +1,3 @@ +This is directory is reserved for screenshots generated by Selenium's webdriver in `test_usage.py`, during the CircleCI builds. + +Please do not add unecessary files to this directory (in fact, the only file should be this `readme.md`), and do not move this file. \ No newline at end of file diff --git a/tests/test_percy_snapshot.py b/tests/test_percy_snapshot.py new file mode 100644 index 00000000..72a20268 --- /dev/null +++ b/tests/test_percy_snapshot.py @@ -0,0 +1,82 @@ +import base64 +import os +import time + +from .IntegrationTests import IntegrationTests +import dash +import dash_html_components as html +import dash_core_components as dcc +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +class Tests(IntegrationTests): + """ + In order to render snapshots, Percy collects the DOM of the project and + uses a custom rendering method, different from Selenium. Therefore, it + is unable to render Canvas elements, so can't render Cytoscape charts + directly. + + Instead, we use Selenium webdrivers to automatically screenshot each of + the apps being tested in test_usage.py, display them in a simple + Dash app, and use Percy to take a snapshot for CVI. + """ + + def test_usage(self): + def encode(name): + path = os.path.join( + os.path.dirname(__file__), + 'screenshots', + name + ) + + with open(path, 'rb') as image_file: + encoded_string = base64.b64encode(image_file.read()) + return "data:image/png;base64," + encoded_string.decode('ascii') + + # Define the app + app = dash.Dash(__name__) + + app.layout = html.Div([ + # represents the URL bar, doesn't render anything + dcc.Location(id='url', refresh=False), + # content will be rendered in this element + html.Div(id='page-content') + ]) + + @app.callback(dash.dependencies.Output('page-content', 'children'), + [dash.dependencies.Input('url', 'pathname')]) + def display_image(pathname): # pylint: disable=W0612 + """ + Assign the url path to return the image it represent. For example, + to return "usage.png", you can visit localhost/usage.png. + :param pathname: name of the screenshot, prefixed with "/" + :return: An html.Img object containing the base64 encoded image + """ + if not pathname or pathname == '/': + return None + + name = pathname.replace('/', '') + return html.Img(id=name, src=encode(name)) + + # Start the app + self.startServer(app) + + # Find the names of all the screenshots + asset_list = os.listdir(os.path.join( + os.path.dirname(__file__), + 'screenshots' + )) + + # Run Percy + for image in asset_list: + if image.endswith('png'): + self.driver.get('http://localhost:8050/{}'.format(image)) + + WebDriverWait(self.driver, 20).until( + EC.presence_of_element_located((By.ID, image)) + ) + + self.percy_snapshot(name=image) + time.sleep(2) diff --git a/tests/test_render.py b/tests/test_render.py deleted file mode 100644 index f494be94..00000000 --- a/tests/test_render.py +++ /dev/null @@ -1,25 +0,0 @@ -from .IntegrationTests import IntegrationTests -import dash -import dash_html_components as html -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - -from dash_cytoscape import ExampleComponent # pylint: disable=no-name-in-module - - -class Tests(IntegrationTests): - def test_render_component(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.Div(id='waitfor'), - ExampleComponent(label='Example Component Label') - ]) - - self.startServer(app) - - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.ID, "waitfor")) - ) - - self.percy_snapshot('Simple Render') diff --git a/tests/test_usage.py b/tests/test_usage.py index 895835a8..4fa3ab1b 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -1,31 +1,56 @@ -from pytest_dash.utils import ( - import_app, - wait_for_text_to_equal, - wait_for_element_by_css_selector -) +import os +import importlib +from .IntegrationTests import IntegrationTests +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC -# Basic test for the component rendering. -def test_render_component(dash_threaded, selenium): - # Start a dash app contained in `usage.py` - # dash_threaded is a fixture by pytest-dash - # It will load a py file containing a Dash instance named `app` - # and start it in a thread. - app = import_app('usage') - dash_threaded(app) +class Tests(IntegrationTests): + def create_usage_test(self, filename): + app = importlib.import_module(filename).app - # Get the generated component input with selenium - # The html input will be a children of the #input dash component - my_component = wait_for_element_by_css_selector(selenium, '#input > input') + self.startServer(app) - assert 'my-value' == my_component.get_attribute('value') + WebDriverWait(self.driver, 20).until( + EC.presence_of_element_located((By.ID, "cytoscape")) + ) - # Clear the input - my_component.clear() + self.driver.save_screenshot(os.path.join( + os.path.dirname(__file__), + 'screenshots', + filename + '.png' + )) - # Send keys to the custom input. - my_component.send_keys('Hello dash') + def test_usage_advanced(self): + self.create_usage_test('usage-advanced') - # Wait for the text to equal, if after the timeout (default 10 seconds) - # the text is not equal it will fail the test. - wait_for_text_to_equal(selenium, '#output', 'You have entered Hello dash') + def test_usage_animated_bfs(self): + self.create_usage_test('demos.usage-animated-bfs') + + def test_usage_breadthfirst_layout(self): + self.create_usage_test('demos.usage-breadthfirst-layout') + + def test_usage_compound_nodes(self): + self.create_usage_test('demos.usage-compound-nodes') + + def test_usage_events(self): + self.create_usage_test('usage-events') + + def test_usage_elements(self): + self.create_usage_test('usage-elements') + + def test_usage_pie_style(self): + self.create_usage_test('demos.usage-pie-style') + + def test_usage_simple(self): + self.create_usage_test('usage') + + def test_usage_stylesheet(self): + self.create_usage_test('usage-stylesheet') + + def test_usage_initialisation(self): + self.create_usage_test('demos.usage-initialisation') + + def test_usage_linkout_example(self): + self.create_usage_test('demos.usage-linkout-example') diff --git a/usage.py b/usage.py index 4bdd7acf..cde948d0 100644 --- a/usage.py +++ b/usage.py @@ -9,9 +9,11 @@ cyto.Cytoscape( id='cytoscape', elements=[ - {'data': {'id': 'one', 'label': 'Node 1'}, 'position': {'x': 50, 'y': 50}}, - {'data': {'id': 'two', 'label': 'Node 2'}, 'position': {'x': 200, 'y': 200}}, - {'data': {'source': 'one', 'target': 'two','label': 'Node 1 to 2'}} + {'data': {'id': 'one', 'label': 'Node 1'}, + 'position': {'x': 50, 'y': 50}}, + {'data': {'id': 'two', 'label': 'Node 2'}, + 'position': {'x': 200, 'y': 200}}, + {'data': {'source': 'one', 'target': 'two', 'label': '1 to 2'}} ], layout={'name': 'preset'} )