diff --git a/.github/workflows/test-component-library-vizro-core.yml b/.github/workflows/test-component-library-vizro-core.yml new file mode 100644 index 000000000..46cbe21e4 --- /dev/null +++ b/.github/workflows/test-component-library-vizro-core.yml @@ -0,0 +1,39 @@ +name: Integration tests Component Library + +defaults: + run: + working-directory: vizro-core + +on: + push: + branches: [main] + pull_request: + branches: + - main + +env: + PYTHONUNBUFFERED: 1 + FORCE_COLOR: 1 + +jobs: + test-component-library-vizro-core: + name: test-component-library-vizro-core + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install Hatch + run: pip install hatch + + - name: Show dependency tree + run: hatch run tests:pip tree + + - name: Run integration tests + run: hatch run tests:test-component-library diff --git a/vizro-core/hatch.toml b/vizro-core/hatch.toml index a8165f25e..da92b5d9c 100644 --- a/vizro-core/hatch.toml +++ b/vizro-core/hatch.toml @@ -56,6 +56,7 @@ schema-check = ["python schemas/generate.py --check"] # See comments added in https://github.com/mckinsey/vizro/pull/444. test = "pytest tests --headless {args}" test-integration = "pytest tests/integration --headless {args}" +test-component-library = "pytest tests/component_library --headless {args}" test-js = "./tools/run_jest.sh {args}" test-unit = "pytest tests/unit {args}" test-unit-coverage = [ @@ -118,6 +119,14 @@ extra-dependencies = [ features = ["kedro"] python = "3.9" +[envs.tests] +extra-dependencies = [ + "imutils", + "opencv-python", + "pyhamcrest" +] +python = "3.12" + [publish.index] disable = true diff --git a/vizro-core/tests/__init__.py b/vizro-core/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vizro-core/tests/component_library/test_component_library.py b/vizro-core/tests/component_library/test_component_library.py new file mode 100644 index 000000000..cc8b23d98 --- /dev/null +++ b/vizro-core/tests/component_library/test_component_library.py @@ -0,0 +1,93 @@ +# ruff: noqa: F403, F405 +import dash_bootstrap_components as dbc +import pandas as pd +import pytest +from dash import Dash, html +from vizro.figures.library import kpi_card, kpi_card_reference + +from tests.helpers.common import compare_images + +df_kpi = pd.DataFrame( + { + "Actual": [100, 200, 700], + "Reference": [100, 300, 500], + "Category": ["A", "B", "C"], + } +) + +example_cards = [ + kpi_card(data_frame=df_kpi, value_column="Actual", title="KPI with value"), + kpi_card( + data_frame=df_kpi, + value_column="Actual", + title="KPI with aggregation", + agg_func="median", + ), + kpi_card( + data_frame=df_kpi, + value_column="Actual", + title="KPI formatted", + value_format="${value:.2f}", + ), + kpi_card( + data_frame=df_kpi, + value_column="Actual", + title="KPI with icon", + icon="shopping_cart", + ), +] + +example_reference_cards = [ + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + title="KPI ref. (pos)", + ), + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + agg_func="median", + title="KPI ref. (neg)", + ), + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + title="KPI ref. formatted", + value_format="{value}€", + reference_format="{delta}€ vs. last year ({reference}€)", + ), + kpi_card_reference( + data_frame=df_kpi, + value_column="Actual", + reference_column="Reference", + title="KPI ref. with icon", + icon="shopping_cart", + ), +] + + +@pytest.mark.filterwarnings("ignore:HTTPResponse.getheader():DeprecationWarning") +@pytest.mark.filterwarnings("ignore::pytest.PytestUnhandledThreadExceptionWarning") +@pytest.mark.filterwarnings("ignore:unclosed file:ResourceWarning") +def test_kpi_card(dash_duo): + app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) + app.layout = dbc.Container( + [ + html.H1(children="KPI Cards"), + dbc.Stack( + children=[ + dbc.Row([dbc.Col(kpi_card) for kpi_card in example_cards]), + dbc.Row([dbc.Col(kpi_card) for kpi_card in example_reference_cards]), + ], + gap=4, + ), + ] + ) + dash_duo.start_server(app) + dash_duo.wait_for_page(timeout=20) + dash_duo.wait_for_element("div[class='card-kpi card']") + compare_images(dash_duo.driver, "base_kpi_comp_lib.png", "tests_kpi_comp_lib") + assert dash_duo.get_logs() == [], "browser console should contain no error" diff --git a/vizro-core/tests/helpers/__init__.py b/vizro-core/tests/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vizro-core/tests/helpers/common.py b/vizro-core/tests/helpers/common.py new file mode 100644 index 000000000..3ad032a57 --- /dev/null +++ b/vizro-core/tests/helpers/common.py @@ -0,0 +1,50 @@ +import subprocess + +import cv2 +import imutils +from hamcrest import assert_that, equal_to + + +def comparison_logic(original_image, new_image): + """Comparison process.""" + difference = cv2.subtract(original_image, new_image) + blue, green, red = cv2.split(difference) + assert_that(cv2.countNonZero(blue), equal_to(0), reason="Blue channel is different") + assert_that( + cv2.countNonZero(green), equal_to(0), reason="Green channel is different" + ) + assert_that(cv2.countNonZero(red), equal_to(0), reason="Red channel is different") + + +def create_image_difference(original, new): + """Creates new image with diff of images comparison.""" + diff = original.copy() + cv2.absdiff(original, new, diff) + gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) + for i in range(0, 3): + dilated = cv2.dilate(gray.copy(), None, iterations=i + 1) + (t_var, thresh) = cv2.threshold(dilated, 3, 255, cv2.THRESH_BINARY) + cnts = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + cnts = imutils.grab_contours(cnts) + for contour in cnts: + (x, y, width, height) = cv2.boundingRect(contour) + cv2.rectangle(new, (x, y), (x + width, y + height), (0, 255, 0), 2) + return new + + +def compare_images(browserdriver, base_image, test_image_name): + """Comparison logic and diff files creation.""" + browserdriver.save_screenshot(f"{test_image_name}_branch.png") + original = cv2.imread(f"screenshots/{base_image}") + new = cv2.imread(f"{test_image_name}_branch.png") + try: + comparison_logic(original, new) + subprocess.call(f"rm -rf {test_image_name}_branch.png", shell=True) + except (AssertionError, AttributeError) as exp: + subprocess.call(f"cp {test_image_name}_branch.png {base_image}", shell=True) + diff = create_image_difference(original=new, new=original) + cv2.imwrite(f"{test_image_name}_diff_main.png", diff) + raise Exception("pictures are not the same") from exp + except cv2.error as exp: + subprocess.call(f"cp {test_image_name}_branch.png {base_image}", shell=True) + raise Exception("pictures has different sizes") from exp \ No newline at end of file diff --git a/vizro-core/tests/screenshots/base_kpi_comp_lib.png b/vizro-core/tests/screenshots/base_kpi_comp_lib.png new file mode 100644 index 000000000..bc9550578 Binary files /dev/null and b/vizro-core/tests/screenshots/base_kpi_comp_lib.png differ