From f5e577e82fe936609b3ffd9c74775d5a7effce13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Nov 2019 15:53:30 -0500 Subject: [PATCH 01/14] failing callback test with not-loaded async component --- .../callbacks/test_basic_callback.py | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index c94d29dd1c..48fd53a7b6 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -4,8 +4,10 @@ import dash_core_components as dcc import dash_html_components as html +import dash_table import dash -from dash.dependencies import Input, Output +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate def test_cbsc001_simple_callback(dash_duo): @@ -140,3 +142,35 @@ def update_input(value): dash_duo.percy_snapshot(name="callback-generating-function-2") assert dash_duo.get_logs() == [], "console is clean" + + +def test_cbsc003_callback_with_unloaded_async_component(dash_duo): + app = dash.Dash() + app.layout = html.Div( + children=[ + dcc.Tabs( + children=[ + dcc.Tab( + children=[ + html.Button(id="btn", children="Update Input"), + html.Div(id="output", children=["Hello"]), + ] + ), + dcc.Tab(children=dash_table.DataTable(id="other-table")), + ] + ) + ] + ) + + @app.callback(Output("output", "children"), [Input("btn", "n_clicks")]) + def update_graph(n_clicks): + if n_clicks is None: + raise PreventUpdate + + return "Bye" + + dash_duo.start_server(app) + + dash_duo.find_element('#btn').click() + assert dash_duo.find_element('#output').text == "Bye" + assert dash_duo.get_logs() == [] From 6e9f11bddceeba35db8a3a3daf84d9dd49c071ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Nov 2019 16:07:44 -0500 Subject: [PATCH 02/14] Update isAppReady check to check only involved (input, state) component ids --- dash-renderer/src/actions/index.js | 36 +++++++-- dash-renderer/src/actions/setAppReadyState.js | 73 +++++++++++-------- 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 029220af9c..dd5ad7921f 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -23,6 +23,7 @@ import { slice, sort, type, + uniq, view, } from 'ramda'; import {createAction} from 'redux-actions'; @@ -221,11 +222,12 @@ export function notifyObservers(payload) { return async function(dispatch, getState) { const {id, props, excludedOutputs} = payload; - const {graphs, isAppReady, requestQueue} = getState(); - - if (isAppReady !== true) { - await isAppReady; - } + const { + dependenciesRequest, + graphs, + isAppReady, + requestQueue, + } = getState(); const {InputGraph} = graphs; /* @@ -365,6 +367,30 @@ export function notifyObservers(payload) { } }); + /** + * Determine the id of all components used as input or state in the callbacks + * triggered by the props change. + * + * Wait for all components associated to these ids to be ready before initiating + * the callbacks. + */ + const deps = queuedObservers.map(output => + dependenciesRequest.content.find( + dependency => dependency.output === output + ) + ); + + const ids = uniq( + flatten( + deps.map(dep => [ + dep.inputs.map(input => input.id), + dep.state.map(state => state.id), + ]) + ) + ); + + await isAppReady(ids); + /* * record the set of output IDs that will eventually need to be * updated in a queue. not all of these requests will be fired in this diff --git a/dash-renderer/src/actions/setAppReadyState.js b/dash-renderer/src/actions/setAppReadyState.js index eeccacd8a9..8586f3228e 100644 --- a/dash-renderer/src/actions/setAppReadyState.js +++ b/dash-renderer/src/actions/setAppReadyState.js @@ -9,7 +9,8 @@ import {isReady} from '@plotly/dash-component-plugins'; const isAppReady = layout => { const queue = [layout]; - const res = {}; + const components = {}; + const ids = {}; /* Would be much simpler if the Registry was aware of what it contained... */ while (queue.length) { @@ -18,12 +19,17 @@ const isAppReady = layout => { continue; } + const id = elementLayout.props && elementLayout.props.id; const children = elementLayout.props && elementLayout.props.children; const namespace = elementLayout.namespace; const type = elementLayout.type; - res[namespace] = res[namespace] || {}; - res[namespace][type] = type; + components[namespace] = components[namespace] || {}; + components[namespace][type] = type; + + if (id) { + ids[id] = {namespace, type}; + } if (children) { const filteredChildren = filter( @@ -35,23 +41,42 @@ const isAppReady = layout => { } } - const promises = []; - Object.entries(res).forEach(([namespace, item]) => { - Object.entries(item).forEach(([type]) => { - const component = Registry.resolve({ - namespace, - type, + return targets => { + console.log('isAppReady called with', targets); + const promises = []; + + if (Array.isArray(targets)) { + targets.forEach(id => { + const target = ids[id]; + if (target) { + const component = Registry.resolve(target); + + const ready = isReady(component); + + if (ready && typeof ready.then === 'function') { + promises.push(ready); + } + } }); + } else { + Object.entries(components).forEach(([namespace, item]) => { + Object.entries(item).forEach(([type]) => { + const component = Registry.resolve({ + namespace, + type, + }); - const ready = isReady(component); + const ready = isReady(component); - if (ready && typeof ready.then === 'function') { - promises.push(ready); - } - }); - }); + if (ready && typeof ready.then === 'function') { + promises.push(ready); + } + }); + }); + } - return promises.length ? Promise.all(promises) : true; + return promises.length ? Promise.all(promises) : true; + }; }; const setAction = createAction(getAction('SET_APP_READY')); @@ -59,19 +84,5 @@ const setAction = createAction(getAction('SET_APP_READY')); export default () => async (dispatch, getState) => { const ready = isAppReady(getState().layout); - if (ready === true) { - /* All async is ready */ - dispatch(setAction(true)); - } else { - /* Waiting on async */ - dispatch(setAction(ready)); - await ready; - /** - * All known async is ready. - * - * Callbacks were blocked while waiting, we can safely - * assume that no update to layout happened to invalidate. - */ - dispatch(setAction(true)); - } + dispatch(setAction(ready)); }; From 2ead77994d3ed3ed4adc9f094e37ac3c223f91e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Nov 2019 16:09:52 -0500 Subject: [PATCH 03/14] lint --- dash-renderer/src/actions/setAppReadyState.js | 1 - 1 file changed, 1 deletion(-) diff --git a/dash-renderer/src/actions/setAppReadyState.js b/dash-renderer/src/actions/setAppReadyState.js index 8586f3228e..651e6bb5e3 100644 --- a/dash-renderer/src/actions/setAppReadyState.js +++ b/dash-renderer/src/actions/setAppReadyState.js @@ -42,7 +42,6 @@ const isAppReady = layout => { } return targets => { - console.log('isAppReady called with', targets); const promises = []; if (Array.isArray(targets)) { From 11b8877eece4bb002bbda9843611e050c041fd8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Nov 2019 16:27:14 -0500 Subject: [PATCH 04/14] fix notifyObserver test --- dash-renderer/tests/notifyObservers.test.js | 23 ++++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/dash-renderer/tests/notifyObservers.test.js b/dash-renderer/tests/notifyObservers.test.js index 3c023ab006..a4b48a3d98 100644 --- a/dash-renderer/tests/notifyObservers.test.js +++ b/dash-renderer/tests/notifyObservers.test.js @@ -5,7 +5,9 @@ const WAIT = 1000; describe('notifyObservers', () => { const thunk = notifyObservers({ id: 'id', - props: {}, + props: { + 'prop1': true + }, undefined }); @@ -16,13 +18,16 @@ describe('notifyObservers', () => { () => ({ graphs: { InputGraph: { - hasNode: () => false, - dependenciesOf: () => [], + hasNode: () => true, + dependenciesOf: () => [ + 'id.prop2' + ], dependantsOf: () => [], overallOrder: () => 0 } }, - isAppReady: true, + isAppReady: () => Promise.resolve(true), + paths: {}, requestQueue: [] }) ).then(() => { done = true; }); @@ -33,23 +38,25 @@ describe('notifyObservers', () => { it('waits on app to be ready', async () => { let resolve; - const isAppReady = new Promise(r => { + const isAppReady = () => new Promise(r => { resolve = r; }); - let done = false; thunk( () => { }, () => ({ graphs: { InputGraph: { - hasNode: () => false, - dependenciesOf: () => [], + hasNode: () => true, + dependenciesOf: () => [ + 'id.prop2' + ], dependantsOf: () => [], overallOrder: () => 0 } }, isAppReady, + paths: {}, requestQueue: [] }) ).then(() => { done = true; }); From 340aa26027baf57df50b6bdafcd3a1176f40d421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Sat, 23 Nov 2019 09:33:08 -0500 Subject: [PATCH 05/14] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c01162d74e..a815905269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ clientside JavaScript callbacks via inline strings. ### Fixed - [#1018](https://github.com/plotly/dash/pull/1006) Fix the `dash.testing` **stop** API with process application runner in Python2. Use `kill()` instead of `communicate()` to avoid hanging. +- [#1027](https://github.com/plotly/dash/pull/1027) Fix bug with renderer callback lock never resolving with non-rendered async component using the asyncDecorator ## [1.6.1] - 2019-11-14 ### Fixed From 324d8d6da8b36bbe882f3975dfff844e84945ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 13:03:13 -0500 Subject: [PATCH 06/14] simpify isReady by (1) only allowing the targets case, (2) using paths information --- dash-renderer/src/actions/setAppReadyState.js | 92 +++++-------------- 1 file changed, 24 insertions(+), 68 deletions(-) diff --git a/dash-renderer/src/actions/setAppReadyState.js b/dash-renderer/src/actions/setAppReadyState.js index 651e6bb5e3..ae2594bac4 100644 --- a/dash-renderer/src/actions/setAppReadyState.js +++ b/dash-renderer/src/actions/setAppReadyState.js @@ -1,87 +1,43 @@ -import {filter} from 'ramda'; -import {createAction} from 'redux-actions'; +import { path } from 'ramda'; +import { createAction } from 'redux-actions'; -import isSimpleComponent from '../isSimpleComponent'; import Registry from './../registry'; -import {getAction} from './constants'; -import {isReady} from '@plotly/dash-component-plugins'; +import { getAction } from './constants'; +import { isReady } from '@plotly/dash-component-plugins'; -const isAppReady = layout => { - const queue = [layout]; - - const components = {}; - const ids = {}; - - /* Would be much simpler if the Registry was aware of what it contained... */ - while (queue.length) { - const elementLayout = queue.shift(); - if (!elementLayout) { - continue; - } - - const id = elementLayout.props && elementLayout.props.id; - const children = elementLayout.props && elementLayout.props.children; - const namespace = elementLayout.namespace; - const type = elementLayout.type; - - components[namespace] = components[namespace] || {}; - components[namespace][type] = type; +const isAppReady = (layout, paths) => targets => { + if (!layout || !paths || !Array.isArray(targets)) { + return true; + } - if (id) { - ids[id] = {namespace, type}; + const promises = []; + targets.forEach(id => { + const pathOfId = paths[id]; + if (!pathOfId) { + return; } - if (children) { - const filteredChildren = filter( - child => !isSimpleComponent(child), - Array.isArray(children) ? children : [children] - ); - - queue.push(...filteredChildren); + const target = path(pathOfId, layout); + if (!target) { + return; } - } - - return targets => { - const promises = []; - - if (Array.isArray(targets)) { - targets.forEach(id => { - const target = ids[id]; - if (target) { - const component = Registry.resolve(target); - - const ready = isReady(component); - - if (ready && typeof ready.then === 'function') { - promises.push(ready); - } - } - }); - } else { - Object.entries(components).forEach(([namespace, item]) => { - Object.entries(item).forEach(([type]) => { - const component = Registry.resolve({ - namespace, - type, - }); - const ready = isReady(component); + const component = Registry.resolve(target); + const ready = isReady(component); - if (ready && typeof ready.then === 'function') { - promises.push(ready); - } - }); - }); + if (ready && typeof ready.then === 'function') { + promises.push(ready); } + }); - return promises.length ? Promise.all(promises) : true; - }; + return promises.length ? Promise.all(promises) : true; }; const setAction = createAction(getAction('SET_APP_READY')); export default () => async (dispatch, getState) => { - const ready = isAppReady(getState().layout); + const { layout, paths } = getState(); + const ready = isAppReady(layout, paths); dispatch(setAction(ready)); }; From bd02bffa99946518457b46b81a6845426bdd01a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 13:06:38 -0500 Subject: [PATCH 07/14] format --- dash-renderer/src/actions/setAppReadyState.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dash-renderer/src/actions/setAppReadyState.js b/dash-renderer/src/actions/setAppReadyState.js index ae2594bac4..9723ebf8cb 100644 --- a/dash-renderer/src/actions/setAppReadyState.js +++ b/dash-renderer/src/actions/setAppReadyState.js @@ -1,9 +1,9 @@ -import { path } from 'ramda'; -import { createAction } from 'redux-actions'; +import {path} from 'ramda'; +import {createAction} from 'redux-actions'; import Registry from './../registry'; -import { getAction } from './constants'; -import { isReady } from '@plotly/dash-component-plugins'; +import {getAction} from './constants'; +import {isReady} from '@plotly/dash-component-plugins'; const isAppReady = (layout, paths) => targets => { if (!layout || !paths || !Array.isArray(targets)) { @@ -36,7 +36,7 @@ const isAppReady = (layout, paths) => targets => { const setAction = createAction(getAction('SET_APP_READY')); export default () => async (dispatch, getState) => { - const { layout, paths } = getState(); + const {layout, paths} = getState(); const ready = isAppReady(layout, paths); dispatch(setAction(ready)); From 50f3e587b5e114f2f95ae689ef8e5b5ae3631f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 14:39:07 -0500 Subject: [PATCH 08/14] refactor isAppReady out of state/store --- dash-renderer/src/APIController.react.js | 2 - dash-renderer/src/actions/constants.js | 1 - dash-renderer/src/actions/index.js | 12 ++- dash-renderer/src/actions/isAppReady.js | 28 +++++++ dash-renderer/src/actions/setAppReadyState.js | 43 ----------- dash-renderer/src/reducers/isAppReady.js | 8 -- dash-renderer/src/reducers/reducer.js | 2 - dash-renderer/tests/notifyObservers.test.js | 75 +++++++------------ 8 files changed, 59 insertions(+), 112 deletions(-) create mode 100644 dash-renderer/src/actions/isAppReady.js delete mode 100644 dash-renderer/src/actions/setAppReadyState.js delete mode 100644 dash-renderer/src/reducers/isAppReady.js diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 1d8e7054ed..ae9f84aa93 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -9,7 +9,6 @@ import { computePaths, hydrateInitialOutputs, setLayout, - setAppIsReady, } from './actions/index'; import {applyPersistence} from './persistence'; import apiThunk from './actions/api'; @@ -55,7 +54,6 @@ class UnconnectedContainer extends Component { dispatch ); dispatch(setLayout(finalLayout)); - dispatch(setAppIsReady()); } else if (isNil(paths)) { dispatch(computePaths({subTree: layout, startingPath: []})); } diff --git a/dash-renderer/src/actions/constants.js b/dash-renderer/src/actions/constants.js index 5438c8b4dc..37866de19d 100644 --- a/dash-renderer/src/actions/constants.js +++ b/dash-renderer/src/actions/constants.js @@ -8,7 +8,6 @@ const actionList = { SET_CONFIG: 'SET_CONFIG', ON_ERROR: 'ON_ERROR', SET_HOOKS: 'SET_HOOKS', - SET_APP_READY: 'SET_APP_READY', }; export const getAction = action => { diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index dd5ad7921f..c6c9b6c772 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -34,7 +34,8 @@ import cookie from 'cookie'; import {uid, urlBase, isMultiOutputProp, parseMultipleOutputs} from '../utils'; import {STATUS} from '../constants/constants'; import {applyPersistence, prunePersistence} from '../persistence'; -import setAppIsReady from './setAppReadyState'; + +import isAppReady from './isAppReady'; export const updateProps = createAction(getAction('ON_PROP_CHANGE')); export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE')); @@ -46,8 +47,6 @@ export const setHooks = createAction(getAction('SET_HOOKS')); export const setLayout = createAction(getAction('SET_LAYOUT')); export const onError = createAction(getAction('ON_ERROR')); -export {setAppIsReady}; - export function hydrateInitialOutputs() { return function(dispatch, getState) { triggerDefaultState(dispatch, getState); @@ -225,7 +224,8 @@ export function notifyObservers(payload) { const { dependenciesRequest, graphs, - isAppReady, + layout, + paths, requestQueue, } = getState(); @@ -389,7 +389,7 @@ export function notifyObservers(payload) { ) ); - await isAppReady(ids); + await isAppReady(layout, paths, ids); /* * record the set of output IDs that will eventually need to be @@ -976,8 +976,6 @@ function updateOutput( ); }); } - - dispatch(setAppIsReady()); } }; if (multi) { diff --git a/dash-renderer/src/actions/isAppReady.js b/dash-renderer/src/actions/isAppReady.js new file mode 100644 index 0000000000..fefe70456a --- /dev/null +++ b/dash-renderer/src/actions/isAppReady.js @@ -0,0 +1,28 @@ +import { path } from 'ramda'; +import { isReady } from '@plotly/dash-component-plugins'; + +import Registry from '../registry'; + +export default (layout, paths, targets) => { + const promises = []; + targets.forEach(id => { + const pathOfId = paths[id]; + if (!pathOfId) { + return; + } + + const target = path(pathOfId, layout); + if (!target) { + return; + } + + const component = Registry.resolve(target); + const ready = isReady(component); + + if (ready && typeof ready.then === 'function') { + promises.push(ready); + } + }); + + return promises.length ? Promise.all(promises) : true; +}; diff --git a/dash-renderer/src/actions/setAppReadyState.js b/dash-renderer/src/actions/setAppReadyState.js deleted file mode 100644 index 9723ebf8cb..0000000000 --- a/dash-renderer/src/actions/setAppReadyState.js +++ /dev/null @@ -1,43 +0,0 @@ -import {path} from 'ramda'; -import {createAction} from 'redux-actions'; - -import Registry from './../registry'; -import {getAction} from './constants'; -import {isReady} from '@plotly/dash-component-plugins'; - -const isAppReady = (layout, paths) => targets => { - if (!layout || !paths || !Array.isArray(targets)) { - return true; - } - - const promises = []; - targets.forEach(id => { - const pathOfId = paths[id]; - if (!pathOfId) { - return; - } - - const target = path(pathOfId, layout); - if (!target) { - return; - } - - const component = Registry.resolve(target); - const ready = isReady(component); - - if (ready && typeof ready.then === 'function') { - promises.push(ready); - } - }); - - return promises.length ? Promise.all(promises) : true; -}; - -const setAction = createAction(getAction('SET_APP_READY')); - -export default () => async (dispatch, getState) => { - const {layout, paths} = getState(); - const ready = isAppReady(layout, paths); - - dispatch(setAction(ready)); -}; diff --git a/dash-renderer/src/reducers/isAppReady.js b/dash-renderer/src/reducers/isAppReady.js deleted file mode 100644 index 2dd86ece71..0000000000 --- a/dash-renderer/src/reducers/isAppReady.js +++ /dev/null @@ -1,8 +0,0 @@ -import {getAction} from '../actions/constants'; - -export default function config(state = false, action) { - if (action.type === getAction('SET_APP_READY')) { - return action.payload; - } - return state; -} diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 22af71779b..1123134675 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -10,7 +10,6 @@ import { view, } from 'ramda'; import {combineReducers} from 'redux'; -import isAppReady from './isAppReady'; import layout from './layout'; import graphs from './dependencyGraph'; import paths from './paths'; @@ -32,7 +31,6 @@ export const apiRequests = [ function mainReducer() { const parts = { appLifecycle, - isAppReady, layout, graphs, paths, diff --git a/dash-renderer/tests/notifyObservers.test.js b/dash-renderer/tests/notifyObservers.test.js index a4b48a3d98..9f9da60bf1 100644 --- a/dash-renderer/tests/notifyObservers.test.js +++ b/dash-renderer/tests/notifyObservers.test.js @@ -1,65 +1,43 @@ -import { notifyObservers } from "../src/actions"; +import isAppReady from "../src/actions/isAppReady"; const WAIT = 1000; describe('notifyObservers', () => { - const thunk = notifyObservers({ - id: 'id', - props: { - 'prop1': true - }, - undefined + let resolve; + beforeEach(() => { + const promise = new Promise(r => { + resolve = r; + }); + + window.__components = { + a: { _dashprivate_isLazyComponentReady: promise }, + b: {} + }; }); it('executes if app is ready', async () => { let done = false; - thunk( - () => { }, - () => ({ - graphs: { - InputGraph: { - hasNode: () => true, - dependenciesOf: () => [ - 'id.prop2' - ], - dependantsOf: () => [], - overallOrder: () => 0 - } - }, - isAppReady: () => Promise.resolve(true), - paths: {}, - requestQueue: [] - }) - ).then(() => { done = true; }); + Promise.resolve(isAppReady( + [{ namespace: '__components', type: 'b', props: { id: 'comp1' } }], + { comp1: [0] }, + ['comp1'] + )).then(() => { + done = true + }); - await new Promise(r => setTimeout(r, 0)); + await new Promise(r => setTimeout(r, WAIT)); expect(done).toEqual(true); }); it('waits on app to be ready', async () => { - let resolve; - const isAppReady = () => new Promise(r => { - resolve = r; - }); let done = false; - thunk( - () => { }, - () => ({ - graphs: { - InputGraph: { - hasNode: () => true, - dependenciesOf: () => [ - 'id.prop2' - ], - dependantsOf: () => [], - overallOrder: () => 0 - } - }, - isAppReady, - paths: {}, - requestQueue: [] - }) - ).then(() => { done = true; }); + Promise.resolve(isAppReady( + [{ namespace: '__components', type: 'a', props: { id: 'comp1' } }], + { comp1: [0] }, + ['comp1'] + )).then(() => { + done = true + }); await new Promise(r => setTimeout(r, WAIT)); expect(done).toEqual(false); @@ -69,5 +47,4 @@ describe('notifyObservers', () => { await new Promise(r => setTimeout(r, WAIT)); expect(done).toEqual(true); }); - }); \ No newline at end of file From 0e2c54a4ea901c8558419dd0f4d4754159086d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 14:41:41 -0500 Subject: [PATCH 09/14] lint --- dash-renderer/src/actions/isAppReady.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/actions/isAppReady.js b/dash-renderer/src/actions/isAppReady.js index fefe70456a..c83019da54 100644 --- a/dash-renderer/src/actions/isAppReady.js +++ b/dash-renderer/src/actions/isAppReady.js @@ -1,5 +1,5 @@ -import { path } from 'ramda'; -import { isReady } from '@plotly/dash-component-plugins'; +import {path} from 'ramda'; +import {isReady} from '@plotly/dash-component-plugins'; import Registry from '../registry'; From 796db0c9203d692fdab1ddb0a0dafe765baa9b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 15:12:45 -0500 Subject: [PATCH 10/14] diff testing vs. dcc with plotly.js 1.51.2 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f1d5e193cb..2259012ae7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -109,7 +109,7 @@ jobs: command: | . venv/bin/activate && pip install --no-cache-dir --upgrade -e . --progress-bar off && mkdir packages cd dash-renderer && renderer build && python setup.py sdist && mv dist/* ../packages/ && cd .. - git clone --depth 1 https://github.com/plotly/dash-core-components.git + git clone --depth 1 --branch v1.5.1 https://github.com/plotly/dash-core-components.git cd dash-core-components && npm ci && npm run build && python setup.py sdist && mv dist/* ../packages/ && cd .. git clone --depth 1 https://github.com/plotly/dash-renderer-test-components cd dash-renderer-test-components && npm ci && npm run build:all && python setup.py sdist && mv dist/* ../packages/ && cd .. From c0aaab2a9d17d5c8ed88501b3a18b810fc8b9276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 15:28:12 -0500 Subject: [PATCH 11/14] plotly.js 1.51.2 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2259012ae7..f1d5e193cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -109,7 +109,7 @@ jobs: command: | . venv/bin/activate && pip install --no-cache-dir --upgrade -e . --progress-bar off && mkdir packages cd dash-renderer && renderer build && python setup.py sdist && mv dist/* ../packages/ && cd .. - git clone --depth 1 --branch v1.5.1 https://github.com/plotly/dash-core-components.git + git clone --depth 1 https://github.com/plotly/dash-core-components.git cd dash-core-components && npm ci && npm run build && python setup.py sdist && mv dist/* ../packages/ && cd .. git clone --depth 1 https://github.com/plotly/dash-renderer-test-components cd dash-renderer-test-components && npm ci && npm run build:all && python setup.py sdist && mv dist/* ../packages/ && cd .. From 73ce153e5ba60f0f5e1a1d2fe35c752c2e7425f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 15:50:03 -0500 Subject: [PATCH 12/14] update test json --- tests/integration/test_render.py | 1592 +++++++++++++++--------------- 1 file changed, 796 insertions(+), 796 deletions(-) diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index ea1915bc7f..f6af77af62 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -91,812 +91,812 @@ def check_undo_redo_exist(self, has_undo, has_redo): for el, text in zip(els, texts): self.assertEqual(el.text, text) - def test_undo_redo(self): - app = Dash(__name__, show_undo_redo=True) - app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + # def test_undo_redo(self): + # app = Dash(__name__, show_undo_redo=True) + # app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) - @app.callback(Output('b', 'children'), [Input('a', 'value')]) - def set_b(a): - return a + # @app.callback(Output('b', 'children'), [Input('a', 'value')]) + # def set_b(a): + # return a - self.startServer(app) - - a = self.wait_for_element_by_css_selector('#a') - a.send_keys('xyz') - - self.wait_for_text_to_equal('#b', 'xyz') - self.check_undo_redo_exist(True, False) - - self.click_undo() - self.wait_for_text_to_equal('#b', 'xy') - self.check_undo_redo_exist(True, True) - - self.click_undo() - self.wait_for_text_to_equal('#b', 'x') - self.check_undo_redo_exist(True, True) - - self.click_redo() - self.wait_for_text_to_equal('#b', 'xy') - self.check_undo_redo_exist(True, True) - - self.percy_snapshot(name='undo-redo') - - self.click_undo() - self.click_undo() - self.wait_for_text_to_equal('#b', '') - self.check_undo_redo_exist(False, True) - - def test_no_undo_redo(self): - app = Dash(__name__) - app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) - - @app.callback(Output('b', 'children'), [Input('a', 'value')]) - def set_b(a): - return a - - self.startServer(app) - - a = self.wait_for_element_by_css_selector('#a') - a.send_keys('xyz') - - self.wait_for_text_to_equal('#b', 'xyz') - toolbar = self.driver.find_elements_by_css_selector('._dash-undo-redo') - self.assertEqual(len(toolbar), 0) - - def test_array_of_falsy_child(self): - app = Dash(__name__) - app.layout = html.Div(id='nully-wrapper', children=[0]) - - self.startServer(app) - - self.wait_for_text_to_equal('#nully-wrapper', '0') - - self.assertTrue(self.is_console_clean()) - - def test_of_falsy_child(self): - app = Dash(__name__) - app.layout = html.Div(id='nully-wrapper', children=0) - - self.startServer(app) - - self.wait_for_text_to_equal('#nully-wrapper', '0') - - self.assertTrue(self.is_console_clean()) - - def test_event_properties(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button('Click Me', id='button'), - html.Div(id='output') - ]) - - call_count = Value('i', 0) - - @app.callback(Output('output', 'children'), - [Input('button', 'n_clicks')]) - def update_output(n_clicks): - if not n_clicks: - raise PreventUpdate - call_count.value += 1 - return 'Click' - - self.startServer(app) - btn = self.driver.find_element_by_id('button') - output = lambda: self.driver.find_element_by_id('output') - self.assertEqual(call_count.value, 0) - self.assertEqual(output().text, '') - - btn.click() - wait_for(lambda: output().text == 'Click') - self.assertEqual(call_count.value, 1) - - def test_chained_dependencies_direct_lineage(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='input-1', value='input 1'), - dcc.Input(id='input-2'), - html.Div('test', id='output') - ]) - input1 = lambda: self.driver.find_element_by_id('input-1') - input2 = lambda: self.driver.find_element_by_id('input-2') - output = lambda: self.driver.find_element_by_id('output') - - call_counts = { - 'output': Value('i', 0), - 'input-2': Value('i', 0) - } - - @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) - def update_input(input1): - call_counts['input-2'].value += 1 - return '<<{}>>'.format(input1) - - @app.callback(Output('output', 'children'), [ - Input('input-1', 'value'), - Input('input-2', 'value') - ]) - def update_output(input1, input2): - call_counts['output'].value += 1 - return '{} + {}'.format(input1, input2) - - self.startServer(app) - - wait_for(lambda: call_counts['output'].value == 1) - wait_for(lambda: call_counts['input-2'].value == 1) - self.assertEqual(input1().get_attribute('value'), 'input 1') - self.assertEqual(input2().get_attribute('value'), '<>') - self.assertEqual(output().text, 'input 1 + <>') - - input1().send_keys('x') - wait_for(lambda: call_counts['output'].value == 2) - wait_for(lambda: call_counts['input-2'].value == 2) - self.assertEqual(input1().get_attribute('value'), 'input 1x') - self.assertEqual(input2().get_attribute('value'), '<>') - self.assertEqual(output().text, 'input 1x + <>') - - input2().send_keys('y') - wait_for(lambda: call_counts['output'].value == 3) - wait_for(lambda: call_counts['input-2'].value == 2) - self.assertEqual(input1().get_attribute('value'), 'input 1x') - self.assertEqual(input2().get_attribute('value'), '<>y') - self.assertEqual(output().text, 'input 1x + <>y') - - def test_chained_dependencies_branched_lineage(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id='grandparent', value='input 1'), - dcc.Input(id='parent-a'), - dcc.Input(id='parent-b'), - html.Div(id='child-a'), - html.Div(id='child-b') - ]) - parenta = lambda: self.driver.find_element_by_id('parent-a') - parentb = lambda: self.driver.find_element_by_id('parent-b') - childa = lambda: self.driver.find_element_by_id('child-a') - childb = lambda: self.driver.find_element_by_id('child-b') - - call_counts = { - 'parent-a': Value('i', 0), - 'parent-b': Value('i', 0), - 'child-a': Value('i', 0), - 'child-b': Value('i', 0) - } - - @app.callback(Output('parent-a', 'value'), - [Input('grandparent', 'value')]) - def update_parenta(value): - call_counts['parent-a'].value += 1 - return 'a: {}'.format(value) - - @app.callback(Output('parent-b', 'value'), - [Input('grandparent', 'value')]) - def update_parentb(value): - time.sleep(0.5) - call_counts['parent-b'].value += 1 - return 'b: {}'.format(value) - - @app.callback(Output('child-a', 'children'), - [Input('parent-a', 'value'), - Input('parent-b', 'value')]) - def update_childa(parenta_value, parentb_value): - time.sleep(1) - call_counts['child-a'].value += 1 - return '{} + {}'.format(parenta_value, parentb_value) - - @app.callback(Output('child-b', 'children'), - [Input('parent-a', 'value'), - Input('parent-b', 'value'), - Input('grandparent', 'value')]) - def update_childb(parenta_value, parentb_value, grandparent_value): - call_counts['child-b'].value += 1 - return '{} + {} + {}'.format( - parenta_value, - parentb_value, - grandparent_value - ) - - self.startServer(app) + # self.startServer(app) - wait_for(lambda: childa().text == 'a: input 1 + b: input 1') - wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') - time.sleep(1) # wait for potential requests of app to settle down - self.assertEqual(parenta().get_attribute('value'), 'a: input 1') - self.assertEqual(parentb().get_attribute('value'), 'b: input 1') - self.assertEqual(call_counts['parent-a'].value, 1) - self.assertEqual(call_counts['parent-b'].value, 1) - self.assertEqual(call_counts['child-a'].value, 1) - self.assertEqual(call_counts['child-b'].value, 1) - - def test_removing_component_while_its_getting_updated(self): - app = Dash(__name__) - app.layout = html.Div([ - dcc.RadioItems( - id='toc', - options=[ - {'label': i, 'value': i} for i in ['1', '2'] - ], - value='1' - ), - html.Div(id='body') - ]) - app.config.suppress_callback_exceptions = True + # a = self.wait_for_element_by_css_selector('#a') + # a.send_keys('xyz') - call_counts = { - 'body': Value('i', 0), - 'button-output': Value('i', 0) - } + # self.wait_for_text_to_equal('#b', 'xyz') + # self.check_undo_redo_exist(True, False) - @app.callback(Output('body', 'children'), [Input('toc', 'value')]) - def update_body(chapter): - call_counts['body'].value += 1 - if chapter == '1': - return [ - html.Div('Chapter 1'), - html.Button( - 'clicking this button takes forever', - id='button' - ), - html.Div(id='button-output') - ] - elif chapter == '2': - return 'Chapter 2' - else: - raise Exception('chapter is {}'.format(chapter)) - - @app.callback( - Output('button-output', 'children'), - [Input('button', 'n_clicks')]) - def this_callback_takes_forever(n_clicks): - if not n_clicks: - # initial value is quick, only new value is slow - # also don't let the initial value increment call_counts - return 'Initial Value' - time.sleep(5) - call_counts['button-output'].value += 1 - return 'New value!' - - body = lambda: self.driver.find_element_by_id('body') - self.startServer(app) - - wait_for(lambda: call_counts['body'].value == 1) - time.sleep(0.5) - self.driver.find_element_by_id('button').click() + # self.click_undo() + # self.wait_for_text_to_equal('#b', 'xy') + # self.check_undo_redo_exist(True, True) - # while that callback is resolving, switch the chapter, - # hiding the `button-output` tag - def chapter2_assertions(): - wait_for(lambda: body().text == 'Chapter 2') + # self.click_undo() + # self.wait_for_text_to_equal('#b', 'x') + # self.check_undo_redo_exist(True, True) - layout = self.driver.execute_script( - 'return JSON.parse(JSON.stringify(' - 'window.store.getState().layout' - '))' - ) - - dcc_radio = layout['props']['children'][0] - html_body = layout['props']['children'][1] - - self.assertEqual(dcc_radio['props']['id'], 'toc') - self.assertEqual(dcc_radio['props']['value'], '2') - - self.assertEqual(html_body['props']['id'], 'body') - self.assertEqual(html_body['props']['children'], 'Chapter 2') - - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() - chapter2_assertions() - self.assertEqual(call_counts['button-output'].value, 0) - time.sleep(5) - wait_for(lambda: call_counts['button-output'].value == 1) - time.sleep(2) # liberally wait for the front-end to process request - chapter2_assertions() - self.assertTrue(self.is_console_clean()) - - def test_rendering_layout_calls_callback_once_per_output(self): - app = Dash(__name__) - call_count = Value('i', 0) - - app.config['suppress_callback_exceptions'] = True - app.layout = html.Div([ - html.Div([ - dcc.Input( - value='Input {}'.format(i), - id='input-{}'.format(i) - ) - for i in range(10) - ]), - html.Div(id='container'), - dcc.RadioItems() - ]) - - @app.callback( - Output('container', 'children'), - [Input('input-{}'.format(i), 'value') for i in range(10)]) - def dynamic_output(*args): - call_count.value += 1 - return json.dumps(args, indent=2) - - self.startServer(app) - - time.sleep(5) - - self.percy_snapshot( - name='test_rendering_layout_calls_callback_once_per_output' - ) - - self.assertEqual(call_count.value, 1) - - def test_rendering_new_content_calls_callback_once_per_output(self): - app = Dash(__name__) - call_count = Value('i', 0) - - app.config['suppress_callback_exceptions'] = True - app.layout = html.Div([ - html.Button( - id='display-content', - children='Display Content', - n_clicks=0 - ), - html.Div(id='container'), - dcc.RadioItems() - ]) - - @app.callback( - Output('container', 'children'), - [Input('display-content', 'n_clicks')]) - def display_output(n_clicks): - if n_clicks == 0: - return '' - return html.Div([ - html.Div([ - dcc.Input( - value='Input {}'.format(i), - id='input-{}'.format(i) - ) - for i in range(10) - ]), - html.Div(id='dynamic-output') - ]) - - @app.callback( - Output('dynamic-output', 'children'), - [Input('input-{}'.format(i), 'value') for i in range(10)]) - def dynamic_output(*args): - call_count.value += 1 - return json.dumps(args, indent=2) - - self.startServer(app) - - self.wait_for_element_by_css_selector('#display-content').click() - - time.sleep(5) - - self.percy_snapshot( - name='test_rendering_new_content_calls_callback_once_per_output' - ) - - self.assertEqual(call_count.value, 1) - - def test_callbacks_called_multiple_times_and_out_of_order_multi_output(self): - app = Dash(__name__) - app.layout = html.Div([ - html.Button(id='input', n_clicks=0), - html.Div(id='output1'), - html.Div(id='output2') - ]) + # self.click_redo() + # self.wait_for_text_to_equal('#b', 'xy') + # self.check_undo_redo_exist(True, True) - call_count = Value('i', 0) - - @app.callback( - [Output('output1', 'children'), - Output('output2', 'children')], - [Input('input', 'n_clicks')] - ) - def update_output(n_clicks): - call_count.value = call_count.value + 1 - if n_clicks == 1: - time.sleep(4) - return n_clicks, n_clicks + 1 - - self.startServer(app) - button = self.wait_for_element_by_css_selector('#input') - button.click() - button.click() - time.sleep(8) - self.percy_snapshot( - name='test_callbacks_called_multiple_times' - '_and_out_of_order_multi_output' - ) - self.assertEqual(call_count.value, 3) - self.wait_for_text_to_equal('#output1', '2') - self.wait_for_text_to_equal('#output2', '3') - request_queue = self.driver.execute_script( - 'return window.store.getState().requestQueue' - ) - self.assertFalse(request_queue[0]['rejected']) - self.assertEqual(len(request_queue), 1) - - def test_callbacks_with_shared_grandparent(self): - app = dash.Dash() - - app.layout = html.Div([ - html.Div(id='session-id', children='id'), - dcc.Dropdown(id='dropdown-1'), - dcc.Dropdown(id='dropdown-2'), - ]) - - options = [{'value': 'a', 'label': 'a'}] - - call_counts = { - 'dropdown_1': Value('i', 0), - 'dropdown_2': Value('i', 0) - } - - @app.callback( - Output('dropdown-1', 'options'), - [Input('dropdown-1', 'value'), - Input('session-id', 'children')]) - def dropdown_1(value, session_id): - call_counts['dropdown_1'].value += 1 - return options - - @app.callback( - Output('dropdown-2', 'options'), - [Input('dropdown-2', 'value'), - Input('session-id', 'children')]) - def dropdown_2(value, session_id): - call_counts['dropdown_2'].value += 1 - return options - - self.startServer(app) - - self.wait_for_element_by_css_selector('#session-id') - time.sleep(2) - self.assertEqual(call_counts['dropdown_1'].value, 1) - self.assertEqual(call_counts['dropdown_2'].value, 1) - - self.assertTrue(self.is_console_clean()) - - def test_callbacks_triggered_on_generated_output(self): - app = dash.Dash() - app.config['suppress_callback_exceptions'] = True - - call_counts = { - 'tab1': Value('i', 0), - 'tab2': Value('i', 0) - } - - app.layout = html.Div([ - dcc.Dropdown( - id='outer-controls', - options=[{'label': i, 'value': i} for i in ['a', 'b']], - value='a' - ), - dcc.RadioItems( - options=[ - {'label': 'Tab 1', 'value': 1}, - {'label': 'Tab 2', 'value': 2} - ], - value=1, - id='tabs', - ), - html.Div(id='tab-output') - ]) - - @app.callback(Output('tab-output', 'children'), - [Input('tabs', 'value')]) - def display_content(value): - return html.Div([ - html.Div(id='tab-{}-output'.format(value)) - ]) - - @app.callback(Output('tab-1-output', 'children'), - [Input('outer-controls', 'value')]) - def display_tab1_output(value): - call_counts['tab1'].value += 1 - return 'Selected "{}" in tab 1'.format(value) - - @app.callback(Output('tab-2-output', 'children'), - [Input('outer-controls', 'value')]) - def display_tab2_output(value): - call_counts['tab2'].value += 1 - return 'Selected "{}" in tab 2'.format(value) - - self.startServer(app) - self.wait_for_element_by_css_selector('#tab-output') - time.sleep(2) - - self.assertEqual(call_counts['tab1'].value, 1) - self.assertEqual(call_counts['tab2'].value, 0) - self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 1') - self.wait_for_text_to_equal('#tab-1-output', 'Selected "a" in tab 1') - - (self.driver.find_elements_by_css_selector( - 'input[type="radio"]' - )[1]).click() - time.sleep(2) - - self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 2') - self.wait_for_text_to_equal('#tab-2-output', 'Selected "a" in tab 2') - self.assertEqual(call_counts['tab1'].value, 1) - self.assertEqual(call_counts['tab2'].value, 1) - - self.assertTrue(self.is_console_clean()) - - def test_initialization_with_overlapping_outputs(self): - app = dash.Dash() - app.layout = html.Div([ - - html.Div(id='input-1', children='input-1'), - html.Div(id='input-2', children='input-2'), - html.Div(id='input-3', children='input-3'), - html.Div(id='input-4', children='input-4'), - html.Div(id='input-5', children='input-5'), - - html.Div(id='output-1'), - html.Div(id='output-2'), - html.Div(id='output-3'), - html.Div(id='output-4'), - - ]) - call_counts = { - 'output-1': Value('i', 0), - 'output-2': Value('i', 0), - 'output-3': Value('i', 0), - 'output-4': Value('i', 0), - } - - def generate_callback(outputid): - def callback(*args): - call_counts[outputid].value += 1 - return '{}, {}'.format(*args) - return callback - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - app.callback( - Output(outputid, 'children'), - [ - Input('input-{}'.format(i), 'children'), - Input('input-{}'.format(i + 1), 'children') - ] - )(generate_callback(outputid)) - - self.startServer(app) - - self.wait_for_element_by_css_selector('#output-1') - time.sleep(5) - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - self.assertEqual(call_counts[outputid].value, 1) - self.wait_for_text_to_equal( - '#{}'.format(outputid), - "input-{}, input-{}".format(i, i + 1) - ) - - def test_generate_overlapping_outputs(self): - app = dash.Dash() - app.config['suppress_callback_exceptions'] = True - block = html.Div([ - - html.Div(id='input-1', children='input-1'), - html.Div(id='input-2', children='input-2'), - html.Div(id='input-3', children='input-3'), - html.Div(id='input-4', children='input-4'), - html.Div(id='input-5', children='input-5'), - - html.Div(id='output-1'), - html.Div(id='output-2'), - html.Div(id='output-3'), - html.Div(id='output-4'), - - ]) - app.layout = html.Div([ - html.Div(id='input'), - html.Div(id='container') - ]) - - call_counts = { - 'container': Value('i', 0), - 'output-1': Value('i', 0), - 'output-2': Value('i', 0), - 'output-3': Value('i', 0), - 'output-4': Value('i', 0), - } - - @app.callback(Output('container', 'children'), - [Input('input', 'children')]) - def display_output(*args): - call_counts['container'].value += 1 - return block - - def generate_callback(outputid): - def callback(*args): - call_counts[outputid].value += 1 - return '{}, {}'.format(*args) - return callback - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - app.callback( - Output(outputid, 'children'), - [Input('input-{}'.format(i), 'children'), - Input('input-{}'.format(i + 1), 'children')] - )(generate_callback(outputid)) - - self.startServer(app) - - wait_for(lambda: call_counts['container'].value == 1) - self.wait_for_element_by_css_selector('#output-1') - time.sleep(5) - - for i in range(1, 5): - outputid = 'output-{}'.format(i) - self.assertEqual(call_counts[outputid].value, 1) - self.wait_for_text_to_equal( - '#{}'.format(outputid), - "input-{}, input-{}".format(i, i + 1) - ) - self.assertEqual(call_counts['container'].value, 1) - - def test_multiple_properties_update_at_same_time_on_same_component(self): - call_count = Value('i', 0) - timestamp_1 = Value('d', -5) - timestamp_2 = Value('d', -5) - - app = dash.Dash() - app.layout = html.Div([ - html.Div(id='container'), - html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1), - html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1) - ]) - - @app.callback( - Output('container', 'children'), - [Input('button-1', 'n_clicks'), - Input('button-1', 'n_clicks_timestamp'), - Input('button-2', 'n_clicks'), - Input('button-2', 'n_clicks_timestamp')]) - def update_output(*args): - call_count.value += 1 - timestamp_1.value = args[1] - timestamp_2.value = args[3] - return '{}, {}'.format(args[0], args[2]) - - self.startServer(app) - - self.wait_for_element_by_css_selector('#container') - time.sleep(2) - self.wait_for_text_to_equal('#container', '0, 0') - self.assertEqual(timestamp_1.value, -1) - self.assertEqual(timestamp_2.value, -1) - self.assertEqual(call_count.value, 1) - self.percy_snapshot('button initialization 1') - - self.driver.find_element_by_css_selector('#button-1').click() - time.sleep(2) - self.wait_for_text_to_equal('#container', '1, 0') - self.assertTrue( - timestamp_1.value > - ((time.time() - (24 * 60 * 60)) * 1000)) - self.assertEqual(timestamp_2.value, -1) - self.assertEqual(call_count.value, 2) - self.percy_snapshot('button-1 click') - prev_timestamp_1 = timestamp_1.value - - self.driver.find_element_by_css_selector('#button-2').click() - time.sleep(2) - self.wait_for_text_to_equal('#container', '1, 1') - self.assertEqual(timestamp_1.value, prev_timestamp_1) - self.assertTrue( - timestamp_2.value > - ((time.time() - 24 * 60 * 60) * 1000)) - self.assertEqual(call_count.value, 3) - self.percy_snapshot('button-2 click') - prev_timestamp_2 = timestamp_2.value - - self.driver.find_element_by_css_selector('#button-2').click() - time.sleep(2) - self.wait_for_text_to_equal('#container', '1, 2') - self.assertEqual(timestamp_1.value, prev_timestamp_1) - self.assertTrue( - timestamp_2.value > - prev_timestamp_2) - self.assertTrue(timestamp_2.value > timestamp_1.value) - self.assertEqual(call_count.value, 4) - self.percy_snapshot('button-2 click again') - - def test_request_hooks(self): - app = Dash(__name__) - - app.index_string = ''' - - - {%metas%} - {%title%} - {%favicon%} - {%css%} - - -
Testing custom DashRenderer
- {%app_entry%} -
- {%config%} - {%scripts%} - -
-
With request hooks
- - ''' - - app.layout = html.Div([ - dcc.Input( - id='input', - value='initial value' - ), - html.Div( - html.Div([ - html.Div(id='output-1'), - html.Div(id='output-pre'), - html.Div(id='output-pre-payload'), - html.Div(id='output-post'), - html.Div(id='output-post-payload'), - html.Div(id='output-post-response') - ]) - ) - ]) - - @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - def update_output(value): - return value - - self.startServer(app) + # self.percy_snapshot(name='undo-redo') - input1 = self.wait_for_element_by_css_selector('#input') - initialValue = input1.get_attribute('value') + # self.click_undo() + # self.click_undo() + # self.wait_for_text_to_equal('#b', '') + # self.check_undo_redo_exist(False, True) - action = ActionChains(self.driver) - action.click(input1) - action = action.send_keys(Keys.BACKSPACE * len(initialValue)) + # def test_no_undo_redo(self): + # app = Dash(__name__) + # app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) - action.send_keys('fire request hooks').perform() + # @app.callback(Output('b', 'children'), [Input('a', 'value')]) + # def set_b(a): + # return a - self.wait_for_text_to_equal('#output-1', 'fire request hooks') - self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') - self.wait_for_text_to_equal('#output-pre-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') - self.wait_for_text_to_equal('#output-post-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - self.wait_for_text_to_equal('#output-post-response', '{"props":{"children":"fire request hooks"}}') - self.percy_snapshot(name='request-hooks render') + # self.startServer(app) + + # a = self.wait_for_element_by_css_selector('#a') + # a.send_keys('xyz') + + # self.wait_for_text_to_equal('#b', 'xyz') + # toolbar = self.driver.find_elements_by_css_selector('._dash-undo-redo') + # self.assertEqual(len(toolbar), 0) + + # def test_array_of_falsy_child(self): + # app = Dash(__name__) + # app.layout = html.Div(id='nully-wrapper', children=[0]) + + # self.startServer(app) + + # self.wait_for_text_to_equal('#nully-wrapper', '0') + + # self.assertTrue(self.is_console_clean()) + + # def test_of_falsy_child(self): + # app = Dash(__name__) + # app.layout = html.Div(id='nully-wrapper', children=0) + + # self.startServer(app) + + # self.wait_for_text_to_equal('#nully-wrapper', '0') + + # self.assertTrue(self.is_console_clean()) + + # def test_event_properties(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # html.Button('Click Me', id='button'), + # html.Div(id='output') + # ]) + + # call_count = Value('i', 0) + + # @app.callback(Output('output', 'children'), + # [Input('button', 'n_clicks')]) + # def update_output(n_clicks): + # if not n_clicks: + # raise PreventUpdate + # call_count.value += 1 + # return 'Click' + + # self.startServer(app) + # btn = self.driver.find_element_by_id('button') + # output = lambda: self.driver.find_element_by_id('output') + # self.assertEqual(call_count.value, 0) + # self.assertEqual(output().text, '') + + # btn.click() + # wait_for(lambda: output().text == 'Click') + # self.assertEqual(call_count.value, 1) + + # def test_chained_dependencies_direct_lineage(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input(id='input-1', value='input 1'), + # dcc.Input(id='input-2'), + # html.Div('test', id='output') + # ]) + # input1 = lambda: self.driver.find_element_by_id('input-1') + # input2 = lambda: self.driver.find_element_by_id('input-2') + # output = lambda: self.driver.find_element_by_id('output') + + # call_counts = { + # 'output': Value('i', 0), + # 'input-2': Value('i', 0) + # } + + # @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) + # def update_input(input1): + # call_counts['input-2'].value += 1 + # return '<<{}>>'.format(input1) + + # @app.callback(Output('output', 'children'), [ + # Input('input-1', 'value'), + # Input('input-2', 'value') + # ]) + # def update_output(input1, input2): + # call_counts['output'].value += 1 + # return '{} + {}'.format(input1, input2) + + # self.startServer(app) + + # wait_for(lambda: call_counts['output'].value == 1) + # wait_for(lambda: call_counts['input-2'].value == 1) + # self.assertEqual(input1().get_attribute('value'), 'input 1') + # self.assertEqual(input2().get_attribute('value'), '<>') + # self.assertEqual(output().text, 'input 1 + <>') + + # input1().send_keys('x') + # wait_for(lambda: call_counts['output'].value == 2) + # wait_for(lambda: call_counts['input-2'].value == 2) + # self.assertEqual(input1().get_attribute('value'), 'input 1x') + # self.assertEqual(input2().get_attribute('value'), '<>') + # self.assertEqual(output().text, 'input 1x + <>') + + # input2().send_keys('y') + # wait_for(lambda: call_counts['output'].value == 3) + # wait_for(lambda: call_counts['input-2'].value == 2) + # self.assertEqual(input1().get_attribute('value'), 'input 1x') + # self.assertEqual(input2().get_attribute('value'), '<>y') + # self.assertEqual(output().text, 'input 1x + <>y') + + # def test_chained_dependencies_branched_lineage(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.Input(id='grandparent', value='input 1'), + # dcc.Input(id='parent-a'), + # dcc.Input(id='parent-b'), + # html.Div(id='child-a'), + # html.Div(id='child-b') + # ]) + # parenta = lambda: self.driver.find_element_by_id('parent-a') + # parentb = lambda: self.driver.find_element_by_id('parent-b') + # childa = lambda: self.driver.find_element_by_id('child-a') + # childb = lambda: self.driver.find_element_by_id('child-b') + + # call_counts = { + # 'parent-a': Value('i', 0), + # 'parent-b': Value('i', 0), + # 'child-a': Value('i', 0), + # 'child-b': Value('i', 0) + # } + + # @app.callback(Output('parent-a', 'value'), + # [Input('grandparent', 'value')]) + # def update_parenta(value): + # call_counts['parent-a'].value += 1 + # return 'a: {}'.format(value) + + # @app.callback(Output('parent-b', 'value'), + # [Input('grandparent', 'value')]) + # def update_parentb(value): + # time.sleep(0.5) + # call_counts['parent-b'].value += 1 + # return 'b: {}'.format(value) + + # @app.callback(Output('child-a', 'children'), + # [Input('parent-a', 'value'), + # Input('parent-b', 'value')]) + # def update_childa(parenta_value, parentb_value): + # time.sleep(1) + # call_counts['child-a'].value += 1 + # return '{} + {}'.format(parenta_value, parentb_value) + + # @app.callback(Output('child-b', 'children'), + # [Input('parent-a', 'value'), + # Input('parent-b', 'value'), + # Input('grandparent', 'value')]) + # def update_childb(parenta_value, parentb_value, grandparent_value): + # call_counts['child-b'].value += 1 + # return '{} + {} + {}'.format( + # parenta_value, + # parentb_value, + # grandparent_value + # ) + + # self.startServer(app) + + # wait_for(lambda: childa().text == 'a: input 1 + b: input 1') + # wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') + # time.sleep(1) # wait for potential requests of app to settle down + # self.assertEqual(parenta().get_attribute('value'), 'a: input 1') + # self.assertEqual(parentb().get_attribute('value'), 'b: input 1') + # self.assertEqual(call_counts['parent-a'].value, 1) + # self.assertEqual(call_counts['parent-b'].value, 1) + # self.assertEqual(call_counts['child-a'].value, 1) + # self.assertEqual(call_counts['child-b'].value, 1) + + # def test_removing_component_while_its_getting_updated(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # dcc.RadioItems( + # id='toc', + # options=[ + # {'label': i, 'value': i} for i in ['1', '2'] + # ], + # value='1' + # ), + # html.Div(id='body') + # ]) + # app.config.suppress_callback_exceptions = True + + # call_counts = { + # 'body': Value('i', 0), + # 'button-output': Value('i', 0) + # } + + # @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + # def update_body(chapter): + # call_counts['body'].value += 1 + # if chapter == '1': + # return [ + # html.Div('Chapter 1'), + # html.Button( + # 'clicking this button takes forever', + # id='button' + # ), + # html.Div(id='button-output') + # ] + # elif chapter == '2': + # return 'Chapter 2' + # else: + # raise Exception('chapter is {}'.format(chapter)) + + # @app.callback( + # Output('button-output', 'children'), + # [Input('button', 'n_clicks')]) + # def this_callback_takes_forever(n_clicks): + # if not n_clicks: + # # initial value is quick, only new value is slow + # # also don't let the initial value increment call_counts + # return 'Initial Value' + # time.sleep(5) + # call_counts['button-output'].value += 1 + # return 'New value!' + + # body = lambda: self.driver.find_element_by_id('body') + # self.startServer(app) + + # wait_for(lambda: call_counts['body'].value == 1) + # time.sleep(0.5) + # self.driver.find_element_by_id('button').click() + + # # while that callback is resolving, switch the chapter, + # # hiding the `button-output` tag + # def chapter2_assertions(): + # wait_for(lambda: body().text == 'Chapter 2') + + # layout = self.driver.execute_script( + # 'return JSON.parse(JSON.stringify(' + # 'window.store.getState().layout' + # '))' + # ) + + # dcc_radio = layout['props']['children'][0] + # html_body = layout['props']['children'][1] + + # self.assertEqual(dcc_radio['props']['id'], 'toc') + # self.assertEqual(dcc_radio['props']['value'], '2') + + # self.assertEqual(html_body['props']['id'], 'body') + # self.assertEqual(html_body['props']['children'], 'Chapter 2') + + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[1]).click() + # chapter2_assertions() + # self.assertEqual(call_counts['button-output'].value, 0) + # time.sleep(5) + # wait_for(lambda: call_counts['button-output'].value == 1) + # time.sleep(2) # liberally wait for the front-end to process request + # chapter2_assertions() + # self.assertTrue(self.is_console_clean()) + + # def test_rendering_layout_calls_callback_once_per_output(self): + # app = Dash(__name__) + # call_count = Value('i', 0) + + # app.config['suppress_callback_exceptions'] = True + # app.layout = html.Div([ + # html.Div([ + # dcc.Input( + # value='Input {}'.format(i), + # id='input-{}'.format(i) + # ) + # for i in range(10) + # ]), + # html.Div(id='container'), + # dcc.RadioItems() + # ]) + + # @app.callback( + # Output('container', 'children'), + # [Input('input-{}'.format(i), 'value') for i in range(10)]) + # def dynamic_output(*args): + # call_count.value += 1 + # return json.dumps(args, indent=2) + + # self.startServer(app) + + # time.sleep(5) + + # self.percy_snapshot( + # name='test_rendering_layout_calls_callback_once_per_output' + # ) + + # self.assertEqual(call_count.value, 1) + + # def test_rendering_new_content_calls_callback_once_per_output(self): + # app = Dash(__name__) + # call_count = Value('i', 0) + + # app.config['suppress_callback_exceptions'] = True + # app.layout = html.Div([ + # html.Button( + # id='display-content', + # children='Display Content', + # n_clicks=0 + # ), + # html.Div(id='container'), + # dcc.RadioItems() + # ]) + + # @app.callback( + # Output('container', 'children'), + # [Input('display-content', 'n_clicks')]) + # def display_output(n_clicks): + # if n_clicks == 0: + # return '' + # return html.Div([ + # html.Div([ + # dcc.Input( + # value='Input {}'.format(i), + # id='input-{}'.format(i) + # ) + # for i in range(10) + # ]), + # html.Div(id='dynamic-output') + # ]) + + # @app.callback( + # Output('dynamic-output', 'children'), + # [Input('input-{}'.format(i), 'value') for i in range(10)]) + # def dynamic_output(*args): + # call_count.value += 1 + # return json.dumps(args, indent=2) + + # self.startServer(app) + + # self.wait_for_element_by_css_selector('#display-content').click() + + # time.sleep(5) + + # self.percy_snapshot( + # name='test_rendering_new_content_calls_callback_once_per_output' + # ) + + # self.assertEqual(call_count.value, 1) + + # def test_callbacks_called_multiple_times_and_out_of_order_multi_output(self): + # app = Dash(__name__) + # app.layout = html.Div([ + # html.Button(id='input', n_clicks=0), + # html.Div(id='output1'), + # html.Div(id='output2') + # ]) + + # call_count = Value('i', 0) + + # @app.callback( + # [Output('output1', 'children'), + # Output('output2', 'children')], + # [Input('input', 'n_clicks')] + # ) + # def update_output(n_clicks): + # call_count.value = call_count.value + 1 + # if n_clicks == 1: + # time.sleep(4) + # return n_clicks, n_clicks + 1 + + # self.startServer(app) + # button = self.wait_for_element_by_css_selector('#input') + # button.click() + # button.click() + # time.sleep(8) + # self.percy_snapshot( + # name='test_callbacks_called_multiple_times' + # '_and_out_of_order_multi_output' + # ) + # self.assertEqual(call_count.value, 3) + # self.wait_for_text_to_equal('#output1', '2') + # self.wait_for_text_to_equal('#output2', '3') + # request_queue = self.driver.execute_script( + # 'return window.store.getState().requestQueue' + # ) + # self.assertFalse(request_queue[0]['rejected']) + # self.assertEqual(len(request_queue), 1) + + # def test_callbacks_with_shared_grandparent(self): + # app = dash.Dash() + + # app.layout = html.Div([ + # html.Div(id='session-id', children='id'), + # dcc.Dropdown(id='dropdown-1'), + # dcc.Dropdown(id='dropdown-2'), + # ]) + + # options = [{'value': 'a', 'label': 'a'}] + + # call_counts = { + # 'dropdown_1': Value('i', 0), + # 'dropdown_2': Value('i', 0) + # } + + # @app.callback( + # Output('dropdown-1', 'options'), + # [Input('dropdown-1', 'value'), + # Input('session-id', 'children')]) + # def dropdown_1(value, session_id): + # call_counts['dropdown_1'].value += 1 + # return options + + # @app.callback( + # Output('dropdown-2', 'options'), + # [Input('dropdown-2', 'value'), + # Input('session-id', 'children')]) + # def dropdown_2(value, session_id): + # call_counts['dropdown_2'].value += 1 + # return options + + # self.startServer(app) + + # self.wait_for_element_by_css_selector('#session-id') + # time.sleep(2) + # self.assertEqual(call_counts['dropdown_1'].value, 1) + # self.assertEqual(call_counts['dropdown_2'].value, 1) + + # self.assertTrue(self.is_console_clean()) + + # def test_callbacks_triggered_on_generated_output(self): + # app = dash.Dash() + # app.config['suppress_callback_exceptions'] = True + + # call_counts = { + # 'tab1': Value('i', 0), + # 'tab2': Value('i', 0) + # } + + # app.layout = html.Div([ + # dcc.Dropdown( + # id='outer-controls', + # options=[{'label': i, 'value': i} for i in ['a', 'b']], + # value='a' + # ), + # dcc.RadioItems( + # options=[ + # {'label': 'Tab 1', 'value': 1}, + # {'label': 'Tab 2', 'value': 2} + # ], + # value=1, + # id='tabs', + # ), + # html.Div(id='tab-output') + # ]) + + # @app.callback(Output('tab-output', 'children'), + # [Input('tabs', 'value')]) + # def display_content(value): + # return html.Div([ + # html.Div(id='tab-{}-output'.format(value)) + # ]) + + # @app.callback(Output('tab-1-output', 'children'), + # [Input('outer-controls', 'value')]) + # def display_tab1_output(value): + # call_counts['tab1'].value += 1 + # return 'Selected "{}" in tab 1'.format(value) + + # @app.callback(Output('tab-2-output', 'children'), + # [Input('outer-controls', 'value')]) + # def display_tab2_output(value): + # call_counts['tab2'].value += 1 + # return 'Selected "{}" in tab 2'.format(value) + + # self.startServer(app) + # self.wait_for_element_by_css_selector('#tab-output') + # time.sleep(2) + + # self.assertEqual(call_counts['tab1'].value, 1) + # self.assertEqual(call_counts['tab2'].value, 0) + # self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 1') + # self.wait_for_text_to_equal('#tab-1-output', 'Selected "a" in tab 1') + + # (self.driver.find_elements_by_css_selector( + # 'input[type="radio"]' + # )[1]).click() + # time.sleep(2) + + # self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 2') + # self.wait_for_text_to_equal('#tab-2-output', 'Selected "a" in tab 2') + # self.assertEqual(call_counts['tab1'].value, 1) + # self.assertEqual(call_counts['tab2'].value, 1) + + # self.assertTrue(self.is_console_clean()) + + # def test_initialization_with_overlapping_outputs(self): + # app = dash.Dash() + # app.layout = html.Div([ + + # html.Div(id='input-1', children='input-1'), + # html.Div(id='input-2', children='input-2'), + # html.Div(id='input-3', children='input-3'), + # html.Div(id='input-4', children='input-4'), + # html.Div(id='input-5', children='input-5'), + + # html.Div(id='output-1'), + # html.Div(id='output-2'), + # html.Div(id='output-3'), + # html.Div(id='output-4'), + + # ]) + # call_counts = { + # 'output-1': Value('i', 0), + # 'output-2': Value('i', 0), + # 'output-3': Value('i', 0), + # 'output-4': Value('i', 0), + # } + + # def generate_callback(outputid): + # def callback(*args): + # call_counts[outputid].value += 1 + # return '{}, {}'.format(*args) + # return callback + + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # app.callback( + # Output(outputid, 'children'), + # [ + # Input('input-{}'.format(i), 'children'), + # Input('input-{}'.format(i + 1), 'children') + # ] + # )(generate_callback(outputid)) + + # self.startServer(app) + + # self.wait_for_element_by_css_selector('#output-1') + # time.sleep(5) + + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # self.assertEqual(call_counts[outputid].value, 1) + # self.wait_for_text_to_equal( + # '#{}'.format(outputid), + # "input-{}, input-{}".format(i, i + 1) + # ) + + # def test_generate_overlapping_outputs(self): + # app = dash.Dash() + # app.config['suppress_callback_exceptions'] = True + # block = html.Div([ + + # html.Div(id='input-1', children='input-1'), + # html.Div(id='input-2', children='input-2'), + # html.Div(id='input-3', children='input-3'), + # html.Div(id='input-4', children='input-4'), + # html.Div(id='input-5', children='input-5'), + + # html.Div(id='output-1'), + # html.Div(id='output-2'), + # html.Div(id='output-3'), + # html.Div(id='output-4'), + + # ]) + # app.layout = html.Div([ + # html.Div(id='input'), + # html.Div(id='container') + # ]) + + # call_counts = { + # 'container': Value('i', 0), + # 'output-1': Value('i', 0), + # 'output-2': Value('i', 0), + # 'output-3': Value('i', 0), + # 'output-4': Value('i', 0), + # } + + # @app.callback(Output('container', 'children'), + # [Input('input', 'children')]) + # def display_output(*args): + # call_counts['container'].value += 1 + # return block + + # def generate_callback(outputid): + # def callback(*args): + # call_counts[outputid].value += 1 + # return '{}, {}'.format(*args) + # return callback + + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # app.callback( + # Output(outputid, 'children'), + # [Input('input-{}'.format(i), 'children'), + # Input('input-{}'.format(i + 1), 'children')] + # )(generate_callback(outputid)) + + # self.startServer(app) + + # wait_for(lambda: call_counts['container'].value == 1) + # self.wait_for_element_by_css_selector('#output-1') + # time.sleep(5) + + # for i in range(1, 5): + # outputid = 'output-{}'.format(i) + # self.assertEqual(call_counts[outputid].value, 1) + # self.wait_for_text_to_equal( + # '#{}'.format(outputid), + # "input-{}, input-{}".format(i, i + 1) + # ) + # self.assertEqual(call_counts['container'].value, 1) + + # def test_multiple_properties_update_at_same_time_on_same_component(self): + # call_count = Value('i', 0) + # timestamp_1 = Value('d', -5) + # timestamp_2 = Value('d', -5) + + # app = dash.Dash() + # app.layout = html.Div([ + # html.Div(id='container'), + # html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1), + # html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1) + # ]) + + # @app.callback( + # Output('container', 'children'), + # [Input('button-1', 'n_clicks'), + # Input('button-1', 'n_clicks_timestamp'), + # Input('button-2', 'n_clicks'), + # Input('button-2', 'n_clicks_timestamp')]) + # def update_output(*args): + # call_count.value += 1 + # timestamp_1.value = args[1] + # timestamp_2.value = args[3] + # return '{}, {}'.format(args[0], args[2]) + + # self.startServer(app) + + # self.wait_for_element_by_css_selector('#container') + # time.sleep(2) + # self.wait_for_text_to_equal('#container', '0, 0') + # self.assertEqual(timestamp_1.value, -1) + # self.assertEqual(timestamp_2.value, -1) + # self.assertEqual(call_count.value, 1) + # self.percy_snapshot('button initialization 1') + + # self.driver.find_element_by_css_selector('#button-1').click() + # time.sleep(2) + # self.wait_for_text_to_equal('#container', '1, 0') + # self.assertTrue( + # timestamp_1.value > + # ((time.time() - (24 * 60 * 60)) * 1000)) + # self.assertEqual(timestamp_2.value, -1) + # self.assertEqual(call_count.value, 2) + # self.percy_snapshot('button-1 click') + # prev_timestamp_1 = timestamp_1.value + + # self.driver.find_element_by_css_selector('#button-2').click() + # time.sleep(2) + # self.wait_for_text_to_equal('#container', '1, 1') + # self.assertEqual(timestamp_1.value, prev_timestamp_1) + # self.assertTrue( + # timestamp_2.value > + # ((time.time() - 24 * 60 * 60) * 1000)) + # self.assertEqual(call_count.value, 3) + # self.percy_snapshot('button-2 click') + # prev_timestamp_2 = timestamp_2.value + + # self.driver.find_element_by_css_selector('#button-2').click() + # time.sleep(2) + # self.wait_for_text_to_equal('#container', '1, 2') + # self.assertEqual(timestamp_1.value, prev_timestamp_1) + # self.assertTrue( + # timestamp_2.value > + # prev_timestamp_2) + # self.assertTrue(timestamp_2.value > timestamp_1.value) + # self.assertEqual(call_count.value, 4) + # self.percy_snapshot('button-2 click again') + + # def test_request_hooks(self): + # app = Dash(__name__) + + # app.index_string = ''' + # + # + # {%metas%} + # {%title%} + # {%favicon%} + # {%css%} + # + # + #
Testing custom DashRenderer
+ # {%app_entry%} + #
+ # {%config%} + # {%scripts%} + # + #
+ #
With request hooks
+ # + # ''' + + # app.layout = html.Div([ + # dcc.Input( + # id='input', + # value='initial value' + # ), + # html.Div( + # html.Div([ + # html.Div(id='output-1'), + # html.Div(id='output-pre'), + # html.Div(id='output-pre-payload'), + # html.Div(id='output-post'), + # html.Div(id='output-post-payload'), + # html.Div(id='output-post-response') + # ]) + # ) + # ]) + + # @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + # def update_output(value): + # return value + + # self.startServer(app) + + # input1 = self.wait_for_element_by_css_selector('#input') + # initialValue = input1.get_attribute('value') + + # action = ActionChains(self.driver) + # action.click(input1) + # action = action.send_keys(Keys.BACKSPACE * len(initialValue)) + + # action.send_keys('fire request hooks').perform() + + # self.wait_for_text_to_equal('#output-1', 'fire request hooks') + # self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') + # self.wait_for_text_to_equal('#output-pre-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') + # self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') + # self.wait_for_text_to_equal('#output-post-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') + # self.wait_for_text_to_equal('#output-post-response', '{"props":{"children":"fire request hooks"}}') + # self.percy_snapshot(name='request-hooks render') def test_graphs_in_tabs_do_not_share_state(self): app = dash.Dash() @@ -972,11 +972,11 @@ def render_content(tab): )[0].click() graph_1_expected_clickdata = { - "points": [{"curveNumber": 0, "pointNumber": 1, "pointIndex": 1, "x": 2, "y": 10}] + "points": [{"curveNumber": 0, "pointNumber": 1, "pointIndex": 1, "x": 2, "y": 10, "label": 2, "value": 10}] } graph_2_expected_clickdata = { - "points": [{"curveNumber": 0, "pointNumber": 1, "pointIndex": 1, "x": 3, "y": 10}] + "points": [{"curveNumber": 0, "pointNumber": 1, "pointIndex": 1, "x": 3, "y": 10, "label": 3, "value": 10}] } self.wait_for_text_to_equal('#graph1_info', json.dumps(graph_1_expected_clickdata)) From 7a68d139d2a469821f63e10a49e9fae6ed16ee3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 15:53:22 -0500 Subject: [PATCH 13/14] uncomment!! --- tests/integration/test_render.py | 1588 +++++++++++++++--------------- 1 file changed, 794 insertions(+), 794 deletions(-) diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index f6af77af62..04e4bbc3c2 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -91,812 +91,812 @@ def check_undo_redo_exist(self, has_undo, has_redo): for el, text in zip(els, texts): self.assertEqual(el.text, text) - # def test_undo_redo(self): - # app = Dash(__name__, show_undo_redo=True) - # app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + def test_undo_redo(self): + app = Dash(__name__, show_undo_redo=True) + app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) - # @app.callback(Output('b', 'children'), [Input('a', 'value')]) - # def set_b(a): - # return a + @app.callback(Output('b', 'children'), [Input('a', 'value')]) + def set_b(a): + return a - # self.startServer(app) + self.startServer(app) + + a = self.wait_for_element_by_css_selector('#a') + a.send_keys('xyz') + + self.wait_for_text_to_equal('#b', 'xyz') + self.check_undo_redo_exist(True, False) + + self.click_undo() + self.wait_for_text_to_equal('#b', 'xy') + self.check_undo_redo_exist(True, True) + + self.click_undo() + self.wait_for_text_to_equal('#b', 'x') + self.check_undo_redo_exist(True, True) + + self.click_redo() + self.wait_for_text_to_equal('#b', 'xy') + self.check_undo_redo_exist(True, True) + + self.percy_snapshot(name='undo-redo') + + self.click_undo() + self.click_undo() + self.wait_for_text_to_equal('#b', '') + self.check_undo_redo_exist(False, True) + + def test_no_undo_redo(self): + app = Dash(__name__) + app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + + @app.callback(Output('b', 'children'), [Input('a', 'value')]) + def set_b(a): + return a + + self.startServer(app) + + a = self.wait_for_element_by_css_selector('#a') + a.send_keys('xyz') + + self.wait_for_text_to_equal('#b', 'xyz') + toolbar = self.driver.find_elements_by_css_selector('._dash-undo-redo') + self.assertEqual(len(toolbar), 0) + + def test_array_of_falsy_child(self): + app = Dash(__name__) + app.layout = html.Div(id='nully-wrapper', children=[0]) + + self.startServer(app) + + self.wait_for_text_to_equal('#nully-wrapper', '0') + + self.assertTrue(self.is_console_clean()) + + def test_of_falsy_child(self): + app = Dash(__name__) + app.layout = html.Div(id='nully-wrapper', children=0) + + self.startServer(app) + + self.wait_for_text_to_equal('#nully-wrapper', '0') + + self.assertTrue(self.is_console_clean()) + + def test_event_properties(self): + app = Dash(__name__) + app.layout = html.Div([ + html.Button('Click Me', id='button'), + html.Div(id='output') + ]) + + call_count = Value('i', 0) + + @app.callback(Output('output', 'children'), + [Input('button', 'n_clicks')]) + def update_output(n_clicks): + if not n_clicks: + raise PreventUpdate + call_count.value += 1 + return 'Click' + + self.startServer(app) + btn = self.driver.find_element_by_id('button') + output = lambda: self.driver.find_element_by_id('output') + self.assertEqual(call_count.value, 0) + self.assertEqual(output().text, '') + + btn.click() + wait_for(lambda: output().text == 'Click') + self.assertEqual(call_count.value, 1) + + def test_chained_dependencies_direct_lineage(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='input-1', value='input 1'), + dcc.Input(id='input-2'), + html.Div('test', id='output') + ]) + input1 = lambda: self.driver.find_element_by_id('input-1') + input2 = lambda: self.driver.find_element_by_id('input-2') + output = lambda: self.driver.find_element_by_id('output') + + call_counts = { + 'output': Value('i', 0), + 'input-2': Value('i', 0) + } + + @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) + def update_input(input1): + call_counts['input-2'].value += 1 + return '<<{}>>'.format(input1) + + @app.callback(Output('output', 'children'), [ + Input('input-1', 'value'), + Input('input-2', 'value') + ]) + def update_output(input1, input2): + call_counts['output'].value += 1 + return '{} + {}'.format(input1, input2) + + self.startServer(app) + + wait_for(lambda: call_counts['output'].value == 1) + wait_for(lambda: call_counts['input-2'].value == 1) + self.assertEqual(input1().get_attribute('value'), 'input 1') + self.assertEqual(input2().get_attribute('value'), '<>') + self.assertEqual(output().text, 'input 1 + <>') + + input1().send_keys('x') + wait_for(lambda: call_counts['output'].value == 2) + wait_for(lambda: call_counts['input-2'].value == 2) + self.assertEqual(input1().get_attribute('value'), 'input 1x') + self.assertEqual(input2().get_attribute('value'), '<>') + self.assertEqual(output().text, 'input 1x + <>') + + input2().send_keys('y') + wait_for(lambda: call_counts['output'].value == 3) + wait_for(lambda: call_counts['input-2'].value == 2) + self.assertEqual(input1().get_attribute('value'), 'input 1x') + self.assertEqual(input2().get_attribute('value'), '<>y') + self.assertEqual(output().text, 'input 1x + <>y') + + def test_chained_dependencies_branched_lineage(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.Input(id='grandparent', value='input 1'), + dcc.Input(id='parent-a'), + dcc.Input(id='parent-b'), + html.Div(id='child-a'), + html.Div(id='child-b') + ]) + parenta = lambda: self.driver.find_element_by_id('parent-a') + parentb = lambda: self.driver.find_element_by_id('parent-b') + childa = lambda: self.driver.find_element_by_id('child-a') + childb = lambda: self.driver.find_element_by_id('child-b') + + call_counts = { + 'parent-a': Value('i', 0), + 'parent-b': Value('i', 0), + 'child-a': Value('i', 0), + 'child-b': Value('i', 0) + } + + @app.callback(Output('parent-a', 'value'), + [Input('grandparent', 'value')]) + def update_parenta(value): + call_counts['parent-a'].value += 1 + return 'a: {}'.format(value) + + @app.callback(Output('parent-b', 'value'), + [Input('grandparent', 'value')]) + def update_parentb(value): + time.sleep(0.5) + call_counts['parent-b'].value += 1 + return 'b: {}'.format(value) + + @app.callback(Output('child-a', 'children'), + [Input('parent-a', 'value'), + Input('parent-b', 'value')]) + def update_childa(parenta_value, parentb_value): + time.sleep(1) + call_counts['child-a'].value += 1 + return '{} + {}'.format(parenta_value, parentb_value) + + @app.callback(Output('child-b', 'children'), + [Input('parent-a', 'value'), + Input('parent-b', 'value'), + Input('grandparent', 'value')]) + def update_childb(parenta_value, parentb_value, grandparent_value): + call_counts['child-b'].value += 1 + return '{} + {} + {}'.format( + parenta_value, + parentb_value, + grandparent_value + ) + + self.startServer(app) - # a = self.wait_for_element_by_css_selector('#a') - # a.send_keys('xyz') + wait_for(lambda: childa().text == 'a: input 1 + b: input 1') + wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') + time.sleep(1) # wait for potential requests of app to settle down + self.assertEqual(parenta().get_attribute('value'), 'a: input 1') + self.assertEqual(parentb().get_attribute('value'), 'b: input 1') + self.assertEqual(call_counts['parent-a'].value, 1) + self.assertEqual(call_counts['parent-b'].value, 1) + self.assertEqual(call_counts['child-a'].value, 1) + self.assertEqual(call_counts['child-b'].value, 1) + + def test_removing_component_while_its_getting_updated(self): + app = Dash(__name__) + app.layout = html.Div([ + dcc.RadioItems( + id='toc', + options=[ + {'label': i, 'value': i} for i in ['1', '2'] + ], + value='1' + ), + html.Div(id='body') + ]) + app.config.suppress_callback_exceptions = True - # self.wait_for_text_to_equal('#b', 'xyz') - # self.check_undo_redo_exist(True, False) + call_counts = { + 'body': Value('i', 0), + 'button-output': Value('i', 0) + } - # self.click_undo() - # self.wait_for_text_to_equal('#b', 'xy') - # self.check_undo_redo_exist(True, True) + @app.callback(Output('body', 'children'), [Input('toc', 'value')]) + def update_body(chapter): + call_counts['body'].value += 1 + if chapter == '1': + return [ + html.Div('Chapter 1'), + html.Button( + 'clicking this button takes forever', + id='button' + ), + html.Div(id='button-output') + ] + elif chapter == '2': + return 'Chapter 2' + else: + raise Exception('chapter is {}'.format(chapter)) + + @app.callback( + Output('button-output', 'children'), + [Input('button', 'n_clicks')]) + def this_callback_takes_forever(n_clicks): + if not n_clicks: + # initial value is quick, only new value is slow + # also don't let the initial value increment call_counts + return 'Initial Value' + time.sleep(5) + call_counts['button-output'].value += 1 + return 'New value!' + + body = lambda: self.driver.find_element_by_id('body') + self.startServer(app) + + wait_for(lambda: call_counts['body'].value == 1) + time.sleep(0.5) + self.driver.find_element_by_id('button').click() - # self.click_undo() - # self.wait_for_text_to_equal('#b', 'x') - # self.check_undo_redo_exist(True, True) + # while that callback is resolving, switch the chapter, + # hiding the `button-output` tag + def chapter2_assertions(): + wait_for(lambda: body().text == 'Chapter 2') - # self.click_redo() - # self.wait_for_text_to_equal('#b', 'xy') - # self.check_undo_redo_exist(True, True) + layout = self.driver.execute_script( + 'return JSON.parse(JSON.stringify(' + 'window.store.getState().layout' + '))' + ) + + dcc_radio = layout['props']['children'][0] + html_body = layout['props']['children'][1] + + self.assertEqual(dcc_radio['props']['id'], 'toc') + self.assertEqual(dcc_radio['props']['value'], '2') + + self.assertEqual(html_body['props']['id'], 'body') + self.assertEqual(html_body['props']['children'], 'Chapter 2') + + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[1]).click() + chapter2_assertions() + self.assertEqual(call_counts['button-output'].value, 0) + time.sleep(5) + wait_for(lambda: call_counts['button-output'].value == 1) + time.sleep(2) # liberally wait for the front-end to process request + chapter2_assertions() + self.assertTrue(self.is_console_clean()) + + def test_rendering_layout_calls_callback_once_per_output(self): + app = Dash(__name__) + call_count = Value('i', 0) + + app.config['suppress_callback_exceptions'] = True + app.layout = html.Div([ + html.Div([ + dcc.Input( + value='Input {}'.format(i), + id='input-{}'.format(i) + ) + for i in range(10) + ]), + html.Div(id='container'), + dcc.RadioItems() + ]) + + @app.callback( + Output('container', 'children'), + [Input('input-{}'.format(i), 'value') for i in range(10)]) + def dynamic_output(*args): + call_count.value += 1 + return json.dumps(args, indent=2) + + self.startServer(app) + + time.sleep(5) + + self.percy_snapshot( + name='test_rendering_layout_calls_callback_once_per_output' + ) + + self.assertEqual(call_count.value, 1) + + def test_rendering_new_content_calls_callback_once_per_output(self): + app = Dash(__name__) + call_count = Value('i', 0) + + app.config['suppress_callback_exceptions'] = True + app.layout = html.Div([ + html.Button( + id='display-content', + children='Display Content', + n_clicks=0 + ), + html.Div(id='container'), + dcc.RadioItems() + ]) + + @app.callback( + Output('container', 'children'), + [Input('display-content', 'n_clicks')]) + def display_output(n_clicks): + if n_clicks == 0: + return '' + return html.Div([ + html.Div([ + dcc.Input( + value='Input {}'.format(i), + id='input-{}'.format(i) + ) + for i in range(10) + ]), + html.Div(id='dynamic-output') + ]) + + @app.callback( + Output('dynamic-output', 'children'), + [Input('input-{}'.format(i), 'value') for i in range(10)]) + def dynamic_output(*args): + call_count.value += 1 + return json.dumps(args, indent=2) + + self.startServer(app) + + self.wait_for_element_by_css_selector('#display-content').click() + + time.sleep(5) + + self.percy_snapshot( + name='test_rendering_new_content_calls_callback_once_per_output' + ) + + self.assertEqual(call_count.value, 1) + + def test_callbacks_called_multiple_times_and_out_of_order_multi_output(self): + app = Dash(__name__) + app.layout = html.Div([ + html.Button(id='input', n_clicks=0), + html.Div(id='output1'), + html.Div(id='output2') + ]) - # self.percy_snapshot(name='undo-redo') + call_count = Value('i', 0) + + @app.callback( + [Output('output1', 'children'), + Output('output2', 'children')], + [Input('input', 'n_clicks')] + ) + def update_output(n_clicks): + call_count.value = call_count.value + 1 + if n_clicks == 1: + time.sleep(4) + return n_clicks, n_clicks + 1 + + self.startServer(app) + button = self.wait_for_element_by_css_selector('#input') + button.click() + button.click() + time.sleep(8) + self.percy_snapshot( + name='test_callbacks_called_multiple_times' + '_and_out_of_order_multi_output' + ) + self.assertEqual(call_count.value, 3) + self.wait_for_text_to_equal('#output1', '2') + self.wait_for_text_to_equal('#output2', '3') + request_queue = self.driver.execute_script( + 'return window.store.getState().requestQueue' + ) + self.assertFalse(request_queue[0]['rejected']) + self.assertEqual(len(request_queue), 1) + + def test_callbacks_with_shared_grandparent(self): + app = dash.Dash() + + app.layout = html.Div([ + html.Div(id='session-id', children='id'), + dcc.Dropdown(id='dropdown-1'), + dcc.Dropdown(id='dropdown-2'), + ]) + + options = [{'value': 'a', 'label': 'a'}] + + call_counts = { + 'dropdown_1': Value('i', 0), + 'dropdown_2': Value('i', 0) + } + + @app.callback( + Output('dropdown-1', 'options'), + [Input('dropdown-1', 'value'), + Input('session-id', 'children')]) + def dropdown_1(value, session_id): + call_counts['dropdown_1'].value += 1 + return options + + @app.callback( + Output('dropdown-2', 'options'), + [Input('dropdown-2', 'value'), + Input('session-id', 'children')]) + def dropdown_2(value, session_id): + call_counts['dropdown_2'].value += 1 + return options + + self.startServer(app) + + self.wait_for_element_by_css_selector('#session-id') + time.sleep(2) + self.assertEqual(call_counts['dropdown_1'].value, 1) + self.assertEqual(call_counts['dropdown_2'].value, 1) + + self.assertTrue(self.is_console_clean()) + + def test_callbacks_triggered_on_generated_output(self): + app = dash.Dash() + app.config['suppress_callback_exceptions'] = True + + call_counts = { + 'tab1': Value('i', 0), + 'tab2': Value('i', 0) + } + + app.layout = html.Div([ + dcc.Dropdown( + id='outer-controls', + options=[{'label': i, 'value': i} for i in ['a', 'b']], + value='a' + ), + dcc.RadioItems( + options=[ + {'label': 'Tab 1', 'value': 1}, + {'label': 'Tab 2', 'value': 2} + ], + value=1, + id='tabs', + ), + html.Div(id='tab-output') + ]) + + @app.callback(Output('tab-output', 'children'), + [Input('tabs', 'value')]) + def display_content(value): + return html.Div([ + html.Div(id='tab-{}-output'.format(value)) + ]) + + @app.callback(Output('tab-1-output', 'children'), + [Input('outer-controls', 'value')]) + def display_tab1_output(value): + call_counts['tab1'].value += 1 + return 'Selected "{}" in tab 1'.format(value) + + @app.callback(Output('tab-2-output', 'children'), + [Input('outer-controls', 'value')]) + def display_tab2_output(value): + call_counts['tab2'].value += 1 + return 'Selected "{}" in tab 2'.format(value) + + self.startServer(app) + self.wait_for_element_by_css_selector('#tab-output') + time.sleep(2) + + self.assertEqual(call_counts['tab1'].value, 1) + self.assertEqual(call_counts['tab2'].value, 0) + self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 1') + self.wait_for_text_to_equal('#tab-1-output', 'Selected "a" in tab 1') + + (self.driver.find_elements_by_css_selector( + 'input[type="radio"]' + )[1]).click() + time.sleep(2) + + self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 2') + self.wait_for_text_to_equal('#tab-2-output', 'Selected "a" in tab 2') + self.assertEqual(call_counts['tab1'].value, 1) + self.assertEqual(call_counts['tab2'].value, 1) + + self.assertTrue(self.is_console_clean()) + + def test_initialization_with_overlapping_outputs(self): + app = dash.Dash() + app.layout = html.Div([ + + html.Div(id='input-1', children='input-1'), + html.Div(id='input-2', children='input-2'), + html.Div(id='input-3', children='input-3'), + html.Div(id='input-4', children='input-4'), + html.Div(id='input-5', children='input-5'), + + html.Div(id='output-1'), + html.Div(id='output-2'), + html.Div(id='output-3'), + html.Div(id='output-4'), + + ]) + call_counts = { + 'output-1': Value('i', 0), + 'output-2': Value('i', 0), + 'output-3': Value('i', 0), + 'output-4': Value('i', 0), + } + + def generate_callback(outputid): + def callback(*args): + call_counts[outputid].value += 1 + return '{}, {}'.format(*args) + return callback + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + app.callback( + Output(outputid, 'children'), + [ + Input('input-{}'.format(i), 'children'), + Input('input-{}'.format(i + 1), 'children') + ] + )(generate_callback(outputid)) + + self.startServer(app) + + self.wait_for_element_by_css_selector('#output-1') + time.sleep(5) + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + self.assertEqual(call_counts[outputid].value, 1) + self.wait_for_text_to_equal( + '#{}'.format(outputid), + "input-{}, input-{}".format(i, i + 1) + ) + + def test_generate_overlapping_outputs(self): + app = dash.Dash() + app.config['suppress_callback_exceptions'] = True + block = html.Div([ + + html.Div(id='input-1', children='input-1'), + html.Div(id='input-2', children='input-2'), + html.Div(id='input-3', children='input-3'), + html.Div(id='input-4', children='input-4'), + html.Div(id='input-5', children='input-5'), + + html.Div(id='output-1'), + html.Div(id='output-2'), + html.Div(id='output-3'), + html.Div(id='output-4'), + + ]) + app.layout = html.Div([ + html.Div(id='input'), + html.Div(id='container') + ]) + + call_counts = { + 'container': Value('i', 0), + 'output-1': Value('i', 0), + 'output-2': Value('i', 0), + 'output-3': Value('i', 0), + 'output-4': Value('i', 0), + } + + @app.callback(Output('container', 'children'), + [Input('input', 'children')]) + def display_output(*args): + call_counts['container'].value += 1 + return block + + def generate_callback(outputid): + def callback(*args): + call_counts[outputid].value += 1 + return '{}, {}'.format(*args) + return callback + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + app.callback( + Output(outputid, 'children'), + [Input('input-{}'.format(i), 'children'), + Input('input-{}'.format(i + 1), 'children')] + )(generate_callback(outputid)) + + self.startServer(app) + + wait_for(lambda: call_counts['container'].value == 1) + self.wait_for_element_by_css_selector('#output-1') + time.sleep(5) + + for i in range(1, 5): + outputid = 'output-{}'.format(i) + self.assertEqual(call_counts[outputid].value, 1) + self.wait_for_text_to_equal( + '#{}'.format(outputid), + "input-{}, input-{}".format(i, i + 1) + ) + self.assertEqual(call_counts['container'].value, 1) + + def test_multiple_properties_update_at_same_time_on_same_component(self): + call_count = Value('i', 0) + timestamp_1 = Value('d', -5) + timestamp_2 = Value('d', -5) + + app = dash.Dash() + app.layout = html.Div([ + html.Div(id='container'), + html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1), + html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1) + ]) + + @app.callback( + Output('container', 'children'), + [Input('button-1', 'n_clicks'), + Input('button-1', 'n_clicks_timestamp'), + Input('button-2', 'n_clicks'), + Input('button-2', 'n_clicks_timestamp')]) + def update_output(*args): + call_count.value += 1 + timestamp_1.value = args[1] + timestamp_2.value = args[3] + return '{}, {}'.format(args[0], args[2]) + + self.startServer(app) + + self.wait_for_element_by_css_selector('#container') + time.sleep(2) + self.wait_for_text_to_equal('#container', '0, 0') + self.assertEqual(timestamp_1.value, -1) + self.assertEqual(timestamp_2.value, -1) + self.assertEqual(call_count.value, 1) + self.percy_snapshot('button initialization 1') + + self.driver.find_element_by_css_selector('#button-1').click() + time.sleep(2) + self.wait_for_text_to_equal('#container', '1, 0') + self.assertTrue( + timestamp_1.value > + ((time.time() - (24 * 60 * 60)) * 1000)) + self.assertEqual(timestamp_2.value, -1) + self.assertEqual(call_count.value, 2) + self.percy_snapshot('button-1 click') + prev_timestamp_1 = timestamp_1.value + + self.driver.find_element_by_css_selector('#button-2').click() + time.sleep(2) + self.wait_for_text_to_equal('#container', '1, 1') + self.assertEqual(timestamp_1.value, prev_timestamp_1) + self.assertTrue( + timestamp_2.value > + ((time.time() - 24 * 60 * 60) * 1000)) + self.assertEqual(call_count.value, 3) + self.percy_snapshot('button-2 click') + prev_timestamp_2 = timestamp_2.value + + self.driver.find_element_by_css_selector('#button-2').click() + time.sleep(2) + self.wait_for_text_to_equal('#container', '1, 2') + self.assertEqual(timestamp_1.value, prev_timestamp_1) + self.assertTrue( + timestamp_2.value > + prev_timestamp_2) + self.assertTrue(timestamp_2.value > timestamp_1.value) + self.assertEqual(call_count.value, 4) + self.percy_snapshot('button-2 click again') + + def test_request_hooks(self): + app = Dash(__name__) + + app.index_string = ''' + + + {%metas%} + {%title%} + {%favicon%} + {%css%} + + +
Testing custom DashRenderer
+ {%app_entry%} +
+ {%config%} + {%scripts%} + +
+
With request hooks
+ + ''' + + app.layout = html.Div([ + dcc.Input( + id='input', + value='initial value' + ), + html.Div( + html.Div([ + html.Div(id='output-1'), + html.Div(id='output-pre'), + html.Div(id='output-pre-payload'), + html.Div(id='output-post'), + html.Div(id='output-post-payload'), + html.Div(id='output-post-response') + ]) + ) + ]) + + @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) + def update_output(value): + return value + + self.startServer(app) - # self.click_undo() - # self.click_undo() - # self.wait_for_text_to_equal('#b', '') - # self.check_undo_redo_exist(False, True) + input1 = self.wait_for_element_by_css_selector('#input') + initialValue = input1.get_attribute('value') - # def test_no_undo_redo(self): - # app = Dash(__name__) - # app.layout = html.Div([dcc.Input(id='a'), html.Div(id='b')]) + action = ActionChains(self.driver) + action.click(input1) + action = action.send_keys(Keys.BACKSPACE * len(initialValue)) - # @app.callback(Output('b', 'children'), [Input('a', 'value')]) - # def set_b(a): - # return a + action.send_keys('fire request hooks').perform() - # self.startServer(app) - - # a = self.wait_for_element_by_css_selector('#a') - # a.send_keys('xyz') - - # self.wait_for_text_to_equal('#b', 'xyz') - # toolbar = self.driver.find_elements_by_css_selector('._dash-undo-redo') - # self.assertEqual(len(toolbar), 0) - - # def test_array_of_falsy_child(self): - # app = Dash(__name__) - # app.layout = html.Div(id='nully-wrapper', children=[0]) - - # self.startServer(app) - - # self.wait_for_text_to_equal('#nully-wrapper', '0') - - # self.assertTrue(self.is_console_clean()) - - # def test_of_falsy_child(self): - # app = Dash(__name__) - # app.layout = html.Div(id='nully-wrapper', children=0) - - # self.startServer(app) - - # self.wait_for_text_to_equal('#nully-wrapper', '0') - - # self.assertTrue(self.is_console_clean()) - - # def test_event_properties(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # html.Button('Click Me', id='button'), - # html.Div(id='output') - # ]) - - # call_count = Value('i', 0) - - # @app.callback(Output('output', 'children'), - # [Input('button', 'n_clicks')]) - # def update_output(n_clicks): - # if not n_clicks: - # raise PreventUpdate - # call_count.value += 1 - # return 'Click' - - # self.startServer(app) - # btn = self.driver.find_element_by_id('button') - # output = lambda: self.driver.find_element_by_id('output') - # self.assertEqual(call_count.value, 0) - # self.assertEqual(output().text, '') - - # btn.click() - # wait_for(lambda: output().text == 'Click') - # self.assertEqual(call_count.value, 1) - - # def test_chained_dependencies_direct_lineage(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input(id='input-1', value='input 1'), - # dcc.Input(id='input-2'), - # html.Div('test', id='output') - # ]) - # input1 = lambda: self.driver.find_element_by_id('input-1') - # input2 = lambda: self.driver.find_element_by_id('input-2') - # output = lambda: self.driver.find_element_by_id('output') - - # call_counts = { - # 'output': Value('i', 0), - # 'input-2': Value('i', 0) - # } - - # @app.callback(Output('input-2', 'value'), [Input('input-1', 'value')]) - # def update_input(input1): - # call_counts['input-2'].value += 1 - # return '<<{}>>'.format(input1) - - # @app.callback(Output('output', 'children'), [ - # Input('input-1', 'value'), - # Input('input-2', 'value') - # ]) - # def update_output(input1, input2): - # call_counts['output'].value += 1 - # return '{} + {}'.format(input1, input2) - - # self.startServer(app) - - # wait_for(lambda: call_counts['output'].value == 1) - # wait_for(lambda: call_counts['input-2'].value == 1) - # self.assertEqual(input1().get_attribute('value'), 'input 1') - # self.assertEqual(input2().get_attribute('value'), '<>') - # self.assertEqual(output().text, 'input 1 + <>') - - # input1().send_keys('x') - # wait_for(lambda: call_counts['output'].value == 2) - # wait_for(lambda: call_counts['input-2'].value == 2) - # self.assertEqual(input1().get_attribute('value'), 'input 1x') - # self.assertEqual(input2().get_attribute('value'), '<>') - # self.assertEqual(output().text, 'input 1x + <>') - - # input2().send_keys('y') - # wait_for(lambda: call_counts['output'].value == 3) - # wait_for(lambda: call_counts['input-2'].value == 2) - # self.assertEqual(input1().get_attribute('value'), 'input 1x') - # self.assertEqual(input2().get_attribute('value'), '<>y') - # self.assertEqual(output().text, 'input 1x + <>y') - - # def test_chained_dependencies_branched_lineage(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.Input(id='grandparent', value='input 1'), - # dcc.Input(id='parent-a'), - # dcc.Input(id='parent-b'), - # html.Div(id='child-a'), - # html.Div(id='child-b') - # ]) - # parenta = lambda: self.driver.find_element_by_id('parent-a') - # parentb = lambda: self.driver.find_element_by_id('parent-b') - # childa = lambda: self.driver.find_element_by_id('child-a') - # childb = lambda: self.driver.find_element_by_id('child-b') - - # call_counts = { - # 'parent-a': Value('i', 0), - # 'parent-b': Value('i', 0), - # 'child-a': Value('i', 0), - # 'child-b': Value('i', 0) - # } - - # @app.callback(Output('parent-a', 'value'), - # [Input('grandparent', 'value')]) - # def update_parenta(value): - # call_counts['parent-a'].value += 1 - # return 'a: {}'.format(value) - - # @app.callback(Output('parent-b', 'value'), - # [Input('grandparent', 'value')]) - # def update_parentb(value): - # time.sleep(0.5) - # call_counts['parent-b'].value += 1 - # return 'b: {}'.format(value) - - # @app.callback(Output('child-a', 'children'), - # [Input('parent-a', 'value'), - # Input('parent-b', 'value')]) - # def update_childa(parenta_value, parentb_value): - # time.sleep(1) - # call_counts['child-a'].value += 1 - # return '{} + {}'.format(parenta_value, parentb_value) - - # @app.callback(Output('child-b', 'children'), - # [Input('parent-a', 'value'), - # Input('parent-b', 'value'), - # Input('grandparent', 'value')]) - # def update_childb(parenta_value, parentb_value, grandparent_value): - # call_counts['child-b'].value += 1 - # return '{} + {} + {}'.format( - # parenta_value, - # parentb_value, - # grandparent_value - # ) - - # self.startServer(app) - - # wait_for(lambda: childa().text == 'a: input 1 + b: input 1') - # wait_for(lambda: childb().text == 'a: input 1 + b: input 1 + input 1') - # time.sleep(1) # wait for potential requests of app to settle down - # self.assertEqual(parenta().get_attribute('value'), 'a: input 1') - # self.assertEqual(parentb().get_attribute('value'), 'b: input 1') - # self.assertEqual(call_counts['parent-a'].value, 1) - # self.assertEqual(call_counts['parent-b'].value, 1) - # self.assertEqual(call_counts['child-a'].value, 1) - # self.assertEqual(call_counts['child-b'].value, 1) - - # def test_removing_component_while_its_getting_updated(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # dcc.RadioItems( - # id='toc', - # options=[ - # {'label': i, 'value': i} for i in ['1', '2'] - # ], - # value='1' - # ), - # html.Div(id='body') - # ]) - # app.config.suppress_callback_exceptions = True - - # call_counts = { - # 'body': Value('i', 0), - # 'button-output': Value('i', 0) - # } - - # @app.callback(Output('body', 'children'), [Input('toc', 'value')]) - # def update_body(chapter): - # call_counts['body'].value += 1 - # if chapter == '1': - # return [ - # html.Div('Chapter 1'), - # html.Button( - # 'clicking this button takes forever', - # id='button' - # ), - # html.Div(id='button-output') - # ] - # elif chapter == '2': - # return 'Chapter 2' - # else: - # raise Exception('chapter is {}'.format(chapter)) - - # @app.callback( - # Output('button-output', 'children'), - # [Input('button', 'n_clicks')]) - # def this_callback_takes_forever(n_clicks): - # if not n_clicks: - # # initial value is quick, only new value is slow - # # also don't let the initial value increment call_counts - # return 'Initial Value' - # time.sleep(5) - # call_counts['button-output'].value += 1 - # return 'New value!' - - # body = lambda: self.driver.find_element_by_id('body') - # self.startServer(app) - - # wait_for(lambda: call_counts['body'].value == 1) - # time.sleep(0.5) - # self.driver.find_element_by_id('button').click() - - # # while that callback is resolving, switch the chapter, - # # hiding the `button-output` tag - # def chapter2_assertions(): - # wait_for(lambda: body().text == 'Chapter 2') - - # layout = self.driver.execute_script( - # 'return JSON.parse(JSON.stringify(' - # 'window.store.getState().layout' - # '))' - # ) - - # dcc_radio = layout['props']['children'][0] - # html_body = layout['props']['children'][1] - - # self.assertEqual(dcc_radio['props']['id'], 'toc') - # self.assertEqual(dcc_radio['props']['value'], '2') - - # self.assertEqual(html_body['props']['id'], 'body') - # self.assertEqual(html_body['props']['children'], 'Chapter 2') - - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[1]).click() - # chapter2_assertions() - # self.assertEqual(call_counts['button-output'].value, 0) - # time.sleep(5) - # wait_for(lambda: call_counts['button-output'].value == 1) - # time.sleep(2) # liberally wait for the front-end to process request - # chapter2_assertions() - # self.assertTrue(self.is_console_clean()) - - # def test_rendering_layout_calls_callback_once_per_output(self): - # app = Dash(__name__) - # call_count = Value('i', 0) - - # app.config['suppress_callback_exceptions'] = True - # app.layout = html.Div([ - # html.Div([ - # dcc.Input( - # value='Input {}'.format(i), - # id='input-{}'.format(i) - # ) - # for i in range(10) - # ]), - # html.Div(id='container'), - # dcc.RadioItems() - # ]) - - # @app.callback( - # Output('container', 'children'), - # [Input('input-{}'.format(i), 'value') for i in range(10)]) - # def dynamic_output(*args): - # call_count.value += 1 - # return json.dumps(args, indent=2) - - # self.startServer(app) - - # time.sleep(5) - - # self.percy_snapshot( - # name='test_rendering_layout_calls_callback_once_per_output' - # ) - - # self.assertEqual(call_count.value, 1) - - # def test_rendering_new_content_calls_callback_once_per_output(self): - # app = Dash(__name__) - # call_count = Value('i', 0) - - # app.config['suppress_callback_exceptions'] = True - # app.layout = html.Div([ - # html.Button( - # id='display-content', - # children='Display Content', - # n_clicks=0 - # ), - # html.Div(id='container'), - # dcc.RadioItems() - # ]) - - # @app.callback( - # Output('container', 'children'), - # [Input('display-content', 'n_clicks')]) - # def display_output(n_clicks): - # if n_clicks == 0: - # return '' - # return html.Div([ - # html.Div([ - # dcc.Input( - # value='Input {}'.format(i), - # id='input-{}'.format(i) - # ) - # for i in range(10) - # ]), - # html.Div(id='dynamic-output') - # ]) - - # @app.callback( - # Output('dynamic-output', 'children'), - # [Input('input-{}'.format(i), 'value') for i in range(10)]) - # def dynamic_output(*args): - # call_count.value += 1 - # return json.dumps(args, indent=2) - - # self.startServer(app) - - # self.wait_for_element_by_css_selector('#display-content').click() - - # time.sleep(5) - - # self.percy_snapshot( - # name='test_rendering_new_content_calls_callback_once_per_output' - # ) - - # self.assertEqual(call_count.value, 1) - - # def test_callbacks_called_multiple_times_and_out_of_order_multi_output(self): - # app = Dash(__name__) - # app.layout = html.Div([ - # html.Button(id='input', n_clicks=0), - # html.Div(id='output1'), - # html.Div(id='output2') - # ]) - - # call_count = Value('i', 0) - - # @app.callback( - # [Output('output1', 'children'), - # Output('output2', 'children')], - # [Input('input', 'n_clicks')] - # ) - # def update_output(n_clicks): - # call_count.value = call_count.value + 1 - # if n_clicks == 1: - # time.sleep(4) - # return n_clicks, n_clicks + 1 - - # self.startServer(app) - # button = self.wait_for_element_by_css_selector('#input') - # button.click() - # button.click() - # time.sleep(8) - # self.percy_snapshot( - # name='test_callbacks_called_multiple_times' - # '_and_out_of_order_multi_output' - # ) - # self.assertEqual(call_count.value, 3) - # self.wait_for_text_to_equal('#output1', '2') - # self.wait_for_text_to_equal('#output2', '3') - # request_queue = self.driver.execute_script( - # 'return window.store.getState().requestQueue' - # ) - # self.assertFalse(request_queue[0]['rejected']) - # self.assertEqual(len(request_queue), 1) - - # def test_callbacks_with_shared_grandparent(self): - # app = dash.Dash() - - # app.layout = html.Div([ - # html.Div(id='session-id', children='id'), - # dcc.Dropdown(id='dropdown-1'), - # dcc.Dropdown(id='dropdown-2'), - # ]) - - # options = [{'value': 'a', 'label': 'a'}] - - # call_counts = { - # 'dropdown_1': Value('i', 0), - # 'dropdown_2': Value('i', 0) - # } - - # @app.callback( - # Output('dropdown-1', 'options'), - # [Input('dropdown-1', 'value'), - # Input('session-id', 'children')]) - # def dropdown_1(value, session_id): - # call_counts['dropdown_1'].value += 1 - # return options - - # @app.callback( - # Output('dropdown-2', 'options'), - # [Input('dropdown-2', 'value'), - # Input('session-id', 'children')]) - # def dropdown_2(value, session_id): - # call_counts['dropdown_2'].value += 1 - # return options - - # self.startServer(app) - - # self.wait_for_element_by_css_selector('#session-id') - # time.sleep(2) - # self.assertEqual(call_counts['dropdown_1'].value, 1) - # self.assertEqual(call_counts['dropdown_2'].value, 1) - - # self.assertTrue(self.is_console_clean()) - - # def test_callbacks_triggered_on_generated_output(self): - # app = dash.Dash() - # app.config['suppress_callback_exceptions'] = True - - # call_counts = { - # 'tab1': Value('i', 0), - # 'tab2': Value('i', 0) - # } - - # app.layout = html.Div([ - # dcc.Dropdown( - # id='outer-controls', - # options=[{'label': i, 'value': i} for i in ['a', 'b']], - # value='a' - # ), - # dcc.RadioItems( - # options=[ - # {'label': 'Tab 1', 'value': 1}, - # {'label': 'Tab 2', 'value': 2} - # ], - # value=1, - # id='tabs', - # ), - # html.Div(id='tab-output') - # ]) - - # @app.callback(Output('tab-output', 'children'), - # [Input('tabs', 'value')]) - # def display_content(value): - # return html.Div([ - # html.Div(id='tab-{}-output'.format(value)) - # ]) - - # @app.callback(Output('tab-1-output', 'children'), - # [Input('outer-controls', 'value')]) - # def display_tab1_output(value): - # call_counts['tab1'].value += 1 - # return 'Selected "{}" in tab 1'.format(value) - - # @app.callback(Output('tab-2-output', 'children'), - # [Input('outer-controls', 'value')]) - # def display_tab2_output(value): - # call_counts['tab2'].value += 1 - # return 'Selected "{}" in tab 2'.format(value) - - # self.startServer(app) - # self.wait_for_element_by_css_selector('#tab-output') - # time.sleep(2) - - # self.assertEqual(call_counts['tab1'].value, 1) - # self.assertEqual(call_counts['tab2'].value, 0) - # self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 1') - # self.wait_for_text_to_equal('#tab-1-output', 'Selected "a" in tab 1') - - # (self.driver.find_elements_by_css_selector( - # 'input[type="radio"]' - # )[1]).click() - # time.sleep(2) - - # self.wait_for_text_to_equal('#tab-output', 'Selected "a" in tab 2') - # self.wait_for_text_to_equal('#tab-2-output', 'Selected "a" in tab 2') - # self.assertEqual(call_counts['tab1'].value, 1) - # self.assertEqual(call_counts['tab2'].value, 1) - - # self.assertTrue(self.is_console_clean()) - - # def test_initialization_with_overlapping_outputs(self): - # app = dash.Dash() - # app.layout = html.Div([ - - # html.Div(id='input-1', children='input-1'), - # html.Div(id='input-2', children='input-2'), - # html.Div(id='input-3', children='input-3'), - # html.Div(id='input-4', children='input-4'), - # html.Div(id='input-5', children='input-5'), - - # html.Div(id='output-1'), - # html.Div(id='output-2'), - # html.Div(id='output-3'), - # html.Div(id='output-4'), - - # ]) - # call_counts = { - # 'output-1': Value('i', 0), - # 'output-2': Value('i', 0), - # 'output-3': Value('i', 0), - # 'output-4': Value('i', 0), - # } - - # def generate_callback(outputid): - # def callback(*args): - # call_counts[outputid].value += 1 - # return '{}, {}'.format(*args) - # return callback - - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # app.callback( - # Output(outputid, 'children'), - # [ - # Input('input-{}'.format(i), 'children'), - # Input('input-{}'.format(i + 1), 'children') - # ] - # )(generate_callback(outputid)) - - # self.startServer(app) - - # self.wait_for_element_by_css_selector('#output-1') - # time.sleep(5) - - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # self.assertEqual(call_counts[outputid].value, 1) - # self.wait_for_text_to_equal( - # '#{}'.format(outputid), - # "input-{}, input-{}".format(i, i + 1) - # ) - - # def test_generate_overlapping_outputs(self): - # app = dash.Dash() - # app.config['suppress_callback_exceptions'] = True - # block = html.Div([ - - # html.Div(id='input-1', children='input-1'), - # html.Div(id='input-2', children='input-2'), - # html.Div(id='input-3', children='input-3'), - # html.Div(id='input-4', children='input-4'), - # html.Div(id='input-5', children='input-5'), - - # html.Div(id='output-1'), - # html.Div(id='output-2'), - # html.Div(id='output-3'), - # html.Div(id='output-4'), - - # ]) - # app.layout = html.Div([ - # html.Div(id='input'), - # html.Div(id='container') - # ]) - - # call_counts = { - # 'container': Value('i', 0), - # 'output-1': Value('i', 0), - # 'output-2': Value('i', 0), - # 'output-3': Value('i', 0), - # 'output-4': Value('i', 0), - # } - - # @app.callback(Output('container', 'children'), - # [Input('input', 'children')]) - # def display_output(*args): - # call_counts['container'].value += 1 - # return block - - # def generate_callback(outputid): - # def callback(*args): - # call_counts[outputid].value += 1 - # return '{}, {}'.format(*args) - # return callback - - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # app.callback( - # Output(outputid, 'children'), - # [Input('input-{}'.format(i), 'children'), - # Input('input-{}'.format(i + 1), 'children')] - # )(generate_callback(outputid)) - - # self.startServer(app) - - # wait_for(lambda: call_counts['container'].value == 1) - # self.wait_for_element_by_css_selector('#output-1') - # time.sleep(5) - - # for i in range(1, 5): - # outputid = 'output-{}'.format(i) - # self.assertEqual(call_counts[outputid].value, 1) - # self.wait_for_text_to_equal( - # '#{}'.format(outputid), - # "input-{}, input-{}".format(i, i + 1) - # ) - # self.assertEqual(call_counts['container'].value, 1) - - # def test_multiple_properties_update_at_same_time_on_same_component(self): - # call_count = Value('i', 0) - # timestamp_1 = Value('d', -5) - # timestamp_2 = Value('d', -5) - - # app = dash.Dash() - # app.layout = html.Div([ - # html.Div(id='container'), - # html.Button('Click', id='button-1', n_clicks=0, n_clicks_timestamp=-1), - # html.Button('Click', id='button-2', n_clicks=0, n_clicks_timestamp=-1) - # ]) - - # @app.callback( - # Output('container', 'children'), - # [Input('button-1', 'n_clicks'), - # Input('button-1', 'n_clicks_timestamp'), - # Input('button-2', 'n_clicks'), - # Input('button-2', 'n_clicks_timestamp')]) - # def update_output(*args): - # call_count.value += 1 - # timestamp_1.value = args[1] - # timestamp_2.value = args[3] - # return '{}, {}'.format(args[0], args[2]) - - # self.startServer(app) - - # self.wait_for_element_by_css_selector('#container') - # time.sleep(2) - # self.wait_for_text_to_equal('#container', '0, 0') - # self.assertEqual(timestamp_1.value, -1) - # self.assertEqual(timestamp_2.value, -1) - # self.assertEqual(call_count.value, 1) - # self.percy_snapshot('button initialization 1') - - # self.driver.find_element_by_css_selector('#button-1').click() - # time.sleep(2) - # self.wait_for_text_to_equal('#container', '1, 0') - # self.assertTrue( - # timestamp_1.value > - # ((time.time() - (24 * 60 * 60)) * 1000)) - # self.assertEqual(timestamp_2.value, -1) - # self.assertEqual(call_count.value, 2) - # self.percy_snapshot('button-1 click') - # prev_timestamp_1 = timestamp_1.value - - # self.driver.find_element_by_css_selector('#button-2').click() - # time.sleep(2) - # self.wait_for_text_to_equal('#container', '1, 1') - # self.assertEqual(timestamp_1.value, prev_timestamp_1) - # self.assertTrue( - # timestamp_2.value > - # ((time.time() - 24 * 60 * 60) * 1000)) - # self.assertEqual(call_count.value, 3) - # self.percy_snapshot('button-2 click') - # prev_timestamp_2 = timestamp_2.value - - # self.driver.find_element_by_css_selector('#button-2').click() - # time.sleep(2) - # self.wait_for_text_to_equal('#container', '1, 2') - # self.assertEqual(timestamp_1.value, prev_timestamp_1) - # self.assertTrue( - # timestamp_2.value > - # prev_timestamp_2) - # self.assertTrue(timestamp_2.value > timestamp_1.value) - # self.assertEqual(call_count.value, 4) - # self.percy_snapshot('button-2 click again') - - # def test_request_hooks(self): - # app = Dash(__name__) - - # app.index_string = ''' - # - # - # {%metas%} - # {%title%} - # {%favicon%} - # {%css%} - # - # - #
Testing custom DashRenderer
- # {%app_entry%} - #
- # {%config%} - # {%scripts%} - # - #
- #
With request hooks
- # - # ''' - - # app.layout = html.Div([ - # dcc.Input( - # id='input', - # value='initial value' - # ), - # html.Div( - # html.Div([ - # html.Div(id='output-1'), - # html.Div(id='output-pre'), - # html.Div(id='output-pre-payload'), - # html.Div(id='output-post'), - # html.Div(id='output-post-payload'), - # html.Div(id='output-post-response') - # ]) - # ) - # ]) - - # @app.callback(Output('output-1', 'children'), [Input('input', 'value')]) - # def update_output(value): - # return value - - # self.startServer(app) - - # input1 = self.wait_for_element_by_css_selector('#input') - # initialValue = input1.get_attribute('value') - - # action = ActionChains(self.driver) - # action.click(input1) - # action = action.send_keys(Keys.BACKSPACE * len(initialValue)) - - # action.send_keys('fire request hooks').perform() - - # self.wait_for_text_to_equal('#output-1', 'fire request hooks') - # self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') - # self.wait_for_text_to_equal('#output-pre-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - # self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') - # self.wait_for_text_to_equal('#output-post-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') - # self.wait_for_text_to_equal('#output-post-response', '{"props":{"children":"fire request hooks"}}') - # self.percy_snapshot(name='request-hooks render') + self.wait_for_text_to_equal('#output-1', 'fire request hooks') + self.wait_for_text_to_equal('#output-pre', 'request_pre changed this text!') + self.wait_for_text_to_equal('#output-pre-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') + self.wait_for_text_to_equal('#output-post', 'request_post changed this text!') + self.wait_for_text_to_equal('#output-post-payload', '{"output":"output-1.children","changedPropIds":["input.value"],"inputs":[{"id":"input","property":"value","value":"fire request hooks"}]}') + self.wait_for_text_to_equal('#output-post-response', '{"props":{"children":"fire request hooks"}}') + self.percy_snapshot(name='request-hooks render') def test_graphs_in_tabs_do_not_share_state(self): app = dash.Dash() From 39a69151bbcd4fe380772a43412aef1f68f7a6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Nov 2019 16:28:35 -0500 Subject: [PATCH 14/14] rename test file to match usage --- .../tests/{notifyObservers.test.js => isAppReady.test.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename dash-renderer/tests/{notifyObservers.test.js => isAppReady.test.js} (97%) diff --git a/dash-renderer/tests/notifyObservers.test.js b/dash-renderer/tests/isAppReady.test.js similarity index 97% rename from dash-renderer/tests/notifyObservers.test.js rename to dash-renderer/tests/isAppReady.test.js index 9f9da60bf1..a85ea81240 100644 --- a/dash-renderer/tests/notifyObservers.test.js +++ b/dash-renderer/tests/isAppReady.test.js @@ -2,7 +2,7 @@ import isAppReady from "../src/actions/isAppReady"; const WAIT = 1000; -describe('notifyObservers', () => { +describe('isAppReady', () => { let resolve; beforeEach(() => { const promise = new Promise(r => {