From a8cc367513cfbc15bd53392921044afcfdc2fc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 27 Aug 2020 16:45:09 -0400 Subject: [PATCH 01/24] - delay `requestedCallbacks` processing so that callbacks triggered by the same user action are grouped - merge duplicate callback props into a new callback if duplicates are found in `requestedCallbacks` --- .../src/observers/requestedCallbacks.ts | 42 ++++++++++++++----- dash-renderer/src/utils/wait.ts | 8 ++++ 2 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 dash-renderer/src/utils/wait.ts diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 2d787a43a1..f83508a91a 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -1,5 +1,6 @@ import { all, + assoc, concat, difference, filter, @@ -10,6 +11,9 @@ import { isEmpty, isNil, map, + mergeAll, + partition, + pluck, values } from 'ramda'; @@ -45,14 +49,18 @@ import { IBlockedCallback } from '../types/callbacks'; +import wait from './../utils/wait'; + import { getPendingCallbacks } from '../utils/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; const observer: IStoreObserverDefinition = { - observer: ({ + observer: async ({ dispatch, getState }) => { + await wait(0); + const { callbacks, callbacks: { prioritized, blocked, executing, watched, stored }, paths } = getState(); let { callbacks: { requested } } = getState(); @@ -79,25 +87,35 @@ const observer: IStoreObserverDefinition = { */ /* - Extract all but the first callback from each IOS-key group - these callbacks are duplicates. + Group callbacks by identifier and partition based on whether there are duplicates or not. */ - const rDuplicates = flatten(map( - group => group.slice(0, -1), - values( - groupBy( - getUniqueIdentifier, - requested - ) + const [rWithoutDuplicates, rWithDuplicates] = partition(rdg => rdg.length === 1, values( + groupBy( + getUniqueIdentifier, + requested ) )); + /* + Flatten all duplicated callbacks into a list for removal + */ + const rDuplicates = flatten(rWithDuplicates); + + /* + Merge duplicate groups into a single callback - merge by giving priority to newer callbacks + */ + const rMergedDuplicates = map(group => assoc( + 'changedPropIds', + mergeAll(pluck('changedPropIds', group)), + group[0] + ), rWithDuplicates); + /* TODO? Clean up the `requested` list - during the dispatch phase, duplicates will be removed for real */ - requested = difference(requested, rDuplicates); + requested = concat(flatten(rWithoutDuplicates), rMergedDuplicates); /* 2. Remove duplicated `prioritized`, `executing` and `watching` callbacks @@ -319,6 +337,8 @@ const observer: IStoreObserverDefinition = { bDuplicates.length ? removeBlockedCallbacks(bDuplicates) : null, eDuplicates.length ? removeExecutingCallbacks(eDuplicates) : null, wDuplicates.length ? removeWatchedCallbacks(wDuplicates) : null, + // Add merged-duplicated callbacks + rMergedDuplicates.length ? addRequestedCallbacks(rMergedDuplicates): null, // Prune callbacks rRemoved.length ? removeRequestedCallbacks(rRemoved) : null, rAdded.length ? addRequestedCallbacks(rAdded) : null, diff --git a/dash-renderer/src/utils/wait.ts b/dash-renderer/src/utils/wait.ts new file mode 100644 index 0000000000..10a2a9ed79 --- /dev/null +++ b/dash-renderer/src/utils/wait.ts @@ -0,0 +1,8 @@ +export default async (duration: number) => { + let _resolve: any; + const p = new Promise(resolve => _resolve = resolve); + + setTimeout(_resolve, duration); + + return p; +} From 074e58a6b0cd9fdd43c347101c76f6751ada2473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 27 Aug 2020 17:09:40 -0400 Subject: [PATCH 02/24] noise From 783953bf915ac172533332018a3ecb1d8bc0ac9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 27 Aug 2020 20:23:09 -0400 Subject: [PATCH 03/24] wait for render event --- dash-renderer/src/APIController.react.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 8479b0fd07..46453046c6 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -20,6 +20,7 @@ import {applyPersistence} from './persistence'; import {getAppState} from './reducers/constants'; import {STATUS} from './constants/constants'; import {getLoadingState, getLoadingHash} from './utils/TreeContainer'; +import wait from './utils/wait'; export const DashContext = createContext({}); @@ -63,8 +64,11 @@ const UnconnectedContainer = props => { useEffect(() => { if (renderedTree.current) { - renderedTree.current = false; - events.current.emit('rendered'); + (async () => { + renderedTree.current = false; + await wait(0); + events.current.emit('rendered'); + })(); } }); From 60e8508abc33d691a261a51ce03aeeb890019810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 27 Aug 2020 20:53:11 -0400 Subject: [PATCH 04/24] multiple output -> tuple or list --- tests/integration/devtools/test_callback_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index a006e23eec..4f57e01254 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -677,7 +677,7 @@ def c2(children): @app.callback([Output("a", "children")], [Input("c", "children")]) def c3(children): - return children + return (children,) dash_duo.start_server(app, **debugging) From a5cb50d8d058096a77eeaa112e140217f687b960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 27 Aug 2020 23:50:21 -0400 Subject: [PATCH 05/24] DocumentTitle observer instead of empty component --- dash-renderer/src/AppContainer.react.js | 2 - dash-renderer/src/StoreObserver.ts | 1 + .../components/core/DocumentTitle.react.js | 51 ------------------ dash-renderer/src/observers/documentTitle.ts | 54 +++++++++++++++++++ dash-renderer/src/store.ts | 2 + 5 files changed, 57 insertions(+), 53 deletions(-) delete mode 100644 dash-renderer/src/components/core/DocumentTitle.react.js create mode 100644 dash-renderer/src/observers/documentTitle.ts diff --git a/dash-renderer/src/AppContainer.react.js b/dash-renderer/src/AppContainer.react.js index 0dcb5e2a65..d03efb0f16 100644 --- a/dash-renderer/src/AppContainer.react.js +++ b/dash-renderer/src/AppContainer.react.js @@ -2,7 +2,6 @@ import {connect} from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; import APIController from './APIController.react'; -import DocumentTitle from './components/core/DocumentTitle.react'; import Loading from './components/core/Loading.react'; import Toolbar from './components/core/Toolbar.react'; import Reloader from './components/core/Reloader.react'; @@ -48,7 +47,6 @@ class UnconnectedAppContainer extends React.Component { {show_undo_redo ? : null} - diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts index 4bc82382f0..45b31e8d2c 100644 --- a/dash-renderer/src/StoreObserver.ts +++ b/dash-renderer/src/StoreObserver.ts @@ -21,6 +21,7 @@ interface IStoreObserverState { export interface IStoreObserverDefinition { observer: Observer>; inputs: string[] + [key: string]: any; } export default class StoreObserver { diff --git a/dash-renderer/src/components/core/DocumentTitle.react.js b/dash-renderer/src/components/core/DocumentTitle.react.js deleted file mode 100644 index bfea61831e..0000000000 --- a/dash-renderer/src/components/core/DocumentTitle.react.js +++ /dev/null @@ -1,51 +0,0 @@ -import {connect} from 'react-redux'; -import {Component} from 'react'; -import PropTypes from 'prop-types'; - -class DocumentTitle extends Component { - constructor(props) { - super(props); - const {update_title} = props.config; - this.state = { - title: document.title, - update_title, - }; - } - - UNSAFE_componentWillReceiveProps(props) { - if (!this.state.update_title) { - // Let callbacks or other components have full control over title - return; - } - if (props.isLoading) { - this.setState({title: document.title}); - if (this.state.update_title) { - document.title = this.state.update_title; - } - } else { - if (document.title === this.state.update_title) { - document.title = this.state.title; - } else { - this.setState({title: document.title}); - } - } - } - - shouldComponentUpdate() { - return false; - } - - render() { - return null; - } -} - -DocumentTitle.propTypes = { - isLoading: PropTypes.bool.isRequired, - config: PropTypes.shape({update_title: PropTypes.string}), -}; - -export default connect(state => ({ - isLoading: state.isLoading, - config: state.config, -}))(DocumentTitle); diff --git a/dash-renderer/src/observers/documentTitle.ts b/dash-renderer/src/observers/documentTitle.ts new file mode 100644 index 0000000000..6d0009fb40 --- /dev/null +++ b/dash-renderer/src/observers/documentTitle.ts @@ -0,0 +1,54 @@ +import { IStoreObserverDefinition } from '../StoreObserver'; +import { IStoreState } from '../store'; + +const updateTitle = (getState: () => IStoreState) => { + const { + config, + isLoading + } = getState(); + + const { update_title } = config; + + if (!update_title) { + return; + } + + if (isLoading) { + if (document.title !== update_title) { + observer.title = document.title; + document.title = update_title; + } + } else { + if (document.title === update_title) { + document.title = observer.title; + } else { + observer.title = document.title; + } + } +}; + +const observer: IStoreObserverDefinition = { + inputs: ['isLoading'], + mutationObserver: undefined, + observer: ({ + getState + }) => { + const { + config + } = getState(); + + if (observer.config !== config) { + observer.config = config; + observer.mutationObserver?.disconnect(); + observer.mutationObserver = new MutationObserver(() => updateTitle(getState)); + observer.mutationObserver.observe( + document.querySelector('title'), + { subtree: true, childList: true, attributes: true, characterData: true } + ); + } + + updateTitle(getState); + } +}; + +export default observer; diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index 3b26f75a92..1989941835 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -7,6 +7,7 @@ import { ICallbacksState } from './reducers/callbacks'; import { LoadingMapState } from './reducers/loadingMap'; import { IsLoadingState } from './reducers/isLoading'; +import documentTitle from './observers/documentTitle'; import executedCallbacks from './observers/executedCallbacks'; import executingCallbacks from './observers/executingCallbacks'; import isLoading from './observers/isLoading' @@ -33,6 +34,7 @@ const storeObserver = new StoreObserver(); const setObservers = once(() => { const observe = storeObserver.observe; + observe(documentTitle); observe(isLoading); observe(loadingMap); observe(requestedCallbacks); From f795fdbbf836fc4e3322d8cb051620f05940a219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 28 Aug 2020 16:39:54 -0400 Subject: [PATCH 06/24] config update_title --- dash-renderer/src/observers/documentTitle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/observers/documentTitle.ts b/dash-renderer/src/observers/documentTitle.ts index 6d0009fb40..0706df4c84 100644 --- a/dash-renderer/src/observers/documentTitle.ts +++ b/dash-renderer/src/observers/documentTitle.ts @@ -7,7 +7,7 @@ const updateTitle = (getState: () => IStoreState) => { isLoading } = getState(); - const { update_title } = config; + const update_title = config?.update_title; if (!update_title) { return; From 8b6ff26341acbd39f0f3d0fa1066bc89d2255eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 28 Aug 2020 17:20:45 -0400 Subject: [PATCH 07/24] - don't group initial callbacks with subsequent callbacks - merge with max value (DIRECT, INDIRECT) --- dash-renderer/src/observers/requestedCallbacks.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index f83508a91a..6b7095490b 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -11,9 +11,10 @@ import { isEmpty, isNil, map, - mergeAll, + mergeWith, partition, pluck, + reduce, values } from 'ramda'; @@ -86,13 +87,15 @@ const observer: IStoreObserverDefinition = { 1. Remove duplicated `requested` callbacks - give precedence to newer callbacks over older ones */ + const [rInitial, rLater] = partition(cb => cb.initialCall, requested); + /* Group callbacks by identifier and partition based on whether there are duplicates or not. */ const [rWithoutDuplicates, rWithDuplicates] = partition(rdg => rdg.length === 1, values( groupBy( getUniqueIdentifier, - requested + rLater ) )); @@ -106,7 +109,7 @@ const observer: IStoreObserverDefinition = { */ const rMergedDuplicates = map(group => assoc( 'changedPropIds', - mergeAll(pluck('changedPropIds', group)), + reduce(mergeWith(Math.max), {}, pluck('changedPropIds', group)), group[0] ), rWithDuplicates); @@ -115,7 +118,7 @@ const observer: IStoreObserverDefinition = { Clean up the `requested` list - during the dispatch phase, duplicates will be removed for real */ - requested = concat(flatten(rWithoutDuplicates), rMergedDuplicates); + requested = concat(rInitial, concat(flatten(rWithoutDuplicates), rMergedDuplicates)); /* 2. Remove duplicated `prioritized`, `executing` and `watching` callbacks From 588afa02ac29088cbda6893344d7a6cc0ad903f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 31 Aug 2020 15:42:26 -0400 Subject: [PATCH 08/24] always drop initialCallback if there's duplicates --- .../src/observers/requestedCallbacks.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 6b7095490b..b0d4422c3d 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -1,18 +1,18 @@ import { all, - assoc, concat, difference, filter, flatten, + forEach, groupBy, includes, intersection, isEmpty, isNil, map, + mergeLeft, mergeWith, - partition, pluck, reduce, values @@ -87,38 +87,46 @@ const observer: IStoreObserverDefinition = { 1. Remove duplicated `requested` callbacks - give precedence to newer callbacks over older ones */ - const [rInitial, rLater] = partition(cb => cb.initialCall, requested); - - /* - Group callbacks by identifier and partition based on whether there are duplicates or not. - */ - const [rWithoutDuplicates, rWithDuplicates] = partition(rdg => rdg.length === 1, values( + let rDuplicates: ICallback[] = []; + let rMergedDuplicates: ICallback[] = []; + + forEach(group => { + if (group.length === 1) { + // keep callback if its the only one of its kind + rMergedDuplicates.push(group[0]); + } else { + const initial = group.find(cb => cb.initialCall); + if (initial) { + // drop the initial callback if it's not alone + rDuplicates.push(initial); + } + + const groupWithoutInitial = group.filter(cb => cb !== initial); + if (groupWithoutInitial.length === 1) { + // if there's only one callback beside the initial one, keep that callback + rMergedDuplicates.push(groupWithoutInitial[0]); + } else { + // otherwise merge all remaining callbacks together + rDuplicates = concat(rDuplicates, groupWithoutInitial); + rMergedDuplicates.push(mergeLeft({ + changedPropIds: reduce(mergeWith(Math.max), {}, pluck('changedPropIds', groupWithoutInitial)), + executionGroup: filter(exg => !!exg, pluck('executionGroup', groupWithoutInitial))[0] + }, groupWithoutInitial.slice(-1)[0]) as ICallback); + } + } + }, values( groupBy( getUniqueIdentifier, - rLater + requested ) )); - /* - Flatten all duplicated callbacks into a list for removal - */ - const rDuplicates = flatten(rWithDuplicates); - - /* - Merge duplicate groups into a single callback - merge by giving priority to newer callbacks - */ - const rMergedDuplicates = map(group => assoc( - 'changedPropIds', - reduce(mergeWith(Math.max), {}, pluck('changedPropIds', group)), - group[0] - ), rWithDuplicates); - /* TODO? Clean up the `requested` list - during the dispatch phase, duplicates will be removed for real */ - requested = concat(rInitial, concat(flatten(rWithoutDuplicates), rMergedDuplicates)); + requested = rMergedDuplicates; /* 2. Remove duplicated `prioritized`, `executing` and `watching` callbacks From d32e88fd92b3bb235bd3b826cb52a9ce4fd4df91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 31 Aug 2020 15:48:35 -0400 Subject: [PATCH 09/24] last exgroup --- dash-renderer/src/observers/requestedCallbacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index b0d4422c3d..42b0b7c558 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -110,7 +110,7 @@ const observer: IStoreObserverDefinition = { rDuplicates = concat(rDuplicates, groupWithoutInitial); rMergedDuplicates.push(mergeLeft({ changedPropIds: reduce(mergeWith(Math.max), {}, pluck('changedPropIds', groupWithoutInitial)), - executionGroup: filter(exg => !!exg, pluck('executionGroup', groupWithoutInitial))[0] + executionGroup: filter(exg => !!exg, pluck('executionGroup', groupWithoutInitial)).slice(-1)[0] }, groupWithoutInitial.slice(-1)[0]) as ICallback); } } From d52cee7fff73110543e5e0f434c51bf8016dfb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 31 Aug 2020 16:07:14 -0400 Subject: [PATCH 10/24] trigger build From f9e9bdb79e99eb200e75b7657f8e832ec7342919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 31 Aug 2020 16:54:02 -0400 Subject: [PATCH 11/24] test clickthrough / callback behavior --- .../callbacks/test_callback_context.py | 126 +++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index f4f4552c4c..e204a2d088 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -1,6 +1,6 @@ import json +import operator import pytest - import dash_html_components as html import dash_core_components as dcc @@ -10,6 +10,8 @@ from dash.exceptions import PreventUpdate, MissingCallbackContextException +from selenium.webdriver.common.action_chains import ActionChains + def test_cbcx001_modified_response(dash_duo): app = Dash(__name__) @@ -96,3 +98,125 @@ def report_triggered(n): 'triggered is truthy, has prop/id ["btn", "n_clicks"], and full value ' '[{"prop_id": "btn.n_clicks", "value": 1}]', ) + + +@pytest.mark.DASH1350 +def test_cbcx005_grouped_clicks(dash_duo): + calls = 0 + callback_contexts = [] + clicks = dict() + + app = Dash(__name__) + app.layout = html.Div( + [ + html.Button("Button 0", id="btn0"), + html.Div( + [ + html.Button("Button 1", id="btn1"), + html.Div( + [html.Div(id="div3"), html.Button("Button 2", id="btn2")], + id="div2", + style=dict(backgroundColor="yellow", padding="50px"), + ), + ], + id="div1", + style=dict(backgroundColor="blue", padding="50px"), + ), + ], + id="div0", + style=dict(backgroundColor="red", padding="50px"), + ) + + @app.callback( + Output("div3", "children"), + [ + Input("div1", "n_clicks"), + Input("div2", "n_clicks"), + Input("btn0", "n_clicks"), + Input("btn1", "n_clicks"), + Input("btn2", "n_clicks"), + ], + prevent_initial_call=True, + ) + def update(div1, div2, btn0, btn1, btn2): + nonlocal calls + nonlocal callback_contexts + nonlocal clicks + + calls = calls + 1 + callback_contexts.append(callback_context.triggered) + clicks["div1"] = div1 + clicks["div2"] = div2 + clicks["btn0"] = btn0 + clicks["btn1"] = btn1 + clicks["btn2"] = btn2 + + def click(target): + ActionChains(dash_duo.driver).move_to_element_with_offset( + target, 5, 5 + ).click().perform() + + dash_duo.start_server(app) + click(dash_duo.find_element("#btn0")) + assert calls == 1 + keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert len(keys) == 1 + assert "btn0.n_clicks" in keys + + assert clicks.get("btn0") == 1 + assert clicks.get("btn1") is None + assert clicks.get("btn2") is None + assert clicks.get("div1") is None + assert clicks.get("div2") is None + + click(dash_duo.find_element("#div1")) + assert calls == 2 + keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert len(keys) == 1 + assert "div1.n_clicks" in keys + + assert clicks.get("btn0") == 1 + assert clicks.get("btn1") is None + assert clicks.get("btn2") is None + assert clicks.get("div1") == 1 + assert clicks.get("div2") is None + + click(dash_duo.find_element("#btn1")) + assert calls == 3 + keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert len(keys) == 2 + assert "btn1.n_clicks" in keys + assert "div1.n_clicks" in keys + + assert clicks.get("btn0") == 1 + assert clicks.get("btn1") == 1 + assert clicks.get("btn2") is None + assert clicks.get("div1") == 2 + assert clicks.get("div2") is None + + click(dash_duo.find_element("#div2")) + assert calls == 4 + keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert len(keys) == 2 + assert "div1.n_clicks" in keys + assert "div2.n_clicks" in keys + + assert clicks.get("btn0") == 1 + assert clicks.get("btn1") == 1 + assert clicks.get("btn2") is None + assert clicks.get("div1") == 3 + assert clicks.get("div2") == 1 + + click(dash_duo.find_element("#btn2")) + assert calls == 5 + keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert len(keys) == 3 + assert "btn2.n_clicks" in keys + assert "div1.n_clicks" in keys + assert "div2.n_clicks" in keys + + assert clicks.get("btn0") == 1 + assert clicks.get("btn1") == 1 + assert clicks.get("btn2") == 1 + assert clicks.get("div1") == 4 + assert clicks.get("div2") == 2 From 0dd6ce10c1fecfabeac26a79cb04ebccce621b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 31 Aug 2020 17:00:04 -0400 Subject: [PATCH 12/24] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fbc913ce2..074763624d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). ## [UNRELEASED] +### Changed +- [#1385](https://github.com/plotly/dash/pull/1385) Closes [#1350](https://github.com/plotly/dash/issues/1350) and fixes a previously undefined callback behavior when multiple elements are stacked on top of one another and their `n_clicks` props are used as inputs of the same callback. The callback will now trigger once with all the triggered `n_clicks` props changes. + ### Fixed - [#1384](https://github.com/plotly/dash/pull/1384) Fixed a bug introduced by [#1180](https://github.com/plotly/dash/pull/1180) breaking use of `prevent_initial_call` as a positional arg in callback definitions From 56cac612ecfb5b57a104c985fd44bfd4a36e6ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 31 Aug 2020 17:54:58 -0400 Subject: [PATCH 13/24] lint --- .../callbacks/test_callback_context.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index e204a2d088..2135911a21 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -100,12 +100,13 @@ def report_triggered(n): ) +calls = 0 +callback_contexts = [] +clicks = dict() + + @pytest.mark.DASH1350 def test_cbcx005_grouped_clicks(dash_duo): - calls = 0 - callback_contexts = [] - clicks = dict() - app = Dash(__name__) app.layout = html.Div( [ @@ -139,9 +140,9 @@ def test_cbcx005_grouped_clicks(dash_duo): prevent_initial_call=True, ) def update(div1, div2, btn0, btn1, btn2): - nonlocal calls - nonlocal callback_contexts - nonlocal clicks + global calls + global callback_contexts + global clicks calls = calls + 1 callback_contexts.append(callback_context.triggered) From 2b8a2e33e78adb5eadb2a10889f221a60da28545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 1 Sep 2020 11:48:55 -0400 Subject: [PATCH 14/24] simplify requestedCallbacks update --- .../src/observers/requestedCallbacks.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 42b0b7c558..bae79e4d5a 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -22,16 +22,16 @@ import { IStoreState } from '../store'; import { aggregateCallbacks, - removeRequestedCallbacks, removePrioritizedCallbacks, removeExecutingCallbacks, removeWatchedCallbacks, - addRequestedCallbacks, addPrioritizedCallbacks, addExecutingCallbacks, addWatchedCallbacks, removeBlockedCallbacks, - addBlockedCallbacks + addBlockedCallbacks, + addRequestedCallbacks, + removeRequestedCallbacks } from '../actions/callbacks'; import { isMultiValued } from '../actions/dependencies'; @@ -65,6 +65,8 @@ const observer: IStoreObserverDefinition = { const { callbacks, callbacks: { prioritized, blocked, executing, watched, stored }, paths } = getState(); let { callbacks: { requested } } = getState(); + const initialRequested = requested.slice(0); + const pendingCallbacks = getPendingCallbacks(callbacks); /* @@ -341,18 +343,24 @@ const observer: IStoreObserverDefinition = { dropped ); + requested = difference( + requested, + readyCallbacks + ); + + const added = difference(requested, initialRequested); + const removed = difference(initialRequested, requested); + dispatch(aggregateCallbacks([ + // Clean up requested callbacks + added.length ? addRequestedCallbacks(added) : null, + removed.length ? removeRequestedCallbacks(removed) : null, // Clean up duplicated callbacks - rDuplicates.length ? removeRequestedCallbacks(rDuplicates) : null, pDuplicates.length ? removePrioritizedCallbacks(pDuplicates) : null, bDuplicates.length ? removeBlockedCallbacks(bDuplicates) : null, eDuplicates.length ? removeExecutingCallbacks(eDuplicates) : null, wDuplicates.length ? removeWatchedCallbacks(wDuplicates) : null, - // Add merged-duplicated callbacks - rMergedDuplicates.length ? addRequestedCallbacks(rMergedDuplicates): null, // Prune callbacks - rRemoved.length ? removeRequestedCallbacks(rRemoved) : null, - rAdded.length ? addRequestedCallbacks(rAdded) : null, pRemoved.length ? removePrioritizedCallbacks(pRemoved) : null, pAdded.length ? addPrioritizedCallbacks(pAdded) : null, bRemoved.length ? removeBlockedCallbacks(bRemoved) : null, @@ -361,15 +369,7 @@ const observer: IStoreObserverDefinition = { eAdded.length ? addExecutingCallbacks(eAdded) : null, wRemoved.length ? removeWatchedCallbacks(wRemoved) : null, wAdded.length ? addWatchedCallbacks(wAdded) : null, - // Prune circular callbacks - rCirculars.length ? removeRequestedCallbacks(rCirculars) : null, - // Prune circular assumptions - oldBlocked.length ? removeRequestedCallbacks(oldBlocked) : null, - newBlocked.length ? addRequestedCallbacks(newBlocked) : null, - // Drop non-triggered initial callbacks - dropped.length ? removeRequestedCallbacks(dropped) : null, // Promote callbacks - readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null, readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null ])); }, From 8471e14551325e875e133348f676332ad7db4b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 1 Sep 2020 12:26:57 -0400 Subject: [PATCH 15/24] context initial callbacks test --- .../callbacks/test_callback_context.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index 2135911a21..a030223b19 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -9,6 +9,7 @@ from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate, MissingCallbackContextException +import dash.testing.wait as wait from selenium.webdriver.common.action_chains import ActionChains @@ -221,3 +222,90 @@ def click(target): assert clicks.get("btn2") == 1 assert clicks.get("div1") == 4 assert clicks.get("div2") == 2 + + +cbcx006_calls = 0 +cbcx006_contexts = [] + + +@pytest.mark.DASH1350 +def test_cbcx006_initial_callback_predecessor(dash_duo): + app = Dash(__name__) + app.layout = html.Div( + [ + html.Div( + style={"display": "block"}, + children=[ + html.Div( + [ + html.Label("ID: input-number-1"), + dcc.Input(id="input-number-1", type="number", value=0), + ] + ), + html.Div( + [ + html.Label("ID: input-number-2"), + dcc.Input(id="input-number-2", type="number", value=0), + ] + ), + html.Div( + [ + html.Label("ID: sum-number"), + dcc.Input( + id="sum-number", type="number", value=0, disabled=True + ), + ] + ), + ], + ), + html.Div(id="results"), + ] + ) + + @app.callback( + Output("sum-number", "value"), + [Input("input-number-1", "value"), Input("input-number-2", "value")], + ) + def update_sum_number(n1, n2): + global cbcx006_calls + global cbcx006_contexts + + cbcx006_calls = cbcx006_calls + 1 + cbcx006_contexts.append(callback_context.triggered) + + return n1 + n2 + + @app.callback( + Output("results", "children"), + [ + Input("input-number-1", "value"), + Input("input-number-2", "value"), + Input("sum-number", "value"), + ], + ) + def update_results(n1, n2, nsum): + global cbcx006_calls + global cbcx006_contexts + + cbcx006_calls = cbcx006_calls + 1 + cbcx006_contexts.append(callback_context.triggered) + + return [ + "{} + {} = {}".format(n1, n2, nsum), + html.Br(), + "ctx.triggered={}".format(callback_context.triggered), + ] + + dash_duo.start_server(app) + + wait.until(lambda: cbcx006_calls == 2, 2) + wait.until(lambda: len(cbcx006_contexts) == 2, 2) + + keys0 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[0])) + # Special case present for backward compatibility + assert len(keys0) == 1 + assert "." in keys0 + + keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[1])) + assert len(keys1) == 1 + assert "sum-number.value" in keys1 From f4086c67bd42d8a6e843db02e282240af8300720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 1 Sep 2020 13:49:30 -0400 Subject: [PATCH 16/24] test initial callback + user action callbacks --- .../callbacks/test_callback_context.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index a030223b19..ff29be7dbc 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -298,6 +298,7 @@ def update_results(n1, n2, nsum): dash_duo.start_server(app) + # Initial Callbacks wait.until(lambda: cbcx006_calls == 2, 2) wait.until(lambda: len(cbcx006_contexts) == 2, 2) @@ -309,3 +310,36 @@ def update_results(n1, n2, nsum): keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[1])) assert len(keys1) == 1 assert "sum-number.value" in keys1 + + # User action & followup callbacks + dash_duo.find_element("#input-number-1").click() + dash_duo.find_element("#input-number-1").send_keys("1") + + wait.until(lambda: cbcx006_calls == 4, 2) + wait.until(lambda: len(cbcx006_contexts) == 4, 2) + + keys0 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[2])) + # Special case present for backward compatibility + assert len(keys0) == 1 + assert "input-number-1.value" in keys0 + + keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[3])) + assert len(keys1) == 2 + assert "sum-number.value" in keys1 + assert "input-number-1.value" in keys1 + + dash_duo.find_element("#input-number-2").click() + dash_duo.find_element("#input-number-2").send_keys("1") + + wait.until(lambda: cbcx006_calls == 6, 2) + wait.until(lambda: len(cbcx006_contexts) == 6, 2) + + keys0 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[4])) + # Special case present for backward compatibility + assert len(keys0) == 1 + assert "input-number-2.value" in keys0 + + keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[5])) + assert len(keys1) == 2 + assert "sum-number.value" in keys1 + assert "input-number-2.value" in keys1 From 90697b15c5600587b36c0e67ffc0a6fe89b03b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 12:12:00 -0400 Subject: [PATCH 17/24] no global variables --- .../callbacks/test_callback_context.py | 145 ++++++++---------- 1 file changed, 68 insertions(+), 77 deletions(-) diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py index ff29be7dbc..3eabe92e64 100644 --- a/tests/integration/callbacks/test_callback_context.py +++ b/tests/integration/callbacks/test_callback_context.py @@ -1,6 +1,7 @@ import json import operator import pytest + import dash_html_components as html import dash_core_components as dcc @@ -101,13 +102,13 @@ def report_triggered(n): ) -calls = 0 -callback_contexts = [] -clicks = dict() - - @pytest.mark.DASH1350 def test_cbcx005_grouped_clicks(dash_duo): + class context: + calls = 0 + callback_contexts = [] + clicks = dict() + app = Dash(__name__) app.layout = html.Div( [ @@ -141,17 +142,13 @@ def test_cbcx005_grouped_clicks(dash_duo): prevent_initial_call=True, ) def update(div1, div2, btn0, btn1, btn2): - global calls - global callback_contexts - global clicks - - calls = calls + 1 - callback_contexts.append(callback_context.triggered) - clicks["div1"] = div1 - clicks["div2"] = div2 - clicks["btn0"] = btn0 - clicks["btn1"] = btn1 - clicks["btn2"] = btn2 + context.calls = context.calls + 1 + context.callback_contexts.append(callback_context.triggered) + context.clicks["div1"] = div1 + context.clicks["div2"] = div2 + context.clicks["btn0"] = btn0 + context.clicks["btn1"] = btn1 + context.clicks["btn2"] = btn2 def click(target): ActionChains(dash_duo.driver).move_to_element_with_offset( @@ -160,76 +157,76 @@ def click(target): dash_duo.start_server(app) click(dash_duo.find_element("#btn0")) - assert calls == 1 - keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert context.calls == 1 + keys = list(map(operator.itemgetter("prop_id"), context.callback_contexts[-1:][0])) assert len(keys) == 1 assert "btn0.n_clicks" in keys - assert clicks.get("btn0") == 1 - assert clicks.get("btn1") is None - assert clicks.get("btn2") is None - assert clicks.get("div1") is None - assert clicks.get("div2") is None + assert context.clicks.get("btn0") == 1 + assert context.clicks.get("btn1") is None + assert context.clicks.get("btn2") is None + assert context.clicks.get("div1") is None + assert context.clicks.get("div2") is None click(dash_duo.find_element("#div1")) - assert calls == 2 - keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert context.calls == 2 + keys = list(map(operator.itemgetter("prop_id"), context.callback_contexts[-1:][0])) assert len(keys) == 1 assert "div1.n_clicks" in keys - assert clicks.get("btn0") == 1 - assert clicks.get("btn1") is None - assert clicks.get("btn2") is None - assert clicks.get("div1") == 1 - assert clicks.get("div2") is None + assert context.clicks.get("btn0") == 1 + assert context.clicks.get("btn1") is None + assert context.clicks.get("btn2") is None + assert context.clicks.get("div1") == 1 + assert context.clicks.get("div2") is None click(dash_duo.find_element("#btn1")) - assert calls == 3 - keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert context.calls == 3 + keys = list(map(operator.itemgetter("prop_id"), context.callback_contexts[-1:][0])) assert len(keys) == 2 assert "btn1.n_clicks" in keys assert "div1.n_clicks" in keys - assert clicks.get("btn0") == 1 - assert clicks.get("btn1") == 1 - assert clicks.get("btn2") is None - assert clicks.get("div1") == 2 - assert clicks.get("div2") is None + assert context.clicks.get("btn0") == 1 + assert context.clicks.get("btn1") == 1 + assert context.clicks.get("btn2") is None + assert context.clicks.get("div1") == 2 + assert context.clicks.get("div2") is None click(dash_duo.find_element("#div2")) - assert calls == 4 - keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert context.calls == 4 + keys = list(map(operator.itemgetter("prop_id"), context.callback_contexts[-1:][0])) assert len(keys) == 2 assert "div1.n_clicks" in keys assert "div2.n_clicks" in keys - assert clicks.get("btn0") == 1 - assert clicks.get("btn1") == 1 - assert clicks.get("btn2") is None - assert clicks.get("div1") == 3 - assert clicks.get("div2") == 1 + assert context.clicks.get("btn0") == 1 + assert context.clicks.get("btn1") == 1 + assert context.clicks.get("btn2") is None + assert context.clicks.get("div1") == 3 + assert context.clicks.get("div2") == 1 click(dash_duo.find_element("#btn2")) - assert calls == 5 - keys = list(map(operator.itemgetter("prop_id"), callback_contexts[-1:][0])) + assert context.calls == 5 + keys = list(map(operator.itemgetter("prop_id"), context.callback_contexts[-1:][0])) assert len(keys) == 3 assert "btn2.n_clicks" in keys assert "div1.n_clicks" in keys assert "div2.n_clicks" in keys - assert clicks.get("btn0") == 1 - assert clicks.get("btn1") == 1 - assert clicks.get("btn2") == 1 - assert clicks.get("div1") == 4 - assert clicks.get("div2") == 2 - - -cbcx006_calls = 0 -cbcx006_contexts = [] + assert context.clicks.get("btn0") == 1 + assert context.clicks.get("btn1") == 1 + assert context.clicks.get("btn2") == 1 + assert context.clicks.get("div1") == 4 + assert context.clicks.get("div2") == 2 @pytest.mark.DASH1350 def test_cbcx006_initial_callback_predecessor(dash_duo): + class context: + calls = 0 + callback_contexts = [] + app = Dash(__name__) app.layout = html.Div( [ @@ -267,11 +264,8 @@ def test_cbcx006_initial_callback_predecessor(dash_duo): [Input("input-number-1", "value"), Input("input-number-2", "value")], ) def update_sum_number(n1, n2): - global cbcx006_calls - global cbcx006_contexts - - cbcx006_calls = cbcx006_calls + 1 - cbcx006_contexts.append(callback_context.triggered) + context.calls = context.calls + 1 + context.callback_contexts.append(callback_context.triggered) return n1 + n2 @@ -284,11 +278,8 @@ def update_sum_number(n1, n2): ], ) def update_results(n1, n2, nsum): - global cbcx006_calls - global cbcx006_contexts - - cbcx006_calls = cbcx006_calls + 1 - cbcx006_contexts.append(callback_context.triggered) + context.calls = context.calls + 1 + context.callback_contexts.append(callback_context.triggered) return [ "{} + {} = {}".format(n1, n2, nsum), @@ -299,15 +290,15 @@ def update_results(n1, n2, nsum): dash_duo.start_server(app) # Initial Callbacks - wait.until(lambda: cbcx006_calls == 2, 2) - wait.until(lambda: len(cbcx006_contexts) == 2, 2) + wait.until(lambda: context.calls == 2, 2) + wait.until(lambda: len(context.callback_contexts) == 2, 2) - keys0 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[0])) + keys0 = list(map(operator.itemgetter("prop_id"), context.callback_contexts[0])) # Special case present for backward compatibility assert len(keys0) == 1 assert "." in keys0 - keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[1])) + keys1 = list(map(operator.itemgetter("prop_id"), context.callback_contexts[1])) assert len(keys1) == 1 assert "sum-number.value" in keys1 @@ -315,15 +306,15 @@ def update_results(n1, n2, nsum): dash_duo.find_element("#input-number-1").click() dash_duo.find_element("#input-number-1").send_keys("1") - wait.until(lambda: cbcx006_calls == 4, 2) - wait.until(lambda: len(cbcx006_contexts) == 4, 2) + wait.until(lambda: context.calls == 4, 2) + wait.until(lambda: len(context.callback_contexts) == 4, 2) - keys0 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[2])) + keys0 = list(map(operator.itemgetter("prop_id"), context.callback_contexts[2])) # Special case present for backward compatibility assert len(keys0) == 1 assert "input-number-1.value" in keys0 - keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[3])) + keys1 = list(map(operator.itemgetter("prop_id"), context.callback_contexts[3])) assert len(keys1) == 2 assert "sum-number.value" in keys1 assert "input-number-1.value" in keys1 @@ -331,15 +322,15 @@ def update_results(n1, n2, nsum): dash_duo.find_element("#input-number-2").click() dash_duo.find_element("#input-number-2").send_keys("1") - wait.until(lambda: cbcx006_calls == 6, 2) - wait.until(lambda: len(cbcx006_contexts) == 6, 2) + wait.until(lambda: context.calls == 6, 2) + wait.until(lambda: len(context.callback_contexts) == 6, 2) - keys0 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[4])) + keys0 = list(map(operator.itemgetter("prop_id"), context.callback_contexts[4])) # Special case present for backward compatibility assert len(keys0) == 1 assert "input-number-2.value" in keys0 - keys1 = list(map(operator.itemgetter("prop_id"), cbcx006_contexts[5])) + keys1 = list(map(operator.itemgetter("prop_id"), context.callback_contexts[5])) assert len(keys1) == 2 assert "sum-number.value" in keys1 assert "input-number-2.value" in keys1 From ed20242c8762060028d1d218b47eb49ec22806ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 12:28:22 -0400 Subject: [PATCH 18/24] trigger build From ae33983eae4a79a848041ebff1cdb9aeedcbc6fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 14:00:55 -0400 Subject: [PATCH 19/24] trigger build From 9c87c660f9e4e4092aaa0740ef977d2f70a19cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 15:33:16 -0400 Subject: [PATCH 20/24] check document.title element exists --- dash-renderer/src/observers/documentTitle.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dash-renderer/src/observers/documentTitle.ts b/dash-renderer/src/observers/documentTitle.ts index 0706df4c84..3cbd893ab2 100644 --- a/dash-renderer/src/observers/documentTitle.ts +++ b/dash-renderer/src/observers/documentTitle.ts @@ -41,10 +41,14 @@ const observer: IStoreObserverDefinition = { observer.config = config; observer.mutationObserver?.disconnect(); observer.mutationObserver = new MutationObserver(() => updateTitle(getState)); - observer.mutationObserver.observe( - document.querySelector('title'), - { subtree: true, childList: true, attributes: true, characterData: true } - ); + + const title = document.querySelector('title'); + if (title) { + observer.mutationObserver.observe( + title, + { subtree: true, childList: true, attributes: true, characterData: true } + ); + } } updateTitle(getState); From 49fb65ea845832106a22e9c9a200976b848d251f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 17:37:06 -0400 Subject: [PATCH 21/24] improve cbsc --- .../callbacks/test_basic_callback.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 13276b1899..c0e03961b2 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -1,5 +1,5 @@ import json -from multiprocessing import Value +from multiprocessing import Lock, Value import pytest @@ -12,6 +12,8 @@ def test_cbsc001_simple_callback(dash_duo): + lock = Lock() + app = dash.Dash(__name__) app.layout = html.Div( [ @@ -23,8 +25,9 @@ def test_cbsc001_simple_callback(dash_duo): @app.callback(Output("output-1", "children"), [Input("input", "value")]) def update_output(value): - call_count.value = call_count.value + 1 - return value + with lock: + call_count.value = call_count.value + 1 + return value dash_duo.start_server(app) @@ -34,7 +37,9 @@ def update_output(value): input_ = dash_duo.find_element("#input") dash_duo.clear_input(input_) - input_.send_keys("hello world") + for key in "hello world": + with lock: + input_.send_keys(key) assert dash_duo.find_element("#output-1").text == "hello world" dash_duo.percy_snapshot(name="simple-callback-hello-world") @@ -345,6 +350,8 @@ def set_path(n): def test_cbsc008_wildcard_prop_callbacks(dash_duo): + lock = Lock() + app = dash.Dash(__name__) app.layout = html.Div( [ @@ -369,8 +376,9 @@ def test_cbsc008_wildcard_prop_callbacks(dash_duo): @app.callback(Output("output-1", "data-cb"), [Input("input", "value")]) def update_data(value): - input_call_count.value += 1 - return value + with lock: + input_call_count.value += 1 + return value @app.callback(Output("output-1", "children"), [Input("output-1", "data-cb")]) def update_text(data): @@ -382,7 +390,10 @@ def update_text(data): input1 = dash_duo.find_element("#input") dash_duo.clear_input(input1) - input1.send_keys("hello world") + + for key in "hello world": + with lock: + input1.send_keys(key) dash_duo.wait_for_text_to_equal("#output-1", "hello world") dash_duo.percy_snapshot(name="wildcard-callback-2") From eca7ed160dfd53cea4847d901131820cb99d80ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 18:48:38 -0400 Subject: [PATCH 22/24] wait --- tests/integration/callbacks/test_basic_callback.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index c0e03961b2..afbd81cd97 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -9,6 +9,7 @@ import dash from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate +from dash.testing import wait def test_cbsc001_simple_callback(dash_duo): @@ -41,7 +42,7 @@ def update_output(value): with lock: input_.send_keys(key) - assert dash_duo.find_element("#output-1").text == "hello world" + wait.until(lambda: dash_duo.find_element("#output-1").text == "hello world", 2) dash_duo.percy_snapshot(name="simple-callback-hello-world") assert call_count.value == 2 + len("hello world"), "initial count + each key stroke" From 45884633c678e1f1e073708635e79fbb44fe6200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 19:07:22 -0400 Subject: [PATCH 23/24] trigger build From cd1abbf9768adfce1954112ad467003b74b7d13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 2 Sep 2020 19:42:23 -0400 Subject: [PATCH 24/24] more locks --- tests/integration/renderer/test_multi_output.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 4741471420..433301ea57 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -1,8 +1,9 @@ -from multiprocessing import Value +from multiprocessing import Lock, Value import dash from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate +from dash.testing import wait import dash_core_components as dcc import dash_html_components as html @@ -47,6 +48,8 @@ def update_output(n_clicks): def test_rdmo002_multi_outputs_on_single_component(dash_duo): + lock = Lock() + call_count = Value("i") app = dash.Dash(__name__) @@ -66,8 +69,9 @@ def test_rdmo002_multi_outputs_on_single_component(dash_duo): [Input("input", "value")], ) def update_output(value): - call_count.value += 1 - return [value, {"fontFamily": value}, value] + with lock: + call_count.value += 1 + return [value, {"fontFamily": value}, value] dash_duo.start_server(app) @@ -79,7 +83,9 @@ def update_output(value): assert call_count.value == 1 - dash_duo.find_element("#input").send_keys(" hello") + for key in " hello": + with lock: + dash_duo.find_element("#input").send_keys(key) dash_duo.wait_for_text_to_equal("#output-container", "dash hello") _html = dash_duo.find_element("#output-container").get_property("innerHTML") @@ -88,7 +94,7 @@ def update_output(value): 'style="font-family: "dash hello";">dash hello' ) - assert call_count.value == 7 + wait.until(lambda: call_count.value == 7, 3) def test_rdmo003_single_output_as_multi(dash_duo):