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
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');
+ })();
}
});
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..3cbd893ab2
--- /dev/null
+++ b/dash-renderer/src/observers/documentTitle.ts
@@ -0,0 +1,58 @@
+import { IStoreObserverDefinition } from '../StoreObserver';
+import { IStoreState } from '../store';
+
+const updateTitle = (getState: () => IStoreState) => {
+ const {
+ config,
+ isLoading
+ } = getState();
+
+ const update_title = config?.update_title;
+
+ 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));
+
+ const title = document.querySelector('title');
+ if (title) {
+ observer.mutationObserver.observe(
+ title,
+ { subtree: true, childList: true, attributes: true, characterData: true }
+ );
+ }
+ }
+
+ updateTitle(getState);
+ }
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts
index 2d787a43a1..bae79e4d5a 100644
--- a/dash-renderer/src/observers/requestedCallbacks.ts
+++ b/dash-renderer/src/observers/requestedCallbacks.ts
@@ -4,12 +4,17 @@ import {
difference,
filter,
flatten,
+ forEach,
groupBy,
includes,
intersection,
isEmpty,
isNil,
map,
+ mergeLeft,
+ mergeWith,
+ pluck,
+ reduce,
values
} from 'ramda';
@@ -17,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';
@@ -45,17 +50,23 @@ 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();
+ const initialRequested = requested.slice(0);
+
const pendingCallbacks = getPendingCallbacks(callbacks);
/*
@@ -78,17 +89,37 @@ const observer: IStoreObserverDefinition = {
1. Remove duplicated `requested` callbacks - give precedence to newer callbacks over older ones
*/
- /*
- Extract all but the first callback from each IOS-key group
- these callbacks are duplicates.
- */
- const rDuplicates = flatten(map(
- group => group.slice(0, -1),
- values(
- groupBy(
- getUniqueIdentifier,
- requested
- )
+ 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)).slice(-1)[0]
+ }, groupWithoutInitial.slice(-1)[0]) as ICallback);
+ }
+ }
+ }, values(
+ groupBy(
+ getUniqueIdentifier,
+ requested
)
));
@@ -97,7 +128,7 @@ const observer: IStoreObserverDefinition = {
Clean up the `requested` list - during the dispatch phase,
duplicates will be removed for real
*/
- requested = difference(requested, rDuplicates);
+ requested = rMergedDuplicates;
/*
2. Remove duplicated `prioritized`, `executing` and `watching` callbacks
@@ -312,16 +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,
// 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,
@@ -330,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
]));
},
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);
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;
+}
diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py
index 13276b1899..afbd81cd97 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
@@ -9,9 +9,12 @@
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):
+ lock = Lock()
+
app = dash.Dash(__name__)
app.layout = html.Div(
[
@@ -23,8 +26,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,9 +38,11 @@ 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"
+ 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"
@@ -345,6 +351,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 +377,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 +391,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")
diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py
index f4f4552c4c..3eabe92e64 100644
--- a/tests/integration/callbacks/test_callback_context.py
+++ b/tests/integration/callbacks/test_callback_context.py
@@ -1,4 +1,5 @@
import json
+import operator
import pytest
import dash_html_components as html
@@ -9,6 +10,9 @@
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
def test_cbcx001_modified_response(dash_duo):
@@ -96,3 +100,237 @@ 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):
+ class context:
+ 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):
+ 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(
+ target, 5, 5
+ ).click().perform()
+
+ dash_duo.start_server(app)
+ click(dash_duo.find_element("#btn0"))
+ 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 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 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 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 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 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 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 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 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 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(
+ [
+ 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):
+ context.calls = context.calls + 1
+ context.callback_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):
+ context.calls = context.calls + 1
+ context.callback_contexts.append(callback_context.triggered)
+
+ return [
+ "{} + {} = {}".format(n1, n2, nsum),
+ html.Br(),
+ "ctx.triggered={}".format(callback_context.triggered),
+ ]
+
+ dash_duo.start_server(app)
+
+ # Initial Callbacks
+ wait.until(lambda: context.calls == 2, 2)
+ wait.until(lambda: len(context.callback_contexts) == 2, 2)
+
+ 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"), context.callback_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: context.calls == 4, 2)
+ wait.until(lambda: len(context.callback_contexts) == 4, 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"), context.callback_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: context.calls == 6, 2)
+ wait.until(lambda: len(context.callback_contexts) == 6, 2)
+
+ 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"), context.callback_contexts[5]))
+ assert len(keys1) == 2
+ assert "sum-number.value" in keys1
+ assert "input-number-2.value" in keys1
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)
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):