Skip to content

Commit f7cc272

Browse files
author
Xing
authored
Merge pull request #35 from xhlulu/master
Add unit tests, setup CircleCI and Percy
2 parents 11525e9 + bfce626 commit f7cc272

File tree

11 files changed

+173
-69
lines changed

11 files changed

+173
-69
lines changed

.circleci/config.yml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ jobs:
2626
when: always
2727

2828

29-
"python-3.6":
29+
"python-3.6": &test-template
3030
docker:
3131
- image: circleci/python:3.6-stretch-browsers
3232

3333
environment:
34-
PERCY_ENABLED: False
34+
PERCY_ENABLED: True
3535

3636
steps:
3737
- checkout
@@ -70,16 +70,31 @@ jobs:
7070
when: always
7171

7272
- run:
73-
name: Integration Tests
73+
name: Integration Tests - Usage
7474
command: |
7575
. venv/bin/activate
76-
python -m unittest tests.test_render
76+
python -m unittest tests.test_usage
7777
when: always
7878

79+
- run:
80+
name: Capture Percy Snapshots
81+
command: |
82+
. venv/bin/activate
83+
python -m unittest tests.test_percy_snapshot
84+
when: always
85+
86+
"python-3.7":
87+
<<: *test-template
88+
docker:
89+
- image: circleci/python:3.7-stretch-browsers
90+
environment:
91+
PERCY_ENABLE: False
92+
7993

