diff --git a/CHANGELOG.md b/CHANGELOG.md index becd2a0893..e50c9449bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Fixed +- [#2898](https://github.com/plotly/dash/pull/2898) Fix error thrown when using non-existent components in callback running keyword. Fixes [#2897](https://github.com/plotly/dash/issues/2897). - [#2892](https://github.com/plotly/dash/pull/2860) Fix ensures dcc.Dropdown menu maxHeight option works with Datatable. Fixes [#2529](https://github.com/plotly/dash/issues/2529) [#2225](https://github.com/plotly/dash/issues/2225) - [#2896](https://github.com/plotly/dash/pull/2896) The tabIndex parameter of Div can accept number or string type. Fixes [#2891](https://github.com/plotly/dash/issues/2891) diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts index 6eac640d12..84907b7a17 100644 --- a/dash/dash-renderer/src/actions/callbacks.ts +++ b/dash/dash-renderer/src/actions/callbacks.ts @@ -36,7 +36,7 @@ import { } from '../types/callbacks'; import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies'; import {urlBase} from './utils'; -import {getCSRFHeader} from '.'; +import {getCSRFHeader, dispatchError} from '.'; import {createAction, Action} from 'redux-actions'; import {addHttpHeaders} from '../actions'; import {notifyObservers, updateProps} from './index'; @@ -330,10 +330,29 @@ async function handleClientside( return result; } -function updateComponent(component_id: any, props: any) { +function updateComponent(component_id: any, props: any, cb: ICallbackPayload) { return function (dispatch: any, getState: any) { - const paths = getState().paths; + const {paths, config} = getState(); const componentPath = getPath(paths, component_id); + if (!componentPath) { + if (!config.suppress_callback_exceptions) { + dispatchError(dispatch)( + 'ID running component not found in layout', + [ + 'Component defined in running keyword not found in layout.', + `Component id: "${stringifyId(component_id)}"`, + 'This ID was used in the callback(s) for Output(s):', + `${cb.output}`, + 'You can suppress this exception by setting', + '`suppress_callback_exceptions=True`.' + ] + ); + } + // We need to stop further processing because functions further on + // can't operate on an 'undefined' object, and they will throw an + // error. + return; + } dispatch( updateProps({ props, @@ -381,7 +400,7 @@ function sideUpdate(outputs: SideUpdateOutput, cb: ICallbackPayload) { return acc; }, [] as any[]) .forEach(([id, idProps]) => { - dispatch(updateComponent(id, idProps)); + dispatch(updateComponent(id, idProps, cb)); }); }; } diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index c8f4158b78..d22e1c3e00 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -823,3 +823,95 @@ def on_click(_): dash_duo.wait_for_text_to_equal("#output", "done") dash_duo.wait_for_text_to_equal("#running", "off") + + +def test_cbsc020_callback_running_non_existing_component(dash_duo): + lock = Lock() + app = Dash(__name__, suppress_callback_exceptions=True) + + app.layout = html.Div( + [ + html.Button("start", id="start"), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), + Input("start", "n_clicks"), + running=[ + [ + Output("non_existent_component", "children"), + html.B("on", id="content"), + "off", + ] + ], + prevent_initial_call=True, + ) + def on_click(_): + with lock: + pass + return "done" + + dash_duo.start_server(app) + with lock: + dash_duo.find_element("#start").click() + + dash_duo.wait_for_text_to_equal("#output", "done") + + +def test_cbsc021_callback_running_non_existing_component(dash_duo): + lock = Lock() + app = Dash(__name__) + + app.layout = html.Div( + [ + html.Button("start", id="start"), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), + Input("start", "n_clicks"), + running=[ + [ + Output("non_existent_component", "children"), + html.B("on", id="content"), + "off", + ] + ], + prevent_initial_call=True, + ) + def on_click(_): + with lock: + pass + return "done" + + dash_duo.start_server( + app, + debug=True, + use_reloader=False, + use_debugger=True, + dev_tools_hot_reload=False, + ) + with lock: + dash_duo.find_element("#start").click() + + dash_duo.wait_for_text_to_equal("#output", "done") + error_title = "ID running component not found in layout" + error_message = [ + "Component defined in running keyword not found in layout.", + 'Component id: "non_existent_component"', + "This ID was used in the callback(s) for Output(s):", + "output.children", + "You can suppress this exception by setting", + "`suppress_callback_exceptions=True`.", + ] + # The error should show twice, once for trying to set running on and once for + # turning it off. + dash_duo.wait_for_text_to_equal(dash_duo.devtools_error_count_locator, "2") + for error in dash_duo.find_elements(".dash-fe-error__title"): + assert error.text == error_title + for error_text in dash_duo.find_elements(".dash-backend-error"): + assert all(line in error_text for line in error_message)