Skip to content

Commit

Permalink
Action loop and callback mapping tests (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
petar-qb authored Oct 10, 2023
1 parent b9d5933 commit c5202f7
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Added
- A bullet item for the Added category.
-->
<!--
### Changed
- A bullet item for the Changed category.
-->
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->

### Fixed

- If the `targets` argument in the `export_data` action function is specified as `"falsy"` value (`None`, `[]`), triggering the action will result in the same outcome as if the argument were not set, exporting data from all charts on the current page. ([#93](https://github.com/mckinsey/vizro/pull/93))

<!--
### Security
- A bullet item for the Security category.
-->
2 changes: 1 addition & 1 deletion vizro-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ exclude_lines = [
"if __name__ == .__main__.:",
"if TYPE_CHECKING:"
]
fail_under = 86
fail_under = 92
show_missing = true
skip_covered = true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@ def _get_action_callback_outputs(action_id: ModelID) -> Dict[str, Output]:
def _get_export_data_callback_outputs(action_id: ModelID) -> Dict[str, List[State]]:
"""Gets mapping of relevant output target name and Outputs for `export_data` action."""
action = model_manager[action_id]

try:
targets = action.function["targets"] # type: ignore[attr-defined]
except KeyError:
targets = None

if not targets:
targets = _get_components_with_data(action_id=action_id)

return {
Expand All @@ -196,6 +200,9 @@ def _get_export_data_callback_components(action_id: ModelID) -> List[dcc.Downloa
try:
targets = action.function["targets"] # type: ignore[attr-defined]
except KeyError:
targets = None

if not targets:
targets = _get_components_with_data(action_id=action_id)

return [
Expand Down
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/actions/export_data_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def export_data(
Returns:
Dict mapping target component id to modified charts/components e.g. {'my_scatter': Figure({})}
"""
if targets is None:
if not targets:
targets = [
output["id"]["target_id"]
for output in ctx.outputs_list
Expand Down
1 change: 0 additions & 1 deletion vizro-core/src/vizro/models/_action/_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@ def build(self):
def callback_wrapper(trigger: None, **inputs: Dict[str, Any]) -> Dict[str, Any]:
return self._action_callback_function(**inputs)

# return action_components
return html.Div(
children=action_components,
id=f"{self.id}_action_model_components_div",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""Unit tests for vizro.actions._action_loop._get_action_loop_components file."""

import json

import dash
import plotly
import pytest
from dash import dcc, html

import vizro.models as vm
import vizro.plotly.express as px
from vizro import Vizro
from vizro.actions import export_data
from vizro.actions._action_loop._get_action_loop_components import _get_action_loop_components
from vizro.managers import model_manager


@pytest.fixture
def fundamental_components():
return [
dcc.Store(id="action_finished"),
dcc.Store(id="remaining_actions", data=[]),
html.Div(id="cycle_breaker_div", style={"display": "hidden"}),
dcc.Store(id="cycle_breaker_empty_output_store"),
]


@pytest.fixture
def gateway_components(request):
components = request.param
actions_chain_ids = [model_manager[component].actions[0].id for component in components]
return [
dcc.Store(
id={"type": "gateway_input", "trigger_id": actions_chain_id},
data=f"{actions_chain_id}",
)
for actions_chain_id in actions_chain_ids
]


@pytest.fixture
def action_trigger_components(request):
components = request.param
actions_ids = [model_manager[component].actions[0].actions[0].id for component in components]
return [dcc.Store(id={"type": "action_trigger", "action_name": action_id}) for action_id in actions_ids]


@pytest.fixture
def action_trigger_actions_id_component(request):
components = request.param
actions_ids = [model_manager[component].actions[0].actions[0].id for component in components]
return dcc.Store(
id="action_trigger_actions_id",
data=actions_ids,
)


@pytest.fixture
def trigger_to_actions_chain_mapper_component(request):
components = request.param
actions_chain_ids = [model_manager[component].actions[0].id for component in components]
return dcc.Store(
id="trigger_to_actions_chain_mapper",
data={
actions_chain_id: [action.id for action in model_manager[actions_chain_id].actions]
for actions_chain_id in actions_chain_ids
},
)


@pytest.fixture
def managers_one_page_two_components_two_controls():
"""Instantiates managers with one page that contains two controls and two components."""
page = vm.Page(
id="test_page",
title="First page",
components=[
vm.Graph(
id="scatter_chart",
figure=px.scatter(px.data.gapminder(), x="lifeExp", y="gdpPercap"),
),
vm.Button(
id="export_data_button",
actions=[vm.Action(id="export_data_action", function=export_data())],
),
],
controls=[
vm.Filter(id="filter_continent", column="continent", selector=vm.Dropdown(id="filter_continent_selector")),
vm.Parameter(
id="parameter_x",
targets=["scatter_chart.x"],
selector=vm.Dropdown(
id="parameter_x_selector",
options=["lifeExp", "gdpPercap", "pop"],
),
),
],
)
# TODO: Call the Dashboard._pre_build() method once the pages registration is moved into this method.
yield Vizro().build(vm.Dashboard(pages=[page]))
del dash.page_registry["test_page"]


@pytest.fixture
def managers_one_page_no_actions():
"""Instantiates managers with one "empty" page."""
page = vm.Page(
id="test_page_no_actions",
title="Second page",
components=[vm.Card(text="")],
)
# TODO: Call the Dashboard._pre_build() method once the pages registration is moved into this method.
yield Vizro().build(vm.Dashboard(pages=[page]))
del dash.page_registry["test_page_no_actions"]


class TestGetActionLoopComponents:
"""Tests getting required components for the action loop."""

@pytest.mark.usefixtures("managers_one_page_no_actions")
def test_no_components(self):
result = _get_action_loop_components()
result = json.loads(json.dumps(result, cls=plotly.utils.PlotlyJSONEncoder))

expected = html.Div(id="action_loop_components_div")
expected = json.loads(json.dumps(expected, cls=plotly.utils.PlotlyJSONEncoder))

assert result == expected

@pytest.mark.usefixtures("managers_one_page_two_components_two_controls")
@pytest.mark.parametrize(
"gateway_components, "
"action_trigger_components, "
"action_trigger_actions_id_component, "
"trigger_to_actions_chain_mapper_component",
[
(
["test_page", "export_data_button", "filter_continent_selector", "parameter_x_selector"],
["test_page", "export_data_button", "filter_continent_selector", "parameter_x_selector"],
["test_page", "export_data_button", "filter_continent_selector", "parameter_x_selector"],
["test_page", "export_data_button", "filter_continent_selector", "parameter_x_selector"],
)
],
indirect=True,
)
def test_all_action_loop_components( # noqa: PLR0913 # pylint: disable=too-many-arguments
self,
fundamental_components,
gateway_components,
action_trigger_components,
action_trigger_actions_id_component,
trigger_to_actions_chain_mapper_component,
):
result = _get_action_loop_components()
result = json.loads(json.dumps(result, cls=plotly.utils.PlotlyJSONEncoder))

all_action_loop_components_expected = (
fundamental_components
+ gateway_components
+ action_trigger_components
+ [action_trigger_actions_id_component]
+ [trigger_to_actions_chain_mapper_component]
)

expected = html.Div(children=all_action_loop_components_expected, id="action_loop_components_div")
expected = json.loads(json.dumps(expected, cls=plotly.utils.PlotlyJSONEncoder))

assert result == expected
Loading

0 comments on commit c5202f7

Please sign in to comment.