Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce assert_component_equal for tests #195

Merged
merged 16 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
A new scriv changelog fragment.

Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨

- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### Removed

- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### Added

- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### Changed

- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### Deprecated

- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### Fixed

- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
<!--
### Security

- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))

-->
8 changes: 1 addition & 7 deletions vizro-core/src/vizro/models/_components/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@ class Button(VizroBaseModel):
@_log_call
def build(self):
return html.Div(
[
dbc.Button(
id=self.id,
children=self.text,
className="button_primary",
),
],
dbc.Button(id=self.id, children=self.text, className="button_primary"),
className="button_container",
id=f"{self.id}_outer",
)
2 changes: 1 addition & 1 deletion vizro-core/tests/integration/test_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ def label_cases(cases, label):
def test_navigation_build(dashboard_result, dashboard_expected):
result = dashboard_result.navigation.build()
expected = dashboard_expected.navigation.build()
assert_component_equal(result, expected)
assert_component_equal(result, expected, keys_to_strip={"id"})
41 changes: 27 additions & 14 deletions vizro-core/tests/tests_utils/asserts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,41 @@
import dash.development
import plotly

STRIP_ALL = object()

def strip_keys(object, keys):
"""Strips all entries with key "id" from a dictionary, regardless of how deeply it's nested.

This makes it easy to compare dictionaries generated from Dash components we've created that contain random IDs.
"""
def _strip_keys(object, keys):
"""Strips from a JSON object all entries where the key is in keys, regardless of how deeply it's nested."""
if isinstance(object, dict):
object = {key: strip_keys(value, keys) for key, value in object.items() if key not in keys}
object = {key: _strip_keys(value, keys) for key, value in object.items() if key not in keys}
elif isinstance(object, list):
object = [strip_keys(item, keys) for item in object]
object = [_strip_keys(item, keys) for item in object]
return object


def component_to_dict(component: dash.development.base_component.Component) -> dict:
def _component_to_dict(component: dash.development.base_component.Component) -> dict:
"""Prepares a Dash component for comparison by conversion to JSON object."""
return json.loads(json.dumps(component, cls=plotly.utils.PlotlyJSONEncoder))


# TODO: implement some sort of depth limit to comparison so can use in high level tests, roll out more widely across
# tests
def assert_component_equal(left, right, keys_to_strip=None):
# Note we check for None explicitly because {} is a valid value for keys_to_strip.
keys_to_strip = keys_to_strip if keys_to_strip is not None else {"id", "class_name", "className"}
left = strip_keys(component_to_dict(left), keys_to_strip)
right = strip_keys(component_to_dict(right), keys_to_strip)
def assert_component_equal(left, right, *, keys_to_strip=None):
"""Checks that the left and right Dash components are equal, ignoring keys_to_strip.

If keys_to_strip is set to STRIP_ALL then only the type and namespace of component
will be compared, similar to doing isinstance.

Examples:
>>> from dash import html
>>> assert_component_equal(html.Div(), html.Div())
>>> assert_component_equal(html.Div(id="a"), html.Div(), keys_to_strip={"id"})
>>> assert_component_equal(html.Div([html.P(), html.P()], id="a"), html.Div(id="a"), keys_to_strip={"children"})
>>> assert_component_equal(html.Div(html.P(), className="blah", id="a"), html.Div(), keys_to_strip=STRIP_ALL)
"""
keys_to_strip = keys_to_strip or {}
if keys_to_strip is STRIP_ALL:
# Remove all properties from the component dictionary, leaving just "type" and "namespace" behind.
keys_to_strip = {"props"}