8094
workflows:
8195
version: 2
8296
build:
8397
jobs:
8498
- "python-3.6"
99+
- "python-3.7"
85100
- "node"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
# Gradle
3737
.idea/**/gradle.xml
3838
.idea/**/libraries
39+
tests/screenshots/*.png
3940

4041
# misc
4142
.DS_Store

dash_cytoscape/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"scripts": {
77
"start": "webpack-serve ./webpack.serve.config.js --open",
88
"validate-init": "python _validate_init.py",
9-
"prepublish": "npm run validate-init",
109
"build:js-dev": "webpack --mode development",
1110
"build:js": "webpack --mode production",
1211
"build:py": "dash-generate-components ./src/lib/components dash_cytoscape",

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"scripts": {
77
"start": "webpack-serve ./webpack.serve.config.js --open",
88
"validate-init": "python _validate_init.py",
9-
"prepublish": "npm run validate-init",
109
"build:js-dev": "webpack --mode development",
1110
"build:js": "webpack --mode production",
1211
"build:py": "dash-generate-components ./src/lib/components dash_cytoscape",

tests/IntegrationTests.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class IntegrationTests(unittest.TestCase):
2020
def percy_snapshot(self, name=''):
2121
if os.environ.get('PERCY_ENABLED', False):
2222
snapshot_name = '{} - {}'.format(name, sys.version_info)
23+
2324
self.percy_runner.snapshot(
2425
name=snapshot_name
2526
)
@@ -32,13 +33,13 @@ def setUpClass(cls):
3233
if 'DASH_TEST_CHROMEPATH' in os.environ:
3334
options.binary_location = os.environ['DASH_TEST_CHROMEPATH']
3435

35-
cls.driver = webdriver.Chrome(chrome_options=options)
36+
cls.driver = webdriver.Chrome(options=options)
37+
cls.driver.set_window_size(1280, 1000)
3638

3739
if os.environ.get('PERCY_ENABLED', False):
38-
loader = percy.ResourceLoader(
39-
webdriver=cls.driver
40-
)
41-
cls.percy_runner = percy.Runner(loader=loader)
40+
loader = percy.ResourceLoader(webdriver=cls.driver)
41+
percy_config = percy.Config(default_widths=[1280])
42+
cls.percy_runner = percy.Runner(loader=loader, config=percy_config)
4243
cls.percy_runner.initialize_build()
4344

4445
@classmethod
@@ -56,21 +57,22 @@ def tearDown(self):
5657
time.sleep(3)
5758
if platform.system() == 'Windows':
5859
requests.get('http://localhost:8050/stop')
60+
sys.exit()
5961
else:
6062
self.server_process.terminate()
6163
time.sleep(3)
6264

63-
def startServer(self, app):
65+
def startServer(self, app, port=8050):
6466
if 'DASH_TEST_PROCESSES' in os.environ:
6567
processes = int(os.environ['DASH_TEST_PROCESSES'])
6668
else:
67-
processes = 4
69+
processes = 1
6870

6971
def run():
7072
app.scripts.config.serve_locally = True
7173
app.css.config.serve_locally = True
7274
app.run_server(
73-
port=8050,
75+
port=port,
7476
debug=False,
7577
processes=processes
7678
)
@@ -86,9 +88,9 @@ def _stop_server_windows():
8688
return 'stop'
8789

8890
app.run_server(
89-
port=8050,
91+
port=port,
9092
debug=False,
91-
threaded=True
93+
threaded=False
9294
)
9395

9496
# Run on a separate process so that it doesn't block
@@ -104,5 +106,5 @@ def _stop_server_windows():
104106
time.sleep(5)
105107

106108
# Visit the dash page
107-
self.driver.get('http://localhost:8050')
109+
self.driver.get('http://localhost:{}'.format(port))
108110
time.sleep(0.5)

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ selenium
1313
flake8
1414
pylint
1515
pytest-dash>=1.0.1
16+
colour==0.1.5

tests/screenshots/readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This is directory is reserved for screenshots generated by Selenium's webdriver in `test_usage.py`, during the CircleCI builds.
2+
3+
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.

tests/test_percy_snapshot.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import base64
2+
import os
3+
import time
4+
5+
from .IntegrationTests import IntegrationTests
6+
import dash
7+
import dash_html_components as html
8+
import dash_core_components as dcc
9+
from selenium.webdriver.common.by import By
10+
from selenium.webdriver.support.ui import WebDriverWait
11+
from selenium.webdriver.support import expected_conditions as EC
12+
13+
14+
class Tests(IntegrationTests):
15+
"""
16+
In order to render snapshots, Percy collects the DOM of the project and
17+
uses a custom rendering method, different from Selenium. Therefore, it
18+
is unable to render Canvas elements, so can't render Cytoscape charts
19+
directly.
20+
21+
Instead, we use Selenium webdrivers to automatically screenshot each of
22+
the apps being tested in test_usage.py, display them in a simple
23+
Dash app, and use Percy to take a snapshot for CVI.
24+
"""
25+
26+
def test_usage(self):
27+
def encode(name):
28+
path = os.path.join(
29+
os.path.dirname(__file__),
30+
'screenshots',
31+
name
32+
)
33+
34+
with open(path, 'rb') as image_file:
35+
encoded_string = base64.b64encode(image_file.read())
36+
return "data:image/png;base64," + encoded_string.decode('ascii')
37+
38+
# Define the app
39+
app = dash.Dash(__name__)
40+
41+
app.layout = html.Div([
42+
# represents the URL bar, doesn't render anything
43+
dcc.Location(id='url', refresh=False),
44+
# content will be rendered in this element
45+
html.Div(id='page-content')
46+
])
47+
48+
@app.callback(dash.dependencies.Output('page-content', 'children'),
49+
[dash.dependencies.Input('url', 'pathname')])
50+
def display_image(pathname): # pylint: disable=W0612
51+
"""
52+
Assign the url path to return the image it represent. For example,
53+
to return "usage.png", you can visit localhost/usage.png.
54+
:param pathname: name of the screenshot, prefixed with "/"
55+
:return: An html.Img object containing the base64 encoded image
56+
"""
57+
if not pathname or pathname == '/':
58+
return None
59+
60+
name = pathname.replace('/', '')
61+
return html.Img(id=name, src=encode(name))
62+
63+
# Start the app
64+
self.startServer(app)
65+
66+
# Find the names of all the screenshots
67+
asset_list = os.listdir(os.path.join(
68+
os.path.dirname(__file__),
69+
'screenshots'
70+
))
71+
72+
# Run Percy
73+
for image in asset_list:
74+
if image.endswith('png'):
75+
self.driver.get('http://localhost:8050/{}'.format(image))
76+
77+
WebDriverWait(self.driver, 20).until(
78+
EC.presence_of_element_located((By.ID, image))
79+
)
80+
81+
self.percy_snapshot(name=image)
82+
time.sleep(2)

tests/test_render.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

tests/test_usage.py

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,56 @@
1-
from pytest_dash.utils import (
2-
import_app,
3-
wait_for_text_to_equal,
4-
wait_for_element_by_css_selector
5-
)
1+
import os
2+
import importlib
3+
from .IntegrationTests import IntegrationTests
4+
from selenium.webdriver.common.by import By
5+
from selenium.webdriver.support.ui import WebDriverWait
6+
from selenium.webdriver.support import expected_conditions as EC
67

78

8-
# Basic test for the component rendering.
9-
def test_render_component(dash_threaded, selenium):
10-
# Start a dash app contained in `usage.py`
11-
# dash_threaded is a fixture by pytest-dash
12-
# It will load a py file containing a Dash instance named `app`
13-
# and start it in a thread.
14-
app = import_app('usage')
15-
dash_threaded(app)
9+
class Tests(IntegrationTests):
10+
def create_usage_test(self, filename):
11+
app = importlib.import_module(filename).app
1612

17-
# Get the generated component input with selenium
18-
# The html input will be a children of the #input dash component
19-
my_component = wait_for_element_by_css_selector(selenium, '#input > input')
13+
self.startServer(app)
2014

21-
assert 'my-value' == my_component.get_attribute('value')
15+
WebDriverWait(self.driver, 20).until(
16+
EC.presence_of_element_located((By.ID, "cytoscape"))
17+
)
2218

23-
# Clear the input
24-
my_component.clear()
19+
self.driver.save_screenshot(os.path.join(
20+
os.path.dirname(__file__),
21+
'screenshots',
22+
filename + '.png'
23+
))
2524

26-
# Send keys to the custom input.
27-
my_component.send_keys('Hello dash')
25+
def test_usage_advanced(self):
26+
self.create_usage_test('usage-advanced')
2827

29-
# Wait for the text to equal, if after the timeout (default 10 seconds)
30-
# the text is not equal it will fail the test.
31-
wait_for_text_to_equal(selenium, '#output', 'You have entered Hello dash')
28+
def test_usage_animated_bfs(self):
29+
self.create_usage_test('demos.usage-animated-bfs')
30+
31+
def test_usage_breadthfirst_layout(self):
32+
self.create_usage_test('demos.usage-breadthfirst-layout')
33+
34+
def test_usage_compound_nodes(self):
35+
self.create_usage_test('demos.usage-compound-nodes')
36+
37+
def test_usage_events(self):
38+
self.create_usage_test('usage-events')
39+
40+
def test_usage_elements(self):
41+
self.create_usage_test('usage-elements')
42+
43+
def test_usage_pie_style(self):
44+
self.create_usage_test('demos.usage-pie-style')
45+
46+
def test_usage_simple(self):
47+
self.create_usage_test('usage')
48+
49+
def test_usage_stylesheet(self):
50+
self.create_usage_test('usage-stylesheet')
51+
52+
def test_usage_initialisation(self):
53+
self.create_usage_test('demos.usage-initialisation')
54+
55+
def test_usage_linkout_example(self):
56+
self.create_usage_test('demos.usage-linkout-example')

0 commit comments

Comments
 (0)