From 24bdf294dce123aae305b2fe2cf1996149235b89 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 09:18:03 -0400 Subject: [PATCH 1/8] markdown: bind highlightCode probably OK as is, but just in case... --- src/components/Markdown.react.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Markdown.react.js b/src/components/Markdown.react.js index 9de196b74..b98c4bdd9 100644 --- a/src/components/Markdown.react.js +++ b/src/components/Markdown.react.js @@ -11,6 +11,11 @@ import './css/highlight.css'; * [react-markdown](https://rexxars.github.io/react-markdown/) under the hood. */ class DashMarkdown extends Component { + constructor(props) { + super(props); + this.highlightCode = this.highlightCode.bind(this); + } + componentDidMount() { this.highlightCode(); } From ebc68564494595513e66be5f2ca99f30ec37dcff Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 20:47:55 -0400 Subject: [PATCH 2/8] remove Markdown containerProps --- src/components/Markdown.react.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/Markdown.react.js b/src/components/Markdown.react.js index b98c4bdd9..d16fce1e9 100644 --- a/src/components/Markdown.react.js +++ b/src/components/Markdown.react.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import {omit, propOr, type} from 'ramda'; +import {type} from 'ramda'; import Markdown from 'react-markdown'; import './css/highlight.css'; @@ -73,12 +73,10 @@ class DashMarkdown extends Component { data-dash-is-loading={ (loading_state && loading_state.is_loading) || undefined } - {...propOr({}, 'containerProps', this.props)} > ); @@ -97,12 +95,6 @@ DashMarkdown.propTypes = { */ className: PropTypes.string, - /** - * An object containing custom element props to put on the container - * element such as id or style - */ - containerProps: PropTypes.object, - /** * A boolean to control raw HTML escaping. * Setting HTML from code is risky because it's easy to From f17e0fac550d23a8226996950be44f0fe61b4e07 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 20:49:25 -0400 Subject: [PATCH 3/8] add Markdown dedent prop and enable it by default --- src/components/Markdown.react.js | 58 ++++++++++++++++++++++-- test/unit/Markdown.test.js | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 test/unit/Markdown.test.js diff --git a/src/components/Markdown.react.js b/src/components/Markdown.react.js index d16fce1e9..b945b35e8 100644 --- a/src/components/Markdown.react.js +++ b/src/components/Markdown.react.js @@ -14,6 +14,7 @@ class DashMarkdown extends Component { constructor(props) { super(props); this.highlightCode = this.highlightCode.bind(this); + this.dedent = this.dedent.bind(this); } componentDidMount() { @@ -38,6 +39,40 @@ class DashMarkdown extends Component { } } + dedent(text) { + const lines = text.split(/\r\n|\r|\n/); + let commonPrefix = null; + for (const line of lines) { + const preMatch = line && line.match(/^\s*(?=\S)/); + if (preMatch) { + const prefix = preMatch[0]; + if (commonPrefix !== null) { + for (let i = 0; i < commonPrefix.length; i++) { + // Like Python's textwrap.dedent, we'll remove both + // space and tab characters, but only if they match + if (prefix[i] !== commonPrefix[i]) { + commonPrefix = commonPrefix.substr(0, i); + break; + } + } + } else { + commonPrefix = prefix; + } + + if (!commonPrefix) { + break; + } + } + } + + const commonLen = commonPrefix ? commonPrefix.length : 0; + return lines + .map(line => { + return line.match(/\S/) ? line.substr(commonLen) : ''; + }) + .join('\n'); + } + render() { const { id, @@ -46,11 +81,14 @@ class DashMarkdown extends Component { highlight_config, loading_state, dangerously_allow_html, + children, + dedent, } = this.props; - if (type(this.props.children) === 'Array') { - this.props.children = this.props.children.join('\n'); - } + const textProp = + type(children) === 'Array' ? children.join('\n') : children; + const displayText = + dedent && textProp ? this.dedent(textProp) : textProp; return (
@@ -111,10 +149,21 @@ DashMarkdown.propTypes = { PropTypes.arrayOf(PropTypes.string), ]), + /** + * Remove matching leading whitespace from all lines. + * Lines that are empty, or contain *only* whitespace, are ignored. + * Both spaces and tab characters are removed, but only if they match; + * we will not convert tabs to spaces or vice versa. + */ + dedent: PropTypes.bool, + /** * Config options for syntax highlighting. */ highlight_config: PropTypes.exact({ + /** + * Color scheme; default 'light' + */ theme: PropTypes.oneOf(['dark', 'light']), }), @@ -145,6 +194,7 @@ DashMarkdown.propTypes = { DashMarkdown.defaultProps = { dangerously_allow_html: false, highlight_config: {}, + dedent: true, }; export default DashMarkdown; diff --git a/test/unit/Markdown.test.js b/test/unit/Markdown.test.js new file mode 100644 index 000000000..454c56ce9 --- /dev/null +++ b/test/unit/Markdown.test.js @@ -0,0 +1,77 @@ +import Markdown from '../../src/components/Markdown.react.js'; +import React from 'react'; +import {shallow, render} from 'enzyme'; + +test('Input renders', () => { + const md = render(); + + expect(md.html()).toBeDefined(); +}); + +describe('dedent', () => { + const md = shallow().instance(); + + test('leading spaces and tabs are removed from a single line', () => { + [ + 'test', + ' test', + ' test', + '\t\t\ttest', + ' \t test', + '\t \ttest', + ].forEach(s => { + expect(md.dedent(s)).toEqual('test'); + }); + + expect(md.dedent(' test ')).toEqual('test '); + }); + + test('same chars are removed from multiple lines, ignoring blanks', () => { + ['', ' ', '\t', '\t\t', ' ', '\t \t'].forEach(pre => { + expect( + md.dedent( + pre + + 'a\n' + + pre + + ' b\r' + + pre + + 'c\r\n' + + pre + + '\td\n' + + '\t\n' + + '\n' + + pre + + 'e\n' + + '\n' + + pre + + 'f' + ) + ).toEqual( + 'a\n' + + ' b\n' + + 'c\n' + + '\td\n' + + '\n' + + '\n' + + 'e\n' + + '\n' + + 'f' + ); + }); + }); + + test('mismatched chars are not removed', () => { + expect(md.dedent(' \ta\n\t b')).toEqual(' \ta\n\t b'); + }); + + test('the dedent prop controls behavior', () => { + const text = ' a\n b'; + const mdDedented = render(); + expect(mdDedented.find('code').length).toEqual(0); + expect(mdDedented.find('p').length).toEqual(1); + + const mdRaw = render(); + expect(mdRaw.find('code').length).toEqual(1); + expect(mdRaw.find('p').length).toEqual(0); + }); +}); From d207da36f9e6ec1d95f27d0b0511b089eb1b6c5c Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 21:51:24 -0400 Subject: [PATCH 4/8] uncomment the bulk of test_integration --- test/test_integration.py | 4670 +++++++++++++++++++------------------- 1 file changed, 2335 insertions(+), 2335 deletions(-) diff --git a/test/test_integration.py b/test/test_integration.py index 7a5d15417..8d2eb38a9 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -71,2341 +71,2341 @@ def snapshot(self, name): print("Percy Snapshot {}".format(python_version)) self.percy_runner.snapshot(name=name) - # def create_upload_component_content_types_test(self, filename): - # app = dash.Dash(__name__) - # - # filepath = os.path.join(os.getcwd(), 'test', 'upload-assets', filename) - # - # pre_style = { - # 'whiteSpace': 'pre-wrap', - # 'wordBreak': 'break-all' - # } - # - # app.layout = html.Div([ - # html.Div(filepath, id='waitfor'), - # html.Div( - # id='upload-div', - # children=dcc.Upload( - # id='upload', - # children=html.Div([ - # 'Drag and Drop or ', - # html.A('Select a File') - # ]), - # style={ - # 'width': '100%', - # 'height': '60px', - # 'lineHeight': '60px', - # 'borderWidth': '1px', - # 'borderStyle': 'dashed', - # 'borderRadius': '5px', - # 'textAlign': 'center' - # } - # ) - # ), - # html.Div(id='output'), - # html.Div(DataTable(data=[{}]), style={'display': 'none'}) - # ]) - # - # @app.callback(Output('output', 'children'), - # [Input('upload', 'contents')]) - # def update_output(contents): - # if contents is not None: - # content_type, content_string = contents.split(',') - # if 'csv' in filepath: - # df = pd.read_csv(io.StringIO(base64.b64decode( - # content_string).decode('utf-8'))) - # return html.Div([ - # DataTable( - # data=df.to_dict('records'), - # columns=[{'id': i} for i in ['city', 'country']]), - # html.Hr(), - # html.Div('Raw Content'), - # html.Pre(contents, style=pre_style) - # ]) - # elif 'xls' in filepath: - # df = pd.read_excel(io.BytesIO(base64.b64decode( - # content_string))) - # return html.Div([ - # DataTable( - # data=df.to_dict('records'), - # columns=[{'id': i} for i in ['city', 'country']]), - # html.Hr(), - # html.Div('Raw Content'), - # html.Pre(contents, style=pre_style) - # ]) - # elif 'image' in content_type: - # return html.Div([ - # html.Img(src=contents), - # html.Hr(), - # html.Div('Raw Content'), - # html.Pre(contents, style=pre_style) - # ]) - # else: - # return html.Div([ - # html.Hr(), - # html.Div('Raw Content'), - # html.Pre(contents, style=pre_style) - # ]) - # - # self.startServer(app) - # - # try: - # self.wait_for_element_by_css_selector('#waitfor') - # except Exception as e: - # print(self.wait_for_element_by_css_selector( - # '#_dash-app-content').get_attribute('innerHTML')) - # raise e - # - # upload_div = self.wait_for_element_by_css_selector( - # '#upload-div input[type=file]') - # - # upload_div.send_keys(filepath) - # time.sleep(5) - # self.snapshot(filename) - # - # def test_upload_csv(self): - # self.create_upload_component_content_types_test('utf8.csv') - # - # def test_upload_xlsx(self): - # self.create_upload_component_content_types_test('utf8.xlsx') - # - # def test_upload_png(self): - # self.create_upload_component_content_types_test('dash-logo-stripe.png') - # - # def test_upload_svg(self): - # self.create_upload_component_content_types_test('dash-logo-stripe.svg') - # - # def test_upload_gallery(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # html.Div(id='waitfor'), - # html.Label('Empty'), - # dcc.Upload(), - # - # html.Label('Button'), - # dcc.Upload(html.Button('Upload File')), - # - # html.Label('Text'), - # dcc.Upload('Upload File'), - # - # html.Label('Link'), - # dcc.Upload(html.A('Upload File')), - # - # html.Label('Style'), - # dcc.Upload([ - # 'Drag and Drop or ', - # html.A('Select a File') - # ], style={ - # 'width': '100%', - # 'height': '60px', - # 'lineHeight': '60px', - # 'borderWidth': '1px', - # 'borderStyle': 'dashed', - # 'borderRadius': '5px', - # 'textAlign': 'center' - # }) - # ]) - # self.startServer(app) - # - # try: - # self.wait_for_element_by_css_selector('#waitfor') - # except Exception as e: - # print(self.wait_for_element_by_css_selector( - # '#_dash-app-content').get_attribute('innerHTML')) - # raise e - # - # self.snapshot('test_upload_gallery') - # - # def test_loading_component_initialization(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.Loading([ - # html.Div(id='div-1') - # ], className='loading') - # ], id='root') - # - # @app.callback( - # Output('div-1', 'children'), - # [Input('root', 'n_clicks')] - # ) - # def updateDiv(children): - # with lock: - # return 'content' - # - # with lock: - # self.startServer(app) - # self.wait_for_element_by_css_selector( - # '.loading .dash-spinner' - # ) - # - # self.wait_for_element_by_css_selector( - # '.loading #div-1' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_loading_component_action(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.Loading([ - # html.Div(id='div-1') - # ], className='loading') - # ], id='root') - # - # @app.callback( - # Output('div-1', 'children'), - # [Input('root', 'n_clicks')] - # ) - # def updateDiv(n_clicks): - # if n_clicks is not None: - # with lock: - # return - # - # return 'content' - # - # with lock: - # self.startServer(app) - # self.wait_for_element_by_css_selector( - # '.loading #div-1' - # ) - # - # self.driver.find_element_by_id('root').click() - # - # self.wait_for_element_by_css_selector( - # '.loading .dash-spinner' - # ) - # - # self.wait_for_element_by_css_selector( - # '.loading #div-1' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_multiple_loading_components(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.Loading([ - # html.Button(id='btn-1') - # ], className='loading-1'), - # dcc.Loading([ - # html.Button(id='btn-2') - # ], className='loading-2') - # ], id='root') - # - # @app.callback( - # Output('btn-1', 'value'), - # [Input('btn-2', 'n_clicks')] - # ) - # def updateDiv(n_clicks): - # if n_clicks is not None: - # with lock: - # return - # - # return 'content' - # - # @app.callback( - # Output('btn-2', 'value'), - # [Input('btn-1', 'n_clicks')] - # ) - # def updateDiv(n_clicks): - # if n_clicks is not None: - # with lock: - # return - # - # return 'content' - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-1' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-2 #btn-2' - # ) - # - # with lock: - # self.driver.find_element_by_id('btn-1').click() - # - # self.wait_for_element_by_css_selector( - # '.loading-2 .dash-spinner' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-1' - # ) - # - # self.wait_for_element_by_css_selector( - # '.loading-2 #btn-2' - # ) - # - # with lock: - # self.driver.find_element_by_id('btn-2').click() - # - # self.wait_for_element_by_css_selector( - # '.loading-1 .dash-spinner' - # ) - # - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-1' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-2 #btn-2' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_nested_loading_components(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.Loading([ - # html.Button(id='btn-1'), - # dcc.Loading([ - # html.Button(id='btn-2') - # ], className='loading-2') - # ], className='loading-1') - # ], id='root') - # - # @app.callback( - # Output('btn-1', 'value'), - # [Input('btn-2', 'n_clicks')] - # ) - # def updateDiv(n_clicks): - # if n_clicks is not None: - # with lock: - # return - # - # return 'content' - # - # @app.callback( - # Output('btn-2', 'value'), - # [Input('btn-1', 'n_clicks')] - # ) - # def updateDiv(n_clicks): - # if n_clicks is not None: - # with lock: - # return - # - # return 'content' - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-1' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-2 #btn-2' - # ) - # - # with lock: - # self.driver.find_element_by_id('btn-1').click() - # - # self.wait_for_element_by_css_selector( - # '.loading-2 .dash-spinner' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-1' - # ) - # - # self.wait_for_element_by_css_selector( - # '.loading-2 #btn-2' - # ) - # - # with lock: - # self.driver.find_element_by_id('btn-2').click() - # - # self.wait_for_element_by_css_selector( - # '.loading-1 .dash-spinner' - # ) - # - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-1' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-2 #btn-2' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_dynamic_loading_component(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # app.config['suppress_callback_exceptions'] = True - # - # app.layout = html.Div([ - # html.Button(id='btn-1'), - # html.Div(id='div-1') - # ]) - # - # @app.callback( - # Output('div-1', 'children'), - # [Input('btn-1', 'n_clicks')] - # ) - # def updateDiv(n_clicks): - # if n_clicks is None: - # return - # - # with lock: - # return html.Div([ - # html.Button(id='btn-2'), - # dcc.Loading([ - # html.Button(id='btn-3') - # ], className='loading-1') - # ]) - # - # @app.callback( - # Output('btn-3', 'content'), - # [Input('btn-2', 'n_clicks')] - # ) - # def updateDynamic(n_clicks): - # if n_clicks is None: - # return - # - # with lock: - # return 'content' - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector( - # '#btn-1' - # ) - # self.wait_for_element_by_css_selector( - # '#div-1' - # ) - # - # self.driver.find_element_by_id('btn-1').click() - # - # self.wait_for_element_by_css_selector( - # '#div-1 #btn-2' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-3' - # ) - # - # with lock: - # self.driver.find_element_by_id('btn-2').click() - # - # self.wait_for_element_by_css_selector( - # '.loading-1 .dash-spinner' - # ) - # - # self.wait_for_element_by_css_selector( - # '#div-1 #btn-2' - # ) - # self.wait_for_element_by_css_selector( - # '.loading-1 #btn-3' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_loading_slider(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Button(id='test-btn'), - # html.Label(id='test-div', children=['Horizontal Slider']), - # dcc.Slider( - # id='horizontal-slider', - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=5, - # ), - # ]) - # - # @app.callback( - # Output('horizontal-slider', 'value'), - # [Input('test-btn', 'n_clicks')] - # ) - # def user_delayed_value(n_clicks): - # with lock: - # return 5 - # - # with lock: - # self.startServer(app) - # - # self.wait_for_element_by_css_selector( - # '#horizontal-slider[data-dash-is-loading="true"]' - # ) - # - # self.wait_for_element_by_css_selector( - # '#horizontal-slider:not([data-dash-is-loading="true"])' - # ) - # - # with lock: - # self.driver.find_element_by_id('test-btn').click() - # - # self.wait_for_element_by_css_selector( - # '#horizontal-slider[data-dash-is-loading="true"]' - # ) - # - # self.wait_for_element_by_css_selector( - # '#horizontal-slider:not([data-dash-is-loading="true"])' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_horizontal_slider(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Label('Horizontal Slider'), - # dcc.Slider( - # id='horizontal-slider', - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=5, - # ), - # ]) - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#horizontal-slider') - # self.snapshot('horizontal slider') - # - # h_slider = self.driver.find_element_by_css_selector( - # '#horizontal-slider div[role="slider"]' - # ) - # h_slider.click() - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_vertical_slider(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Label('Vertical Slider'), - # dcc.Slider( - # id='vertical-slider', - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=5, - # vertical=True, - # ), - # ], style={'height': '500px'}) - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#vertical-slider') - # self.snapshot('vertical slider') - # - # v_slider = self.driver.find_element_by_css_selector( - # '#vertical-slider div[role="slider"]' - # ) - # v_slider.click() - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_loading_range_slider(self): - # lock = Lock() - # - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Button(id='test-btn'), - # html.Label(id='test-div', children=['Horizontal Range Slider']), - # dcc.RangeSlider( - # id='horizontal-range-slider', - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=[4, 6], - # ), - # ]) - # - # @app.callback( - # Output('horizontal-range-slider', 'value'), - # [Input('test-btn', 'n_clicks')] - # ) - # def delayed_value(children): - # with lock: - # return [4, 6] - # - # with lock: - # self.startServer(app) - # - # self.wait_for_element_by_css_selector( - # '#horizontal-range-slider[data-dash-is-loading="true"]' - # ) - # - # self.wait_for_element_by_css_selector( - # '#horizontal-range-slider:not([data-dash-is-loading="true"])' - # ) - # - # with lock: - # self.driver.find_element_by_id('test-btn').click() - # - # self.wait_for_element_by_css_selector( - # '#horizontal-range-slider[data-dash-is-loading="true"]' - # ) - # - # self.wait_for_element_by_css_selector( - # '#horizontal-range-slider:not([data-dash-is-loading="true"])' - # ) - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_horizontal_range_slider(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Label('Horizontal Range Slider'), - # dcc.RangeSlider( - # id='horizontal-range-slider', - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=[4, 6], - # ), - # ]) - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#horizontal-range-slider') - # self.snapshot('horizontal range slider') - # - # h_slider_1 = self.driver.find_element_by_css_selector( - # '#horizontal-range-slider div.rc-slider-handle-1[role="slider"]' - # ) - # h_slider_1.click() - # - # h_slider_2 = self.driver.find_element_by_css_selector( - # '#horizontal-range-slider div.rc-slider-handle-2[role="slider"]' - # ) - # h_slider_2.click() - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_vertical_range_slider(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Label('Vertical Range Slider'), - # dcc.RangeSlider( - # id='vertical-range-slider', - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=[4, 6], - # vertical=True, - # ), - # ], style={'height': '500px'}) - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#vertical-range-slider') - # self.snapshot('vertical range slider') - # - # v_slider_1 = self.driver.find_element_by_css_selector( - # '#vertical-range-slider div.rc-slider-handle-1[role="slider"]' - # ) - # v_slider_1.click() - # - # v_slider_2 = self.driver.find_element_by_css_selector( - # '#vertical-range-slider div.rc-slider-handle-2[role="slider"]' - # ) - # v_slider_2.click() - # - # for entry in self.get_log(): - # raise Exception('browser error logged during test', entry) - # - # def test_gallery(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Div(id='waitfor'), - # html.Label('Upload'), - # dcc.Upload(), - # - # html.Label('Horizontal Tabs'), - # dcc.Tabs(id="tabs", children=[ - # dcc.Tab(label='Tab one', className='test', style={'border': '1px solid magenta'}, children=[ - # html.Div(['Test']) - # ]), - # dcc.Tab(label='Tab two', children=[ - # html.Div([ - # html.H1("This is the content in tab 2"), - # html.P("A graph here would be nice!") - # ]) - # ], id='tab-one'), - # dcc.Tab(label='Tab three', children=[ - # html.Div([ - # html.H1("This is the content in tab 3"), - # ]) - # ]), - # ], - # style={ - # 'fontFamily': 'system-ui' - # }, - # content_style={ - # 'border': '1px solid #d6d6d6', - # 'padding': '44px' - # }, - # parent_style={ - # 'maxWidth': '1000px', - # 'margin': '0 auto' - # } - # ), - # - # html.Label('Vertical Tabs'), - # dcc.Tabs(id="tabs1", vertical=True, children=[ - # dcc.Tab(label='Tab one', children=[ - # html.Div(['Test']) - # ]), - # dcc.Tab(label='Tab two', children=[ - # html.Div([ - # html.H1("This is the content in tab 2"), - # html.P("A graph here would be nice!") - # ]) - # ]), - # dcc.Tab(label='Tab three', children=[ - # html.Div([ - # html.H1("This is the content in tab 3"), - # ]) - # ]), - # ] - # ), - # - # html.Label('Dropdown'), - # dcc.Dropdown( - # options=[ - # {'label': 'New York City', 'value': 'NYC'}, - # {'label': u'Montréal', 'value': 'MTL'}, - # {'label': 'San Francisco', 'value': 'SF'}, - # {'label': u'北京', 'value': u'北京'} - # ], - # value='MTL', - # id='dropdown' - # ), - # - # html.Label('Multi-Select Dropdown'), - # dcc.Dropdown( - # options=[ - # {'label': 'New York City', 'value': 'NYC'}, - # {'label': u'Montréal', 'value': 'MTL'}, - # {'label': 'San Francisco', 'value': 'SF'}, - # {'label': u'北京', 'value': u'北京'} - # ], - # value=['MTL', 'SF'], - # multi=True - # ), - # - # html.Label('Radio Items'), - # dcc.RadioItems( - # options=[ - # {'label': 'New York City', 'value': 'NYC'}, - # {'label': u'Montréal', 'value': 'MTL'}, - # {'label': 'San Francisco', 'value': 'SF'}, - # {'label': u'北京', 'value': u'北京'} - # ], - # value='MTL' - # ), - # - # html.Label('Checkboxes'), - # dcc.Checklist( - # options=[ - # {'label': 'New York City', 'value': 'NYC'}, - # {'label': u'Montréal', 'value': 'MTL'}, - # {'label': 'San Francisco', 'value': 'SF'}, - # {'label': u'北京', 'value': u'北京'} - # ], - # value=['MTL', 'SF'] - # ), - # - # html.Label('Text Input'), - # dcc.Input(value='', placeholder='type here', id='textinput'), - # html.Label('Disabled Text Input'), - # dcc.Input(value='disabled', type='text', - # id='disabled-textinput', disabled=True), - # - # html.Label('Slider'), - # dcc.Slider( - # min=0, - # max=9, - # marks={i: 'Label {}'.format(i) if i == 1 else str(i) - # for i in range(1, 6)}, - # value=5, - # ), - # - # html.Label('Graph'), - # dcc.Graph( - # id='graph', - # figure={ - # 'data': [{ - # 'x': [1, 2, 3], - # 'y': [4, 1, 4] - # }], - # 'layout': { - # 'title': u'北京' - # } - # } - # ), - # - # html.Div([ - # html.Label('DatePickerSingle'), - # dcc.DatePickerSingle( - # id='date-picker-single', - # date=datetime(1997, 5, 10) - # ), - # html.Div([ - # html.Label('DatePickerSingle - empty input'), - # dcc.DatePickerSingle(), - # ], id='dt-single-no-date-value' - # ), - # html.Div([ - # html.Label('DatePickerSingle - initial visible month (May 97)'), - # dcc.DatePickerSingle( - # initial_visible_month=datetime(1997, 5, 10) - # ), - # ], id='dt-single-no-date-value-init-month' - # ), - # ]), - # - # html.Div([ - # html.Label('DatePickerRange'), - # dcc.DatePickerRange( - # id='date-picker-range', - # start_date_id='startDate', - # end_date_id='endDate', - # start_date=datetime(1997, 5, 3), - # end_date_placeholder_text='Select a date!' - # ), - # html.Div([ - # html.Label('DatePickerRange - empty input'), - # dcc.DatePickerRange( - # start_date_id='startDate', - # end_date_id='endDate', - # start_date_placeholder_text='Start date', - # end_date_placeholder_text='End date' - # ), - # ], id='dt-range-no-date-values' - # ), - # html.Div([ - # html.Label('DatePickerRange - initial visible month (May 97)'), - # dcc.DatePickerRange( - # start_date_id='startDate', - # end_date_id='endDate', - # start_date_placeholder_text='Start date', - # end_date_placeholder_text='End date', - # initial_visible_month=datetime(1997, 5, 10) - # ), - # ], id='dt-range-no-date-values-init-month' - # ), - # ]), - # - # html.Label('TextArea'), - # dcc.Textarea( - # placeholder='Enter a value... 北京', - # style={'width': '100%'} - # ), - # - # html.Label('Markdown'), - # dcc.Markdown(''' - # #### Dash and Markdown - # - # Dash supports [Markdown](https://rexxars.github.io/react-markdown/). - # - # Markdown is a simple way to write and format text. - # It includes a syntax for things like **bold text** and *italics*, - # [links](https://rexxars.github.io/react-markdown/), inline `code` snippets, lists, - # quotes, and more. - # - # 1. Links are auto-rendered: https://dash.plot.ly. - # 2. This uses ~commonmark~ GitHub flavored markdown. - # - # Tables are also supported: - # - # | First Header | Second Header | - # | ------------- | ------------- | - # | Content Cell | Content Cell | - # | Content Cell | Content Cell | - # - # 北京 - # '''.replace(' ', '')), - # dcc.Markdown(['# Line one', '## Line two']), - # dcc.Markdown(), - # dcc.SyntaxHighlighter(dedent('''import python - # print(3)'''), language='python'), - # dcc.SyntaxHighlighter([ - # 'import python', - # 'print(3)' - # ], language='python'), - # dcc.SyntaxHighlighter() - # ]) - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('#waitfor') - # - # self.snapshot('gallery') - # - # self.driver.find_element_by_css_selector( - # '#dropdown .Select-input input' - # ).send_keys(u'北') - # self.snapshot('gallery - chinese character') - # - # text_input = self.driver.find_element_by_id('textinput') - # # verify that type has the right default - # # Can't use text_input.get_attribute('type') - that pulls the - # # default value even if none is specified. - # get_type = ('return document.getElementById("textinput")' - # '.getAttribute("type");') - # self.assertEqual(self.driver.execute_script(get_type), 'text') - # disabled_text_input = self.driver.find_element_by_id( - # 'disabled-textinput') - # text_input.send_keys('HODOR') - # - # # It seems selenium errors when send(ing)_keys on a disabled element. - # # In case this changes we try anyway and catch the particular - # # exception. In any case Percy will snapshot the disabled input style - # # so we are not totally dependent on the send_keys behaviour for - # # testing disabled state. - # try: - # disabled_text_input.send_keys('RODOH') - # except InvalidElementStateException: - # pass - # - # self.snapshot('gallery - text input') - # - # # DatePickerSingle and DatePickerRange test - # # for issue with datepicker when date value is `None` - # dt_input_1 = self.driver.find_element_by_css_selector( - # '#dt-single-no-date-value #date' - # ) - # dt_input_1.click() - # self.snapshot('gallery - DatePickerSingle\'s datepicker ' - # 'when no date value and no initial month specified') - # dt_length = len(dt_input_1.get_attribute('value')) - # dt_input_1.send_keys(dt_length * Keys.BACKSPACE) - # dt_input_1.send_keys("1997-05-03") - # - # dt_input_2 = self.driver.find_element_by_css_selector( - # '#dt-single-no-date-value-init-month #date' - # ) - # self.driver.find_element_by_css_selector( - # 'label' - # ).click() - # dt_input_2.click() - # self.snapshot('gallery - DatePickerSingle\'s datepicker ' - # 'when no date value, but initial month is specified') - # dt_length = len(dt_input_2.get_attribute('value')) - # dt_input_2.send_keys(dt_length * Keys.BACKSPACE) - # dt_input_2.send_keys("1997-05-03") - # - # dt_input_3 = self.driver.find_element_by_css_selector( - # '#dt-range-no-date-values #endDate' - # ) - # self.driver.find_element_by_css_selector( - # 'label' - # ).click() - # dt_input_3.click() - # self.snapshot('gallery - DatePickerRange\'s datepicker ' - # 'when neither start date nor end date ' - # 'nor initial month is specified') - # dt_length = len(dt_input_3.get_attribute('value')) - # dt_input_3.send_keys(dt_length * Keys.BACKSPACE) - # dt_input_3.send_keys("1997-05-03") - # - # dt_input_4 = self.driver.find_element_by_css_selector( - # '#dt-range-no-date-values-init-month #endDate' - # ) - # self.driver.find_element_by_css_selector( - # 'label' - # ).click() - # dt_input_4.click() - # self.snapshot('gallery - DatePickerRange\'s datepicker ' - # 'when neither start date nor end date is specified, ' - # 'but initial month is') - # dt_length = len(dt_input_4.get_attribute('value')) - # dt_input_4.send_keys(dt_length * Keys.BACKSPACE) - # dt_input_4.send_keys("1997-05-03") - # - # def test_tabs_in_vertical_mode(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.Tabs(id="tabs", value='tab-3', children=[ - # dcc.Tab(label='Tab one', value='tab-1', id='tab-1', children=[ - # html.Div('Tab One Content') - # ]), - # dcc.Tab(label='Tab two', value='tab-2', id='tab-2', children=[ - # html.Div('Tab Two Content') - # ]), - # dcc.Tab(label='Tab three', value='tab-3', id='tab-3', children=[ - # html.Div('Tab Three Content') - # ]), - # ], vertical=True), - # html.Div(id='tabs-content') - # ]) - # - # self.startServer(app=app) - # self.wait_for_text_to_equal('#tab-3', 'Tab three') - # - # self.snapshot('Tabs - vertical mode') - # - # def test_tabs_without_children(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.H1('Dash Tabs component demo'), - # dcc.Tabs(id="tabs", value='tab-2', children=[ - # dcc.Tab(label='Tab one', value='tab-1', id='tab-1'), - # dcc.Tab(label='Tab two', value='tab-2', id='tab-2'), - # ]), - # html.Div(id='tabs-content') - # ]) - # - # @app.callback(dash.dependencies.Output('tabs-content', 'children'), - # [dash.dependencies.Input('tabs', 'value')]) - # def render_content(tab): - # if tab == 'tab-1': - # return html.Div([ - # html.H3('Test content 1') - # ], id='test-tab-1') - # elif tab == 'tab-2': - # return html.Div([ - # html.H3('Test content 2') - # ], id='test-tab-2') - # - # self.startServer(app=app) - # - # self.wait_for_text_to_equal('#tabs-content', 'Test content 2') - # self.snapshot('initial tab - tab 2') - # - # selected_tab = self.wait_for_element_by_css_selector('#tab-1') - # selected_tab.click() - # time.sleep(1) - # self.wait_for_text_to_equal('#tabs-content', 'Test content 1') - # - # def test_tabs_with_children_undefined(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.H1('Dash Tabs component demo'), - # dcc.Tabs(id="tabs", value='tab-1'), - # html.Div(id='tabs-content') - # ]) - # - # self.startServer(app=app) - # - # self.wait_for_element_by_css_selector('#tabs-content') - # - # self.snapshot('Tabs component with children undefined') - # - # def test_tabs_render_without_selected(self): - # app = dash.Dash(__name__) - # - # data = [ - # {'id': 'one', 'value': 1}, - # {'id': 'two', 'value': 2}, - # ] - # - # menu = html.Div([ - # html.Div('one', id='one'), - # html.Div('two', id='two') - # ]) - # - # tabs_one = html.Div([ - # dcc.Tabs([ - # dcc.Tab(dcc.Graph(id='graph-one'), label='tab-one-one'), - # ]) - # ], id='tabs-one', style={'display': 'none'}) - # - # tabs_two = html.Div([ - # dcc.Tabs([ - # dcc.Tab(dcc.Graph(id='graph-two'), label='tab-two-one'), - # ]) - # ], id='tabs-two', style={'display': 'none'}) - # - # app.layout = html.Div([ - # menu, - # tabs_one, - # tabs_two - # ]) - # - # for i in ('one', 'two'): - # - # @app.callback(Output('tabs-{}'.format(i), 'style'), - # [Input(i, 'n_clicks')]) - # def on_click(n_clicks): - # if n_clicks is None: - # raise PreventUpdate - # - # if n_clicks % 2 == 1: - # return {'display': 'block'} - # return {'display': 'none'} - # - # @app.callback(Output('graph-{}'.format(i), 'figure'), - # [Input(i, 'n_clicks')]) - # def on_click(n_clicks): - # if n_clicks is None: - # raise PreventUpdate - # - # return { - # 'data': [ - # { - # 'x': [1, 2, 3, 4], - # 'y': [4, 3, 2, 1] - # } - # ], - # 'layout': { - # 'width': 700, - # 'height': 450 - # } - # } - # - # self.startServer(app=app) - # - # button_one = self.wait_for_element_by_css_selector('#one') - # button_two = self.wait_for_element_by_css_selector('#two') - # - # button_one.click() - # - # # wait for tabs to be loaded after clicking - # WebDriverWait(self.driver, 10).until( - # EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-one .main-svg")) - # ) - # - # time.sleep(1) - # self.snapshot("Tabs 1 rendered ") - # - # button_two.click() - # - # # wait for tabs to be loaded after clicking - # WebDriverWait(self.driver, 10).until( - # EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-two .main-svg")) - # ) - # - # time.sleep(1) - # self.snapshot("Tabs 2 rendered ") - # - # # do some extra tests while we're here - # # and have access to Graph and plotly.js - # self.check_graph_config_shape() - # self.check_plotlyjs() - # - # def check_plotlyjs(self): - # # find plotly.js files in the dist folder, check that there's only one - # all_dist = os.listdir(dcc.__path__[0]) - # js_re = r'^plotly-(.*)\.min\.js$' - # plotlyjs_dist = [fn for fn in all_dist if re.match(js_re, fn)] - # - # self.assertEqual(len(plotlyjs_dist), 1) - # - # # check that the version matches what we see in the page - # page_version = self.driver.execute_script('return Plotly.version;') - # dist_version = re.match(js_re, plotlyjs_dist[0]).groups()[0] - # self.assertEqual(page_version, dist_version) - # - # def check_graph_config_shape(self): - # config_schema = self.driver.execute_script( - # 'return Plotly.PlotSchema.get().config;' - # ) - # with open(os.path.join(dcc.__path__[0], 'metadata.json')) as meta: - # graph_meta = json.load(meta)['src/components/Graph.react.js'] - # config_prop_shape = graph_meta['props']['config']['type']['value'] - # - # ignored_config = [ - # 'setBackground', - # 'showSources', - # 'logging', - # 'globalTransforms', - # 'role' - # ] - # - # def crawl(schema, props): - # for prop_name in props: - # self.assertIn(prop_name, schema) - # - # for item_name, item in schema.items(): - # if item_name in ignored_config: - # continue - # - # self.assertIn(item_name, props) - # if 'valType' not in item: - # crawl(item, props[item_name]['value']) - # - # crawl(config_schema, config_prop_shape) - # - # def test_tabs_without_value(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.H1('Dash Tabs component demo'), - # dcc.Tabs(id="tabs-without-value", children=[ - # dcc.Tab(label='Tab One', value='tab-1'), - # dcc.Tab(label='Tab Two', value='tab-2'), - # ]), - # html.Div(id='tabs-content') - # ]) - # - # @app.callback(Output('tabs-content', 'children'), - # [Input('tabs-without-value', 'value')]) - # def render_content(tab): - # if tab == 'tab-1': - # return html.H3('Default selected Tab content 1') - # elif tab == 'tab-2': - # return html.H3('Tab content 2') - # - # self.startServer(app=app) - # - # self.wait_for_text_to_equal('#tabs-content', 'Default selected Tab content 1') - # - # self.snapshot('Tab 1 should be selected by default') - # - # def test_graph_does_not_resize_in_tabs(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # html.H1('Dash Tabs component demo'), - # dcc.Tabs(id="tabs-example", value='tab-1-example', children=[ - # dcc.Tab(label='Tab One', value='tab-1-example', id='tab-1'), - # dcc.Tab(label='Tab Two', value='tab-2-example', id='tab-2'), - # ]), - # html.Div(id='tabs-content-example') - # ]) - # - # @app.callback(Output('tabs-content-example', 'children'), - # [Input('tabs-example', 'value')]) - # def render_content(tab): - # if tab == 'tab-1-example': - # return html.Div([ - # html.H3('Tab content 1'), - # dcc.Graph( - # id='graph-1-tabs', - # figure={ - # 'data': [{ - # 'x': [1, 2, 3], - # 'y': [3, 1, 2], - # 'type': 'bar' - # }] - # } - # ) - # ]) - # elif tab == 'tab-2-example': - # return html.Div([ - # html.H3('Tab content 2'), - # dcc.Graph( - # id='graph-2-tabs', - # figure={ - # 'data': [{ - # 'x': [1, 2, 3], - # 'y': [5, 10, 6], - # 'type': 'bar' - # }] - # } - # ) - # ]) - # self.startServer(app=app) - # - # tab_one = self.wait_for_element_by_css_selector('#tab-1') - # tab_two = self.wait_for_element_by_css_selector('#tab-2') - # - # WebDriverWait(self.driver, 10).until( - # EC.element_to_be_clickable((By.ID, "tab-2")) - # ) - # - # self.snapshot("Tabs with Graph - initial (graph should not resize)") - # tab_two.click() - # - # # wait for Graph's internal svg to be loaded after clicking - # WebDriverWait(self.driver, 10).until( - # EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-2-tabs .main-svg")) - # ) - # - # self.snapshot("Tabs with Graph - clicked tab 2 (graph should not resize)") - # - # WebDriverWait(self.driver, 10).until( - # EC.element_to_be_clickable((By.ID, "tab-1")) - # ) - # - # tab_one.click() - # - # # wait for Graph to be loaded after clicking - # WebDriverWait(self.driver, 10).until( - # EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-1-tabs .main-svg")) - # ) - # - # self.snapshot("Tabs with Graph - clicked tab 1 (graph should not resize)") - # - # def test_location_link(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Div(id='waitfor'), - # dcc.Location(id='test-location', refresh=False), - # - # dcc.Link( - # html.Button('I am a clickable button'), - # id='test-link', - # href='/test/pathname'), - # dcc.Link( - # html.Button('I am a clickable hash button'), - # id='test-link-hash', - # href='#test'), - # dcc.Link( - # html.Button('I am a clickable search button'), - # id='test-link-search', - # href='?testQuery=testValue', - # refresh=False), - # html.Button('I am a magic button that updates pathname', - # id='test-button'), - # html.A('link to click', href='/test/pathname/a', id='test-a'), - # html.A('link to click', href='#test-hash', id='test-a-hash'), - # html.A('link to click', href='?queryA=valueA', id='test-a-query'), - # html.Div(id='test-pathname', children=[]), - # html.Div(id='test-hash', children=[]), - # html.Div(id='test-search', children=[]), - # ]) - # - # @app.callback( - # output=Output(component_id='test-pathname', - # component_property='children'), - # inputs=[Input(component_id='test-location', component_property='pathname')]) - # def update_location_on_page(pathname): - # return pathname - # - # @app.callback( - # output=Output(component_id='test-hash', - # component_property='children'), - # inputs=[Input(component_id='test-location', component_property='hash')]) - # def update_location_on_page(hash_val): - # if hash_val is None: - # return '' - # - # return hash_val - # - # @app.callback( - # output=Output(component_id='test-search', - # component_property='children'), - # inputs=[Input(component_id='test-location', component_property='search')]) - # def update_location_on_page(search): - # if search is None: - # return '' - # - # return search - # - # @app.callback( - # output=Output(component_id='test-location', - # component_property='pathname'), - # inputs=[Input(component_id='test-button', - # component_property='n_clicks')], - # state=[State(component_id='test-location', component_property='pathname')]) - # def update_pathname(n_clicks, current_pathname): - # if n_clicks is not None: - # return '/new/pathname' - # - # return current_pathname - # - # self.startServer(app=app) - # - # time.sleep(1) - # self.snapshot('link -- location') - # - # # Check that link updates pathname - # self.wait_for_element_by_css_selector('#test-link').click() - # self.assertEqual( - # self.driver.current_url.replace('http://localhost:8050', ''), - # '/test/pathname') - # self.wait_for_text_to_equal('#test-pathname', '/test/pathname') - # - # # Check that hash is updated in the Location - # self.wait_for_element_by_css_selector('#test-link-hash').click() - # self.wait_for_text_to_equal('#test-pathname', '/test/pathname') - # self.wait_for_text_to_equal('#test-hash', '#test') - # self.snapshot('link -- /test/pathname#test') - # - # # Check that search is updated in the Location -- note that this goes through href and therefore wipes the hash - # self.wait_for_element_by_css_selector('#test-link-search').click() - # self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') - # self.wait_for_text_to_equal('#test-hash', '') - # self.snapshot('link -- /test/pathname?testQuery=testValue') - # - # # Check that pathname is updated through a Button click via props - # self.wait_for_element_by_css_selector('#test-button').click() - # self.wait_for_text_to_equal('#test-pathname', '/new/pathname') - # self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') - # self.snapshot('link -- /new/pathname?testQuery=testValue') - # - # # Check that pathname is updated through an a tag click via props - # self.wait_for_element_by_css_selector('#test-a').click() - # try: - # self.wait_for_element_by_css_selector('#waitfor') - # except Exception as e: - # print(self.wait_for_element_by_css_selector( - # '#_dash-app-content').get_attribute('innerHTML')) - # raise e - # - # self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') - # self.wait_for_text_to_equal('#test-search', '') - # self.wait_for_text_to_equal('#test-hash', '') - # self.snapshot('link -- /test/pathname/a') - # - # # Check that hash is updated through an a tag click via props - # self.wait_for_element_by_css_selector('#test-a-hash').click() - # self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') - # self.wait_for_text_to_equal('#test-search', '') - # self.wait_for_text_to_equal('#test-hash', '#test-hash') - # self.snapshot('link -- /test/pathname/a#test-hash') - # - # # Check that hash is updated through an a tag click via props - # self.wait_for_element_by_css_selector('#test-a-query').click() - # self.wait_for_element_by_css_selector('#waitfor') - # self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') - # self.wait_for_text_to_equal('#test-search', '?queryA=valueA') - # self.wait_for_text_to_equal('#test-hash', '') - # self.snapshot('link -- /test/pathname/a?queryA=valueA') - # - # def test_link_scroll(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # dcc.Location(id='test-url', refresh=False), - # - # html.Div(id='push-to-bottom', children=[], style={ - # 'display': 'block', - # 'height': '200vh' - # }), - # html.Div(id='page-content'), - # dcc.Link('Test link', href='/test-link', id='test-link') - # ]) - # - # call_count = Value('i', 0) - # - # @app.callback(Output('page-content', 'children'), - # [Input('test-url', 'pathname')]) - # def display_page(pathname): - # call_count.value = call_count.value + 1 - # return 'You are on page {}'.format(pathname) - # - # self.startServer(app=app) - # - # time.sleep(2) - # - # # callback is called twice when defined - # self.assertEqual( - # call_count.value, - # 2 - # ) - # - # # test if link correctly scrolls back to top of page - # test_link = self.wait_for_element_by_css_selector('#test-link') - # test_link.send_keys(Keys.NULL) - # test_link.click() - # time.sleep(2) - # - # # test link still fires update on Location - # page_content = self.wait_for_element_by_css_selector('#page-content') - # self.assertNotEqual(page_content.text, 'You are on page /') - # - # self.wait_for_text_to_equal( - # '#page-content', 'You are on page /test-link') - # - # # test if rendered Link's tag has a href attribute - # link_href = test_link.get_attribute("href") - # self.assertEqual(link_href, 'http://localhost:8050/test-link') - # - # # test if callback is only fired once (offset of 2) - # self.assertEqual( - # call_count.value, - # 3 - # ) - # - # def test_candlestick(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # html.Button( - # id='button', - # children='Update Candlestick', - # n_clicks=0 - # ), - # dcc.Graph(id='graph') - # ]) - # - # @app.callback(Output('graph', 'figure'), [Input('button', 'n_clicks')]) - # def update_graph(n_clicks): - # return { - # 'data': [{ - # 'open': [1] * 5, - # 'high': [3] * 5, - # 'low': [0] * 5, - # 'close': [2] * 5, - # 'x': [n_clicks] * 5, - # 'type': 'candlestick' - # }] - # } - # self.startServer(app=app) - # - # button = self.wait_for_element_by_css_selector('#button') - # self.snapshot('candlestick - initial') - # button.click() - # time.sleep(1) - # self.snapshot('candlestick - 1 click') - # - # button.click() - # time.sleep(1) - # self.snapshot('candlestick - 2 click') - # - # def test_graphs_with_different_figures(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # dcc.Graph( - # id='example-graph', - # figure={ - # 'data': [ - # {'x': [1, 2, 3], 'y': [4, 1, 2], - # 'type': 'bar', 'name': 'SF'}, - # {'x': [1, 2, 3], 'y': [2, 4, 5], - # 'type': 'bar', 'name': u'Montréal'}, - # ], - # 'layout': { - # 'title': 'Dash Data Visualization' - # } - # } - # ), - # dcc.Graph( - # id='example-graph-2', - # figure={ - # 'data': [ - # {'x': [20, 24, 33], 'y': [5, 2, 3], - # 'type': 'bar', 'name': 'SF'}, - # {'x': [11, 22, 33], 'y': [22, 44, 55], - # 'type': 'bar', 'name': u'Montréal'}, - # ], - # 'layout': { - # 'title': 'Dash Data Visualization' - # } - # } - # ), - # html.Div(id='restyle-data'), - # html.Div(id='relayout-data') - # ]) - # - # @app.callback(Output('restyle-data', 'children'), [Input('example-graph', 'restyleData')]) - # def show_restyle_data(data): - # if data is None: # ignore initial - # return '' - # return json.dumps(data) - # - # @app.callback(Output('relayout-data', 'children'), [Input('example-graph', 'relayoutData')]) - # def show_relayout_data(data): - # if data is None or 'autosize' in data: # ignore initial & auto width - # return '' - # return json.dumps(data) - # - # self.startServer(app=app) - # - # # use this opportunity to test restyleData, since there are multiple - # # traces on this graph - # legendToggle = self.driver.find_element_by_css_selector('#example-graph .traces:first-child .legendtoggle') - # legendToggle.click() - # self.wait_for_text_to_equal('#restyle-data', '[{"visible": ["legendonly"]}, [0]]') - # - # # move snapshot after click, so it's more stable with the wait - # self.snapshot('2 graphs with different figures') - # - # # and test relayoutData while we're at it - # autoscale = self.driver.find_element_by_css_selector('#example-graph .ewdrag') - # autoscale.click() - # autoscale.click() - # self.wait_for_text_to_equal('#relayout-data', '{"xaxis.autorange": true}') - # - # def test_graphs_without_ids(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # dcc.Graph(className='graph-no-id-1'), - # dcc.Graph(className='graph-no-id-2'), - # ]) - # - # self.startServer(app=app) - # - # graph_1 = self.wait_for_element_by_css_selector('.graph-no-id-1') - # graph_2 = self.wait_for_element_by_css_selector('.graph-no-id-2') - # - # self.assertNotEqual(graph_1.get_attribute('id'), graph_2.get_attribute('id')) - # - # def test_datepickerrange_updatemodes(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.DatePickerRange( - # id='date-picker-range', - # start_date_id='startDate', - # end_date_id='endDate', - # start_date_placeholder_text='Select a start date!', - # end_date_placeholder_text='Select an end date!', - # updatemode='bothdates' - # ), - # html.Div(id='date-picker-range-output') - # ]) - # - # @app.callback( - # dash.dependencies.Output('date-picker-range-output', 'children'), - # [dash.dependencies.Input('date-picker-range', 'start_date'), - # dash.dependencies.Input('date-picker-range', 'end_date')]) - # def update_output(start_date, end_date): - # return '{} - {}'.format(start_date, end_date) - # - # self.startServer(app=app) - # - # start_date = self.wait_for_element_by_css_selector('#startDate') - # start_date.click() - # - # end_date = self.wait_for_element_by_css_selector('#endDate') - # end_date.click() - # - # self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') - # - # # using mouse click with fixed day range, this can be improved - # # once we start refactoring the test structure - # start_date.click() - # - # sday = self.driver.find_element_by_xpath("//td[text()='1' and @tabindex='0']") - # sday.click() - # self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') - # - # eday = self.driver.find_elements_by_xpath("//td[text()='28']")[1] - # eday.click() - # - # date_tokens = set(start_date.get_attribute('value').split('/')) - # date_tokens.update(end_date.get_attribute('value').split('/')) - # - # self.assertEqual( - # set(itertools.chain(*[ - # _.split('-') - # for _ in self.driver.find_element_by_css_selector( - # '#date-picker-range-output').text.split(' - ')])), - # date_tokens, - # "date should match the callback output") - # - # def test_interval(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # html.Div(id='output'), - # dcc.Interval(id='interval', interval=1, max_intervals=2) - # ]) - # - # @app.callback(Output('output', 'children'), - # [Input('interval', 'n_intervals')]) - # def update_text(n): - # return "{}".format(n) - # - # self.startServer(app=app) - # - # # wait for interval to finish - # time.sleep(5) - # - # self.wait_for_text_to_equal('#output', '2') - # - # def test_if_interval_can_be_restarted(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # dcc.Interval( - # id='interval', - # interval=100, - # n_intervals=0, - # max_intervals=-1 - # ), - # - # html.Button('Start', id='start', n_clicks_timestamp=-1), - # html.Button('Stop', id='stop', n_clicks_timestamp=-1), - # - # html.Div(id='output') - # - # ]) - # - # @app.callback( - # Output('interval', 'max_intervals'), - # [Input('start', 'n_clicks_timestamp'), - # Input('stop', 'n_clicks_timestamp')]) - # def start_stop(start, stop): - # if start < stop: - # return 0 - # else: - # return -1 - # - # @app.callback(Output('output', 'children'), [Input('interval', 'n_intervals')]) - # def display_data(n_intervals): - # return 'Updated {}'.format(n_intervals) - # - # self.startServer(app=app) - # - # start_button = self.wait_for_element_by_css_selector('#start') - # stop_button = self.wait_for_element_by_css_selector('#stop') - # - # # interval will start itself, we wait a second before pressing 'stop' - # time.sleep(1) - # - # # get the output after running it for a bit - # output = self.wait_for_element_by_css_selector('#output') - # stop_button.click() - # - # time.sleep(1) - # - # # get the output after it's stopped, it shouldn't be higher than before - # output_stopped = self.wait_for_element_by_css_selector('#output') - # - # self.wait_for_text_to_equal("#output", output_stopped.text) - # - # # This test logic is bad - # # same element check for same text will always be true. - # self.assertEqual(output.text, output_stopped.text) - # - # def _test_confirm(self, app, test_name, add_callback=True): - # count = Value('i', 0) - # - # if add_callback: - # @app.callback(Output('confirmed', 'children'), - # [Input('confirm', 'submit_n_clicks'), - # Input('confirm', 'cancel_n_clicks')], - # [State('confirm', 'submit_n_clicks_timestamp'), - # State('confirm', 'cancel_n_clicks_timestamp')]) - # def _on_confirmed(submit_n_clicks, cancel_n_clicks, - # submit_timestamp, cancel_timestamp): - # if not submit_n_clicks and not cancel_n_clicks: - # return '' - # count.value += 1 - # if (submit_timestamp and cancel_timestamp is None) or\ - # (submit_timestamp > cancel_timestamp): - # return 'confirmed' - # else: - # return 'canceled' - # - # self.startServer(app) - # button = self.wait_for_element_by_css_selector('#button') - # self.snapshot(test_name + ' -> initial') - # - # button.click() - # time.sleep(1) - # self.driver.switch_to.alert.accept() - # - # if add_callback: - # self.wait_for_text_to_equal('#confirmed', 'confirmed') - # self.snapshot(test_name + ' -> confirmed') - # - # button.click() - # time.sleep(0.5) - # self.driver.switch_to.alert.dismiss() - # time.sleep(0.5) - # - # if add_callback: - # self.wait_for_text_to_equal('#confirmed', 'canceled') - # self.snapshot(test_name + ' -> canceled') - # - # if add_callback: - # self.assertEqual(2, count.value, - # 'Expected 2 callback but got ' + str(count.value)) - # - # def test_confirm(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Button(id='button', children='Send confirm', n_clicks=0), - # dcc.ConfirmDialog(id='confirm', message='Please confirm.'), - # html.Div(id='confirmed') - # ]) - # - # @app.callback(Output('confirm', 'displayed'), - # [Input('button', 'n_clicks')]) - # def on_click_confirm(n_clicks): - # if n_clicks: - # return True - # - # self._test_confirm(app, 'ConfirmDialog') - # - # def test_confirm_dialog_provider(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # dcc.ConfirmDialogProvider( - # html.Button('click me', id='button'), - # id='confirm', message='Please confirm.'), - # html.Div(id='confirmed') - # ]) - # - # self._test_confirm(app, 'ConfirmDialogProvider') - # - # def test_confirm_without_callback(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # dcc.ConfirmDialogProvider( - # html.Button('click me', id='button'), - # id='confirm', message='Please confirm.'), - # html.Div(id='confirmed') - # ]) - # self._test_confirm(app, 'ConfirmDialogProviderWithoutCallback', - # add_callback=False) - # - # def test_confirm_as_children(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Button(id='button', children='Send confirm'), - # html.Div(id='confirm-container'), - # dcc.Location(id='dummy-location') - # ]) - # - # @app.callback(Output('confirm-container', 'children'), - # [Input('button', 'n_clicks')]) - # def on_click(n_clicks): - # if n_clicks: - # return dcc.ConfirmDialog( - # displayed=True, - # id='confirm', - # message='Please confirm.') - # - # self.startServer(app) - # - # button = self.wait_for_element_by_css_selector('#button') - # - # button.click() - # time.sleep(2) - # - # self.driver.switch_to.alert.accept() - # - # def test_empty_graph(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div([ - # html.Button(id='click', children='Click me'), - # dcc.Graph( - # id='graph', - # figure={ - # 'data': [dict(x=[1, 2, 3], y=[1, 2, 3], type='scatter')] - # } - # ) - # ]) - # - # @app.callback(dash.dependencies.Output('graph', 'figure'), - # [dash.dependencies.Input('click', 'n_clicks')], - # [dash.dependencies.State('graph', 'figure')]) - # def render_content(click, prev_graph): - # if click: - # return {} - # return prev_graph - # - # self.startServer(app) - # button = self.wait_for_element_by_css_selector('#click') - # button.click() - # time.sleep(2) # Wait for graph to re-render - # self.snapshot('render-empty-graph') - # - # def test_graph_extend_trace(self): - # app = dash.Dash(__name__) - # - # def generate_with_id(id, data=None): - # if data is None: - # data = [{'x': [0, 1, 2, 3, 4], - # 'y': [0, .5, 1, .5, 0] - # }] - # - # return html.Div([html.P(id), - # dcc.Graph(id=id, - # figure=dict(data=data)), - # html.Div(id='output_{}'.format(id))]) - # - # figs = ['trace_will_extend', - # 'trace_will_extend_with_no_indices', - # 'trace_will_extend_with_max_points'] - # - # layout = [generate_with_id(id) for id in figs] - # - # figs.append('trace_will_allow_repeated_extend') - # data = [{'y': [0, 0, 0]}] - # layout.append(generate_with_id(figs[-1], data)) - # - # figs.append('trace_will_extend_selectively') - # data = [{'x': [0, 1, 2, 3, 4], 'y': [0, .5, 1, .5, 0]}, - # {'x': [0, 1, 2, 3, 4], 'y': [1, 1, 1, 1, 1]}] - # layout.append(generate_with_id(figs[-1], data)) - # - # layout.append(dcc.Interval( - # id='interval_extendablegraph_update', - # interval=10, - # n_intervals=0, - # max_intervals=1)) - # - # layout.append(dcc.Interval( - # id='interval_extendablegraph_extendtwice', - # interval=500, - # n_intervals=0, - # max_intervals=2)) - # - # app.layout = html.Div(layout) - # - # @app.callback(Output('trace_will_allow_repeated_extend', 'extendData'), - # [Input('interval_extendablegraph_extendtwice', 'n_intervals')]) - # def trace_will_allow_repeated_extend(n_intervals): - # if n_intervals is None or n_intervals < 1: - # raise PreventUpdate - # - # return dict(y=[[.1, .2, .3, .4, .5]]) - # - # @app.callback(Output('trace_will_extend', 'extendData'), - # [Input('interval_extendablegraph_update', 'n_intervals')]) - # def trace_will_extend(n_intervals): - # if n_intervals is None or n_intervals < 1: - # raise PreventUpdate - # - # x_new = [5, 6, 7, 8, 9] - # y_new = [.1, .2, .3, .4, .5] - # return dict(x=[x_new], y=[y_new]), [0] - # - # @app.callback(Output('trace_will_extend_selectively', 'extendData'), - # [Input('interval_extendablegraph_update', 'n_intervals')]) - # def trace_will_extend_selectively(n_intervals): - # if n_intervals is None or n_intervals < 1: - # raise PreventUpdate - # - # x_new = [5, 6, 7, 8, 9] - # y_new = [.1, .2, .3, .4, .5] - # return dict(x=[x_new], y=[y_new]), [1] - # - # @app.callback(Output('trace_will_extend_with_no_indices', 'extendData'), - # [Input('interval_extendablegraph_update', 'n_intervals')]) - # def trace_will_extend_with_no_indices(n_intervals): - # if n_intervals is None or n_intervals < 1: - # raise PreventUpdate - # - # x_new = [5, 6, 7, 8, 9] - # y_new = [.1, .2, .3, .4, .5] - # return dict(x=[x_new], y=[y_new]) - # - # @app.callback(Output('trace_will_extend_with_max_points', 'extendData'), - # [Input('interval_extendablegraph_update', 'n_intervals')]) - # def trace_will_extend_with_max_points(n_intervals): - # if n_intervals is None or n_intervals < 1: - # raise PreventUpdate - # - # x_new = [5, 6, 7, 8, 9] - # y_new = [.1, .2, .3, .4, .5] - # return dict(x=[x_new], y=[y_new]), [0], 7 - # - # for id in figs: - # @app.callback(Output('output_{}'.format(id), 'children'), - # [Input(id, 'extendData')], - # [State(id, 'figure')]) - # def display_data(trigger, fig): - # return json.dumps(fig['data']) - # - # self.startServer(app) - # - # comparison = json.dumps([ - # dict( - # x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - # y=[0, .5, 1, .5, 0, .1, .2, .3, .4, .5] - # ) - # ]) - # self.wait_for_text_to_equal('#output_trace_will_extend', comparison) - # self.wait_for_text_to_equal('#output_trace_will_extend_with_no_indices', comparison) - # comparison = json.dumps([ - # dict( - # x=[0, 1, 2, 3, 4], - # y=[0, .5, 1, .5, 0] - # ), - # dict( - # x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - # y=[1, 1, 1, 1, 1, .1, .2, .3, .4, .5] - # ) - # ]) - # self.wait_for_text_to_equal('#output_trace_will_extend_selectively', comparison) - # - # comparison = json.dumps([ - # dict( - # x=[3, 4, 5, 6, 7, 8, 9], - # y=[.5, 0, .1, .2, .3, .4, .5] - # ) - # ]) - # self.wait_for_text_to_equal('#output_trace_will_extend_with_max_points', comparison) - # - # comparison = json.dumps([ - # dict( - # y=[0, 0, 0, .1, .2, .3, .4, .5, .1, .2, .3, .4, .5] - # ) - # ]) - # self.wait_for_text_to_equal('#output_trace_will_allow_repeated_extend', comparison) - # - # def test_storage_component(self): - # app = dash.Dash(__name__) - # - # getter = 'return JSON.parse(window.{}.getItem("{}"));' - # clicked_getter = getter.format('localStorage', 'storage') - # dummy_getter = getter.format('sessionStorage', 'dummy') - # dummy_data = 'Hello dummy' - # - # app.layout = html.Div([ - # dcc.Store(id='storage', - # storage_type='local'), - # html.Button('click me', id='btn'), - # html.Button('clear', id='clear-btn'), - # html.Button('set-init-storage', - # id='set-init-storage'), - # dcc.Store(id='dummy', - # storage_type='session', - # data=dummy_data), - # dcc.Store(id='memory', - # storage_type='memory'), - # html.Div(id='memory-output'), - # dcc.Store(id='initial-storage', - # storage_type='session'), - # html.Div(id='init-output') - # ]) - # - # @app.callback(Output('storage', 'data'), - # [Input('btn', 'n_clicks')], - # [State('storage', 'data')]) - # def on_click(n_clicks, storage): - # if n_clicks is None: - # return - # storage = storage or {} - # return {'clicked': storage.get('clicked', 0) + 1} - # - # @app.callback(Output('storage', 'clear_data'), - # [Input('clear-btn', 'n_clicks')]) - # def on_clear(n_clicks): - # if n_clicks is None: - # return - # return True - # - # @app.callback(Output('memory', 'data'), [Input('storage', 'data')]) - # def on_memory(data): - # return data - # - # @app.callback(Output('memory-output', 'children'), - # [Input('memory', 'data')]) - # def on_memory2(data): - # if data is None: - # return '' - # return json.dumps(data) - # - # @app.callback(Output('initial-storage', 'data'), - # [Input('set-init-storage', 'n_clicks')]) - # def on_init(n_clicks): - # if n_clicks is None: - # raise PreventUpdate - # - # return 'initialized' - # - # @app.callback(Output('init-output', 'children'), - # [Input('initial-storage', 'modified_timestamp')], - # [State('initial-storage', 'data')]) - # def init_output(ts, data): - # return json.dumps({'data': data, 'ts': ts}) - # - # self.startServer(app) - # - # time.sleep(1) - # - # dummy = self.driver.execute_script(dummy_getter) - # self.assertEqual(dummy_data, dummy) - # - # click_btn = self.wait_for_element_by_css_selector('#btn') - # clear_btn = self.wait_for_element_by_css_selector('#clear-btn') - # mem = self.wait_for_element_by_css_selector('#memory-output') - # - # for i in range(1, 11): - # click_btn.click() - # time.sleep(1) - # - # click_data = self.driver.execute_script(clicked_getter) - # self.assertEqual(i, click_data.get('clicked')) - # self.assertEqual(i, int(json.loads(mem.text).get('clicked'))) - # - # clear_btn.click() - # time.sleep(1) - # - # cleared_data = self.driver.execute_script(clicked_getter) - # self.assertTrue(cleared_data is None) - # # Did mem also got cleared ? - # self.assertFalse(mem.text) - # - # # Test initial timestamp output - # init_btn = self.wait_for_element_by_css_selector('#set-init-storage') - # init_btn.click() - # ts = int(time.time() * 1000) - # time.sleep(1) - # self.driver.refresh() - # time.sleep(2) - # init = self.wait_for_element_by_css_selector('#init-output') - # init = json.loads(init.text) - # self.assertAlmostEqual(ts, init.get('ts'), delta=1000) - # self.assertEqual('initialized', init.get('data')) - # - # def test_store_nested_data(self): - # app = dash.Dash(__name__) - # - # nested = {'nested': {'nest': 'much'}} - # nested_list = dict(my_list=[1, 2, 3]) - # - # app.layout = html.Div([ - # dcc.Store(id='store', storage_type='local'), - # html.Button('set object as key', id='obj-btn'), - # html.Button('set list as key', id='list-btn'), - # html.Output(id='output') - # ]) - # - # @app.callback(Output('store', 'data'), - # [Input('obj-btn', 'n_clicks_timestamp'), - # Input('list-btn', 'n_clicks_timestamp')]) - # def on_obj_click(obj_ts, list_ts): - # if obj_ts is None and list_ts is None: - # raise PreventUpdate - # - # # python 3 got the default props bug. plotly/dash#396 - # if (obj_ts and not list_ts) or obj_ts > list_ts: - # return nested - # else: - # return nested_list - # - # @app.callback(Output('output', 'children'), - # [Input('store', 'modified_timestamp')], - # [State('store', 'data')]) - # def on_ts(ts, data): - # if ts is None: - # raise PreventUpdate - # return json.dumps(data) - # - # self.startServer(app) - # - # obj_btn = self.wait_for_element_by_css_selector('#obj-btn') - # list_btn = self.wait_for_element_by_css_selector('#list-btn') - # - # obj_btn.click() - # time.sleep(1) - # self.wait_for_text_to_equal('#output', json.dumps(nested)) - # # it would of crashed the app before adding the recursive check. - # - # list_btn.click() - # time.sleep(1) - # self.wait_for_text_to_equal('#output', json.dumps(nested_list)) - # - # def test_user_supplied_css(self): - # app = dash.Dash(__name__) - # - # app.layout = html.Div(className="test-input-css", children=[dcc.Input()]) - # - # self.startServer(app) - # - # self.wait_for_element_by_css_selector('.test-input-css') - # self.snapshot('styled input - width: 100%, border-color: hotpink') - # - # def test_logout_btn(self): - # app = dash.Dash(__name__) - # - # @app.server.route('/_logout', methods=['POST']) - # def on_logout(): - # rep = flask.redirect('/logged-out') - # rep.set_cookie('logout-cookie', '', 0) - # return rep - # - # app.layout = html.Div([ - # html.H2('Logout test'), - # dcc.Location(id='location'), - # html.Div(id='content'), - # ]) - # - # @app.callback(Output('content', 'children'), - # [Input('location', 'pathname')]) - # def on_location(location_path): - # if location_path is None: - # raise PreventUpdate - # - # if 'logged-out' in location_path: - # return 'Logged out' - # else: - # - # @flask.after_this_request - # def _insert_cookie(rep): - # rep.set_cookie('logout-cookie', 'logged-in') - # return rep - # - # return dcc.LogoutButton(id='logout-btn', logout_url='/_logout') - # - # self.startServer(app) - # time.sleep(1) - # self.snapshot('Logout button') - # - # self.assertEqual( - # 'logged-in', - # self.driver.get_cookie('logout-cookie')['value']) - # logout_button = self.wait_for_element_by_css_selector('#logout-btn') - # logout_button.click() - # self.wait_for_text_to_equal('#content', 'Logged out') - # - # self.assertFalse(self.driver.get_cookie('logout-cookie')) - # - # def test_state_and_inputs(self): - # 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'), - # inputs=[Input('input', 'value')], - # state=[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') # noqa: E731 - # input = lambda: self.driver.find_element_by_id('input') # noqa: E731 - # state = lambda: self.driver.find_element_by_id('state') # noqa: E731 - # - # # callback gets called with initial input - # wait_for(lambda: call_count.value == 1) - # 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_simple_callback(self): - # app = dash.Dash(__name__) - # app.layout = html.Div([ - # dcc.Input( - # id='input', - # ), - # 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) - # - # input1 = self.wait_for_element_by_css_selector('#input') - # input1.send_keys('hello world') - # output1 = self.wait_for_element_by_css_selector('#output-1') - # self.wait_for_text_to_equal('#output-1', 'hello world') - # output1.click() # Lose focus, no callback sent for value. - # - # self.assertEqual( - # call_count.value, - # # an initial call to retrieve the first value - # # plus one for each hello world character - # 1 + len('hello world') - # ) - # - # def test_store_type_updates(self): - # app = dash.Dash(__name__) - # - # types = [ - # ('str', 'hello'), - # ('number', 1), - # ('dict', {'data': [2, 3, None]}), - # ('list', [5, 6, 7]), - # ('null', None), - # ('bool', True), - # ('bool', False), - # ('empty-dict', {}), - # ] - # types_changes = list( - # itertools.chain(*itertools.combinations(types, 2)) - # ) + [ # No combinations as it add much test time. - # ('list-dict-1', [1, 2, {'data': [55, 66, 77], 'dummy': 'dum'}]), - # ('list-dict-2', [1, 2, {'data': [111, 99, 88]}]), - # ('dict-3', {'a': 1, 'c': 1}), - # ('dict-2', {'a': 1, 'b': None}), - # ] - # - # app.layout = html.Div([ - # html.Div(id='output'), - # html.Button('click', id='click'), - # dcc.Store(id='store') - # ]) - # - # @app.callback(Output('output', 'children'), - # [Input('store', 'modified_timestamp')], - # [State('store', 'data')]) - # def on_data(ts, data): - # if ts is None: - # raise PreventUpdate - # - # return json.dumps(data) - # - # @app.callback(Output('store', 'data'), [Input('click', 'n_clicks')]) - # def on_click(n_clicks): - # if n_clicks is None: - # raise PreventUpdate - # - # return types_changes[n_clicks - 1][1] - # - # self.startServer(app) - # - # button = self.wait_for_element_by_css_selector('#click') - # - # for i, type_change in enumerate(types_changes): - # button.click() - # try: - # self.wait_for_text_to_equal( - # '#output', json.dumps(type_change[1]), - # ) - # except TimeoutException: - # raise Exception( - # 'Output type did not change from {} to {}'.format( - # types_changes[i - 1], - # type_change - # ) - # ) - # + def create_upload_component_content_types_test(self, filename): + app = dash.Dash(__name__) + + filepath = os.path.join(os.getcwd(), 'test', 'upload-assets', filename) + + pre_style = { + 'whiteSpace': 'pre-wrap', + 'wordBreak': 'break-all' + } + + app.layout = html.Div([ + html.Div(filepath, id='waitfor'), + html.Div( + id='upload-div', + children=dcc.Upload( + id='upload', + children=html.Div([ + 'Drag and Drop or ', + html.A('Select a File') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center' + } + ) + ), + html.Div(id='output'), + html.Div(DataTable(data=[{}]), style={'display': 'none'}) + ]) + + @app.callback(Output('output', 'children'), + [Input('upload', 'contents')]) + def update_output(contents): + if contents is not None: + content_type, content_string = contents.split(',') + if 'csv' in filepath: + df = pd.read_csv(io.StringIO(base64.b64decode( + content_string).decode('utf-8'))) + return html.Div([ + DataTable( + data=df.to_dict('records'), + columns=[{'id': i} for i in ['city', 'country']]), + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + elif 'xls' in filepath: + df = pd.read_excel(io.BytesIO(base64.b64decode( + content_string))) + return html.Div([ + DataTable( + data=df.to_dict('records'), + columns=[{'id': i} for i in ['city', 'country']]), + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + elif 'image' in content_type: + return html.Div([ + html.Img(src=contents), + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + else: + return html.Div([ + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + + self.startServer(app) + + try: + self.wait_for_element_by_css_selector('#waitfor') + except Exception as e: + print(self.wait_for_element_by_css_selector( + '#_dash-app-content').get_attribute('innerHTML')) + raise e + + upload_div = self.wait_for_element_by_css_selector( + '#upload-div input[type=file]') + + upload_div.send_keys(filepath) + time.sleep(5) + self.snapshot(filename) + + def test_upload_csv(self): + self.create_upload_component_content_types_test('utf8.csv') + + def test_upload_xlsx(self): + self.create_upload_component_content_types_test('utf8.xlsx') + + def test_upload_png(self): + self.create_upload_component_content_types_test('dash-logo-stripe.png') + + def test_upload_svg(self): + self.create_upload_component_content_types_test('dash-logo-stripe.svg') + + def test_upload_gallery(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Div(id='waitfor'), + html.Label('Empty'), + dcc.Upload(), + + html.Label('Button'), + dcc.Upload(html.Button('Upload File')), + + html.Label('Text'), + dcc.Upload('Upload File'), + + html.Label('Link'), + dcc.Upload(html.A('Upload File')), + + html.Label('Style'), + dcc.Upload([ + 'Drag and Drop or ', + html.A('Select a File') + ], style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center' + }) + ]) + self.startServer(app) + + try: + self.wait_for_element_by_css_selector('#waitfor') + except Exception as e: + print(self.wait_for_element_by_css_selector( + '#_dash-app-content').get_attribute('innerHTML')) + raise e + + self.snapshot('test_upload_gallery') + + def test_loading_component_initialization(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Div(id='div-1') + ], className='loading') + ], id='root') + + @app.callback( + Output('div-1', 'children'), + [Input('root', 'n_clicks')] + ) + def updateDiv(children): + with lock: + return 'content' + + with lock: + self.startServer(app) + self.wait_for_element_by_css_selector( + '.loading .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_component_action(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Div(id='div-1') + ], className='loading') + ], id='root') + + @app.callback( + Output('div-1', 'children'), + [Input('root', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + with lock: + self.startServer(app) + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + self.driver.find_element_by_id('root').click() + + self.wait_for_element_by_css_selector( + '.loading .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_multiple_loading_components(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Button(id='btn-1') + ], className='loading-1'), + dcc.Loading([ + html.Button(id='btn-2') + ], className='loading-2') + ], id='root') + + @app.callback( + Output('btn-1', 'value'), + [Input('btn-2', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + @app.callback( + Output('btn-2', 'value'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '.loading-2 .dash-spinner' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_nested_loading_components(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Button(id='btn-1'), + dcc.Loading([ + html.Button(id='btn-2') + ], className='loading-2') + ], className='loading-1') + ], id='root') + + @app.callback( + Output('btn-1', 'value'), + [Input('btn-2', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + @app.callback( + Output('btn-2', 'value'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '.loading-2 .dash-spinner' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_dynamic_loading_component(self): + lock = Lock() + + app = dash.Dash(__name__) + app.config['suppress_callback_exceptions'] = True + + app.layout = html.Div([ + html.Button(id='btn-1'), + html.Div(id='div-1') + ]) + + @app.callback( + Output('div-1', 'children'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is None: + return + + with lock: + return html.Div([ + html.Button(id='btn-2'), + dcc.Loading([ + html.Button(id='btn-3') + ], className='loading-1') + ]) + + @app.callback( + Output('btn-3', 'content'), + [Input('btn-2', 'n_clicks')] + ) + def updateDynamic(n_clicks): + if n_clicks is None: + return + + with lock: + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#btn-1' + ) + self.wait_for_element_by_css_selector( + '#div-1' + ) + + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '#div-1 #btn-2' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-3' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '#div-1 #btn-2' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-3' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_slider(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='test-btn'), + html.Label(id='test-div', children=['Horizontal Slider']), + dcc.Slider( + id='horizontal-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + ), + ]) + + @app.callback( + Output('horizontal-slider', 'value'), + [Input('test-btn', 'n_clicks')] + ) + def user_delayed_value(n_clicks): + with lock: + return 5 + + with lock: + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#horizontal-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-slider:not([data-dash-is-loading="true"])' + ) + + with lock: + self.driver.find_element_by_id('test-btn').click() + + self.wait_for_element_by_css_selector( + '#horizontal-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-slider:not([data-dash-is-loading="true"])' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_horizontal_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Horizontal Slider'), + dcc.Slider( + id='horizontal-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + ), + ]) + self.startServer(app) + + self.wait_for_element_by_css_selector('#horizontal-slider') + self.snapshot('horizontal slider') + + h_slider = self.driver.find_element_by_css_selector( + '#horizontal-slider div[role="slider"]' + ) + h_slider.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_vertical_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Vertical Slider'), + dcc.Slider( + id='vertical-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + vertical=True, + ), + ], style={'height': '500px'}) + self.startServer(app) + + self.wait_for_element_by_css_selector('#vertical-slider') + self.snapshot('vertical slider') + + v_slider = self.driver.find_element_by_css_selector( + '#vertical-slider div[role="slider"]' + ) + v_slider.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_range_slider(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='test-btn'), + html.Label(id='test-div', children=['Horizontal Range Slider']), + dcc.RangeSlider( + id='horizontal-range-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=[4, 6], + ), + ]) + + @app.callback( + Output('horizontal-range-slider', 'value'), + [Input('test-btn', 'n_clicks')] + ) + def delayed_value(children): + with lock: + return [4, 6] + + with lock: + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider:not([data-dash-is-loading="true"])' + ) + + with lock: + self.driver.find_element_by_id('test-btn').click() + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider:not([data-dash-is-loading="true"])' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_horizontal_range_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Horizontal Range Slider'), + dcc.RangeSlider( + id='horizontal-range-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=[4, 6], + ), + ]) + self.startServer(app) + + self.wait_for_element_by_css_selector('#horizontal-range-slider') + self.snapshot('horizontal range slider') + + h_slider_1 = self.driver.find_element_by_css_selector( + '#horizontal-range-slider div.rc-slider-handle-1[role="slider"]' + ) + h_slider_1.click() + + h_slider_2 = self.driver.find_element_by_css_selector( + '#horizontal-range-slider div.rc-slider-handle-2[role="slider"]' + ) + h_slider_2.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_vertical_range_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Vertical Range Slider'), + dcc.RangeSlider( + id='vertical-range-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=[4, 6], + vertical=True, + ), + ], style={'height': '500px'}) + self.startServer(app) + + self.wait_for_element_by_css_selector('#vertical-range-slider') + self.snapshot('vertical range slider') + + v_slider_1 = self.driver.find_element_by_css_selector( + '#vertical-range-slider div.rc-slider-handle-1[role="slider"]' + ) + v_slider_1.click() + + v_slider_2 = self.driver.find_element_by_css_selector( + '#vertical-range-slider div.rc-slider-handle-2[role="slider"]' + ) + v_slider_2.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_gallery(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Div(id='waitfor'), + html.Label('Upload'), + dcc.Upload(), + + html.Label('Horizontal Tabs'), + dcc.Tabs(id="tabs", children=[ + dcc.Tab(label='Tab one', className='test', style={'border': '1px solid magenta'}, children=[ + html.Div(['Test']) + ]), + dcc.Tab(label='Tab two', children=[ + html.Div([ + html.H1("This is the content in tab 2"), + html.P("A graph here would be nice!") + ]) + ], id='tab-one'), + dcc.Tab(label='Tab three', children=[ + html.Div([ + html.H1("This is the content in tab 3"), + ]) + ]), + ], + style={ + 'fontFamily': 'system-ui' + }, + content_style={ + 'border': '1px solid #d6d6d6', + 'padding': '44px' + }, + parent_style={ + 'maxWidth': '1000px', + 'margin': '0 auto' + } + ), + + html.Label('Vertical Tabs'), + dcc.Tabs(id="tabs1", vertical=True, children=[ + dcc.Tab(label='Tab one', children=[ + html.Div(['Test']) + ]), + dcc.Tab(label='Tab two', children=[ + html.Div([ + html.H1("This is the content in tab 2"), + html.P("A graph here would be nice!") + ]) + ]), + dcc.Tab(label='Tab three', children=[ + html.Div([ + html.H1("This is the content in tab 3"), + ]) + ]), + ] + ), + + html.Label('Dropdown'), + dcc.Dropdown( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value='MTL', + id='dropdown' + ), + + html.Label('Multi-Select Dropdown'), + dcc.Dropdown( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value=['MTL', 'SF'], + multi=True + ), + + html.Label('Radio Items'), + dcc.RadioItems( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value='MTL' + ), + + html.Label('Checkboxes'), + dcc.Checklist( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value=['MTL', 'SF'] + ), + + html.Label('Text Input'), + dcc.Input(value='', placeholder='type here', id='textinput'), + html.Label('Disabled Text Input'), + dcc.Input(value='disabled', type='text', + id='disabled-textinput', disabled=True), + + html.Label('Slider'), + dcc.Slider( + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + ), + + html.Label('Graph'), + dcc.Graph( + id='graph', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [4, 1, 4] + }], + 'layout': { + 'title': u'北京' + } + } + ), + + html.Div([ + html.Label('DatePickerSingle'), + dcc.DatePickerSingle( + id='date-picker-single', + date=datetime(1997, 5, 10) + ), + html.Div([ + html.Label('DatePickerSingle - empty input'), + dcc.DatePickerSingle(), + ], id='dt-single-no-date-value' + ), + html.Div([ + html.Label('DatePickerSingle - initial visible month (May 97)'), + dcc.DatePickerSingle( + initial_visible_month=datetime(1997, 5, 10) + ), + ], id='dt-single-no-date-value-init-month' + ), + ]), + + html.Div([ + html.Label('DatePickerRange'), + dcc.DatePickerRange( + id='date-picker-range', + start_date_id='startDate', + end_date_id='endDate', + start_date=datetime(1997, 5, 3), + end_date_placeholder_text='Select a date!' + ), + html.Div([ + html.Label('DatePickerRange - empty input'), + dcc.DatePickerRange( + start_date_id='startDate', + end_date_id='endDate', + start_date_placeholder_text='Start date', + end_date_placeholder_text='End date' + ), + ], id='dt-range-no-date-values' + ), + html.Div([ + html.Label('DatePickerRange - initial visible month (May 97)'), + dcc.DatePickerRange( + start_date_id='startDate', + end_date_id='endDate', + start_date_placeholder_text='Start date', + end_date_placeholder_text='End date', + initial_visible_month=datetime(1997, 5, 10) + ), + ], id='dt-range-no-date-values-init-month' + ), + ]), + + html.Label('TextArea'), + dcc.Textarea( + placeholder='Enter a value... 北京', + style={'width': '100%'} + ), + + html.Label('Markdown'), + dcc.Markdown(''' + #### Dash and Markdown + + Dash supports [Markdown](https://rexxars.github.io/react-markdown/). + + Markdown is a simple way to write and format text. + It includes a syntax for things like **bold text** and *italics*, + [links](https://rexxars.github.io/react-markdown/), inline `code` snippets, lists, + quotes, and more. + + 1. Links are auto-rendered: https://dash.plot.ly. + 2. This uses ~commonmark~ GitHub flavored markdown. + + Tables are also supported: + + | First Header | Second Header | + | ------------- | ------------- | + | Content Cell | Content Cell | + | Content Cell | Content Cell | + + 北京 + '''.replace(' ', '')), + dcc.Markdown(['# Line one', '## Line two']), + dcc.Markdown(), + dcc.SyntaxHighlighter(dedent('''import python + print(3)'''), language='python'), + dcc.SyntaxHighlighter([ + 'import python', + 'print(3)' + ], language='python'), + dcc.SyntaxHighlighter() + ]) + self.startServer(app) + + self.wait_for_element_by_css_selector('#waitfor') + + self.snapshot('gallery') + + self.driver.find_element_by_css_selector( + '#dropdown .Select-input input' + ).send_keys(u'北') + self.snapshot('gallery - chinese character') + + text_input = self.driver.find_element_by_id('textinput') + # verify that type has the right default + # Can't use text_input.get_attribute('type') - that pulls the + # default value even if none is specified. + get_type = ('return document.getElementById("textinput")' + '.getAttribute("type");') + self.assertEqual(self.driver.execute_script(get_type), 'text') + disabled_text_input = self.driver.find_element_by_id( + 'disabled-textinput') + text_input.send_keys('HODOR') + + # It seems selenium errors when send(ing)_keys on a disabled element. + # In case this changes we try anyway and catch the particular + # exception. In any case Percy will snapshot the disabled input style + # so we are not totally dependent on the send_keys behaviour for + # testing disabled state. + try: + disabled_text_input.send_keys('RODOH') + except InvalidElementStateException: + pass + + self.snapshot('gallery - text input') + + # DatePickerSingle and DatePickerRange test + # for issue with datepicker when date value is `None` + dt_input_1 = self.driver.find_element_by_css_selector( + '#dt-single-no-date-value #date' + ) + dt_input_1.click() + self.snapshot('gallery - DatePickerSingle\'s datepicker ' + 'when no date value and no initial month specified') + dt_length = len(dt_input_1.get_attribute('value')) + dt_input_1.send_keys(dt_length * Keys.BACKSPACE) + dt_input_1.send_keys("1997-05-03") + + dt_input_2 = self.driver.find_element_by_css_selector( + '#dt-single-no-date-value-init-month #date' + ) + self.driver.find_element_by_css_selector( + 'label' + ).click() + dt_input_2.click() + self.snapshot('gallery - DatePickerSingle\'s datepicker ' + 'when no date value, but initial month is specified') + dt_length = len(dt_input_2.get_attribute('value')) + dt_input_2.send_keys(dt_length * Keys.BACKSPACE) + dt_input_2.send_keys("1997-05-03") + + dt_input_3 = self.driver.find_element_by_css_selector( + '#dt-range-no-date-values #endDate' + ) + self.driver.find_element_by_css_selector( + 'label' + ).click() + dt_input_3.click() + self.snapshot('gallery - DatePickerRange\'s datepicker ' + 'when neither start date nor end date ' + 'nor initial month is specified') + dt_length = len(dt_input_3.get_attribute('value')) + dt_input_3.send_keys(dt_length * Keys.BACKSPACE) + dt_input_3.send_keys("1997-05-03") + + dt_input_4 = self.driver.find_element_by_css_selector( + '#dt-range-no-date-values-init-month #endDate' + ) + self.driver.find_element_by_css_selector( + 'label' + ).click() + dt_input_4.click() + self.snapshot('gallery - DatePickerRange\'s datepicker ' + 'when neither start date nor end date is specified, ' + 'but initial month is') + dt_length = len(dt_input_4.get_attribute('value')) + dt_input_4.send_keys(dt_length * Keys.BACKSPACE) + dt_input_4.send_keys("1997-05-03") + + def test_tabs_in_vertical_mode(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Tabs(id="tabs", value='tab-3', children=[ + dcc.Tab(label='Tab one', value='tab-1', id='tab-1', children=[ + html.Div('Tab One Content') + ]), + dcc.Tab(label='Tab two', value='tab-2', id='tab-2', children=[ + html.Div('Tab Two Content') + ]), + dcc.Tab(label='Tab three', value='tab-3', id='tab-3', children=[ + html.Div('Tab Three Content') + ]), + ], vertical=True), + html.Div(id='tabs-content') + ]) + + self.startServer(app=app) + self.wait_for_text_to_equal('#tab-3', 'Tab three') + + self.snapshot('Tabs - vertical mode') + + def test_tabs_without_children(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs", value='tab-2', children=[ + dcc.Tab(label='Tab one', value='tab-1', id='tab-1'), + dcc.Tab(label='Tab two', value='tab-2', id='tab-2'), + ]), + html.Div(id='tabs-content') + ]) + + @app.callback(dash.dependencies.Output('tabs-content', 'children'), + [dash.dependencies.Input('tabs', 'value')]) + def render_content(tab): + if tab == 'tab-1': + return html.Div([ + html.H3('Test content 1') + ], id='test-tab-1') + elif tab == 'tab-2': + return html.Div([ + html.H3('Test content 2') + ], id='test-tab-2') + + self.startServer(app=app) + + self.wait_for_text_to_equal('#tabs-content', 'Test content 2') + self.snapshot('initial tab - tab 2') + + selected_tab = self.wait_for_element_by_css_selector('#tab-1') + selected_tab.click() + time.sleep(1) + self.wait_for_text_to_equal('#tabs-content', 'Test content 1') + + def test_tabs_with_children_undefined(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs", value='tab-1'), + html.Div(id='tabs-content') + ]) + + self.startServer(app=app) + + self.wait_for_element_by_css_selector('#tabs-content') + + self.snapshot('Tabs component with children undefined') + + def test_tabs_render_without_selected(self): + app = dash.Dash(__name__) + + data = [ + {'id': 'one', 'value': 1}, + {'id': 'two', 'value': 2}, + ] + + menu = html.Div([ + html.Div('one', id='one'), + html.Div('two', id='two') + ]) + + tabs_one = html.Div([ + dcc.Tabs([ + dcc.Tab(dcc.Graph(id='graph-one'), label='tab-one-one'), + ]) + ], id='tabs-one', style={'display': 'none'}) + + tabs_two = html.Div([ + dcc.Tabs([ + dcc.Tab(dcc.Graph(id='graph-two'), label='tab-two-one'), + ]) + ], id='tabs-two', style={'display': 'none'}) + + app.layout = html.Div([ + menu, + tabs_one, + tabs_two + ]) + + for i in ('one', 'two'): + + @app.callback(Output('tabs-{}'.format(i), 'style'), + [Input(i, 'n_clicks')]) + def on_click(n_clicks): + if n_clicks is None: + raise PreventUpdate + + if n_clicks % 2 == 1: + return {'display': 'block'} + return {'display': 'none'} + + @app.callback(Output('graph-{}'.format(i), 'figure'), + [Input(i, 'n_clicks')]) + def on_click(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return { + 'data': [ + { + 'x': [1, 2, 3, 4], + 'y': [4, 3, 2, 1] + } + ], + 'layout': { + 'width': 700, + 'height': 450 + } + } + + self.startServer(app=app) + + button_one = self.wait_for_element_by_css_selector('#one') + button_two = self.wait_for_element_by_css_selector('#two') + + button_one.click() + + # wait for tabs to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-one .main-svg")) + ) + + time.sleep(1) + self.snapshot("Tabs 1 rendered ") + + button_two.click() + + # wait for tabs to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-two .main-svg")) + ) + + time.sleep(1) + self.snapshot("Tabs 2 rendered ") + + # do some extra tests while we're here + # and have access to Graph and plotly.js + self.check_graph_config_shape() + self.check_plotlyjs() + + def check_plotlyjs(self): + # find plotly.js files in the dist folder, check that there's only one + all_dist = os.listdir(dcc.__path__[0]) + js_re = r'^plotly-(.*)\.min\.js$' + plotlyjs_dist = [fn for fn in all_dist if re.match(js_re, fn)] + + self.assertEqual(len(plotlyjs_dist), 1) + + # check that the version matches what we see in the page + page_version = self.driver.execute_script('return Plotly.version;') + dist_version = re.match(js_re, plotlyjs_dist[0]).groups()[0] + self.assertEqual(page_version, dist_version) + + def check_graph_config_shape(self): + config_schema = self.driver.execute_script( + 'return Plotly.PlotSchema.get().config;' + ) + with open(os.path.join(dcc.__path__[0], 'metadata.json')) as meta: + graph_meta = json.load(meta)['src/components/Graph.react.js'] + config_prop_shape = graph_meta['props']['config']['type']['value'] + + ignored_config = [ + 'setBackground', + 'showSources', + 'logging', + 'globalTransforms', + 'role' + ] + + def crawl(schema, props): + for prop_name in props: + self.assertIn(prop_name, schema) + + for item_name, item in schema.items(): + if item_name in ignored_config: + continue + + self.assertIn(item_name, props) + if 'valType' not in item: + crawl(item, props[item_name]['value']) + + crawl(config_schema, config_prop_shape) + + def test_tabs_without_value(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs-without-value", children=[ + dcc.Tab(label='Tab One', value='tab-1'), + dcc.Tab(label='Tab Two', value='tab-2'), + ]), + html.Div(id='tabs-content') + ]) + + @app.callback(Output('tabs-content', 'children'), + [Input('tabs-without-value', 'value')]) + def render_content(tab): + if tab == 'tab-1': + return html.H3('Default selected Tab content 1') + elif tab == 'tab-2': + return html.H3('Tab content 2') + + self.startServer(app=app) + + self.wait_for_text_to_equal('#tabs-content', 'Default selected Tab content 1') + + self.snapshot('Tab 1 should be selected by default') + + def test_graph_does_not_resize_in_tabs(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs-example", value='tab-1-example', children=[ + dcc.Tab(label='Tab One', value='tab-1-example', id='tab-1'), + dcc.Tab(label='Tab Two', value='tab-2-example', id='tab-2'), + ]), + html.Div(id='tabs-content-example') + ]) + + @app.callback(Output('tabs-content-example', 'children'), + [Input('tabs-example', 'value')]) + def render_content(tab): + if tab == 'tab-1-example': + return html.Div([ + html.H3('Tab content 1'), + dcc.Graph( + id='graph-1-tabs', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [3, 1, 2], + 'type': 'bar' + }] + } + ) + ]) + elif tab == 'tab-2-example': + return html.Div([ + html.H3('Tab content 2'), + dcc.Graph( + id='graph-2-tabs', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + self.startServer(app=app) + + tab_one = self.wait_for_element_by_css_selector('#tab-1') + tab_two = self.wait_for_element_by_css_selector('#tab-2') + + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.ID, "tab-2")) + ) + + self.snapshot("Tabs with Graph - initial (graph should not resize)") + tab_two.click() + + # wait for Graph's internal svg to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-2-tabs .main-svg")) + ) + + self.snapshot("Tabs with Graph - clicked tab 2 (graph should not resize)") + + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.ID, "tab-1")) + ) + + tab_one.click() + + # wait for Graph to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-1-tabs .main-svg")) + ) + + self.snapshot("Tabs with Graph - clicked tab 1 (graph should not resize)") + + def test_location_link(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Div(id='waitfor'), + dcc.Location(id='test-location', refresh=False), + + dcc.Link( + html.Button('I am a clickable button'), + id='test-link', + href='/test/pathname'), + dcc.Link( + html.Button('I am a clickable hash button'), + id='test-link-hash', + href='#test'), + dcc.Link( + html.Button('I am a clickable search button'), + id='test-link-search', + href='?testQuery=testValue', + refresh=False), + html.Button('I am a magic button that updates pathname', + id='test-button'), + html.A('link to click', href='/test/pathname/a', id='test-a'), + html.A('link to click', href='#test-hash', id='test-a-hash'), + html.A('link to click', href='?queryA=valueA', id='test-a-query'), + html.Div(id='test-pathname', children=[]), + html.Div(id='test-hash', children=[]), + html.Div(id='test-search', children=[]), + ]) + + @app.callback( + output=Output(component_id='test-pathname', + component_property='children'), + inputs=[Input(component_id='test-location', component_property='pathname')]) + def update_location_on_page(pathname): + return pathname + + @app.callback( + output=Output(component_id='test-hash', + component_property='children'), + inputs=[Input(component_id='test-location', component_property='hash')]) + def update_location_on_page(hash_val): + if hash_val is None: + return '' + + return hash_val + + @app.callback( + output=Output(component_id='test-search', + component_property='children'), + inputs=[Input(component_id='test-location', component_property='search')]) + def update_location_on_page(search): + if search is None: + return '' + + return search + + @app.callback( + output=Output(component_id='test-location', + component_property='pathname'), + inputs=[Input(component_id='test-button', + component_property='n_clicks')], + state=[State(component_id='test-location', component_property='pathname')]) + def update_pathname(n_clicks, current_pathname): + if n_clicks is not None: + return '/new/pathname' + + return current_pathname + + self.startServer(app=app) + + time.sleep(1) + self.snapshot('link -- location') + + # Check that link updates pathname + self.wait_for_element_by_css_selector('#test-link').click() + self.assertEqual( + self.driver.current_url.replace('http://localhost:8050', ''), + '/test/pathname') + self.wait_for_text_to_equal('#test-pathname', '/test/pathname') + + # Check that hash is updated in the Location + self.wait_for_element_by_css_selector('#test-link-hash').click() + self.wait_for_text_to_equal('#test-pathname', '/test/pathname') + self.wait_for_text_to_equal('#test-hash', '#test') + self.snapshot('link -- /test/pathname#test') + + # Check that search is updated in the Location -- note that this goes through href and therefore wipes the hash + self.wait_for_element_by_css_selector('#test-link-search').click() + self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') + self.wait_for_text_to_equal('#test-hash', '') + self.snapshot('link -- /test/pathname?testQuery=testValue') + + # Check that pathname is updated through a Button click via props + self.wait_for_element_by_css_selector('#test-button').click() + self.wait_for_text_to_equal('#test-pathname', '/new/pathname') + self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') + self.snapshot('link -- /new/pathname?testQuery=testValue') + + # Check that pathname is updated through an a tag click via props + self.wait_for_element_by_css_selector('#test-a').click() + try: + self.wait_for_element_by_css_selector('#waitfor') + except Exception as e: + print(self.wait_for_element_by_css_selector( + '#_dash-app-content').get_attribute('innerHTML')) + raise e + + self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') + self.wait_for_text_to_equal('#test-search', '') + self.wait_for_text_to_equal('#test-hash', '') + self.snapshot('link -- /test/pathname/a') + + # Check that hash is updated through an a tag click via props + self.wait_for_element_by_css_selector('#test-a-hash').click() + self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') + self.wait_for_text_to_equal('#test-search', '') + self.wait_for_text_to_equal('#test-hash', '#test-hash') + self.snapshot('link -- /test/pathname/a#test-hash') + + # Check that hash is updated through an a tag click via props + self.wait_for_element_by_css_selector('#test-a-query').click() + self.wait_for_element_by_css_selector('#waitfor') + self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') + self.wait_for_text_to_equal('#test-search', '?queryA=valueA') + self.wait_for_text_to_equal('#test-hash', '') + self.snapshot('link -- /test/pathname/a?queryA=valueA') + + def test_link_scroll(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Location(id='test-url', refresh=False), + + html.Div(id='push-to-bottom', children=[], style={ + 'display': 'block', + 'height': '200vh' + }), + html.Div(id='page-content'), + dcc.Link('Test link', href='/test-link', id='test-link') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('page-content', 'children'), + [Input('test-url', 'pathname')]) + def display_page(pathname): + call_count.value = call_count.value + 1 + return 'You are on page {}'.format(pathname) + + self.startServer(app=app) + + time.sleep(2) + + # callback is called twice when defined + self.assertEqual( + call_count.value, + 2 + ) + + # test if link correctly scrolls back to top of page + test_link = self.wait_for_element_by_css_selector('#test-link') + test_link.send_keys(Keys.NULL) + test_link.click() + time.sleep(2) + + # test link still fires update on Location + page_content = self.wait_for_element_by_css_selector('#page-content') + self.assertNotEqual(page_content.text, 'You are on page /') + + self.wait_for_text_to_equal( + '#page-content', 'You are on page /test-link') + + # test if rendered Link's tag has a href attribute + link_href = test_link.get_attribute("href") + self.assertEqual(link_href, 'http://localhost:8050/test-link') + + # test if callback is only fired once (offset of 2) + self.assertEqual( + call_count.value, + 3 + ) + + def test_candlestick(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Button( + id='button', + children='Update Candlestick', + n_clicks=0 + ), + dcc.Graph(id='graph') + ]) + + @app.callback(Output('graph', 'figure'), [Input('button', 'n_clicks')]) + def update_graph(n_clicks): + return { + 'data': [{ + 'open': [1] * 5, + 'high': [3] * 5, + 'low': [0] * 5, + 'close': [2] * 5, + 'x': [n_clicks] * 5, + 'type': 'candlestick' + }] + } + self.startServer(app=app) + + button = self.wait_for_element_by_css_selector('#button') + self.snapshot('candlestick - initial') + button.click() + time.sleep(1) + self.snapshot('candlestick - 1 click') + + button.click() + time.sleep(1) + self.snapshot('candlestick - 2 click') + + def test_graphs_with_different_figures(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Graph( + id='example-graph', + figure={ + 'data': [ + {'x': [1, 2, 3], 'y': [4, 1, 2], + 'type': 'bar', 'name': 'SF'}, + {'x': [1, 2, 3], 'y': [2, 4, 5], + 'type': 'bar', 'name': u'Montréal'}, + ], + 'layout': { + 'title': 'Dash Data Visualization' + } + } + ), + dcc.Graph( + id='example-graph-2', + figure={ + 'data': [ + {'x': [20, 24, 33], 'y': [5, 2, 3], + 'type': 'bar', 'name': 'SF'}, + {'x': [11, 22, 33], 'y': [22, 44, 55], + 'type': 'bar', 'name': u'Montréal'}, + ], + 'layout': { + 'title': 'Dash Data Visualization' + } + } + ), + html.Div(id='restyle-data'), + html.Div(id='relayout-data') + ]) + + @app.callback(Output('restyle-data', 'children'), [Input('example-graph', 'restyleData')]) + def show_restyle_data(data): + if data is None: # ignore initial + return '' + return json.dumps(data) + + @app.callback(Output('relayout-data', 'children'), [Input('example-graph', 'relayoutData')]) + def show_relayout_data(data): + if data is None or 'autosize' in data: # ignore initial & auto width + return '' + return json.dumps(data) + + self.startServer(app=app) + + # use this opportunity to test restyleData, since there are multiple + # traces on this graph + legendToggle = self.driver.find_element_by_css_selector('#example-graph .traces:first-child .legendtoggle') + legendToggle.click() + self.wait_for_text_to_equal('#restyle-data', '[{"visible": ["legendonly"]}, [0]]') + + # move snapshot after click, so it's more stable with the wait + self.snapshot('2 graphs with different figures') + + # and test relayoutData while we're at it + autoscale = self.driver.find_element_by_css_selector('#example-graph .ewdrag') + autoscale.click() + autoscale.click() + self.wait_for_text_to_equal('#relayout-data', '{"xaxis.autorange": true}') + + def test_graphs_without_ids(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Graph(className='graph-no-id-1'), + dcc.Graph(className='graph-no-id-2'), + ]) + + self.startServer(app=app) + + graph_1 = self.wait_for_element_by_css_selector('.graph-no-id-1') + graph_2 = self.wait_for_element_by_css_selector('.graph-no-id-2') + + self.assertNotEqual(graph_1.get_attribute('id'), graph_2.get_attribute('id')) + + def test_datepickerrange_updatemodes(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.DatePickerRange( + id='date-picker-range', + start_date_id='startDate', + end_date_id='endDate', + start_date_placeholder_text='Select a start date!', + end_date_placeholder_text='Select an end date!', + updatemode='bothdates' + ), + html.Div(id='date-picker-range-output') + ]) + + @app.callback( + dash.dependencies.Output('date-picker-range-output', 'children'), + [dash.dependencies.Input('date-picker-range', 'start_date'), + dash.dependencies.Input('date-picker-range', 'end_date')]) + def update_output(start_date, end_date): + return '{} - {}'.format(start_date, end_date) + + self.startServer(app=app) + + start_date = self.wait_for_element_by_css_selector('#startDate') + start_date.click() + + end_date = self.wait_for_element_by_css_selector('#endDate') + end_date.click() + + self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') + + # using mouse click with fixed day range, this can be improved + # once we start refactoring the test structure + start_date.click() + + sday = self.driver.find_element_by_xpath("//td[text()='1' and @tabindex='0']") + sday.click() + self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') + + eday = self.driver.find_elements_by_xpath("//td[text()='28']")[1] + eday.click() + + date_tokens = set(start_date.get_attribute('value').split('/')) + date_tokens.update(end_date.get_attribute('value').split('/')) + + self.assertEqual( + set(itertools.chain(*[ + _.split('-') + for _ in self.driver.find_element_by_css_selector( + '#date-picker-range-output').text.split(' - ')])), + date_tokens, + "date should match the callback output") + + def test_interval(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Div(id='output'), + dcc.Interval(id='interval', interval=1, max_intervals=2) + ]) + + @app.callback(Output('output', 'children'), + [Input('interval', 'n_intervals')]) + def update_text(n): + return "{}".format(n) + + self.startServer(app=app) + + # wait for interval to finish + time.sleep(5) + + self.wait_for_text_to_equal('#output', '2') + + def test_if_interval_can_be_restarted(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Interval( + id='interval', + interval=100, + n_intervals=0, + max_intervals=-1 + ), + + html.Button('Start', id='start', n_clicks_timestamp=-1), + html.Button('Stop', id='stop', n_clicks_timestamp=-1), + + html.Div(id='output') + + ]) + + @app.callback( + Output('interval', 'max_intervals'), + [Input('start', 'n_clicks_timestamp'), + Input('stop', 'n_clicks_timestamp')]) + def start_stop(start, stop): + if start < stop: + return 0 + else: + return -1 + + @app.callback(Output('output', 'children'), [Input('interval', 'n_intervals')]) + def display_data(n_intervals): + return 'Updated {}'.format(n_intervals) + + self.startServer(app=app) + + start_button = self.wait_for_element_by_css_selector('#start') + stop_button = self.wait_for_element_by_css_selector('#stop') + + # interval will start itself, we wait a second before pressing 'stop' + time.sleep(1) + + # get the output after running it for a bit + output = self.wait_for_element_by_css_selector('#output') + stop_button.click() + + time.sleep(1) + + # get the output after it's stopped, it shouldn't be higher than before + output_stopped = self.wait_for_element_by_css_selector('#output') + + self.wait_for_text_to_equal("#output", output_stopped.text) + + # This test logic is bad + # same element check for same text will always be true. + self.assertEqual(output.text, output_stopped.text) + + def _test_confirm(self, app, test_name, add_callback=True): + count = Value('i', 0) + + if add_callback: + @app.callback(Output('confirmed', 'children'), + [Input('confirm', 'submit_n_clicks'), + Input('confirm', 'cancel_n_clicks')], + [State('confirm', 'submit_n_clicks_timestamp'), + State('confirm', 'cancel_n_clicks_timestamp')]) + def _on_confirmed(submit_n_clicks, cancel_n_clicks, + submit_timestamp, cancel_timestamp): + if not submit_n_clicks and not cancel_n_clicks: + return '' + count.value += 1 + if (submit_timestamp and cancel_timestamp is None) or\ + (submit_timestamp > cancel_timestamp): + return 'confirmed' + else: + return 'canceled' + + self.startServer(app) + button = self.wait_for_element_by_css_selector('#button') + self.snapshot(test_name + ' -> initial') + + button.click() + time.sleep(1) + self.driver.switch_to.alert.accept() + + if add_callback: + self.wait_for_text_to_equal('#confirmed', 'confirmed') + self.snapshot(test_name + ' -> confirmed') + + button.click() + time.sleep(0.5) + self.driver.switch_to.alert.dismiss() + time.sleep(0.5) + + if add_callback: + self.wait_for_text_to_equal('#confirmed', 'canceled') + self.snapshot(test_name + ' -> canceled') + + if add_callback: + self.assertEqual(2, count.value, + 'Expected 2 callback but got ' + str(count.value)) + + def test_confirm(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='button', children='Send confirm', n_clicks=0), + dcc.ConfirmDialog(id='confirm', message='Please confirm.'), + html.Div(id='confirmed') + ]) + + @app.callback(Output('confirm', 'displayed'), + [Input('button', 'n_clicks')]) + def on_click_confirm(n_clicks): + if n_clicks: + return True + + self._test_confirm(app, 'ConfirmDialog') + + def test_confirm_dialog_provider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.ConfirmDialogProvider( + html.Button('click me', id='button'), + id='confirm', message='Please confirm.'), + html.Div(id='confirmed') + ]) + + self._test_confirm(app, 'ConfirmDialogProvider') + + def test_confirm_without_callback(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.ConfirmDialogProvider( + html.Button('click me', id='button'), + id='confirm', message='Please confirm.'), + html.Div(id='confirmed') + ]) + self._test_confirm(app, 'ConfirmDialogProviderWithoutCallback', + add_callback=False) + + def test_confirm_as_children(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='button', children='Send confirm'), + html.Div(id='confirm-container'), + dcc.Location(id='dummy-location') + ]) + + @app.callback(Output('confirm-container', 'children'), + [Input('button', 'n_clicks')]) + def on_click(n_clicks): + if n_clicks: + return dcc.ConfirmDialog( + displayed=True, + id='confirm', + message='Please confirm.') + + self.startServer(app) + + button = self.wait_for_element_by_css_selector('#button') + + button.click() + time.sleep(2) + + self.driver.switch_to.alert.accept() + + def test_empty_graph(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='click', children='Click me'), + dcc.Graph( + id='graph', + figure={ + 'data': [dict(x=[1, 2, 3], y=[1, 2, 3], type='scatter')] + } + ) + ]) + + @app.callback(dash.dependencies.Output('graph', 'figure'), + [dash.dependencies.Input('click', 'n_clicks')], + [dash.dependencies.State('graph', 'figure')]) + def render_content(click, prev_graph): + if click: + return {} + return prev_graph + + self.startServer(app) + button = self.wait_for_element_by_css_selector('#click') + button.click() + time.sleep(2) # Wait for graph to re-render + self.snapshot('render-empty-graph') + + def test_graph_extend_trace(self): + app = dash.Dash(__name__) + + def generate_with_id(id, data=None): + if data is None: + data = [{'x': [0, 1, 2, 3, 4], + 'y': [0, .5, 1, .5, 0] + }] + + return html.Div([html.P(id), + dcc.Graph(id=id, + figure=dict(data=data)), + html.Div(id='output_{}'.format(id))]) + + figs = ['trace_will_extend', + 'trace_will_extend_with_no_indices', + 'trace_will_extend_with_max_points'] + + layout = [generate_with_id(id) for id in figs] + + figs.append('trace_will_allow_repeated_extend') + data = [{'y': [0, 0, 0]}] + layout.append(generate_with_id(figs[-1], data)) + + figs.append('trace_will_extend_selectively') + data = [{'x': [0, 1, 2, 3, 4], 'y': [0, .5, 1, .5, 0]}, + {'x': [0, 1, 2, 3, 4], 'y': [1, 1, 1, 1, 1]}] + layout.append(generate_with_id(figs[-1], data)) + + layout.append(dcc.Interval( + id='interval_extendablegraph_update', + interval=10, + n_intervals=0, + max_intervals=1)) + + layout.append(dcc.Interval( + id='interval_extendablegraph_extendtwice', + interval=500, + n_intervals=0, + max_intervals=2)) + + app.layout = html.Div(layout) + + @app.callback(Output('trace_will_allow_repeated_extend', 'extendData'), + [Input('interval_extendablegraph_extendtwice', 'n_intervals')]) + def trace_will_allow_repeated_extend(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + return dict(y=[[.1, .2, .3, .4, .5]]) + + @app.callback(Output('trace_will_extend', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]), [0] + + @app.callback(Output('trace_will_extend_selectively', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend_selectively(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]), [1] + + @app.callback(Output('trace_will_extend_with_no_indices', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend_with_no_indices(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]) + + @app.callback(Output('trace_will_extend_with_max_points', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend_with_max_points(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]), [0], 7 + + for id in figs: + @app.callback(Output('output_{}'.format(id), 'children'), + [Input(id, 'extendData')], + [State(id, 'figure')]) + def display_data(trigger, fig): + return json.dumps(fig['data']) + + self.startServer(app) + + comparison = json.dumps([ + dict( + x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + y=[0, .5, 1, .5, 0, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_extend', comparison) + self.wait_for_text_to_equal('#output_trace_will_extend_with_no_indices', comparison) + comparison = json.dumps([ + dict( + x=[0, 1, 2, 3, 4], + y=[0, .5, 1, .5, 0] + ), + dict( + x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + y=[1, 1, 1, 1, 1, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_extend_selectively', comparison) + + comparison = json.dumps([ + dict( + x=[3, 4, 5, 6, 7, 8, 9], + y=[.5, 0, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_extend_with_max_points', comparison) + + comparison = json.dumps([ + dict( + y=[0, 0, 0, .1, .2, .3, .4, .5, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_allow_repeated_extend', comparison) + + def test_storage_component(self): + app = dash.Dash(__name__) + + getter = 'return JSON.parse(window.{}.getItem("{}"));' + clicked_getter = getter.format('localStorage', 'storage') + dummy_getter = getter.format('sessionStorage', 'dummy') + dummy_data = 'Hello dummy' + + app.layout = html.Div([ + dcc.Store(id='storage', + storage_type='local'), + html.Button('click me', id='btn'), + html.Button('clear', id='clear-btn'), + html.Button('set-init-storage', + id='set-init-storage'), + dcc.Store(id='dummy', + storage_type='session', + data=dummy_data), + dcc.Store(id='memory', + storage_type='memory'), + html.Div(id='memory-output'), + dcc.Store(id='initial-storage', + storage_type='session'), + html.Div(id='init-output') + ]) + + @app.callback(Output('storage', 'data'), + [Input('btn', 'n_clicks')], + [State('storage', 'data')]) + def on_click(n_clicks, storage): + if n_clicks is None: + return + storage = storage or {} + return {'clicked': storage.get('clicked', 0) + 1} + + @app.callback(Output('storage', 'clear_data'), + [Input('clear-btn', 'n_clicks')]) + def on_clear(n_clicks): + if n_clicks is None: + return + return True + + @app.callback(Output('memory', 'data'), [Input('storage', 'data')]) + def on_memory(data): + return data + + @app.callback(Output('memory-output', 'children'), + [Input('memory', 'data')]) + def on_memory2(data): + if data is None: + return '' + return json.dumps(data) + + @app.callback(Output('initial-storage', 'data'), + [Input('set-init-storage', 'n_clicks')]) + def on_init(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return 'initialized' + + @app.callback(Output('init-output', 'children'), + [Input('initial-storage', 'modified_timestamp')], + [State('initial-storage', 'data')]) + def init_output(ts, data): + return json.dumps({'data': data, 'ts': ts}) + + self.startServer(app) + + time.sleep(1) + + dummy = self.driver.execute_script(dummy_getter) + self.assertEqual(dummy_data, dummy) + + click_btn = self.wait_for_element_by_css_selector('#btn') + clear_btn = self.wait_for_element_by_css_selector('#clear-btn') + mem = self.wait_for_element_by_css_selector('#memory-output') + + for i in range(1, 11): + click_btn.click() + time.sleep(1) + + click_data = self.driver.execute_script(clicked_getter) + self.assertEqual(i, click_data.get('clicked')) + self.assertEqual(i, int(json.loads(mem.text).get('clicked'))) + + clear_btn.click() + time.sleep(1) + + cleared_data = self.driver.execute_script(clicked_getter) + self.assertTrue(cleared_data is None) + # Did mem also got cleared ? + self.assertFalse(mem.text) + + # Test initial timestamp output + init_btn = self.wait_for_element_by_css_selector('#set-init-storage') + init_btn.click() + ts = int(time.time() * 1000) + time.sleep(1) + self.driver.refresh() + time.sleep(2) + init = self.wait_for_element_by_css_selector('#init-output') + init = json.loads(init.text) + self.assertAlmostEqual(ts, init.get('ts'), delta=1000) + self.assertEqual('initialized', init.get('data')) + + def test_store_nested_data(self): + app = dash.Dash(__name__) + + nested = {'nested': {'nest': 'much'}} + nested_list = dict(my_list=[1, 2, 3]) + + app.layout = html.Div([ + dcc.Store(id='store', storage_type='local'), + html.Button('set object as key', id='obj-btn'), + html.Button('set list as key', id='list-btn'), + html.Output(id='output') + ]) + + @app.callback(Output('store', 'data'), + [Input('obj-btn', 'n_clicks_timestamp'), + Input('list-btn', 'n_clicks_timestamp')]) + def on_obj_click(obj_ts, list_ts): + if obj_ts is None and list_ts is None: + raise PreventUpdate + + # python 3 got the default props bug. plotly/dash#396 + if (obj_ts and not list_ts) or obj_ts > list_ts: + return nested + else: + return nested_list + + @app.callback(Output('output', 'children'), + [Input('store', 'modified_timestamp')], + [State('store', 'data')]) + def on_ts(ts, data): + if ts is None: + raise PreventUpdate + return json.dumps(data) + + self.startServer(app) + + obj_btn = self.wait_for_element_by_css_selector('#obj-btn') + list_btn = self.wait_for_element_by_css_selector('#list-btn') + + obj_btn.click() + time.sleep(1) + self.wait_for_text_to_equal('#output', json.dumps(nested)) + # it would of crashed the app before adding the recursive check. + + list_btn.click() + time.sleep(1) + self.wait_for_text_to_equal('#output', json.dumps(nested_list)) + + def test_user_supplied_css(self): + app = dash.Dash(__name__) + + app.layout = html.Div(className="test-input-css", children=[dcc.Input()]) + + self.startServer(app) + + self.wait_for_element_by_css_selector('.test-input-css') + self.snapshot('styled input - width: 100%, border-color: hotpink') + + def test_logout_btn(self): + app = dash.Dash(__name__) + + @app.server.route('/_logout', methods=['POST']) + def on_logout(): + rep = flask.redirect('/logged-out') + rep.set_cookie('logout-cookie', '', 0) + return rep + + app.layout = html.Div([ + html.H2('Logout test'), + dcc.Location(id='location'), + html.Div(id='content'), + ]) + + @app.callback(Output('content', 'children'), + [Input('location', 'pathname')]) + def on_location(location_path): + if location_path is None: + raise PreventUpdate + + if 'logged-out' in location_path: + return 'Logged out' + else: + + @flask.after_this_request + def _insert_cookie(rep): + rep.set_cookie('logout-cookie', 'logged-in') + return rep + + return dcc.LogoutButton(id='logout-btn', logout_url='/_logout') + + self.startServer(app) + time.sleep(1) + self.snapshot('Logout button') + + self.assertEqual( + 'logged-in', + self.driver.get_cookie('logout-cookie')['value']) + logout_button = self.wait_for_element_by_css_selector('#logout-btn') + logout_button.click() + self.wait_for_text_to_equal('#content', 'Logged out') + + self.assertFalse(self.driver.get_cookie('logout-cookie')) + + def test_state_and_inputs(self): + 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'), + inputs=[Input('input', 'value')], + state=[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') # noqa: E731 + input = lambda: self.driver.find_element_by_id('input') # noqa: E731 + state = lambda: self.driver.find_element_by_id('state') # noqa: E731 + + # callback gets called with initial input + wait_for(lambda: call_count.value == 1) + 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_simple_callback(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Input( + id='input', + ), + 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) + + input1 = self.wait_for_element_by_css_selector('#input') + input1.send_keys('hello world') + output1 = self.wait_for_element_by_css_selector('#output-1') + self.wait_for_text_to_equal('#output-1', 'hello world') + output1.click() # Lose focus, no callback sent for value. + + self.assertEqual( + call_count.value, + # an initial call to retrieve the first value + # plus one for each hello world character + 1 + len('hello world') + ) + + def test_store_type_updates(self): + app = dash.Dash(__name__) + + types = [ + ('str', 'hello'), + ('number', 1), + ('dict', {'data': [2, 3, None]}), + ('list', [5, 6, 7]), + ('null', None), + ('bool', True), + ('bool', False), + ('empty-dict', {}), + ] + types_changes = list( + itertools.chain(*itertools.combinations(types, 2)) + ) + [ # No combinations as it add much test time. + ('list-dict-1', [1, 2, {'data': [55, 66, 77], 'dummy': 'dum'}]), + ('list-dict-2', [1, 2, {'data': [111, 99, 88]}]), + ('dict-3', {'a': 1, 'c': 1}), + ('dict-2', {'a': 1, 'b': None}), + ] + + app.layout = html.Div([ + html.Div(id='output'), + html.Button('click', id='click'), + dcc.Store(id='store') + ]) + + @app.callback(Output('output', 'children'), + [Input('store', 'modified_timestamp')], + [State('store', 'data')]) + def on_data(ts, data): + if ts is None: + raise PreventUpdate + + return json.dumps(data) + + @app.callback(Output('store', 'data'), [Input('click', 'n_clicks')]) + def on_click(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return types_changes[n_clicks - 1][1] + + self.startServer(app) + + button = self.wait_for_element_by_css_selector('#click') + + for i, type_change in enumerate(types_changes): + button.click() + try: + self.wait_for_text_to_equal( + '#output', json.dumps(type_change[1]), + ) + except TimeoutException: + raise Exception( + 'Output type did not change from {} to {}'.format( + types_changes[i - 1], + type_change + ) + ) + def test_disabled_tab(self): app = dash.Dash(__name__) app.layout = html.Div([ From df5192d1f4f9fd3557d4c17100eff6153f63b8c4 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 22:11:09 -0400 Subject: [PATCH 5/8] changelog for markdown highlighting (and syntaxhighlighter removal) and dedent --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd20c8c9..f3c60659c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +- `Markdown` components support code highlighting - no need to switch to `SyntaxHighlighter`, which has been removed. Use triple backticks, with the opening backticks followed by the language name or abbreviation. [#562](https://github.com/plotly/dash-core-components/pull/562) Supported languages: + - Bash + - CSS + - HTTP + - JavaScript + - Python + - JSON + - Markdown + - HTML, XML + - R + - Ruby + - SQL + - Shell Session + - YAML +- Added a `dedent` prop to `Markdown` components, and enabled it by default - removing all matching leading whitespace from every line that has any non-whitespace content. You can disable this with `dedent=False`. [#569](https://github.com/plotly/dash-core-components/pull/569) - Ability to add tooltips to `Slider` and `RangeSlider`, which can be visible always or on hover. Tooltips also take a position argument. [#564](https://github.com/plotly/dash-core-components/pull/564) ### Fixed @@ -14,6 +29,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Changed `dcc.Checklist` prop `values` to `value`, to match all the other input components [#558](https://github.com/plotly/dash-core-components/pull/558). Also improved prop types for `Dropdown` and `RadioItems` `value` props to consistently accept both strings and numbers. +### Removed +- 💥 Removed the `SyntaxHighlighter` component. This is now built into `Markdown` [#562](https://github.com/plotly/dash-core-components/pull/562). +- Removed the `containerProps` prop in `Markdown` - after the refactor of [#562](https://github.com/plotly/dash-core-components/pull/562), its function is served by the `id`, `className`, and `style` props. [#569](https://github.com/plotly/dash-core-components/pull/569) + ## [0.48.0] - 2019-05-15 ### Added - `figure` prop in `dcc.Graph` now accepts a `frames` key From 6eeb96adb7fc5c91dfab5906f0aa5ec145c4d0d3 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 22:40:05 -0400 Subject: [PATCH 6/8] fix indentation in test_integration --- test/test_integration.py | 4664 +++++++++++++++++++------------------- 1 file changed, 2332 insertions(+), 2332 deletions(-) diff --git a/test/test_integration.py b/test/test_integration.py index 8d2eb38a9..81727dce9 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -71,2340 +71,2340 @@ def snapshot(self, name): print("Percy Snapshot {}".format(python_version)) self.percy_runner.snapshot(name=name) - def create_upload_component_content_types_test(self, filename): - app = dash.Dash(__name__) - - filepath = os.path.join(os.getcwd(), 'test', 'upload-assets', filename) - - pre_style = { - 'whiteSpace': 'pre-wrap', - 'wordBreak': 'break-all' - } - - app.layout = html.Div([ - html.Div(filepath, id='waitfor'), - html.Div( - id='upload-div', - children=dcc.Upload( - id='upload', - children=html.Div([ - 'Drag and Drop or ', - html.A('Select a File') - ]), - style={ - 'width': '100%', - 'height': '60px', - 'lineHeight': '60px', - 'borderWidth': '1px', - 'borderStyle': 'dashed', - 'borderRadius': '5px', - 'textAlign': 'center' - } - ) - ), - html.Div(id='output'), - html.Div(DataTable(data=[{}]), style={'display': 'none'}) - ]) - - @app.callback(Output('output', 'children'), - [Input('upload', 'contents')]) - def update_output(contents): - if contents is not None: - content_type, content_string = contents.split(',') - if 'csv' in filepath: - df = pd.read_csv(io.StringIO(base64.b64decode( - content_string).decode('utf-8'))) - return html.Div([ - DataTable( - data=df.to_dict('records'), - columns=[{'id': i} for i in ['city', 'country']]), - html.Hr(), - html.Div('Raw Content'), - html.Pre(contents, style=pre_style) - ]) - elif 'xls' in filepath: - df = pd.read_excel(io.BytesIO(base64.b64decode( - content_string))) - return html.Div([ - DataTable( - data=df.to_dict('records'), - columns=[{'id': i} for i in ['city', 'country']]), - html.Hr(), - html.Div('Raw Content'), - html.Pre(contents, style=pre_style) - ]) - elif 'image' in content_type: - return html.Div([ - html.Img(src=contents), - html.Hr(), - html.Div('Raw Content'), - html.Pre(contents, style=pre_style) - ]) - else: - return html.Div([ - html.Hr(), - html.Div('Raw Content'), - html.Pre(contents, style=pre_style) - ]) - - self.startServer(app) - - try: - self.wait_for_element_by_css_selector('#waitfor') - except Exception as e: - print(self.wait_for_element_by_css_selector( - '#_dash-app-content').get_attribute('innerHTML')) - raise e - - upload_div = self.wait_for_element_by_css_selector( - '#upload-div input[type=file]') - - upload_div.send_keys(filepath) - time.sleep(5) - self.snapshot(filename) - - def test_upload_csv(self): - self.create_upload_component_content_types_test('utf8.csv') - - def test_upload_xlsx(self): - self.create_upload_component_content_types_test('utf8.xlsx') - - def test_upload_png(self): - self.create_upload_component_content_types_test('dash-logo-stripe.png') - - def test_upload_svg(self): - self.create_upload_component_content_types_test('dash-logo-stripe.svg') - - def test_upload_gallery(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.Div(id='waitfor'), - html.Label('Empty'), - dcc.Upload(), - - html.Label('Button'), - dcc.Upload(html.Button('Upload File')), - - html.Label('Text'), - dcc.Upload('Upload File'), - - html.Label('Link'), - dcc.Upload(html.A('Upload File')), - - html.Label('Style'), - dcc.Upload([ - 'Drag and Drop or ', - html.A('Select a File') - ], style={ - 'width': '100%', - 'height': '60px', - 'lineHeight': '60px', - 'borderWidth': '1px', - 'borderStyle': 'dashed', - 'borderRadius': '5px', - 'textAlign': 'center' - }) - ]) - self.startServer(app) - - try: - self.wait_for_element_by_css_selector('#waitfor') - except Exception as e: - print(self.wait_for_element_by_css_selector( - '#_dash-app-content').get_attribute('innerHTML')) - raise e - - self.snapshot('test_upload_gallery') - - def test_loading_component_initialization(self): - lock = Lock() - - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.Loading([ - html.Div(id='div-1') - ], className='loading') - ], id='root') - - @app.callback( - Output('div-1', 'children'), - [Input('root', 'n_clicks')] - ) - def updateDiv(children): - with lock: - return 'content' - - with lock: - self.startServer(app) - self.wait_for_element_by_css_selector( - '.loading .dash-spinner' - ) - - self.wait_for_element_by_css_selector( - '.loading #div-1' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_loading_component_action(self): - lock = Lock() - - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.Loading([ - html.Div(id='div-1') - ], className='loading') - ], id='root') - - @app.callback( - Output('div-1', 'children'), - [Input('root', 'n_clicks')] - ) - def updateDiv(n_clicks): - if n_clicks is not None: - with lock: - return - - return 'content' - - with lock: - self.startServer(app) - self.wait_for_element_by_css_selector( - '.loading #div-1' - ) - - self.driver.find_element_by_id('root').click() - - self.wait_for_element_by_css_selector( - '.loading .dash-spinner' - ) - - self.wait_for_element_by_css_selector( - '.loading #div-1' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_multiple_loading_components(self): - lock = Lock() - - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.Loading([ - html.Button(id='btn-1') - ], className='loading-1'), - dcc.Loading([ - html.Button(id='btn-2') - ], className='loading-2') - ], id='root') - - @app.callback( - Output('btn-1', 'value'), - [Input('btn-2', 'n_clicks')] - ) - def updateDiv(n_clicks): - if n_clicks is not None: - with lock: - return - - return 'content' - - @app.callback( - Output('btn-2', 'value'), - [Input('btn-1', 'n_clicks')] - ) - def updateDiv(n_clicks): - if n_clicks is not None: - with lock: - return - - return 'content' - - self.startServer(app) - - self.wait_for_element_by_css_selector( - '.loading-1 #btn-1' - ) - self.wait_for_element_by_css_selector( - '.loading-2 #btn-2' - ) - - with lock: - self.driver.find_element_by_id('btn-1').click() - - self.wait_for_element_by_css_selector( - '.loading-2 .dash-spinner' - ) - self.wait_for_element_by_css_selector( - '.loading-1 #btn-1' - ) - - self.wait_for_element_by_css_selector( - '.loading-2 #btn-2' - ) - - with lock: - self.driver.find_element_by_id('btn-2').click() - - self.wait_for_element_by_css_selector( - '.loading-1 .dash-spinner' - ) - - self.wait_for_element_by_css_selector( - '.loading-1 #btn-1' - ) - self.wait_for_element_by_css_selector( - '.loading-2 #btn-2' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_nested_loading_components(self): - lock = Lock() - - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.Loading([ - html.Button(id='btn-1'), - dcc.Loading([ - html.Button(id='btn-2') - ], className='loading-2') - ], className='loading-1') - ], id='root') - - @app.callback( - Output('btn-1', 'value'), - [Input('btn-2', 'n_clicks')] - ) - def updateDiv(n_clicks): - if n_clicks is not None: - with lock: - return - - return 'content' - - @app.callback( - Output('btn-2', 'value'), - [Input('btn-1', 'n_clicks')] - ) - def updateDiv(n_clicks): - if n_clicks is not None: - with lock: - return - - return 'content' - - self.startServer(app) - - self.wait_for_element_by_css_selector( - '.loading-1 #btn-1' - ) - self.wait_for_element_by_css_selector( - '.loading-2 #btn-2' - ) - - with lock: - self.driver.find_element_by_id('btn-1').click() - - self.wait_for_element_by_css_selector( - '.loading-2 .dash-spinner' - ) - self.wait_for_element_by_css_selector( - '.loading-1 #btn-1' - ) - - self.wait_for_element_by_css_selector( - '.loading-2 #btn-2' - ) - - with lock: - self.driver.find_element_by_id('btn-2').click() - - self.wait_for_element_by_css_selector( - '.loading-1 .dash-spinner' - ) - - self.wait_for_element_by_css_selector( - '.loading-1 #btn-1' - ) - self.wait_for_element_by_css_selector( - '.loading-2 #btn-2' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_dynamic_loading_component(self): - lock = Lock() - - app = dash.Dash(__name__) - app.config['suppress_callback_exceptions'] = True - - app.layout = html.Div([ - html.Button(id='btn-1'), - html.Div(id='div-1') - ]) - - @app.callback( - Output('div-1', 'children'), - [Input('btn-1', 'n_clicks')] - ) - def updateDiv(n_clicks): - if n_clicks is None: - return - - with lock: - return html.Div([ - html.Button(id='btn-2'), - dcc.Loading([ - html.Button(id='btn-3') - ], className='loading-1') - ]) - - @app.callback( - Output('btn-3', 'content'), - [Input('btn-2', 'n_clicks')] - ) - def updateDynamic(n_clicks): - if n_clicks is None: - return - - with lock: - return 'content' - - self.startServer(app) - - self.wait_for_element_by_css_selector( - '#btn-1' - ) - self.wait_for_element_by_css_selector( - '#div-1' - ) - - self.driver.find_element_by_id('btn-1').click() - - self.wait_for_element_by_css_selector( - '#div-1 #btn-2' - ) - self.wait_for_element_by_css_selector( - '.loading-1 #btn-3' - ) - - with lock: - self.driver.find_element_by_id('btn-2').click() - - self.wait_for_element_by_css_selector( - '.loading-1 .dash-spinner' - ) - - self.wait_for_element_by_css_selector( - '#div-1 #btn-2' - ) - self.wait_for_element_by_css_selector( - '.loading-1 #btn-3' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_loading_slider(self): - lock = Lock() - - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='test-btn'), - html.Label(id='test-div', children=['Horizontal Slider']), - dcc.Slider( - id='horizontal-slider', - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=5, - ), - ]) - - @app.callback( - Output('horizontal-slider', 'value'), - [Input('test-btn', 'n_clicks')] - ) - def user_delayed_value(n_clicks): - with lock: - return 5 - - with lock: - self.startServer(app) - - self.wait_for_element_by_css_selector( - '#horizontal-slider[data-dash-is-loading="true"]' - ) - - self.wait_for_element_by_css_selector( - '#horizontal-slider:not([data-dash-is-loading="true"])' - ) - - with lock: - self.driver.find_element_by_id('test-btn').click() - - self.wait_for_element_by_css_selector( - '#horizontal-slider[data-dash-is-loading="true"]' - ) - - self.wait_for_element_by_css_selector( - '#horizontal-slider:not([data-dash-is-loading="true"])' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_horizontal_slider(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Label('Horizontal Slider'), - dcc.Slider( - id='horizontal-slider', - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=5, - ), - ]) - self.startServer(app) - - self.wait_for_element_by_css_selector('#horizontal-slider') - self.snapshot('horizontal slider') - - h_slider = self.driver.find_element_by_css_selector( - '#horizontal-slider div[role="slider"]' - ) - h_slider.click() - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_vertical_slider(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Label('Vertical Slider'), - dcc.Slider( - id='vertical-slider', - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=5, - vertical=True, - ), - ], style={'height': '500px'}) - self.startServer(app) - - self.wait_for_element_by_css_selector('#vertical-slider') - self.snapshot('vertical slider') - - v_slider = self.driver.find_element_by_css_selector( - '#vertical-slider div[role="slider"]' - ) - v_slider.click() - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_loading_range_slider(self): - lock = Lock() - - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='test-btn'), - html.Label(id='test-div', children=['Horizontal Range Slider']), - dcc.RangeSlider( - id='horizontal-range-slider', - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=[4, 6], - ), - ]) - - @app.callback( - Output('horizontal-range-slider', 'value'), - [Input('test-btn', 'n_clicks')] - ) - def delayed_value(children): - with lock: - return [4, 6] - - with lock: - self.startServer(app) - - self.wait_for_element_by_css_selector( - '#horizontal-range-slider[data-dash-is-loading="true"]' - ) - - self.wait_for_element_by_css_selector( - '#horizontal-range-slider:not([data-dash-is-loading="true"])' - ) - - with lock: - self.driver.find_element_by_id('test-btn').click() - - self.wait_for_element_by_css_selector( - '#horizontal-range-slider[data-dash-is-loading="true"]' - ) - - self.wait_for_element_by_css_selector( - '#horizontal-range-slider:not([data-dash-is-loading="true"])' - ) - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_horizontal_range_slider(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Label('Horizontal Range Slider'), - dcc.RangeSlider( - id='horizontal-range-slider', - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=[4, 6], - ), - ]) - self.startServer(app) - - self.wait_for_element_by_css_selector('#horizontal-range-slider') - self.snapshot('horizontal range slider') - - h_slider_1 = self.driver.find_element_by_css_selector( - '#horizontal-range-slider div.rc-slider-handle-1[role="slider"]' - ) - h_slider_1.click() - - h_slider_2 = self.driver.find_element_by_css_selector( - '#horizontal-range-slider div.rc-slider-handle-2[role="slider"]' - ) - h_slider_2.click() - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_vertical_range_slider(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Label('Vertical Range Slider'), - dcc.RangeSlider( - id='vertical-range-slider', - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=[4, 6], - vertical=True, - ), - ], style={'height': '500px'}) - self.startServer(app) - - self.wait_for_element_by_css_selector('#vertical-range-slider') - self.snapshot('vertical range slider') - - v_slider_1 = self.driver.find_element_by_css_selector( - '#vertical-range-slider div.rc-slider-handle-1[role="slider"]' - ) - v_slider_1.click() - - v_slider_2 = self.driver.find_element_by_css_selector( - '#vertical-range-slider div.rc-slider-handle-2[role="slider"]' - ) - v_slider_2.click() - - for entry in self.get_log(): - raise Exception('browser error logged during test', entry) - - def test_gallery(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Div(id='waitfor'), - html.Label('Upload'), - dcc.Upload(), - - html.Label('Horizontal Tabs'), - dcc.Tabs(id="tabs", children=[ - dcc.Tab(label='Tab one', className='test', style={'border': '1px solid magenta'}, children=[ - html.Div(['Test']) - ]), - dcc.Tab(label='Tab two', children=[ - html.Div([ - html.H1("This is the content in tab 2"), - html.P("A graph here would be nice!") - ]) - ], id='tab-one'), - dcc.Tab(label='Tab three', children=[ - html.Div([ - html.H1("This is the content in tab 3"), - ]) - ]), - ], - style={ - 'fontFamily': 'system-ui' - }, - content_style={ - 'border': '1px solid #d6d6d6', - 'padding': '44px' - }, - parent_style={ - 'maxWidth': '1000px', - 'margin': '0 auto' - } - ), - - html.Label('Vertical Tabs'), - dcc.Tabs(id="tabs1", vertical=True, children=[ - dcc.Tab(label='Tab one', children=[ - html.Div(['Test']) - ]), - dcc.Tab(label='Tab two', children=[ - html.Div([ - html.H1("This is the content in tab 2"), - html.P("A graph here would be nice!") - ]) - ]), - dcc.Tab(label='Tab three', children=[ - html.Div([ - html.H1("This is the content in tab 3"), - ]) - ]), - ] - ), - - html.Label('Dropdown'), - dcc.Dropdown( - options=[ - {'label': 'New York City', 'value': 'NYC'}, - {'label': u'Montréal', 'value': 'MTL'}, - {'label': 'San Francisco', 'value': 'SF'}, - {'label': u'北京', 'value': u'北京'} - ], - value='MTL', - id='dropdown' - ), - - html.Label('Multi-Select Dropdown'), - dcc.Dropdown( - options=[ - {'label': 'New York City', 'value': 'NYC'}, - {'label': u'Montréal', 'value': 'MTL'}, - {'label': 'San Francisco', 'value': 'SF'}, - {'label': u'北京', 'value': u'北京'} - ], - value=['MTL', 'SF'], - multi=True - ), - - html.Label('Radio Items'), - dcc.RadioItems( - options=[ - {'label': 'New York City', 'value': 'NYC'}, - {'label': u'Montréal', 'value': 'MTL'}, - {'label': 'San Francisco', 'value': 'SF'}, - {'label': u'北京', 'value': u'北京'} - ], - value='MTL' - ), - - html.Label('Checkboxes'), - dcc.Checklist( - options=[ - {'label': 'New York City', 'value': 'NYC'}, - {'label': u'Montréal', 'value': 'MTL'}, - {'label': 'San Francisco', 'value': 'SF'}, - {'label': u'北京', 'value': u'北京'} - ], - value=['MTL', 'SF'] - ), - - html.Label('Text Input'), - dcc.Input(value='', placeholder='type here', id='textinput'), - html.Label('Disabled Text Input'), - dcc.Input(value='disabled', type='text', - id='disabled-textinput', disabled=True), - - html.Label('Slider'), - dcc.Slider( - min=0, - max=9, - marks={i: 'Label {}'.format(i) if i == 1 else str(i) - for i in range(1, 6)}, - value=5, - ), - - html.Label('Graph'), - dcc.Graph( - id='graph', - figure={ - 'data': [{ - 'x': [1, 2, 3], - 'y': [4, 1, 4] - }], - 'layout': { - 'title': u'北京' - } - } - ), - - html.Div([ - html.Label('DatePickerSingle'), - dcc.DatePickerSingle( - id='date-picker-single', - date=datetime(1997, 5, 10) - ), - html.Div([ - html.Label('DatePickerSingle - empty input'), - dcc.DatePickerSingle(), - ], id='dt-single-no-date-value' - ), - html.Div([ - html.Label('DatePickerSingle - initial visible month (May 97)'), - dcc.DatePickerSingle( - initial_visible_month=datetime(1997, 5, 10) - ), - ], id='dt-single-no-date-value-init-month' - ), - ]), - - html.Div([ - html.Label('DatePickerRange'), - dcc.DatePickerRange( - id='date-picker-range', - start_date_id='startDate', - end_date_id='endDate', - start_date=datetime(1997, 5, 3), - end_date_placeholder_text='Select a date!' - ), - html.Div([ - html.Label('DatePickerRange - empty input'), - dcc.DatePickerRange( - start_date_id='startDate', - end_date_id='endDate', - start_date_placeholder_text='Start date', - end_date_placeholder_text='End date' - ), - ], id='dt-range-no-date-values' - ), - html.Div([ - html.Label('DatePickerRange - initial visible month (May 97)'), - dcc.DatePickerRange( - start_date_id='startDate', - end_date_id='endDate', - start_date_placeholder_text='Start date', - end_date_placeholder_text='End date', - initial_visible_month=datetime(1997, 5, 10) - ), - ], id='dt-range-no-date-values-init-month' - ), - ]), - - html.Label('TextArea'), - dcc.Textarea( - placeholder='Enter a value... 北京', - style={'width': '100%'} - ), - - html.Label('Markdown'), - dcc.Markdown(''' - #### Dash and Markdown - - Dash supports [Markdown](https://rexxars.github.io/react-markdown/). - - Markdown is a simple way to write and format text. - It includes a syntax for things like **bold text** and *italics*, - [links](https://rexxars.github.io/react-markdown/), inline `code` snippets, lists, - quotes, and more. - - 1. Links are auto-rendered: https://dash.plot.ly. - 2. This uses ~commonmark~ GitHub flavored markdown. - - Tables are also supported: - - | First Header | Second Header | - | ------------- | ------------- | - | Content Cell | Content Cell | - | Content Cell | Content Cell | - - 北京 - '''.replace(' ', '')), - dcc.Markdown(['# Line one', '## Line two']), - dcc.Markdown(), - dcc.SyntaxHighlighter(dedent('''import python - print(3)'''), language='python'), - dcc.SyntaxHighlighter([ - 'import python', - 'print(3)' - ], language='python'), - dcc.SyntaxHighlighter() - ]) - self.startServer(app) - - self.wait_for_element_by_css_selector('#waitfor') - - self.snapshot('gallery') - - self.driver.find_element_by_css_selector( - '#dropdown .Select-input input' - ).send_keys(u'北') - self.snapshot('gallery - chinese character') - - text_input = self.driver.find_element_by_id('textinput') - # verify that type has the right default - # Can't use text_input.get_attribute('type') - that pulls the - # default value even if none is specified. - get_type = ('return document.getElementById("textinput")' - '.getAttribute("type");') - self.assertEqual(self.driver.execute_script(get_type), 'text') - disabled_text_input = self.driver.find_element_by_id( - 'disabled-textinput') - text_input.send_keys('HODOR') - - # It seems selenium errors when send(ing)_keys on a disabled element. - # In case this changes we try anyway and catch the particular - # exception. In any case Percy will snapshot the disabled input style - # so we are not totally dependent on the send_keys behaviour for - # testing disabled state. - try: - disabled_text_input.send_keys('RODOH') - except InvalidElementStateException: - pass - - self.snapshot('gallery - text input') - - # DatePickerSingle and DatePickerRange test - # for issue with datepicker when date value is `None` - dt_input_1 = self.driver.find_element_by_css_selector( - '#dt-single-no-date-value #date' - ) - dt_input_1.click() - self.snapshot('gallery - DatePickerSingle\'s datepicker ' - 'when no date value and no initial month specified') - dt_length = len(dt_input_1.get_attribute('value')) - dt_input_1.send_keys(dt_length * Keys.BACKSPACE) - dt_input_1.send_keys("1997-05-03") - - dt_input_2 = self.driver.find_element_by_css_selector( - '#dt-single-no-date-value-init-month #date' - ) - self.driver.find_element_by_css_selector( - 'label' - ).click() - dt_input_2.click() - self.snapshot('gallery - DatePickerSingle\'s datepicker ' - 'when no date value, but initial month is specified') - dt_length = len(dt_input_2.get_attribute('value')) - dt_input_2.send_keys(dt_length * Keys.BACKSPACE) - dt_input_2.send_keys("1997-05-03") - - dt_input_3 = self.driver.find_element_by_css_selector( - '#dt-range-no-date-values #endDate' - ) - self.driver.find_element_by_css_selector( - 'label' - ).click() - dt_input_3.click() - self.snapshot('gallery - DatePickerRange\'s datepicker ' - 'when neither start date nor end date ' - 'nor initial month is specified') - dt_length = len(dt_input_3.get_attribute('value')) - dt_input_3.send_keys(dt_length * Keys.BACKSPACE) - dt_input_3.send_keys("1997-05-03") - - dt_input_4 = self.driver.find_element_by_css_selector( - '#dt-range-no-date-values-init-month #endDate' - ) - self.driver.find_element_by_css_selector( - 'label' - ).click() - dt_input_4.click() - self.snapshot('gallery - DatePickerRange\'s datepicker ' - 'when neither start date nor end date is specified, ' - 'but initial month is') - dt_length = len(dt_input_4.get_attribute('value')) - dt_input_4.send_keys(dt_length * Keys.BACKSPACE) - dt_input_4.send_keys("1997-05-03") - - def test_tabs_in_vertical_mode(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.Tabs(id="tabs", value='tab-3', children=[ - dcc.Tab(label='Tab one', value='tab-1', id='tab-1', children=[ - html.Div('Tab One Content') - ]), - dcc.Tab(label='Tab two', value='tab-2', id='tab-2', children=[ - html.Div('Tab Two Content') - ]), - dcc.Tab(label='Tab three', value='tab-3', id='tab-3', children=[ - html.Div('Tab Three Content') - ]), - ], vertical=True), - html.Div(id='tabs-content') - ]) - - self.startServer(app=app) - self.wait_for_text_to_equal('#tab-3', 'Tab three') - - self.snapshot('Tabs - vertical mode') - - def test_tabs_without_children(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.H1('Dash Tabs component demo'), - dcc.Tabs(id="tabs", value='tab-2', children=[ - dcc.Tab(label='Tab one', value='tab-1', id='tab-1'), - dcc.Tab(label='Tab two', value='tab-2', id='tab-2'), - ]), - html.Div(id='tabs-content') - ]) - - @app.callback(dash.dependencies.Output('tabs-content', 'children'), - [dash.dependencies.Input('tabs', 'value')]) - def render_content(tab): - if tab == 'tab-1': - return html.Div([ - html.H3('Test content 1') - ], id='test-tab-1') - elif tab == 'tab-2': - return html.Div([ - html.H3('Test content 2') - ], id='test-tab-2') - - self.startServer(app=app) - - self.wait_for_text_to_equal('#tabs-content', 'Test content 2') - self.snapshot('initial tab - tab 2') - - selected_tab = self.wait_for_element_by_css_selector('#tab-1') - selected_tab.click() - time.sleep(1) - self.wait_for_text_to_equal('#tabs-content', 'Test content 1') - - def test_tabs_with_children_undefined(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.H1('Dash Tabs component demo'), - dcc.Tabs(id="tabs", value='tab-1'), - html.Div(id='tabs-content') - ]) - - self.startServer(app=app) - - self.wait_for_element_by_css_selector('#tabs-content') - - self.snapshot('Tabs component with children undefined') - - def test_tabs_render_without_selected(self): - app = dash.Dash(__name__) - - data = [ - {'id': 'one', 'value': 1}, - {'id': 'two', 'value': 2}, - ] - - menu = html.Div([ - html.Div('one', id='one'), - html.Div('two', id='two') - ]) - - tabs_one = html.Div([ - dcc.Tabs([ - dcc.Tab(dcc.Graph(id='graph-one'), label='tab-one-one'), - ]) - ], id='tabs-one', style={'display': 'none'}) - - tabs_two = html.Div([ - dcc.Tabs([ - dcc.Tab(dcc.Graph(id='graph-two'), label='tab-two-one'), - ]) - ], id='tabs-two', style={'display': 'none'}) - - app.layout = html.Div([ - menu, - tabs_one, - tabs_two - ]) - - for i in ('one', 'two'): - - @app.callback(Output('tabs-{}'.format(i), 'style'), - [Input(i, 'n_clicks')]) - def on_click(n_clicks): - if n_clicks is None: - raise PreventUpdate - - if n_clicks % 2 == 1: - return {'display': 'block'} - return {'display': 'none'} - - @app.callback(Output('graph-{}'.format(i), 'figure'), - [Input(i, 'n_clicks')]) - def on_click(n_clicks): - if n_clicks is None: - raise PreventUpdate - - return { - 'data': [ - { - 'x': [1, 2, 3, 4], - 'y': [4, 3, 2, 1] - } - ], - 'layout': { - 'width': 700, - 'height': 450 - } - } - - self.startServer(app=app) - - button_one = self.wait_for_element_by_css_selector('#one') - button_two = self.wait_for_element_by_css_selector('#two') - - button_one.click() - - # wait for tabs to be loaded after clicking - WebDriverWait(self.driver, 10).until( - EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-one .main-svg")) - ) - - time.sleep(1) - self.snapshot("Tabs 1 rendered ") - - button_two.click() - - # wait for tabs to be loaded after clicking - WebDriverWait(self.driver, 10).until( - EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-two .main-svg")) - ) - - time.sleep(1) - self.snapshot("Tabs 2 rendered ") - - # do some extra tests while we're here - # and have access to Graph and plotly.js - self.check_graph_config_shape() - self.check_plotlyjs() - - def check_plotlyjs(self): - # find plotly.js files in the dist folder, check that there's only one - all_dist = os.listdir(dcc.__path__[0]) - js_re = r'^plotly-(.*)\.min\.js$' - plotlyjs_dist = [fn for fn in all_dist if re.match(js_re, fn)] - - self.assertEqual(len(plotlyjs_dist), 1) - - # check that the version matches what we see in the page - page_version = self.driver.execute_script('return Plotly.version;') - dist_version = re.match(js_re, plotlyjs_dist[0]).groups()[0] - self.assertEqual(page_version, dist_version) - - def check_graph_config_shape(self): - config_schema = self.driver.execute_script( - 'return Plotly.PlotSchema.get().config;' - ) - with open(os.path.join(dcc.__path__[0], 'metadata.json')) as meta: - graph_meta = json.load(meta)['src/components/Graph.react.js'] - config_prop_shape = graph_meta['props']['config']['type']['value'] - - ignored_config = [ - 'setBackground', - 'showSources', - 'logging', - 'globalTransforms', - 'role' - ] - - def crawl(schema, props): - for prop_name in props: - self.assertIn(prop_name, schema) - - for item_name, item in schema.items(): - if item_name in ignored_config: - continue - - self.assertIn(item_name, props) - if 'valType' not in item: - crawl(item, props[item_name]['value']) - - crawl(config_schema, config_prop_shape) - - def test_tabs_without_value(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.H1('Dash Tabs component demo'), - dcc.Tabs(id="tabs-without-value", children=[ - dcc.Tab(label='Tab One', value='tab-1'), - dcc.Tab(label='Tab Two', value='tab-2'), - ]), - html.Div(id='tabs-content') - ]) - - @app.callback(Output('tabs-content', 'children'), - [Input('tabs-without-value', 'value')]) - def render_content(tab): - if tab == 'tab-1': - return html.H3('Default selected Tab content 1') - elif tab == 'tab-2': - return html.H3('Tab content 2') - - self.startServer(app=app) - - self.wait_for_text_to_equal('#tabs-content', 'Default selected Tab content 1') - - self.snapshot('Tab 1 should be selected by default') - - def test_graph_does_not_resize_in_tabs(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.H1('Dash Tabs component demo'), - dcc.Tabs(id="tabs-example", value='tab-1-example', children=[ - dcc.Tab(label='Tab One', value='tab-1-example', id='tab-1'), - dcc.Tab(label='Tab Two', value='tab-2-example', id='tab-2'), - ]), - html.Div(id='tabs-content-example') - ]) - - @app.callback(Output('tabs-content-example', 'children'), - [Input('tabs-example', 'value')]) - def render_content(tab): - if tab == 'tab-1-example': - return html.Div([ - html.H3('Tab content 1'), - dcc.Graph( - id='graph-1-tabs', - figure={ - 'data': [{ - 'x': [1, 2, 3], - 'y': [3, 1, 2], - 'type': 'bar' - }] - } - ) - ]) - elif tab == 'tab-2-example': - return html.Div([ - html.H3('Tab content 2'), - dcc.Graph( - id='graph-2-tabs', - figure={ - 'data': [{ - 'x': [1, 2, 3], - 'y': [5, 10, 6], - 'type': 'bar' - }] - } - ) - ]) - self.startServer(app=app) - - tab_one = self.wait_for_element_by_css_selector('#tab-1') - tab_two = self.wait_for_element_by_css_selector('#tab-2') - - WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable((By.ID, "tab-2")) - ) - - self.snapshot("Tabs with Graph - initial (graph should not resize)") - tab_two.click() - - # wait for Graph's internal svg to be loaded after clicking - WebDriverWait(self.driver, 10).until( - EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-2-tabs .main-svg")) - ) - - self.snapshot("Tabs with Graph - clicked tab 2 (graph should not resize)") - - WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable((By.ID, "tab-1")) - ) - - tab_one.click() - - # wait for Graph to be loaded after clicking - WebDriverWait(self.driver, 10).until( - EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-1-tabs .main-svg")) - ) - - self.snapshot("Tabs with Graph - clicked tab 1 (graph should not resize)") - - def test_location_link(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Div(id='waitfor'), - dcc.Location(id='test-location', refresh=False), - - dcc.Link( - html.Button('I am a clickable button'), - id='test-link', - href='/test/pathname'), - dcc.Link( - html.Button('I am a clickable hash button'), - id='test-link-hash', - href='#test'), - dcc.Link( - html.Button('I am a clickable search button'), - id='test-link-search', - href='?testQuery=testValue', - refresh=False), - html.Button('I am a magic button that updates pathname', - id='test-button'), - html.A('link to click', href='/test/pathname/a', id='test-a'), - html.A('link to click', href='#test-hash', id='test-a-hash'), - html.A('link to click', href='?queryA=valueA', id='test-a-query'), - html.Div(id='test-pathname', children=[]), - html.Div(id='test-hash', children=[]), - html.Div(id='test-search', children=[]), - ]) - - @app.callback( - output=Output(component_id='test-pathname', - component_property='children'), - inputs=[Input(component_id='test-location', component_property='pathname')]) - def update_location_on_page(pathname): - return pathname - - @app.callback( - output=Output(component_id='test-hash', - component_property='children'), - inputs=[Input(component_id='test-location', component_property='hash')]) - def update_location_on_page(hash_val): - if hash_val is None: - return '' - - return hash_val - - @app.callback( - output=Output(component_id='test-search', - component_property='children'), - inputs=[Input(component_id='test-location', component_property='search')]) - def update_location_on_page(search): - if search is None: - return '' - - return search - - @app.callback( - output=Output(component_id='test-location', - component_property='pathname'), - inputs=[Input(component_id='test-button', - component_property='n_clicks')], - state=[State(component_id='test-location', component_property='pathname')]) - def update_pathname(n_clicks, current_pathname): - if n_clicks is not None: - return '/new/pathname' - - return current_pathname - - self.startServer(app=app) - - time.sleep(1) - self.snapshot('link -- location') - - # Check that link updates pathname - self.wait_for_element_by_css_selector('#test-link').click() - self.assertEqual( - self.driver.current_url.replace('http://localhost:8050', ''), - '/test/pathname') - self.wait_for_text_to_equal('#test-pathname', '/test/pathname') - - # Check that hash is updated in the Location - self.wait_for_element_by_css_selector('#test-link-hash').click() - self.wait_for_text_to_equal('#test-pathname', '/test/pathname') - self.wait_for_text_to_equal('#test-hash', '#test') - self.snapshot('link -- /test/pathname#test') - - # Check that search is updated in the Location -- note that this goes through href and therefore wipes the hash - self.wait_for_element_by_css_selector('#test-link-search').click() - self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') - self.wait_for_text_to_equal('#test-hash', '') - self.snapshot('link -- /test/pathname?testQuery=testValue') - - # Check that pathname is updated through a Button click via props - self.wait_for_element_by_css_selector('#test-button').click() - self.wait_for_text_to_equal('#test-pathname', '/new/pathname') - self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') - self.snapshot('link -- /new/pathname?testQuery=testValue') - - # Check that pathname is updated through an a tag click via props - self.wait_for_element_by_css_selector('#test-a').click() - try: - self.wait_for_element_by_css_selector('#waitfor') - except Exception as e: - print(self.wait_for_element_by_css_selector( - '#_dash-app-content').get_attribute('innerHTML')) - raise e - - self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') - self.wait_for_text_to_equal('#test-search', '') - self.wait_for_text_to_equal('#test-hash', '') - self.snapshot('link -- /test/pathname/a') - - # Check that hash is updated through an a tag click via props - self.wait_for_element_by_css_selector('#test-a-hash').click() - self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') - self.wait_for_text_to_equal('#test-search', '') - self.wait_for_text_to_equal('#test-hash', '#test-hash') - self.snapshot('link -- /test/pathname/a#test-hash') - - # Check that hash is updated through an a tag click via props - self.wait_for_element_by_css_selector('#test-a-query').click() - self.wait_for_element_by_css_selector('#waitfor') - self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') - self.wait_for_text_to_equal('#test-search', '?queryA=valueA') - self.wait_for_text_to_equal('#test-hash', '') - self.snapshot('link -- /test/pathname/a?queryA=valueA') - - def test_link_scroll(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Location(id='test-url', refresh=False), - - html.Div(id='push-to-bottom', children=[], style={ - 'display': 'block', - 'height': '200vh' - }), - html.Div(id='page-content'), - dcc.Link('Test link', href='/test-link', id='test-link') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('page-content', 'children'), - [Input('test-url', 'pathname')]) - def display_page(pathname): - call_count.value = call_count.value + 1 - return 'You are on page {}'.format(pathname) - - self.startServer(app=app) - - time.sleep(2) - - # callback is called twice when defined - self.assertEqual( - call_count.value, - 2 - ) - - # test if link correctly scrolls back to top of page - test_link = self.wait_for_element_by_css_selector('#test-link') - test_link.send_keys(Keys.NULL) - test_link.click() - time.sleep(2) - - # test link still fires update on Location - page_content = self.wait_for_element_by_css_selector('#page-content') - self.assertNotEqual(page_content.text, 'You are on page /') - - self.wait_for_text_to_equal( - '#page-content', 'You are on page /test-link') - - # test if rendered Link's tag has a href attribute - link_href = test_link.get_attribute("href") - self.assertEqual(link_href, 'http://localhost:8050/test-link') - - # test if callback is only fired once (offset of 2) - self.assertEqual( - call_count.value, - 3 - ) - - def test_candlestick(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.Button( - id='button', - children='Update Candlestick', - n_clicks=0 - ), - dcc.Graph(id='graph') - ]) - - @app.callback(Output('graph', 'figure'), [Input('button', 'n_clicks')]) - def update_graph(n_clicks): - return { - 'data': [{ - 'open': [1] * 5, - 'high': [3] * 5, - 'low': [0] * 5, - 'close': [2] * 5, - 'x': [n_clicks] * 5, - 'type': 'candlestick' - }] - } - self.startServer(app=app) - - button = self.wait_for_element_by_css_selector('#button') - self.snapshot('candlestick - initial') - button.click() - time.sleep(1) - self.snapshot('candlestick - 1 click') - - button.click() - time.sleep(1) - self.snapshot('candlestick - 2 click') - - def test_graphs_with_different_figures(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Graph( - id='example-graph', - figure={ - 'data': [ - {'x': [1, 2, 3], 'y': [4, 1, 2], - 'type': 'bar', 'name': 'SF'}, - {'x': [1, 2, 3], 'y': [2, 4, 5], - 'type': 'bar', 'name': u'Montréal'}, - ], - 'layout': { - 'title': 'Dash Data Visualization' - } - } - ), - dcc.Graph( - id='example-graph-2', - figure={ - 'data': [ - {'x': [20, 24, 33], 'y': [5, 2, 3], - 'type': 'bar', 'name': 'SF'}, - {'x': [11, 22, 33], 'y': [22, 44, 55], - 'type': 'bar', 'name': u'Montréal'}, - ], - 'layout': { - 'title': 'Dash Data Visualization' - } - } - ), - html.Div(id='restyle-data'), - html.Div(id='relayout-data') - ]) - - @app.callback(Output('restyle-data', 'children'), [Input('example-graph', 'restyleData')]) - def show_restyle_data(data): - if data is None: # ignore initial - return '' - return json.dumps(data) - - @app.callback(Output('relayout-data', 'children'), [Input('example-graph', 'relayoutData')]) - def show_relayout_data(data): - if data is None or 'autosize' in data: # ignore initial & auto width - return '' - return json.dumps(data) - - self.startServer(app=app) - - # use this opportunity to test restyleData, since there are multiple - # traces on this graph - legendToggle = self.driver.find_element_by_css_selector('#example-graph .traces:first-child .legendtoggle') - legendToggle.click() - self.wait_for_text_to_equal('#restyle-data', '[{"visible": ["legendonly"]}, [0]]') - - # move snapshot after click, so it's more stable with the wait - self.snapshot('2 graphs with different figures') - - # and test relayoutData while we're at it - autoscale = self.driver.find_element_by_css_selector('#example-graph .ewdrag') - autoscale.click() - autoscale.click() - self.wait_for_text_to_equal('#relayout-data', '{"xaxis.autorange": true}') - - def test_graphs_without_ids(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Graph(className='graph-no-id-1'), - dcc.Graph(className='graph-no-id-2'), - ]) - - self.startServer(app=app) - - graph_1 = self.wait_for_element_by_css_selector('.graph-no-id-1') - graph_2 = self.wait_for_element_by_css_selector('.graph-no-id-2') - - self.assertNotEqual(graph_1.get_attribute('id'), graph_2.get_attribute('id')) - - def test_datepickerrange_updatemodes(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.DatePickerRange( - id='date-picker-range', - start_date_id='startDate', - end_date_id='endDate', - start_date_placeholder_text='Select a start date!', - end_date_placeholder_text='Select an end date!', - updatemode='bothdates' - ), - html.Div(id='date-picker-range-output') - ]) - - @app.callback( - dash.dependencies.Output('date-picker-range-output', 'children'), - [dash.dependencies.Input('date-picker-range', 'start_date'), - dash.dependencies.Input('date-picker-range', 'end_date')]) - def update_output(start_date, end_date): - return '{} - {}'.format(start_date, end_date) - - self.startServer(app=app) - - start_date = self.wait_for_element_by_css_selector('#startDate') - start_date.click() - - end_date = self.wait_for_element_by_css_selector('#endDate') - end_date.click() - - self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') - - # using mouse click with fixed day range, this can be improved - # once we start refactoring the test structure - start_date.click() - - sday = self.driver.find_element_by_xpath("//td[text()='1' and @tabindex='0']") - sday.click() - self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') - - eday = self.driver.find_elements_by_xpath("//td[text()='28']")[1] - eday.click() - - date_tokens = set(start_date.get_attribute('value').split('/')) - date_tokens.update(end_date.get_attribute('value').split('/')) - - self.assertEqual( - set(itertools.chain(*[ - _.split('-') - for _ in self.driver.find_element_by_css_selector( - '#date-picker-range-output').text.split(' - ')])), - date_tokens, - "date should match the callback output") - - def test_interval(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - html.Div(id='output'), - dcc.Interval(id='interval', interval=1, max_intervals=2) - ]) - - @app.callback(Output('output', 'children'), - [Input('interval', 'n_intervals')]) - def update_text(n): - return "{}".format(n) - - self.startServer(app=app) - - # wait for interval to finish - time.sleep(5) - - self.wait_for_text_to_equal('#output', '2') - - def test_if_interval_can_be_restarted(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Interval( - id='interval', - interval=100, - n_intervals=0, - max_intervals=-1 - ), - - html.Button('Start', id='start', n_clicks_timestamp=-1), - html.Button('Stop', id='stop', n_clicks_timestamp=-1), - - html.Div(id='output') - - ]) - - @app.callback( - Output('interval', 'max_intervals'), - [Input('start', 'n_clicks_timestamp'), - Input('stop', 'n_clicks_timestamp')]) - def start_stop(start, stop): - if start < stop: - return 0 - else: - return -1 - - @app.callback(Output('output', 'children'), [Input('interval', 'n_intervals')]) - def display_data(n_intervals): - return 'Updated {}'.format(n_intervals) - - self.startServer(app=app) - - start_button = self.wait_for_element_by_css_selector('#start') - stop_button = self.wait_for_element_by_css_selector('#stop') - - # interval will start itself, we wait a second before pressing 'stop' - time.sleep(1) - - # get the output after running it for a bit - output = self.wait_for_element_by_css_selector('#output') - stop_button.click() - - time.sleep(1) - - # get the output after it's stopped, it shouldn't be higher than before - output_stopped = self.wait_for_element_by_css_selector('#output') - - self.wait_for_text_to_equal("#output", output_stopped.text) - - # This test logic is bad - # same element check for same text will always be true. - self.assertEqual(output.text, output_stopped.text) - - def _test_confirm(self, app, test_name, add_callback=True): - count = Value('i', 0) - - if add_callback: - @app.callback(Output('confirmed', 'children'), - [Input('confirm', 'submit_n_clicks'), - Input('confirm', 'cancel_n_clicks')], - [State('confirm', 'submit_n_clicks_timestamp'), - State('confirm', 'cancel_n_clicks_timestamp')]) - def _on_confirmed(submit_n_clicks, cancel_n_clicks, - submit_timestamp, cancel_timestamp): - if not submit_n_clicks and not cancel_n_clicks: - return '' - count.value += 1 - if (submit_timestamp and cancel_timestamp is None) or\ - (submit_timestamp > cancel_timestamp): - return 'confirmed' - else: - return 'canceled' - - self.startServer(app) - button = self.wait_for_element_by_css_selector('#button') - self.snapshot(test_name + ' -> initial') - - button.click() - time.sleep(1) - self.driver.switch_to.alert.accept() - - if add_callback: - self.wait_for_text_to_equal('#confirmed', 'confirmed') - self.snapshot(test_name + ' -> confirmed') - - button.click() - time.sleep(0.5) - self.driver.switch_to.alert.dismiss() - time.sleep(0.5) - - if add_callback: - self.wait_for_text_to_equal('#confirmed', 'canceled') - self.snapshot(test_name + ' -> canceled') - - if add_callback: - self.assertEqual(2, count.value, - 'Expected 2 callback but got ' + str(count.value)) - - def test_confirm(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='button', children='Send confirm', n_clicks=0), - dcc.ConfirmDialog(id='confirm', message='Please confirm.'), - html.Div(id='confirmed') - ]) - - @app.callback(Output('confirm', 'displayed'), - [Input('button', 'n_clicks')]) - def on_click_confirm(n_clicks): - if n_clicks: - return True - - self._test_confirm(app, 'ConfirmDialog') - - def test_confirm_dialog_provider(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - dcc.ConfirmDialogProvider( - html.Button('click me', id='button'), - id='confirm', message='Please confirm.'), - html.Div(id='confirmed') - ]) - - self._test_confirm(app, 'ConfirmDialogProvider') - - def test_confirm_without_callback(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.ConfirmDialogProvider( - html.Button('click me', id='button'), - id='confirm', message='Please confirm.'), - html.Div(id='confirmed') - ]) - self._test_confirm(app, 'ConfirmDialogProviderWithoutCallback', - add_callback=False) - - def test_confirm_as_children(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='button', children='Send confirm'), - html.Div(id='confirm-container'), - dcc.Location(id='dummy-location') - ]) - - @app.callback(Output('confirm-container', 'children'), - [Input('button', 'n_clicks')]) - def on_click(n_clicks): - if n_clicks: - return dcc.ConfirmDialog( - displayed=True, - id='confirm', - message='Please confirm.') - - self.startServer(app) - - button = self.wait_for_element_by_css_selector('#button') - - button.click() - time.sleep(2) - - self.driver.switch_to.alert.accept() - - def test_empty_graph(self): - app = dash.Dash(__name__) - - app.layout = html.Div([ - html.Button(id='click', children='Click me'), - dcc.Graph( - id='graph', - figure={ - 'data': [dict(x=[1, 2, 3], y=[1, 2, 3], type='scatter')] - } - ) - ]) - - @app.callback(dash.dependencies.Output('graph', 'figure'), - [dash.dependencies.Input('click', 'n_clicks')], - [dash.dependencies.State('graph', 'figure')]) - def render_content(click, prev_graph): - if click: - return {} - return prev_graph - - self.startServer(app) - button = self.wait_for_element_by_css_selector('#click') - button.click() - time.sleep(2) # Wait for graph to re-render - self.snapshot('render-empty-graph') - - def test_graph_extend_trace(self): - app = dash.Dash(__name__) - - def generate_with_id(id, data=None): - if data is None: - data = [{'x': [0, 1, 2, 3, 4], - 'y': [0, .5, 1, .5, 0] + def create_upload_component_content_types_test(self, filename): + app = dash.Dash(__name__) + + filepath = os.path.join(os.getcwd(), 'test', 'upload-assets', filename) + + pre_style = { + 'whiteSpace': 'pre-wrap', + 'wordBreak': 'break-all' + } + + app.layout = html.Div([ + html.Div(filepath, id='waitfor'), + html.Div( + id='upload-div', + children=dcc.Upload( + id='upload', + children=html.Div([ + 'Drag and Drop or ', + html.A('Select a File') + ]), + style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center' + } + ) + ), + html.Div(id='output'), + html.Div(DataTable(data=[{}]), style={'display': 'none'}) + ]) + + @app.callback(Output('output', 'children'), + [Input('upload', 'contents')]) + def update_output(contents): + if contents is not None: + content_type, content_string = contents.split(',') + if 'csv' in filepath: + df = pd.read_csv(io.StringIO(base64.b64decode( + content_string).decode('utf-8'))) + return html.Div([ + DataTable( + data=df.to_dict('records'), + columns=[{'id': i} for i in ['city', 'country']]), + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + elif 'xls' in filepath: + df = pd.read_excel(io.BytesIO(base64.b64decode( + content_string))) + return html.Div([ + DataTable( + data=df.to_dict('records'), + columns=[{'id': i} for i in ['city', 'country']]), + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + elif 'image' in content_type: + return html.Div([ + html.Img(src=contents), + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + else: + return html.Div([ + html.Hr(), + html.Div('Raw Content'), + html.Pre(contents, style=pre_style) + ]) + + self.startServer(app) + + try: + self.wait_for_element_by_css_selector('#waitfor') + except Exception as e: + print(self.wait_for_element_by_css_selector( + '#_dash-app-content').get_attribute('innerHTML')) + raise e + + upload_div = self.wait_for_element_by_css_selector( + '#upload-div input[type=file]') + + upload_div.send_keys(filepath) + time.sleep(5) + self.snapshot(filename) + + def test_upload_csv(self): + self.create_upload_component_content_types_test('utf8.csv') + + def test_upload_xlsx(self): + self.create_upload_component_content_types_test('utf8.xlsx') + + def test_upload_png(self): + self.create_upload_component_content_types_test('dash-logo-stripe.png') + + def test_upload_svg(self): + self.create_upload_component_content_types_test('dash-logo-stripe.svg') + + def test_upload_gallery(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Div(id='waitfor'), + html.Label('Empty'), + dcc.Upload(), + + html.Label('Button'), + dcc.Upload(html.Button('Upload File')), + + html.Label('Text'), + dcc.Upload('Upload File'), + + html.Label('Link'), + dcc.Upload(html.A('Upload File')), + + html.Label('Style'), + dcc.Upload([ + 'Drag and Drop or ', + html.A('Select a File') + ], style={ + 'width': '100%', + 'height': '60px', + 'lineHeight': '60px', + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center' + }) + ]) + self.startServer(app) + + try: + self.wait_for_element_by_css_selector('#waitfor') + except Exception as e: + print(self.wait_for_element_by_css_selector( + '#_dash-app-content').get_attribute('innerHTML')) + raise e + + self.snapshot('test_upload_gallery') + + def test_loading_component_initialization(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Div(id='div-1') + ], className='loading') + ], id='root') + + @app.callback( + Output('div-1', 'children'), + [Input('root', 'n_clicks')] + ) + def updateDiv(children): + with lock: + return 'content' + + with lock: + self.startServer(app) + self.wait_for_element_by_css_selector( + '.loading .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_component_action(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Div(id='div-1') + ], className='loading') + ], id='root') + + @app.callback( + Output('div-1', 'children'), + [Input('root', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + with lock: + self.startServer(app) + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + self.driver.find_element_by_id('root').click() + + self.wait_for_element_by_css_selector( + '.loading .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading #div-1' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_multiple_loading_components(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Button(id='btn-1') + ], className='loading-1'), + dcc.Loading([ + html.Button(id='btn-2') + ], className='loading-2') + ], id='root') + + @app.callback( + Output('btn-1', 'value'), + [Input('btn-2', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + @app.callback( + Output('btn-2', 'value'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '.loading-2 .dash-spinner' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_nested_loading_components(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Loading([ + html.Button(id='btn-1'), + dcc.Loading([ + html.Button(id='btn-2') + ], className='loading-2') + ], className='loading-1') + ], id='root') + + @app.callback( + Output('btn-1', 'value'), + [Input('btn-2', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + @app.callback( + Output('btn-2', 'value'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is not None: + with lock: + return + + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '.loading-2 .dash-spinner' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '.loading-1 #btn-1' + ) + self.wait_for_element_by_css_selector( + '.loading-2 #btn-2' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_dynamic_loading_component(self): + lock = Lock() + + app = dash.Dash(__name__) + app.config['suppress_callback_exceptions'] = True + + app.layout = html.Div([ + html.Button(id='btn-1'), + html.Div(id='div-1') + ]) + + @app.callback( + Output('div-1', 'children'), + [Input('btn-1', 'n_clicks')] + ) + def updateDiv(n_clicks): + if n_clicks is None: + return + + with lock: + return html.Div([ + html.Button(id='btn-2'), + dcc.Loading([ + html.Button(id='btn-3') + ], className='loading-1') + ]) + + @app.callback( + Output('btn-3', 'content'), + [Input('btn-2', 'n_clicks')] + ) + def updateDynamic(n_clicks): + if n_clicks is None: + return + + with lock: + return 'content' + + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#btn-1' + ) + self.wait_for_element_by_css_selector( + '#div-1' + ) + + self.driver.find_element_by_id('btn-1').click() + + self.wait_for_element_by_css_selector( + '#div-1 #btn-2' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-3' + ) + + with lock: + self.driver.find_element_by_id('btn-2').click() + + self.wait_for_element_by_css_selector( + '.loading-1 .dash-spinner' + ) + + self.wait_for_element_by_css_selector( + '#div-1 #btn-2' + ) + self.wait_for_element_by_css_selector( + '.loading-1 #btn-3' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_slider(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='test-btn'), + html.Label(id='test-div', children=['Horizontal Slider']), + dcc.Slider( + id='horizontal-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + ), + ]) + + @app.callback( + Output('horizontal-slider', 'value'), + [Input('test-btn', 'n_clicks')] + ) + def user_delayed_value(n_clicks): + with lock: + return 5 + + with lock: + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#horizontal-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-slider:not([data-dash-is-loading="true"])' + ) + + with lock: + self.driver.find_element_by_id('test-btn').click() + + self.wait_for_element_by_css_selector( + '#horizontal-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-slider:not([data-dash-is-loading="true"])' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_horizontal_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Horizontal Slider'), + dcc.Slider( + id='horizontal-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + ), + ]) + self.startServer(app) + + self.wait_for_element_by_css_selector('#horizontal-slider') + self.snapshot('horizontal slider') + + h_slider = self.driver.find_element_by_css_selector( + '#horizontal-slider div[role="slider"]' + ) + h_slider.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_vertical_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Vertical Slider'), + dcc.Slider( + id='vertical-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + vertical=True, + ), + ], style={'height': '500px'}) + self.startServer(app) + + self.wait_for_element_by_css_selector('#vertical-slider') + self.snapshot('vertical slider') + + v_slider = self.driver.find_element_by_css_selector( + '#vertical-slider div[role="slider"]' + ) + v_slider.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_loading_range_slider(self): + lock = Lock() + + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='test-btn'), + html.Label(id='test-div', children=['Horizontal Range Slider']), + dcc.RangeSlider( + id='horizontal-range-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=[4, 6], + ), + ]) + + @app.callback( + Output('horizontal-range-slider', 'value'), + [Input('test-btn', 'n_clicks')] + ) + def delayed_value(children): + with lock: + return [4, 6] + + with lock: + self.startServer(app) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider:not([data-dash-is-loading="true"])' + ) + + with lock: + self.driver.find_element_by_id('test-btn').click() + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider[data-dash-is-loading="true"]' + ) + + self.wait_for_element_by_css_selector( + '#horizontal-range-slider:not([data-dash-is-loading="true"])' + ) + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_horizontal_range_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Horizontal Range Slider'), + dcc.RangeSlider( + id='horizontal-range-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=[4, 6], + ), + ]) + self.startServer(app) + + self.wait_for_element_by_css_selector('#horizontal-range-slider') + self.snapshot('horizontal range slider') + + h_slider_1 = self.driver.find_element_by_css_selector( + '#horizontal-range-slider div.rc-slider-handle-1[role="slider"]' + ) + h_slider_1.click() + + h_slider_2 = self.driver.find_element_by_css_selector( + '#horizontal-range-slider div.rc-slider-handle-2[role="slider"]' + ) + h_slider_2.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_vertical_range_slider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Label('Vertical Range Slider'), + dcc.RangeSlider( + id='vertical-range-slider', + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=[4, 6], + vertical=True, + ), + ], style={'height': '500px'}) + self.startServer(app) + + self.wait_for_element_by_css_selector('#vertical-range-slider') + self.snapshot('vertical range slider') + + v_slider_1 = self.driver.find_element_by_css_selector( + '#vertical-range-slider div.rc-slider-handle-1[role="slider"]' + ) + v_slider_1.click() + + v_slider_2 = self.driver.find_element_by_css_selector( + '#vertical-range-slider div.rc-slider-handle-2[role="slider"]' + ) + v_slider_2.click() + + for entry in self.get_log(): + raise Exception('browser error logged during test', entry) + + def test_gallery(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Div(id='waitfor'), + html.Label('Upload'), + dcc.Upload(), + + html.Label('Horizontal Tabs'), + dcc.Tabs(id="tabs", children=[ + dcc.Tab(label='Tab one', className='test', style={'border': '1px solid magenta'}, children=[ + html.Div(['Test']) + ]), + dcc.Tab(label='Tab two', children=[ + html.Div([ + html.H1("This is the content in tab 2"), + html.P("A graph here would be nice!") + ]) + ], id='tab-one'), + dcc.Tab(label='Tab three', children=[ + html.Div([ + html.H1("This is the content in tab 3"), + ]) + ]), + ], + style={ + 'fontFamily': 'system-ui' + }, + content_style={ + 'border': '1px solid #d6d6d6', + 'padding': '44px' + }, + parent_style={ + 'maxWidth': '1000px', + 'margin': '0 auto' + } + ), + + html.Label('Vertical Tabs'), + dcc.Tabs(id="tabs1", vertical=True, children=[ + dcc.Tab(label='Tab one', children=[ + html.Div(['Test']) + ]), + dcc.Tab(label='Tab two', children=[ + html.Div([ + html.H1("This is the content in tab 2"), + html.P("A graph here would be nice!") + ]) + ]), + dcc.Tab(label='Tab three', children=[ + html.Div([ + html.H1("This is the content in tab 3"), + ]) + ]), + ] + ), + + html.Label('Dropdown'), + dcc.Dropdown( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value='MTL', + id='dropdown' + ), + + html.Label('Multi-Select Dropdown'), + dcc.Dropdown( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value=['MTL', 'SF'], + multi=True + ), + + html.Label('Radio Items'), + dcc.RadioItems( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value='MTL' + ), + + html.Label('Checkboxes'), + dcc.Checklist( + options=[ + {'label': 'New York City', 'value': 'NYC'}, + {'label': u'Montréal', 'value': 'MTL'}, + {'label': 'San Francisco', 'value': 'SF'}, + {'label': u'北京', 'value': u'北京'} + ], + value=['MTL', 'SF'] + ), + + html.Label('Text Input'), + dcc.Input(value='', placeholder='type here', id='textinput'), + html.Label('Disabled Text Input'), + dcc.Input(value='disabled', type='text', + id='disabled-textinput', disabled=True), + + html.Label('Slider'), + dcc.Slider( + min=0, + max=9, + marks={i: 'Label {}'.format(i) if i == 1 else str(i) + for i in range(1, 6)}, + value=5, + ), + + html.Label('Graph'), + dcc.Graph( + id='graph', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [4, 1, 4] + }], + 'layout': { + 'title': u'北京' + } + } + ), + + html.Div([ + html.Label('DatePickerSingle'), + dcc.DatePickerSingle( + id='date-picker-single', + date=datetime(1997, 5, 10) + ), + html.Div([ + html.Label('DatePickerSingle - empty input'), + dcc.DatePickerSingle(), + ], id='dt-single-no-date-value' + ), + html.Div([ + html.Label('DatePickerSingle - initial visible month (May 97)'), + dcc.DatePickerSingle( + initial_visible_month=datetime(1997, 5, 10) + ), + ], id='dt-single-no-date-value-init-month' + ), + ]), + + html.Div([ + html.Label('DatePickerRange'), + dcc.DatePickerRange( + id='date-picker-range', + start_date_id='startDate', + end_date_id='endDate', + start_date=datetime(1997, 5, 3), + end_date_placeholder_text='Select a date!' + ), + html.Div([ + html.Label('DatePickerRange - empty input'), + dcc.DatePickerRange( + start_date_id='startDate', + end_date_id='endDate', + start_date_placeholder_text='Start date', + end_date_placeholder_text='End date' + ), + ], id='dt-range-no-date-values' + ), + html.Div([ + html.Label('DatePickerRange - initial visible month (May 97)'), + dcc.DatePickerRange( + start_date_id='startDate', + end_date_id='endDate', + start_date_placeholder_text='Start date', + end_date_placeholder_text='End date', + initial_visible_month=datetime(1997, 5, 10) + ), + ], id='dt-range-no-date-values-init-month' + ), + ]), + + html.Label('TextArea'), + dcc.Textarea( + placeholder='Enter a value... 北京', + style={'width': '100%'} + ), + + html.Label('Markdown'), + dcc.Markdown(''' + #### Dash and Markdown + + Dash supports [Markdown](https://rexxars.github.io/react-markdown/). + + Markdown is a simple way to write and format text. + It includes a syntax for things like **bold text** and *italics*, + [links](https://rexxars.github.io/react-markdown/), inline `code` snippets, lists, + quotes, and more. + + 1. Links are auto-rendered: https://dash.plot.ly. + 2. This uses ~commonmark~ GitHub flavored markdown. + + Tables are also supported: + + | First Header | Second Header | + | ------------- | ------------- | + | Content Cell | Content Cell | + | Content Cell | Content Cell | + + 北京 + '''.replace(' ', '')), + dcc.Markdown(['# Line one', '## Line two']), + dcc.Markdown(), + dcc.SyntaxHighlighter(dedent('''import python + print(3)'''), language='python'), + dcc.SyntaxHighlighter([ + 'import python', + 'print(3)' + ], language='python'), + dcc.SyntaxHighlighter() + ]) + self.startServer(app) + + self.wait_for_element_by_css_selector('#waitfor') + + self.snapshot('gallery') + + self.driver.find_element_by_css_selector( + '#dropdown .Select-input input' + ).send_keys(u'北') + self.snapshot('gallery - chinese character') + + text_input = self.driver.find_element_by_id('textinput') + # verify that type has the right default + # Can't use text_input.get_attribute('type') - that pulls the + # default value even if none is specified. + get_type = ('return document.getElementById("textinput")' + '.getAttribute("type");') + self.assertEqual(self.driver.execute_script(get_type), 'text') + disabled_text_input = self.driver.find_element_by_id( + 'disabled-textinput') + text_input.send_keys('HODOR') + + # It seems selenium errors when send(ing)_keys on a disabled element. + # In case this changes we try anyway and catch the particular + # exception. In any case Percy will snapshot the disabled input style + # so we are not totally dependent on the send_keys behaviour for + # testing disabled state. + try: + disabled_text_input.send_keys('RODOH') + except InvalidElementStateException: + pass + + self.snapshot('gallery - text input') + + # DatePickerSingle and DatePickerRange test + # for issue with datepicker when date value is `None` + dt_input_1 = self.driver.find_element_by_css_selector( + '#dt-single-no-date-value #date' + ) + dt_input_1.click() + self.snapshot('gallery - DatePickerSingle\'s datepicker ' + 'when no date value and no initial month specified') + dt_length = len(dt_input_1.get_attribute('value')) + dt_input_1.send_keys(dt_length * Keys.BACKSPACE) + dt_input_1.send_keys("1997-05-03") + + dt_input_2 = self.driver.find_element_by_css_selector( + '#dt-single-no-date-value-init-month #date' + ) + self.driver.find_element_by_css_selector( + 'label' + ).click() + dt_input_2.click() + self.snapshot('gallery - DatePickerSingle\'s datepicker ' + 'when no date value, but initial month is specified') + dt_length = len(dt_input_2.get_attribute('value')) + dt_input_2.send_keys(dt_length * Keys.BACKSPACE) + dt_input_2.send_keys("1997-05-03") + + dt_input_3 = self.driver.find_element_by_css_selector( + '#dt-range-no-date-values #endDate' + ) + self.driver.find_element_by_css_selector( + 'label' + ).click() + dt_input_3.click() + self.snapshot('gallery - DatePickerRange\'s datepicker ' + 'when neither start date nor end date ' + 'nor initial month is specified') + dt_length = len(dt_input_3.get_attribute('value')) + dt_input_3.send_keys(dt_length * Keys.BACKSPACE) + dt_input_3.send_keys("1997-05-03") + + dt_input_4 = self.driver.find_element_by_css_selector( + '#dt-range-no-date-values-init-month #endDate' + ) + self.driver.find_element_by_css_selector( + 'label' + ).click() + dt_input_4.click() + self.snapshot('gallery - DatePickerRange\'s datepicker ' + 'when neither start date nor end date is specified, ' + 'but initial month is') + dt_length = len(dt_input_4.get_attribute('value')) + dt_input_4.send_keys(dt_length * Keys.BACKSPACE) + dt_input_4.send_keys("1997-05-03") + + def test_tabs_in_vertical_mode(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.Tabs(id="tabs", value='tab-3', children=[ + dcc.Tab(label='Tab one', value='tab-1', id='tab-1', children=[ + html.Div('Tab One Content') + ]), + dcc.Tab(label='Tab two', value='tab-2', id='tab-2', children=[ + html.Div('Tab Two Content') + ]), + dcc.Tab(label='Tab three', value='tab-3', id='tab-3', children=[ + html.Div('Tab Three Content') + ]), + ], vertical=True), + html.Div(id='tabs-content') + ]) + + self.startServer(app=app) + self.wait_for_text_to_equal('#tab-3', 'Tab three') + + self.snapshot('Tabs - vertical mode') + + def test_tabs_without_children(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs", value='tab-2', children=[ + dcc.Tab(label='Tab one', value='tab-1', id='tab-1'), + dcc.Tab(label='Tab two', value='tab-2', id='tab-2'), + ]), + html.Div(id='tabs-content') + ]) + + @app.callback(dash.dependencies.Output('tabs-content', 'children'), + [dash.dependencies.Input('tabs', 'value')]) + def render_content(tab): + if tab == 'tab-1': + return html.Div([ + html.H3('Test content 1') + ], id='test-tab-1') + elif tab == 'tab-2': + return html.Div([ + html.H3('Test content 2') + ], id='test-tab-2') + + self.startServer(app=app) + + self.wait_for_text_to_equal('#tabs-content', 'Test content 2') + self.snapshot('initial tab - tab 2') + + selected_tab = self.wait_for_element_by_css_selector('#tab-1') + selected_tab.click() + time.sleep(1) + self.wait_for_text_to_equal('#tabs-content', 'Test content 1') + + def test_tabs_with_children_undefined(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs", value='tab-1'), + html.Div(id='tabs-content') + ]) + + self.startServer(app=app) + + self.wait_for_element_by_css_selector('#tabs-content') + + self.snapshot('Tabs component with children undefined') + + def test_tabs_render_without_selected(self): + app = dash.Dash(__name__) + + data = [ + {'id': 'one', 'value': 1}, + {'id': 'two', 'value': 2}, + ] + + menu = html.Div([ + html.Div('one', id='one'), + html.Div('two', id='two') + ]) + + tabs_one = html.Div([ + dcc.Tabs([ + dcc.Tab(dcc.Graph(id='graph-one'), label='tab-one-one'), + ]) + ], id='tabs-one', style={'display': 'none'}) + + tabs_two = html.Div([ + dcc.Tabs([ + dcc.Tab(dcc.Graph(id='graph-two'), label='tab-two-one'), + ]) + ], id='tabs-two', style={'display': 'none'}) + + app.layout = html.Div([ + menu, + tabs_one, + tabs_two + ]) + + for i in ('one', 'two'): + + @app.callback(Output('tabs-{}'.format(i), 'style'), + [Input(i, 'n_clicks')]) + def on_click(n_clicks): + if n_clicks is None: + raise PreventUpdate + + if n_clicks % 2 == 1: + return {'display': 'block'} + return {'display': 'none'} + + @app.callback(Output('graph-{}'.format(i), 'figure'), + [Input(i, 'n_clicks')]) + def on_click(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return { + 'data': [ + { + 'x': [1, 2, 3, 4], + 'y': [4, 3, 2, 1] + } + ], + 'layout': { + 'width': 700, + 'height': 450 + } + } + + self.startServer(app=app) + + button_one = self.wait_for_element_by_css_selector('#one') + button_two = self.wait_for_element_by_css_selector('#two') + + button_one.click() + + # wait for tabs to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-one .main-svg")) + ) + + time.sleep(1) + self.snapshot("Tabs 1 rendered ") + + button_two.click() + + # wait for tabs to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-two .main-svg")) + ) + + time.sleep(1) + self.snapshot("Tabs 2 rendered ") + + # do some extra tests while we're here + # and have access to Graph and plotly.js + self.check_graph_config_shape() + self.check_plotlyjs() + + def check_plotlyjs(self): + # find plotly.js files in the dist folder, check that there's only one + all_dist = os.listdir(dcc.__path__[0]) + js_re = r'^plotly-(.*)\.min\.js$' + plotlyjs_dist = [fn for fn in all_dist if re.match(js_re, fn)] + + self.assertEqual(len(plotlyjs_dist), 1) + + # check that the version matches what we see in the page + page_version = self.driver.execute_script('return Plotly.version;') + dist_version = re.match(js_re, plotlyjs_dist[0]).groups()[0] + self.assertEqual(page_version, dist_version) + + def check_graph_config_shape(self): + config_schema = self.driver.execute_script( + 'return Plotly.PlotSchema.get().config;' + ) + with open(os.path.join(dcc.__path__[0], 'metadata.json')) as meta: + graph_meta = json.load(meta)['src/components/Graph.react.js'] + config_prop_shape = graph_meta['props']['config']['type']['value'] + + ignored_config = [ + 'setBackground', + 'showSources', + 'logging', + 'globalTransforms', + 'role' + ] + + def crawl(schema, props): + for prop_name in props: + self.assertIn(prop_name, schema) + + for item_name, item in schema.items(): + if item_name in ignored_config: + continue + + self.assertIn(item_name, props) + if 'valType' not in item: + crawl(item, props[item_name]['value']) + + crawl(config_schema, config_prop_shape) + + def test_tabs_without_value(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs-without-value", children=[ + dcc.Tab(label='Tab One', value='tab-1'), + dcc.Tab(label='Tab Two', value='tab-2'), + ]), + html.Div(id='tabs-content') + ]) + + @app.callback(Output('tabs-content', 'children'), + [Input('tabs-without-value', 'value')]) + def render_content(tab): + if tab == 'tab-1': + return html.H3('Default selected Tab content 1') + elif tab == 'tab-2': + return html.H3('Tab content 2') + + self.startServer(app=app) + + self.wait_for_text_to_equal('#tabs-content', 'Default selected Tab content 1') + + self.snapshot('Tab 1 should be selected by default') + + def test_graph_does_not_resize_in_tabs(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.H1('Dash Tabs component demo'), + dcc.Tabs(id="tabs-example", value='tab-1-example', children=[ + dcc.Tab(label='Tab One', value='tab-1-example', id='tab-1'), + dcc.Tab(label='Tab Two', value='tab-2-example', id='tab-2'), + ]), + html.Div(id='tabs-content-example') + ]) + + @app.callback(Output('tabs-content-example', 'children'), + [Input('tabs-example', 'value')]) + def render_content(tab): + if tab == 'tab-1-example': + return html.Div([ + html.H3('Tab content 1'), + dcc.Graph( + id='graph-1-tabs', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [3, 1, 2], + 'type': 'bar' }] + } + ) + ]) + elif tab == 'tab-2-example': + return html.Div([ + html.H3('Tab content 2'), + dcc.Graph( + id='graph-2-tabs', + figure={ + 'data': [{ + 'x': [1, 2, 3], + 'y': [5, 10, 6], + 'type': 'bar' + }] + } + ) + ]) + self.startServer(app=app) + + tab_one = self.wait_for_element_by_css_selector('#tab-1') + tab_two = self.wait_for_element_by_css_selector('#tab-2') + + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.ID, "tab-2")) + ) + + self.snapshot("Tabs with Graph - initial (graph should not resize)") + tab_two.click() + + # wait for Graph's internal svg to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-2-tabs .main-svg")) + ) - return html.Div([html.P(id), - dcc.Graph(id=id, - figure=dict(data=data)), - html.Div(id='output_{}'.format(id))]) - - figs = ['trace_will_extend', - 'trace_will_extend_with_no_indices', - 'trace_will_extend_with_max_points'] - - layout = [generate_with_id(id) for id in figs] - - figs.append('trace_will_allow_repeated_extend') - data = [{'y': [0, 0, 0]}] - layout.append(generate_with_id(figs[-1], data)) - - figs.append('trace_will_extend_selectively') - data = [{'x': [0, 1, 2, 3, 4], 'y': [0, .5, 1, .5, 0]}, - {'x': [0, 1, 2, 3, 4], 'y': [1, 1, 1, 1, 1]}] - layout.append(generate_with_id(figs[-1], data)) - - layout.append(dcc.Interval( - id='interval_extendablegraph_update', - interval=10, - n_intervals=0, - max_intervals=1)) - - layout.append(dcc.Interval( - id='interval_extendablegraph_extendtwice', - interval=500, - n_intervals=0, - max_intervals=2)) - - app.layout = html.Div(layout) - - @app.callback(Output('trace_will_allow_repeated_extend', 'extendData'), - [Input('interval_extendablegraph_extendtwice', 'n_intervals')]) - def trace_will_allow_repeated_extend(n_intervals): - if n_intervals is None or n_intervals < 1: - raise PreventUpdate - - return dict(y=[[.1, .2, .3, .4, .5]]) - - @app.callback(Output('trace_will_extend', 'extendData'), - [Input('interval_extendablegraph_update', 'n_intervals')]) - def trace_will_extend(n_intervals): - if n_intervals is None or n_intervals < 1: - raise PreventUpdate - - x_new = [5, 6, 7, 8, 9] - y_new = [.1, .2, .3, .4, .5] - return dict(x=[x_new], y=[y_new]), [0] - - @app.callback(Output('trace_will_extend_selectively', 'extendData'), - [Input('interval_extendablegraph_update', 'n_intervals')]) - def trace_will_extend_selectively(n_intervals): - if n_intervals is None or n_intervals < 1: - raise PreventUpdate - - x_new = [5, 6, 7, 8, 9] - y_new = [.1, .2, .3, .4, .5] - return dict(x=[x_new], y=[y_new]), [1] - - @app.callback(Output('trace_will_extend_with_no_indices', 'extendData'), - [Input('interval_extendablegraph_update', 'n_intervals')]) - def trace_will_extend_with_no_indices(n_intervals): - if n_intervals is None or n_intervals < 1: - raise PreventUpdate - - x_new = [5, 6, 7, 8, 9] - y_new = [.1, .2, .3, .4, .5] - return dict(x=[x_new], y=[y_new]) - - @app.callback(Output('trace_will_extend_with_max_points', 'extendData'), - [Input('interval_extendablegraph_update', 'n_intervals')]) - def trace_will_extend_with_max_points(n_intervals): - if n_intervals is None or n_intervals < 1: - raise PreventUpdate - - x_new = [5, 6, 7, 8, 9] - y_new = [.1, .2, .3, .4, .5] - return dict(x=[x_new], y=[y_new]), [0], 7 - - for id in figs: - @app.callback(Output('output_{}'.format(id), 'children'), - [Input(id, 'extendData')], - [State(id, 'figure')]) - def display_data(trigger, fig): - return json.dumps(fig['data']) - - self.startServer(app) - - comparison = json.dumps([ - dict( - x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - y=[0, .5, 1, .5, 0, .1, .2, .3, .4, .5] - ) - ]) - self.wait_for_text_to_equal('#output_trace_will_extend', comparison) - self.wait_for_text_to_equal('#output_trace_will_extend_with_no_indices', comparison) - comparison = json.dumps([ - dict( - x=[0, 1, 2, 3, 4], - y=[0, .5, 1, .5, 0] - ), - dict( - x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - y=[1, 1, 1, 1, 1, .1, .2, .3, .4, .5] - ) - ]) - self.wait_for_text_to_equal('#output_trace_will_extend_selectively', comparison) - - comparison = json.dumps([ - dict( - x=[3, 4, 5, 6, 7, 8, 9], - y=[.5, 0, .1, .2, .3, .4, .5] - ) - ]) - self.wait_for_text_to_equal('#output_trace_will_extend_with_max_points', comparison) - - comparison = json.dumps([ - dict( - y=[0, 0, 0, .1, .2, .3, .4, .5, .1, .2, .3, .4, .5] - ) - ]) - self.wait_for_text_to_equal('#output_trace_will_allow_repeated_extend', comparison) - - def test_storage_component(self): - app = dash.Dash(__name__) - - getter = 'return JSON.parse(window.{}.getItem("{}"));' - clicked_getter = getter.format('localStorage', 'storage') - dummy_getter = getter.format('sessionStorage', 'dummy') - dummy_data = 'Hello dummy' - - app.layout = html.Div([ - dcc.Store(id='storage', - storage_type='local'), - html.Button('click me', id='btn'), - html.Button('clear', id='clear-btn'), - html.Button('set-init-storage', - id='set-init-storage'), - dcc.Store(id='dummy', - storage_type='session', - data=dummy_data), - dcc.Store(id='memory', - storage_type='memory'), - html.Div(id='memory-output'), - dcc.Store(id='initial-storage', - storage_type='session'), - html.Div(id='init-output') - ]) - - @app.callback(Output('storage', 'data'), - [Input('btn', 'n_clicks')], - [State('storage', 'data')]) - def on_click(n_clicks, storage): - if n_clicks is None: - return - storage = storage or {} - return {'clicked': storage.get('clicked', 0) + 1} - - @app.callback(Output('storage', 'clear_data'), - [Input('clear-btn', 'n_clicks')]) - def on_clear(n_clicks): - if n_clicks is None: - return - return True - - @app.callback(Output('memory', 'data'), [Input('storage', 'data')]) - def on_memory(data): - return data - - @app.callback(Output('memory-output', 'children'), - [Input('memory', 'data')]) - def on_memory2(data): - if data is None: - return '' - return json.dumps(data) - - @app.callback(Output('initial-storage', 'data'), - [Input('set-init-storage', 'n_clicks')]) - def on_init(n_clicks): - if n_clicks is None: - raise PreventUpdate - - return 'initialized' - - @app.callback(Output('init-output', 'children'), - [Input('initial-storage', 'modified_timestamp')], - [State('initial-storage', 'data')]) - def init_output(ts, data): - return json.dumps({'data': data, 'ts': ts}) - - self.startServer(app) - - time.sleep(1) - - dummy = self.driver.execute_script(dummy_getter) - self.assertEqual(dummy_data, dummy) - - click_btn = self.wait_for_element_by_css_selector('#btn') - clear_btn = self.wait_for_element_by_css_selector('#clear-btn') - mem = self.wait_for_element_by_css_selector('#memory-output') - - for i in range(1, 11): - click_btn.click() - time.sleep(1) - - click_data = self.driver.execute_script(clicked_getter) - self.assertEqual(i, click_data.get('clicked')) - self.assertEqual(i, int(json.loads(mem.text).get('clicked'))) - - clear_btn.click() - time.sleep(1) - - cleared_data = self.driver.execute_script(clicked_getter) - self.assertTrue(cleared_data is None) - # Did mem also got cleared ? - self.assertFalse(mem.text) - - # Test initial timestamp output - init_btn = self.wait_for_element_by_css_selector('#set-init-storage') - init_btn.click() - ts = int(time.time() * 1000) - time.sleep(1) - self.driver.refresh() - time.sleep(2) - init = self.wait_for_element_by_css_selector('#init-output') - init = json.loads(init.text) - self.assertAlmostEqual(ts, init.get('ts'), delta=1000) - self.assertEqual('initialized', init.get('data')) - - def test_store_nested_data(self): - app = dash.Dash(__name__) - - nested = {'nested': {'nest': 'much'}} - nested_list = dict(my_list=[1, 2, 3]) - - app.layout = html.Div([ - dcc.Store(id='store', storage_type='local'), - html.Button('set object as key', id='obj-btn'), - html.Button('set list as key', id='list-btn'), - html.Output(id='output') - ]) - - @app.callback(Output('store', 'data'), - [Input('obj-btn', 'n_clicks_timestamp'), - Input('list-btn', 'n_clicks_timestamp')]) - def on_obj_click(obj_ts, list_ts): - if obj_ts is None and list_ts is None: - raise PreventUpdate - - # python 3 got the default props bug. plotly/dash#396 - if (obj_ts and not list_ts) or obj_ts > list_ts: - return nested - else: - return nested_list - - @app.callback(Output('output', 'children'), - [Input('store', 'modified_timestamp')], - [State('store', 'data')]) - def on_ts(ts, data): - if ts is None: - raise PreventUpdate - return json.dumps(data) - - self.startServer(app) - - obj_btn = self.wait_for_element_by_css_selector('#obj-btn') - list_btn = self.wait_for_element_by_css_selector('#list-btn') - - obj_btn.click() - time.sleep(1) - self.wait_for_text_to_equal('#output', json.dumps(nested)) - # it would of crashed the app before adding the recursive check. - - list_btn.click() - time.sleep(1) - self.wait_for_text_to_equal('#output', json.dumps(nested_list)) - - def test_user_supplied_css(self): - app = dash.Dash(__name__) - - app.layout = html.Div(className="test-input-css", children=[dcc.Input()]) - - self.startServer(app) - - self.wait_for_element_by_css_selector('.test-input-css') - self.snapshot('styled input - width: 100%, border-color: hotpink') - - def test_logout_btn(self): - app = dash.Dash(__name__) - - @app.server.route('/_logout', methods=['POST']) - def on_logout(): - rep = flask.redirect('/logged-out') - rep.set_cookie('logout-cookie', '', 0) - return rep - - app.layout = html.Div([ - html.H2('Logout test'), - dcc.Location(id='location'), - html.Div(id='content'), - ]) - - @app.callback(Output('content', 'children'), - [Input('location', 'pathname')]) - def on_location(location_path): - if location_path is None: - raise PreventUpdate - - if 'logged-out' in location_path: - return 'Logged out' - else: - - @flask.after_this_request - def _insert_cookie(rep): - rep.set_cookie('logout-cookie', 'logged-in') - return rep - - return dcc.LogoutButton(id='logout-btn', logout_url='/_logout') - - self.startServer(app) - time.sleep(1) - self.snapshot('Logout button') - - self.assertEqual( - 'logged-in', - self.driver.get_cookie('logout-cookie')['value']) - logout_button = self.wait_for_element_by_css_selector('#logout-btn') - logout_button.click() - self.wait_for_text_to_equal('#content', 'Logged out') - - self.assertFalse(self.driver.get_cookie('logout-cookie')) - - def test_state_and_inputs(self): - 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'), - inputs=[Input('input', 'value')], - state=[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') # noqa: E731 - input = lambda: self.driver.find_element_by_id('input') # noqa: E731 - state = lambda: self.driver.find_element_by_id('state') # noqa: E731 - - # callback gets called with initial input - wait_for(lambda: call_count.value == 1) - 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_simple_callback(self): - app = dash.Dash(__name__) - app.layout = html.Div([ - dcc.Input( - id='input', - ), - 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) - - input1 = self.wait_for_element_by_css_selector('#input') - input1.send_keys('hello world') - output1 = self.wait_for_element_by_css_selector('#output-1') - self.wait_for_text_to_equal('#output-1', 'hello world') - output1.click() # Lose focus, no callback sent for value. - - self.assertEqual( - call_count.value, - # an initial call to retrieve the first value - # plus one for each hello world character - 1 + len('hello world') - ) - - def test_store_type_updates(self): - app = dash.Dash(__name__) - - types = [ - ('str', 'hello'), - ('number', 1), - ('dict', {'data': [2, 3, None]}), - ('list', [5, 6, 7]), - ('null', None), - ('bool', True), - ('bool', False), - ('empty-dict', {}), - ] - types_changes = list( - itertools.chain(*itertools.combinations(types, 2)) - ) + [ # No combinations as it add much test time. - ('list-dict-1', [1, 2, {'data': [55, 66, 77], 'dummy': 'dum'}]), - ('list-dict-2', [1, 2, {'data': [111, 99, 88]}]), - ('dict-3', {'a': 1, 'c': 1}), - ('dict-2', {'a': 1, 'b': None}), - ] - - app.layout = html.Div([ - html.Div(id='output'), - html.Button('click', id='click'), - dcc.Store(id='store') - ]) - - @app.callback(Output('output', 'children'), - [Input('store', 'modified_timestamp')], - [State('store', 'data')]) - def on_data(ts, data): - if ts is None: - raise PreventUpdate - - return json.dumps(data) - - @app.callback(Output('store', 'data'), [Input('click', 'n_clicks')]) - def on_click(n_clicks): - if n_clicks is None: - raise PreventUpdate - - return types_changes[n_clicks - 1][1] - - self.startServer(app) - - button = self.wait_for_element_by_css_selector('#click') - - for i, type_change in enumerate(types_changes): - button.click() - try: - self.wait_for_text_to_equal( - '#output', json.dumps(type_change[1]), - ) - except TimeoutException: - raise Exception( - 'Output type did not change from {} to {}'.format( - types_changes[i - 1], - type_change - ) - ) + self.snapshot("Tabs with Graph - clicked tab 2 (graph should not resize)") + + WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.ID, "tab-1")) + ) + + tab_one.click() + + # wait for Graph to be loaded after clicking + WebDriverWait(self.driver, 10).until( + EC.visibility_of_element_located((By.CSS_SELECTOR, "#graph-1-tabs .main-svg")) + ) + + self.snapshot("Tabs with Graph - clicked tab 1 (graph should not resize)") + + def test_location_link(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Div(id='waitfor'), + dcc.Location(id='test-location', refresh=False), + + dcc.Link( + html.Button('I am a clickable button'), + id='test-link', + href='/test/pathname'), + dcc.Link( + html.Button('I am a clickable hash button'), + id='test-link-hash', + href='#test'), + dcc.Link( + html.Button('I am a clickable search button'), + id='test-link-search', + href='?testQuery=testValue', + refresh=False), + html.Button('I am a magic button that updates pathname', + id='test-button'), + html.A('link to click', href='/test/pathname/a', id='test-a'), + html.A('link to click', href='#test-hash', id='test-a-hash'), + html.A('link to click', href='?queryA=valueA', id='test-a-query'), + html.Div(id='test-pathname', children=[]), + html.Div(id='test-hash', children=[]), + html.Div(id='test-search', children=[]), + ]) + + @app.callback( + output=Output(component_id='test-pathname', + component_property='children'), + inputs=[Input(component_id='test-location', component_property='pathname')]) + def update_location_on_page(pathname): + return pathname + + @app.callback( + output=Output(component_id='test-hash', + component_property='children'), + inputs=[Input(component_id='test-location', component_property='hash')]) + def update_location_on_page(hash_val): + if hash_val is None: + return '' + + return hash_val + + @app.callback( + output=Output(component_id='test-search', + component_property='children'), + inputs=[Input(component_id='test-location', component_property='search')]) + def update_location_on_page(search): + if search is None: + return '' + + return search + + @app.callback( + output=Output(component_id='test-location', + component_property='pathname'), + inputs=[Input(component_id='test-button', + component_property='n_clicks')], + state=[State(component_id='test-location', component_property='pathname')]) + def update_pathname(n_clicks, current_pathname): + if n_clicks is not None: + return '/new/pathname' + + return current_pathname + + self.startServer(app=app) + + time.sleep(1) + self.snapshot('link -- location') + + # Check that link updates pathname + self.wait_for_element_by_css_selector('#test-link').click() + self.assertEqual( + self.driver.current_url.replace('http://localhost:8050', ''), + '/test/pathname') + self.wait_for_text_to_equal('#test-pathname', '/test/pathname') + + # Check that hash is updated in the Location + self.wait_for_element_by_css_selector('#test-link-hash').click() + self.wait_for_text_to_equal('#test-pathname', '/test/pathname') + self.wait_for_text_to_equal('#test-hash', '#test') + self.snapshot('link -- /test/pathname#test') + + # Check that search is updated in the Location -- note that this goes through href and therefore wipes the hash + self.wait_for_element_by_css_selector('#test-link-search').click() + self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') + self.wait_for_text_to_equal('#test-hash', '') + self.snapshot('link -- /test/pathname?testQuery=testValue') + + # Check that pathname is updated through a Button click via props + self.wait_for_element_by_css_selector('#test-button').click() + self.wait_for_text_to_equal('#test-pathname', '/new/pathname') + self.wait_for_text_to_equal('#test-search', '?testQuery=testValue') + self.snapshot('link -- /new/pathname?testQuery=testValue') + + # Check that pathname is updated through an a tag click via props + self.wait_for_element_by_css_selector('#test-a').click() + try: + self.wait_for_element_by_css_selector('#waitfor') + except Exception as e: + print(self.wait_for_element_by_css_selector( + '#_dash-app-content').get_attribute('innerHTML')) + raise e + + self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') + self.wait_for_text_to_equal('#test-search', '') + self.wait_for_text_to_equal('#test-hash', '') + self.snapshot('link -- /test/pathname/a') + + # Check that hash is updated through an a tag click via props + self.wait_for_element_by_css_selector('#test-a-hash').click() + self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') + self.wait_for_text_to_equal('#test-search', '') + self.wait_for_text_to_equal('#test-hash', '#test-hash') + self.snapshot('link -- /test/pathname/a#test-hash') + + # Check that hash is updated through an a tag click via props + self.wait_for_element_by_css_selector('#test-a-query').click() + self.wait_for_element_by_css_selector('#waitfor') + self.wait_for_text_to_equal('#test-pathname', '/test/pathname/a') + self.wait_for_text_to_equal('#test-search', '?queryA=valueA') + self.wait_for_text_to_equal('#test-hash', '') + self.snapshot('link -- /test/pathname/a?queryA=valueA') + + def test_link_scroll(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Location(id='test-url', refresh=False), + + html.Div(id='push-to-bottom', children=[], style={ + 'display': 'block', + 'height': '200vh' + }), + html.Div(id='page-content'), + dcc.Link('Test link', href='/test-link', id='test-link') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('page-content', 'children'), + [Input('test-url', 'pathname')]) + def display_page(pathname): + call_count.value = call_count.value + 1 + return 'You are on page {}'.format(pathname) + + self.startServer(app=app) + + time.sleep(2) + + # callback is called twice when defined + self.assertEqual( + call_count.value, + 2 + ) + + # test if link correctly scrolls back to top of page + test_link = self.wait_for_element_by_css_selector('#test-link') + test_link.send_keys(Keys.NULL) + test_link.click() + time.sleep(2) + + # test link still fires update on Location + page_content = self.wait_for_element_by_css_selector('#page-content') + self.assertNotEqual(page_content.text, 'You are on page /') + + self.wait_for_text_to_equal( + '#page-content', 'You are on page /test-link') + + # test if rendered Link's tag has a href attribute + link_href = test_link.get_attribute("href") + self.assertEqual(link_href, 'http://localhost:8050/test-link') + + # test if callback is only fired once (offset of 2) + self.assertEqual( + call_count.value, + 3 + ) + + def test_candlestick(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Button( + id='button', + children='Update Candlestick', + n_clicks=0 + ), + dcc.Graph(id='graph') + ]) + + @app.callback(Output('graph', 'figure'), [Input('button', 'n_clicks')]) + def update_graph(n_clicks): + return { + 'data': [{ + 'open': [1] * 5, + 'high': [3] * 5, + 'low': [0] * 5, + 'close': [2] * 5, + 'x': [n_clicks] * 5, + 'type': 'candlestick' + }] + } + self.startServer(app=app) + + button = self.wait_for_element_by_css_selector('#button') + self.snapshot('candlestick - initial') + button.click() + time.sleep(1) + self.snapshot('candlestick - 1 click') + + button.click() + time.sleep(1) + self.snapshot('candlestick - 2 click') + + def test_graphs_with_different_figures(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Graph( + id='example-graph', + figure={ + 'data': [ + {'x': [1, 2, 3], 'y': [4, 1, 2], + 'type': 'bar', 'name': 'SF'}, + {'x': [1, 2, 3], 'y': [2, 4, 5], + 'type': 'bar', 'name': u'Montréal'}, + ], + 'layout': { + 'title': 'Dash Data Visualization' + } + } + ), + dcc.Graph( + id='example-graph-2', + figure={ + 'data': [ + {'x': [20, 24, 33], 'y': [5, 2, 3], + 'type': 'bar', 'name': 'SF'}, + {'x': [11, 22, 33], 'y': [22, 44, 55], + 'type': 'bar', 'name': u'Montréal'}, + ], + 'layout': { + 'title': 'Dash Data Visualization' + } + } + ), + html.Div(id='restyle-data'), + html.Div(id='relayout-data') + ]) + + @app.callback(Output('restyle-data', 'children'), [Input('example-graph', 'restyleData')]) + def show_restyle_data(data): + if data is None: # ignore initial + return '' + return json.dumps(data) + + @app.callback(Output('relayout-data', 'children'), [Input('example-graph', 'relayoutData')]) + def show_relayout_data(data): + if data is None or 'autosize' in data: # ignore initial & auto width + return '' + return json.dumps(data) + + self.startServer(app=app) + + # use this opportunity to test restyleData, since there are multiple + # traces on this graph + legendToggle = self.driver.find_element_by_css_selector('#example-graph .traces:first-child .legendtoggle') + legendToggle.click() + self.wait_for_text_to_equal('#restyle-data', '[{"visible": ["legendonly"]}, [0]]') + + # move snapshot after click, so it's more stable with the wait + self.snapshot('2 graphs with different figures') + + # and test relayoutData while we're at it + autoscale = self.driver.find_element_by_css_selector('#example-graph .ewdrag') + autoscale.click() + autoscale.click() + self.wait_for_text_to_equal('#relayout-data', '{"xaxis.autorange": true}') + + def test_graphs_without_ids(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Graph(className='graph-no-id-1'), + dcc.Graph(className='graph-no-id-2'), + ]) + + self.startServer(app=app) + + graph_1 = self.wait_for_element_by_css_selector('.graph-no-id-1') + graph_2 = self.wait_for_element_by_css_selector('.graph-no-id-2') + + self.assertNotEqual(graph_1.get_attribute('id'), graph_2.get_attribute('id')) + + def test_datepickerrange_updatemodes(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.DatePickerRange( + id='date-picker-range', + start_date_id='startDate', + end_date_id='endDate', + start_date_placeholder_text='Select a start date!', + end_date_placeholder_text='Select an end date!', + updatemode='bothdates' + ), + html.Div(id='date-picker-range-output') + ]) + + @app.callback( + dash.dependencies.Output('date-picker-range-output', 'children'), + [dash.dependencies.Input('date-picker-range', 'start_date'), + dash.dependencies.Input('date-picker-range', 'end_date')]) + def update_output(start_date, end_date): + return '{} - {}'.format(start_date, end_date) + + self.startServer(app=app) + + start_date = self.wait_for_element_by_css_selector('#startDate') + start_date.click() + + end_date = self.wait_for_element_by_css_selector('#endDate') + end_date.click() + + self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') + + # using mouse click with fixed day range, this can be improved + # once we start refactoring the test structure + start_date.click() + + sday = self.driver.find_element_by_xpath("//td[text()='1' and @tabindex='0']") + sday.click() + self.wait_for_text_to_equal('#date-picker-range-output', 'None - None') + + eday = self.driver.find_elements_by_xpath("//td[text()='28']")[1] + eday.click() + + date_tokens = set(start_date.get_attribute('value').split('/')) + date_tokens.update(end_date.get_attribute('value').split('/')) + + self.assertEqual( + set(itertools.chain(*[ + _.split('-') + for _ in self.driver.find_element_by_css_selector( + '#date-picker-range-output').text.split(' - ')])), + date_tokens, + "date should match the callback output") + + def test_interval(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + html.Div(id='output'), + dcc.Interval(id='interval', interval=1, max_intervals=2) + ]) + + @app.callback(Output('output', 'children'), + [Input('interval', 'n_intervals')]) + def update_text(n): + return "{}".format(n) + + self.startServer(app=app) + + # wait for interval to finish + time.sleep(5) + + self.wait_for_text_to_equal('#output', '2') + + def test_if_interval_can_be_restarted(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Interval( + id='interval', + interval=100, + n_intervals=0, + max_intervals=-1 + ), + + html.Button('Start', id='start', n_clicks_timestamp=-1), + html.Button('Stop', id='stop', n_clicks_timestamp=-1), + + html.Div(id='output') + + ]) + + @app.callback( + Output('interval', 'max_intervals'), + [Input('start', 'n_clicks_timestamp'), + Input('stop', 'n_clicks_timestamp')]) + def start_stop(start, stop): + if start < stop: + return 0 + else: + return -1 + + @app.callback(Output('output', 'children'), [Input('interval', 'n_intervals')]) + def display_data(n_intervals): + return 'Updated {}'.format(n_intervals) + + self.startServer(app=app) + + start_button = self.wait_for_element_by_css_selector('#start') + stop_button = self.wait_for_element_by_css_selector('#stop') + + # interval will start itself, we wait a second before pressing 'stop' + time.sleep(1) + + # get the output after running it for a bit + output = self.wait_for_element_by_css_selector('#output') + stop_button.click() + + time.sleep(1) + + # get the output after it's stopped, it shouldn't be higher than before + output_stopped = self.wait_for_element_by_css_selector('#output') + + self.wait_for_text_to_equal("#output", output_stopped.text) + + # This test logic is bad + # same element check for same text will always be true. + self.assertEqual(output.text, output_stopped.text) + + def _test_confirm(self, app, test_name, add_callback=True): + count = Value('i', 0) + + if add_callback: + @app.callback(Output('confirmed', 'children'), + [Input('confirm', 'submit_n_clicks'), + Input('confirm', 'cancel_n_clicks')], + [State('confirm', 'submit_n_clicks_timestamp'), + State('confirm', 'cancel_n_clicks_timestamp')]) + def _on_confirmed(submit_n_clicks, cancel_n_clicks, + submit_timestamp, cancel_timestamp): + if not submit_n_clicks and not cancel_n_clicks: + return '' + count.value += 1 + if (submit_timestamp and cancel_timestamp is None) or\ + (submit_timestamp > cancel_timestamp): + return 'confirmed' + else: + return 'canceled' + + self.startServer(app) + button = self.wait_for_element_by_css_selector('#button') + self.snapshot(test_name + ' -> initial') + + button.click() + time.sleep(1) + self.driver.switch_to.alert.accept() + + if add_callback: + self.wait_for_text_to_equal('#confirmed', 'confirmed') + self.snapshot(test_name + ' -> confirmed') + + button.click() + time.sleep(0.5) + self.driver.switch_to.alert.dismiss() + time.sleep(0.5) + + if add_callback: + self.wait_for_text_to_equal('#confirmed', 'canceled') + self.snapshot(test_name + ' -> canceled') + + if add_callback: + self.assertEqual(2, count.value, + 'Expected 2 callback but got ' + str(count.value)) + + def test_confirm(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='button', children='Send confirm', n_clicks=0), + dcc.ConfirmDialog(id='confirm', message='Please confirm.'), + html.Div(id='confirmed') + ]) + + @app.callback(Output('confirm', 'displayed'), + [Input('button', 'n_clicks')]) + def on_click_confirm(n_clicks): + if n_clicks: + return True + + self._test_confirm(app, 'ConfirmDialog') + + def test_confirm_dialog_provider(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + dcc.ConfirmDialogProvider( + html.Button('click me', id='button'), + id='confirm', message='Please confirm.'), + html.Div(id='confirmed') + ]) + + self._test_confirm(app, 'ConfirmDialogProvider') + + def test_confirm_without_callback(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.ConfirmDialogProvider( + html.Button('click me', id='button'), + id='confirm', message='Please confirm.'), + html.Div(id='confirmed') + ]) + self._test_confirm(app, 'ConfirmDialogProviderWithoutCallback', + add_callback=False) + + def test_confirm_as_children(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='button', children='Send confirm'), + html.Div(id='confirm-container'), + dcc.Location(id='dummy-location') + ]) + + @app.callback(Output('confirm-container', 'children'), + [Input('button', 'n_clicks')]) + def on_click(n_clicks): + if n_clicks: + return dcc.ConfirmDialog( + displayed=True, + id='confirm', + message='Please confirm.') + + self.startServer(app) + + button = self.wait_for_element_by_css_selector('#button') + + button.click() + time.sleep(2) + + self.driver.switch_to.alert.accept() + + def test_empty_graph(self): + app = dash.Dash(__name__) + + app.layout = html.Div([ + html.Button(id='click', children='Click me'), + dcc.Graph( + id='graph', + figure={ + 'data': [dict(x=[1, 2, 3], y=[1, 2, 3], type='scatter')] + } + ) + ]) + + @app.callback(dash.dependencies.Output('graph', 'figure'), + [dash.dependencies.Input('click', 'n_clicks')], + [dash.dependencies.State('graph', 'figure')]) + def render_content(click, prev_graph): + if click: + return {} + return prev_graph + + self.startServer(app) + button = self.wait_for_element_by_css_selector('#click') + button.click() + time.sleep(2) # Wait for graph to re-render + self.snapshot('render-empty-graph') + + def test_graph_extend_trace(self): + app = dash.Dash(__name__) + + def generate_with_id(id, data=None): + if data is None: + data = [{'x': [0, 1, 2, 3, 4], + 'y': [0, .5, 1, .5, 0] + }] + + return html.Div([html.P(id), + dcc.Graph(id=id, + figure=dict(data=data)), + html.Div(id='output_{}'.format(id))]) + + figs = ['trace_will_extend', + 'trace_will_extend_with_no_indices', + 'trace_will_extend_with_max_points'] + + layout = [generate_with_id(id) for id in figs] + + figs.append('trace_will_allow_repeated_extend') + data = [{'y': [0, 0, 0]}] + layout.append(generate_with_id(figs[-1], data)) + + figs.append('trace_will_extend_selectively') + data = [{'x': [0, 1, 2, 3, 4], 'y': [0, .5, 1, .5, 0]}, + {'x': [0, 1, 2, 3, 4], 'y': [1, 1, 1, 1, 1]}] + layout.append(generate_with_id(figs[-1], data)) + + layout.append(dcc.Interval( + id='interval_extendablegraph_update', + interval=10, + n_intervals=0, + max_intervals=1)) + + layout.append(dcc.Interval( + id='interval_extendablegraph_extendtwice', + interval=500, + n_intervals=0, + max_intervals=2)) + + app.layout = html.Div(layout) + + @app.callback(Output('trace_will_allow_repeated_extend', 'extendData'), + [Input('interval_extendablegraph_extendtwice', 'n_intervals')]) + def trace_will_allow_repeated_extend(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + return dict(y=[[.1, .2, .3, .4, .5]]) + + @app.callback(Output('trace_will_extend', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]), [0] + + @app.callback(Output('trace_will_extend_selectively', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend_selectively(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]), [1] + + @app.callback(Output('trace_will_extend_with_no_indices', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend_with_no_indices(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]) + + @app.callback(Output('trace_will_extend_with_max_points', 'extendData'), + [Input('interval_extendablegraph_update', 'n_intervals')]) + def trace_will_extend_with_max_points(n_intervals): + if n_intervals is None or n_intervals < 1: + raise PreventUpdate + + x_new = [5, 6, 7, 8, 9] + y_new = [.1, .2, .3, .4, .5] + return dict(x=[x_new], y=[y_new]), [0], 7 + + for id in figs: + @app.callback(Output('output_{}'.format(id), 'children'), + [Input(id, 'extendData')], + [State(id, 'figure')]) + def display_data(trigger, fig): + return json.dumps(fig['data']) + + self.startServer(app) + + comparison = json.dumps([ + dict( + x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + y=[0, .5, 1, .5, 0, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_extend', comparison) + self.wait_for_text_to_equal('#output_trace_will_extend_with_no_indices', comparison) + comparison = json.dumps([ + dict( + x=[0, 1, 2, 3, 4], + y=[0, .5, 1, .5, 0] + ), + dict( + x=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + y=[1, 1, 1, 1, 1, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_extend_selectively', comparison) + + comparison = json.dumps([ + dict( + x=[3, 4, 5, 6, 7, 8, 9], + y=[.5, 0, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_extend_with_max_points', comparison) + + comparison = json.dumps([ + dict( + y=[0, 0, 0, .1, .2, .3, .4, .5, .1, .2, .3, .4, .5] + ) + ]) + self.wait_for_text_to_equal('#output_trace_will_allow_repeated_extend', comparison) + + def test_storage_component(self): + app = dash.Dash(__name__) + + getter = 'return JSON.parse(window.{}.getItem("{}"));' + clicked_getter = getter.format('localStorage', 'storage') + dummy_getter = getter.format('sessionStorage', 'dummy') + dummy_data = 'Hello dummy' + + app.layout = html.Div([ + dcc.Store(id='storage', + storage_type='local'), + html.Button('click me', id='btn'), + html.Button('clear', id='clear-btn'), + html.Button('set-init-storage', + id='set-init-storage'), + dcc.Store(id='dummy', + storage_type='session', + data=dummy_data), + dcc.Store(id='memory', + storage_type='memory'), + html.Div(id='memory-output'), + dcc.Store(id='initial-storage', + storage_type='session'), + html.Div(id='init-output') + ]) + + @app.callback(Output('storage', 'data'), + [Input('btn', 'n_clicks')], + [State('storage', 'data')]) + def on_click(n_clicks, storage): + if n_clicks is None: + return + storage = storage or {} + return {'clicked': storage.get('clicked', 0) + 1} + + @app.callback(Output('storage', 'clear_data'), + [Input('clear-btn', 'n_clicks')]) + def on_clear(n_clicks): + if n_clicks is None: + return + return True + + @app.callback(Output('memory', 'data'), [Input('storage', 'data')]) + def on_memory(data): + return data + + @app.callback(Output('memory-output', 'children'), + [Input('memory', 'data')]) + def on_memory2(data): + if data is None: + return '' + return json.dumps(data) + + @app.callback(Output('initial-storage', 'data'), + [Input('set-init-storage', 'n_clicks')]) + def on_init(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return 'initialized' + + @app.callback(Output('init-output', 'children'), + [Input('initial-storage', 'modified_timestamp')], + [State('initial-storage', 'data')]) + def init_output(ts, data): + return json.dumps({'data': data, 'ts': ts}) + + self.startServer(app) + + time.sleep(1) + + dummy = self.driver.execute_script(dummy_getter) + self.assertEqual(dummy_data, dummy) + + click_btn = self.wait_for_element_by_css_selector('#btn') + clear_btn = self.wait_for_element_by_css_selector('#clear-btn') + mem = self.wait_for_element_by_css_selector('#memory-output') + + for i in range(1, 11): + click_btn.click() + time.sleep(1) + + click_data = self.driver.execute_script(clicked_getter) + self.assertEqual(i, click_data.get('clicked')) + self.assertEqual(i, int(json.loads(mem.text).get('clicked'))) + + clear_btn.click() + time.sleep(1) + + cleared_data = self.driver.execute_script(clicked_getter) + self.assertTrue(cleared_data is None) + # Did mem also got cleared ? + self.assertFalse(mem.text) + + # Test initial timestamp output + init_btn = self.wait_for_element_by_css_selector('#set-init-storage') + init_btn.click() + ts = int(time.time() * 1000) + time.sleep(1) + self.driver.refresh() + time.sleep(2) + init = self.wait_for_element_by_css_selector('#init-output') + init = json.loads(init.text) + self.assertAlmostEqual(ts, init.get('ts'), delta=1000) + self.assertEqual('initialized', init.get('data')) + + def test_store_nested_data(self): + app = dash.Dash(__name__) + + nested = {'nested': {'nest': 'much'}} + nested_list = dict(my_list=[1, 2, 3]) + + app.layout = html.Div([ + dcc.Store(id='store', storage_type='local'), + html.Button('set object as key', id='obj-btn'), + html.Button('set list as key', id='list-btn'), + html.Output(id='output') + ]) + + @app.callback(Output('store', 'data'), + [Input('obj-btn', 'n_clicks_timestamp'), + Input('list-btn', 'n_clicks_timestamp')]) + def on_obj_click(obj_ts, list_ts): + if obj_ts is None and list_ts is None: + raise PreventUpdate + + # python 3 got the default props bug. plotly/dash#396 + if (obj_ts and not list_ts) or obj_ts > list_ts: + return nested + else: + return nested_list + + @app.callback(Output('output', 'children'), + [Input('store', 'modified_timestamp')], + [State('store', 'data')]) + def on_ts(ts, data): + if ts is None: + raise PreventUpdate + return json.dumps(data) + + self.startServer(app) + + obj_btn = self.wait_for_element_by_css_selector('#obj-btn') + list_btn = self.wait_for_element_by_css_selector('#list-btn') + + obj_btn.click() + time.sleep(1) + self.wait_for_text_to_equal('#output', json.dumps(nested)) + # it would of crashed the app before adding the recursive check. + + list_btn.click() + time.sleep(1) + self.wait_for_text_to_equal('#output', json.dumps(nested_list)) + + def test_user_supplied_css(self): + app = dash.Dash(__name__) + + app.layout = html.Div(className="test-input-css", children=[dcc.Input()]) + + self.startServer(app) + + self.wait_for_element_by_css_selector('.test-input-css') + self.snapshot('styled input - width: 100%, border-color: hotpink') + + def test_logout_btn(self): + app = dash.Dash(__name__) + + @app.server.route('/_logout', methods=['POST']) + def on_logout(): + rep = flask.redirect('/logged-out') + rep.set_cookie('logout-cookie', '', 0) + return rep + + app.layout = html.Div([ + html.H2('Logout test'), + dcc.Location(id='location'), + html.Div(id='content'), + ]) + + @app.callback(Output('content', 'children'), + [Input('location', 'pathname')]) + def on_location(location_path): + if location_path is None: + raise PreventUpdate + + if 'logged-out' in location_path: + return 'Logged out' + else: + + @flask.after_this_request + def _insert_cookie(rep): + rep.set_cookie('logout-cookie', 'logged-in') + return rep + + return dcc.LogoutButton(id='logout-btn', logout_url='/_logout') + + self.startServer(app) + time.sleep(1) + self.snapshot('Logout button') + + self.assertEqual( + 'logged-in', + self.driver.get_cookie('logout-cookie')['value']) + logout_button = self.wait_for_element_by_css_selector('#logout-btn') + logout_button.click() + self.wait_for_text_to_equal('#content', 'Logged out') + + self.assertFalse(self.driver.get_cookie('logout-cookie')) + + def test_state_and_inputs(self): + 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'), + inputs=[Input('input', 'value')], + state=[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') # noqa: E731 + input = lambda: self.driver.find_element_by_id('input') # noqa: E731 + state = lambda: self.driver.find_element_by_id('state') # noqa: E731 + + # callback gets called with initial input + wait_for(lambda: call_count.value == 1) + 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_simple_callback(self): + app = dash.Dash(__name__) + app.layout = html.Div([ + dcc.Input( + id='input', + ), + 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) + + input1 = self.wait_for_element_by_css_selector('#input') + input1.send_keys('hello world') + output1 = self.wait_for_element_by_css_selector('#output-1') + self.wait_for_text_to_equal('#output-1', 'hello world') + output1.click() # Lose focus, no callback sent for value. + + self.assertEqual( + call_count.value, + # an initial call to retrieve the first value + # plus one for each hello world character + 1 + len('hello world') + ) + + def test_store_type_updates(self): + app = dash.Dash(__name__) + + types = [ + ('str', 'hello'), + ('number', 1), + ('dict', {'data': [2, 3, None]}), + ('list', [5, 6, 7]), + ('null', None), + ('bool', True), + ('bool', False), + ('empty-dict', {}), + ] + types_changes = list( + itertools.chain(*itertools.combinations(types, 2)) + ) + [ # No combinations as it add much test time. + ('list-dict-1', [1, 2, {'data': [55, 66, 77], 'dummy': 'dum'}]), + ('list-dict-2', [1, 2, {'data': [111, 99, 88]}]), + ('dict-3', {'a': 1, 'c': 1}), + ('dict-2', {'a': 1, 'b': None}), + ] + + app.layout = html.Div([ + html.Div(id='output'), + html.Button('click', id='click'), + dcc.Store(id='store') + ]) + + @app.callback(Output('output', 'children'), + [Input('store', 'modified_timestamp')], + [State('store', 'data')]) + def on_data(ts, data): + if ts is None: + raise PreventUpdate + + return json.dumps(data) + + @app.callback(Output('store', 'data'), [Input('click', 'n_clicks')]) + def on_click(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return types_changes[n_clicks - 1][1] + + self.startServer(app) + + button = self.wait_for_element_by_css_selector('#click') + + for i, type_change in enumerate(types_changes): + button.click() + try: + self.wait_for_text_to_equal( + '#output', json.dumps(type_change[1]), + ) + except TimeoutException: + raise Exception( + 'Output type did not change from {} to {}'.format( + types_changes[i - 1], + type_change + ) + ) def test_disabled_tab(self): app = dash.Dash(__name__) From b0f22e2cf6bc2fff7c35a4b9554fdfd77e43282f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Thu, 13 Jun 2019 23:02:53 -0400 Subject: [PATCH 7/8] remove SyntaxHighlighter from tests --- test/test_integration.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/test_integration.py b/test/test_integration.py index 81727dce9..05da27f55 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -951,13 +951,18 @@ def test_gallery(self): '''.replace(' ', '')), dcc.Markdown(['# Line one', '## Line two']), dcc.Markdown(), - dcc.SyntaxHighlighter(dedent('''import python - print(3)'''), language='python'), - dcc.SyntaxHighlighter([ + dcc.Markdown(''' + ```py + import python + print(3) + ```'''), + dcc.Markdown([ + '```py' 'import python', - 'print(3)' - ], language='python'), - dcc.SyntaxHighlighter() + 'print(3)', + '```' + ]), + dcc.Markdown() ]) self.startServer(app) From a1c36d37443c9ff6c65a2e90e7567cffd6bee00f Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Fri, 14 Jun 2019 09:00:23 -0400 Subject: [PATCH 8/8] do not highlight inline code, and fix code highlight test --- src/components/Markdown.react.js | 2 +- test/test_integration.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Markdown.react.js b/src/components/Markdown.react.js index b945b35e8..3b98d720f 100644 --- a/src/components/Markdown.react.js +++ b/src/components/Markdown.react.js @@ -31,7 +31,7 @@ class DashMarkdown extends Component { return; } if (this.mdContainer) { - const nodes = this.mdContainer.getElementsByTagName('code'); + const nodes = this.mdContainer.querySelectorAll('pre code'); for (let i = 0; i < nodes.length; i++) { window.hljs.highlightBlock(nodes[i]); diff --git a/test/test_integration.py b/test/test_integration.py index 05da27f55..310c4a3f3 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -957,7 +957,7 @@ def test_gallery(self): print(3) ```'''), dcc.Markdown([ - '```py' + '```py', 'import python', 'print(3)', '```'