left = _strip_keys(_component_to_dict(left), keys_to_strip)
right = _strip_keys(_component_to_dict(right), keys_to_strip)
petar-qb marked this conversation as resolved.
Show resolved Hide resolved
assert left == right
48 changes: 48 additions & 0 deletions vizro-core/tests/tests_utils/demo_asserts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Demo to show how to use asserts. These are not real tests that are run as part of testing, just a teaching aid."""
from typing import List

from asserts import STRIP_ALL, assert_component_equal
from dash import html

from vizro.models import VizroBaseModel


class X(VizroBaseModel):
# Low-level contents model.
text: str

def build(self):
return html.Div(
[html.H1("Heading"), html.P(self.text, id=self.id), html.Hr(), html.H2("Something")], className="inner"
)


class Y(VizroBaseModel):
# Higher-level container model.
children: List[X]

def build(self):
return html.Div([child.build() for child in self.children], id=self.id, className="container")


def test_X_build():
# Test for low-level contents: compare the whole component tree.
# Sometimes setting keys_to_strip={"className"} is useful here.
result = X(id="x", text="Hello world").build()
expected = html.Div(
[html.H1("Heading"), html.P("Hello world", id="x"), html.Hr(), html.H2("Something")], className="inner"
)
assert_component_equal(result, expected)


def test_Y_build():
# Test for higher-level container.
many_x = [X(text="Hello world") for _ in range(4)]
result = Y(id="y", children=many_x).build()
# We don't want to compare the whole component tree here. Instead compare the bit that's specifically in Y and
# ignore the children:
assert_component_equal(result, html.Div(id="y", className="container"), keys_to_strip={"children"})
# And also compare the "interface" between X and Y. Use STRIP_ALL to not look at any properties of the html.Div.
# This is basically the same as doing:
# assert all(isinstance(child, html.Div) for child in result.children) and len(result.children) == 4
assert_component_equal(result.children, [html.Div()] * 4, keys_to_strip=STRIP_ALL)
32 changes: 9 additions & 23 deletions vizro-core/tests/unit/vizro/models/_components/test_button.py
antonymilne marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
"""Unit tests for vizro.models.Button."""
import json

import dash_bootstrap_components as dbc
import plotly
import pytest
from asserts import assert_component_equal
from dash import html

import vizro.models as vm
from vizro.actions import export_data


@pytest.fixture
def expected_button():
return html.Div(
[
dbc.Button(
id="button_id",
children="Click me!",
className="button_primary",
),
],
className="button_container",
id="button_id_outer",
)


class TestButtonInstantiation:
"""Tests model instantiation and the validators run at that time."""

Expand Down Expand Up @@ -53,9 +37,11 @@ def test_set_action_via_validator(self):


class TestBuildMethod:
def test_button_build(self, expected_button):
button = vm.Button(id="button_id", text="Click me!").build()
result = json.loads(json.dumps(button, cls=plotly.utils.PlotlyJSONEncoder))
expected = json.loads(json.dumps(expected_button, cls=plotly.utils.PlotlyJSONEncoder))

assert result == expected
def test_button_build(self):
button = vm.Button(id="button_id", text="My text").build()
expected = html.Div(
dbc.Button(id="button_id", children="My text", className="button_primary"),
className="button_container",
id="button_id_outer",
)
assert_component_equal(button, expected)
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_invalid_page(self, pages):
class TestAccordionBuild:
"""Tests accordion build method."""

common_args = {"always_open": True, "persistence": True, "persistence_type": "session"}
common_args = {"always_open": True, "persistence": True, "persistence_type": "session", "id": "accordion"}

test_cases = [
(
Expand Down Expand Up @@ -113,9 +113,11 @@ class TestAccordionBuild:
@pytest.mark.parametrize("pages, expected", test_cases)
def test_accordion(self, pages, expected):
accordion = vm.Accordion(id="accordion", pages=pages).build(active_page_id="Page 1")
assert_component_equal(accordion, html.Div(id="nav_panel_outer"), keys_to_strip={"children", "className"})
assert_component_equal(accordion["accordion"], expected)
assert_component_equal(
accordion, html.Div(id="nav_panel_outer", className="nav_panel"), keys_to_strip={"children"}
)
assert_component_equal(accordion["accordion"], expected, keys_to_strip={"class_name", "className"})

def test_accordion_one_page(self):
accordion = vm.Accordion(pages={"Group": ["Page 1"]}).build(active_page_id="Page 1")
assert_component_equal(accordion, html.Div(hidden=True, id="nav_panel_outer"), keys_to_strip={})
assert_component_equal(accordion, html.Div(hidden=True, id="nav_panel_outer"))
22 changes: 10 additions & 12 deletions vizro-core/tests/unit/vizro/models/_navigation/test_nav_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
import pytest
from asserts import assert_component_equal
from asserts import STRIP_ALL, assert_component_equal
from dash import html

try:
Expand Down Expand Up @@ -93,11 +93,13 @@ def test_nav_bar_active_pages_as_dict(self, pages_as_dict):
)
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button, keys_to_strip={"id", "className"})
assert_component_equal(
built_nav_bar["nav_panel_outer"], html.Div(id="nav_panel_outer"), keys_to_strip={"children", "className"}
built_nav_bar["nav_panel_outer"],
html.Div(id="nav_panel_outer", className="nav_panel"),
keys_to_strip={"children"},
)
assert all(isinstance(child, dbc.Accordion) for child in built_nav_bar["nav_panel_outer"].children)
assert_component_equal(built_nav_bar["nav_panel_outer"].children, [dbc.Accordion()], keys_to_strip=STRIP_ALL)

def test_nav_bar_active_pages_as_list(self, pages_as_list):
nav_bar = vm.NavBar(pages=pages_as_list)
Expand All @@ -117,11 +119,10 @@ def test_nav_bar_active_pages_as_list(self, pages_as_list):
),
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons, keys_to_strip={"id", "className"})
assert_component_equal(
built_nav_bar["nav_panel_outer"],
html.Div(id="nav_panel_outer", hidden=True),
keys_to_strip={"children", "className"},
)

def test_nav_bar_not_active_pages_as_dict(self, pages_as_dict):
Expand All @@ -137,10 +138,8 @@ def test_nav_bar_not_active_pages_as_dict(self, pages_as_dict):
)
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button)
assert_component_equal(
built_nav_bar["nav_panel_outer"], html.Div(hidden=True, id="nav_panel_outer"), keys_to_strip={}
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_button, keys_to_strip={"id", "className"})
assert_component_equal(built_nav_bar["nav_panel_outer"], html.Div(hidden=True, id="nav_panel_outer"))

def test_nav_bar_not_active_pages_as_list(self, pages_as_list):
nav_bar = vm.NavBar(pages=pages_as_list)
Expand All @@ -160,9 +159,8 @@ def test_nav_bar_not_active_pages_as_list(self, pages_as_list):
),
]
)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons)
assert_component_equal(built_nav_bar["nav_bar_outer"], expected_buttons, keys_to_strip={"id", "className"})
assert_component_equal(
built_nav_bar["nav_panel_outer"],
html.Div(id="nav_panel_outer", hidden=True),
keys_to_strip={},
)
24 changes: 20 additions & 4 deletions vizro-core/tests/unit/vizro/models/_navigation/test_nav_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
import pytest
from asserts import assert_component_equal
from asserts import STRIP_ALL, assert_component_equal
from dash import html

try:
Expand Down Expand Up @@ -86,22 +86,38 @@ def test_nav_link_active(self, pages, request):
nav_link.pre_build()
built_nav_link = nav_link.build(active_page_id="Page 1")
expected_button = dbc.Button(
children=[dmc.Tooltip(label="Label", children=[html.Span("icon")], **self.common_args)],
children=[
dmc.Tooltip(
label="Label",
children=[html.Span("icon", className="material-symbols-outlined")],
**self.common_args,
)
],
active=True,
href="/",
className="icon-button",
id="nav_link",
)
assert_component_equal(built_nav_link["nav_link"], expected_button)
assert all(isinstance(child, dbc.Accordion) for child in built_nav_link["nav_panel_outer"].children)
assert_component_equal(built_nav_link["nav_panel_outer"].children, [dbc.Accordion()], keys_to_strip=STRIP_ALL)

def test_nav_link_not_active(self, pages, request):
pages = request.getfixturevalue(pages)
nav_link = vm.NavLink(id="nav_link", label="Label", icon="icon", pages=pages)
nav_link.pre_build()
built_nav_link = nav_link.build(active_page_id="Page 3")
expected_button = dbc.Button(
children=[dmc.Tooltip(label="Label", children=[html.Span("icon")], **self.common_args)],
children=[
dmc.Tooltip(
label="Label",
children=[html.Span("icon", className="material-symbols-outlined")],
**self.common_args,
)
],
active=False,
href="/",
className="icon-button",
id="nav_link",
)
assert_component_equal(built_nav_link["nav_link"], expected_button)
assert "nav_panel_outer" not in built_nav_link
Loading
Loading