From 25a9ce4ca9b9e5dbfb51c0727d50e62b9b377c06 Mon Sep 17 00:00:00 2001 From: Li Nguyen <90609403+huong-li-nguyen@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:05:20 +0100 Subject: [PATCH] Add unit tests for `Container` (#265) Co-authored-by: petar-qb Co-authored-by: Antony Milne <49395058+antonymilne@users.noreply.github.com> --- ...40112_120344_huong_li_nguyen_containers.md | 48 ++++++++++++++++ .../test_get_action_callback_mapping.py | 54 +++++++++++------- .../models/_components/test_container.py | 57 +++++++++++++++++++ .../tests/unit/vizro/models/conftest.py | 6 ++ .../tests/unit/vizro/models/test_layout.py | 47 ++++++++++++--- .../unit/vizro/models/test_models_utils.py | 22 +++++++ .../tests/unit/vizro/models/test_page.py | 18 ------ 7 files changed, 206 insertions(+), 46 deletions(-) create mode 100644 vizro-core/changelog.d/20240112_120344_huong_li_nguyen_containers.md create mode 100644 vizro-core/tests/unit/vizro/models/_components/test_container.py create mode 100644 vizro-core/tests/unit/vizro/models/test_models_utils.py diff --git a/vizro-core/changelog.d/20240112_120344_huong_li_nguyen_containers.md b/vizro-core/changelog.d/20240112_120344_huong_li_nguyen_containers.md new file mode 100644 index 000000000..f1f65e73c --- /dev/null +++ b/vizro-core/changelog.d/20240112_120344_huong_li_nguyen_containers.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py b/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py index 10641e63a..0c1478d0d 100644 --- a/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py +++ b/vizro-core/tests/unit/vizro/actions/_callback_mapping/test_get_action_callback_mapping.py @@ -38,26 +38,40 @@ def managers_one_page_four_controls_three_figures_filter_interaction(request, da id="test_page", title="My first dashboard", components=[ - vm.Graph( - id="scatter_chart", - figure=px.scatter(px.data.gapminder(), x="lifeExp", y="gdpPercap", custom_data=["continent"]), - actions=[ - vm.Action(id="filter_interaction_action", function=filter_interaction(targets=["scatter_chart_2"])) - ], - ), - vm.Graph( - id="scatter_chart_2", - figure=px.scatter(px.data.gapminder(), x="lifeExp", y="gdpPercap", custom_data=["continent"]), - actions=[vm.Action(id="custom_action", function=custom_action_example())], - ), - vm.Table( - id="vizro_table", - figure=dash_data_table_with_id, - actions=[ - vm.Action( - id="table_filter_interaction_action", - function=filter_interaction(targets=["scatter_chart", "scatter_chart_2"]), - ) + vm.Container( + title="test_container_1", + components=[ + vm.Graph( + id="scatter_chart", + figure=px.scatter(px.data.gapminder(), x="lifeExp", y="gdpPercap", custom_data=["continent"]), + actions=[ + vm.Action( + id="filter_interaction_action", function=filter_interaction(targets=["scatter_chart_2"]) + ) + ], + ), + vm.Container( + title="test_container_2", + components=[ + vm.Graph( + id="scatter_chart_2", + figure=px.scatter( + px.data.gapminder(), x="lifeExp", y="gdpPercap", custom_data=["continent"] + ), + actions=[vm.Action(id="custom_action", function=custom_action_example())], + ), + vm.Table( + id="vizro_table", + figure=dash_data_table_with_id, + actions=[ + vm.Action( + id="table_filter_interaction_action", + function=filter_interaction(targets=["scatter_chart", "scatter_chart_2"]), + ) + ], + ), + ], + ), ], ), vm.Button( diff --git a/vizro-core/tests/unit/vizro/models/_components/test_container.py b/vizro-core/tests/unit/vizro/models/_components/test_container.py new file mode 100644 index 000000000..f5f804a24 --- /dev/null +++ b/vizro-core/tests/unit/vizro/models/_components/test_container.py @@ -0,0 +1,57 @@ +"""Unit tests for vizro.models.Container.""" +import dash_bootstrap_components as dbc +import pytest +from asserts import STRIP_ALL, assert_component_equal +from dash import html + +try: + from pydantic.v1 import ValidationError +except ImportError: # pragma: no cov + from pydantic import ValidationError + +import vizro.models as vm + + +class TestContainerInstantiation: + """Tests model instantiation and the validators run at that time.""" + + def test_create_container_mandatory_only(self): + container = vm.Container(title="Title", components=[vm.Button(), vm.Button()]) + assert isinstance(container.components[0], vm.Button) and isinstance(container.components[1], vm.Button) + assert container.layout.grid == [[0], [1]] + assert container.title == "Title" + + def test_create_container_mandatory_and_optional(self): + container = vm.Container( + title="Title", + components=[vm.Button(), vm.Button()], + id="my-id", + layout=vm.Layout(grid=[[0, 1]]), + ) + assert isinstance(container.components[0], vm.Button) and isinstance(container.components[1], vm.Button) + assert container.layout.grid == [[0, 1]] + assert container.title == "Title" + assert container.id == "my-id" + + def test_mandatory_title_missing(self): + with pytest.raises(ValidationError, match="field required"): + vm.Container(components=[vm.Button()]) + + def test_mandatory_components_missing(self): + with pytest.raises(ValidationError, match="field required"): + vm.Container(title="Title") + + +class TestContainerBuildMethod: + def test_container_build(self): + result = vm.Container( + id="container", title="Title", components=[vm.Button()], layout=vm.Layout(id="layout_id", grid=[[0]]) + ).build() + assert_component_equal( + result, html.Div(className="page-component-container", id="container"), keys_to_strip={"children"} + ) + assert_component_equal(result.children, [html.H3(), html.Div()], keys_to_strip=STRIP_ALL) + # We still want to test the exact H3 produced in Container.build: + assert_component_equal(result.children[0], html.H3("Title")) + # And also that a button has been inserted in the right place: + assert_component_equal(result["layout_id_0"].children.children, dbc.Button(), keys_to_strip=STRIP_ALL) diff --git a/vizro-core/tests/unit/vizro/models/conftest.py b/vizro-core/tests/unit/vizro/models/conftest.py index 237ffefdb..ecc41c3eb 100644 --- a/vizro-core/tests/unit/vizro/models/conftest.py +++ b/vizro-core/tests/unit/vizro/models/conftest.py @@ -1,5 +1,6 @@ import pytest +import vizro.models as vm from vizro.models.types import capture @@ -10,3 +11,8 @@ def _identity_action_function(arg=None): return arg return _identity_action_function + + +@pytest.fixture(params=[vm.Container, vm.Page]) +def model_with_layout(request): + return request.param diff --git a/vizro-core/tests/unit/vizro/models/test_layout.py b/vizro-core/tests/unit/vizro/models/test_layout.py index 2cac7cfb2..3b5ded6cb 100755 --- a/vizro-core/tests/unit/vizro/models/test_layout.py +++ b/vizro-core/tests/unit/vizro/models/test_layout.py @@ -1,4 +1,3 @@ -import numpy as np import pytest try: @@ -6,6 +5,10 @@ except ImportError: # pragma: no cov from pydantic import ValidationError +import numpy as np +from asserts import assert_component_equal +from dash import html + import vizro.models as vm from vizro.models._layout import GAP_DEFAULT, MIN_DEFAULT, ColRowGridLines, _get_unique_grid_component_ids @@ -147,10 +150,38 @@ def test_working_grid(self, grid): assert False, f"{grid} raised a value error {ve}." -@pytest.mark.parametrize("grid", [[[0, -1], [1, 2]], [[0, -1, 1, 2]], [[-1, -1, -1], [0, 1, 2]]]) -def test_get_unique_grid_component_ids(grid): - result = _get_unique_grid_component_ids(grid) - expected = np.array([0, 1, 2]) - - assert isinstance(result, np.ndarray) - assert (result == expected).all() +class TestSharedLayoutHelpers: + @pytest.mark.parametrize("grid", [[[0, -1], [1, 2]], [[0, -1, 1, 2]], [[-1, -1, -1], [0, 1, 2]]]) + def test_get_unique_grid_component_ids(self, grid): + result = _get_unique_grid_component_ids(grid) + expected = np.array([0, 1, 2]) + + np.testing.assert_array_equal(result, expected) + + def test_set_layout_valid(self, model_with_layout): + model_with_layout(title="Title", components=[vm.Button(), vm.Button()], layout=vm.Layout(grid=[[0, 1]])) + + def test_set_layout_invalid(self, model_with_layout): + with pytest.raises(ValidationError, match="Number of page and grid components need to be the same."): + model_with_layout(title="Title", components=[vm.Button()], layout=vm.Layout(grid=[[0, 1]])) + + +class TestLayoutBuild: + def test_layout_build(self): + result = vm.Layout(grid=[[0, 1], [0, 2]], id="layout_id").build() + expected = html.Div( + [ + html.Div(id="layout_id_0", style={"gridColumn": "1/2", "gridRow": "1/3"}), + html.Div(id="layout_id_1", style={"gridColumn": "2/3", "gridRow": "1/2"}), + html.Div(id="layout_id_2", style={"gridColumn": "2/3", "gridRow": "2/3"}), + ], + style={ + "gridRowGap": "12px", + "gridColumnGap": "12px", + "gridTemplateColumns": f"repeat(2," f"minmax({'0px'}, 1fr))", + "gridTemplateRows": f"repeat(2," f"minmax({'0px'}, 1fr))", + }, + className="grid-layout", + id="layout_id", + ) + assert_component_equal(result, expected) diff --git a/vizro-core/tests/unit/vizro/models/test_models_utils.py b/vizro-core/tests/unit/vizro/models/test_models_utils.py new file mode 100644 index 000000000..dc88c3905 --- /dev/null +++ b/vizro-core/tests/unit/vizro/models/test_models_utils.py @@ -0,0 +1,22 @@ +import re + +import pytest + +try: + from pydantic.v1 import ValidationError +except ImportError: # pragma: no cov + from pydantic import ValidationError + +import vizro.models as vm + + +class TestSharedValidators: + def test_set_components_validator(self, model_with_layout): + with pytest.raises(ValidationError, match="Ensure this value has at least 1 item."): + model_with_layout(title="Title", components=[]) + + def test_check_for_valid_component_types(self, model_with_layout): + with pytest.raises( + ValidationError, match=re.escape("(allowed values: 'button', 'card', 'container', 'graph', 'table')") + ): + model_with_layout(title="Page Title", components=[vm.Checklist()]) diff --git a/vizro-core/tests/unit/vizro/models/test_page.py b/vizro-core/tests/unit/vizro/models/test_page.py index af1abd9ed..6f7159e4e 100644 --- a/vizro-core/tests/unit/vizro/models/test_page.py +++ b/vizro-core/tests/unit/vizro/models/test_page.py @@ -49,10 +49,6 @@ def test_mandatory_components_missing(self): with pytest.raises(ValidationError, match="field required"): vm.Page(title="Page 1") - def test_mandatory_components_invalid(self): - with pytest.raises(ValidationError, match="Ensure this value has at least 1 item."): - vm.Page(title="Page 5", components=[]) - def test_set_id_duplicate_title_valid(self): vm.Page(title="Page 1", components=[vm.Button()], id="my-id-1") vm.Page(title="Page 1", components=[vm.Button()], id="my-id-2") @@ -85,20 +81,6 @@ def test_set_path_invalid(self, test_path): page = vm.Page(title="Page 1", components=[vm.Button()], path=test_path) assert page.path == "/this-needs-fixing" - def test_set_layout_valid(self): - vm.Page(title="Page 1", components=[vm.Button(), vm.Button()], layout=vm.Layout(grid=[[0, 1]])) - - def test_set_layout_invalid(self): - with pytest.raises(ValidationError, match="Number of page and grid components need to be the same."): - vm.Page(title="Page 4", components=[vm.Button()], layout=vm.Layout(grid=[[0, 1]])) - - def test_check_for_valid_component_types(self): - with pytest.raises( - ValidationError, - match=re.escape("(allowed values: 'button', 'card', 'container', 'graph', 'table')"), - ): - vm.Page(title="Page Title", components=[vm.Checklist()]) - def test_check_for_valid_control_types(self): with pytest.raises(ValidationError, match=re.escape("(allowed values: 'filter', 'parameter')")): vm.Page(title="Page Title", components=[vm.Button()], controls=[vm.Button()])