diff --git a/.github/actions/failed-artifacts-and-slack-notifications/action.yml b/.github/actions/failed-artifacts-and-slack-notifications/action.yml
index 716a8d686..16bfe4dd3 100644
--- a/.github/actions/failed-artifacts-and-slack-notifications/action.yml
+++ b/.github/actions/failed-artifacts-and-slack-notifications/action.yml
@@ -7,16 +7,17 @@ runs:
- name: Copy failed screenshots
shell: bash
run: |
- mkdir ${{ env.PROJECT_PATH }}failed_screenshots/
+ mkdir ${{ env.PROJECT_PATH }}failed_screenshots
cd ${{ env.PROJECT_PATH }}
- cp *.png failed_screenshots
+ cp -r /tmp/dash_artifacts failed_screenshots
+ cp *.png failed_screenshots || true
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: Failed screenshots
path: |
- ${{ env.PROJECT_PATH }}failed_screenshots/*.png
+ ${{ env.PROJECT_PATH }}failed_screenshots
- name: Send custom JSON data to Slack
id: slack
diff --git a/.github/workflows/test-e2e-vizro-dom-elements.yml b/.github/workflows/test-e2e-vizro-dom-elements.yml
new file mode 100644
index 000000000..22ec97c3c
--- /dev/null
+++ b/.github/workflows/test-e2e-vizro-dom-elements.yml
@@ -0,0 +1,47 @@
+name: e2e vizro dom elements tests for Vizro
+
+defaults:
+ run:
+ working-directory: vizro-core
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches:
+ - main
+
+env:
+ PYTHONUNBUFFERED: 1
+ FORCE_COLOR: 1
+ PYTHON_VERSION: "3.12"
+
+jobs:
+ test-e2e-vizro-dom-elements:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+
+ - name: Install Hatch
+ run: pip install hatch
+
+ - name: Show dependency tree
+ run: hatch run pip tree
+
+ - name: Run e2e vizro-dom-elements tests
+ run: |
+ hatch run test-e2e-vizro-dom-elements
+
+ - name: Create artifacts and slack notifications
+ if: failure()
+ uses: ./.github/actions/failed-artifacts-and-slack-notifications
+ env:
+ TESTS_NAME: Vizro e2e vizro dom elements tests
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+ PROJECT_PATH: /home/runner/work/vizro/vizro/vizro-core/
diff --git a/vizro-core/hatch.toml b/vizro-core/hatch.toml
index 3ea51049f..b31535182 100644
--- a/vizro-core/hatch.toml
+++ b/vizro-core/hatch.toml
@@ -22,6 +22,7 @@ dependencies = [
"coverage[toml]>=6.5",
"pytest",
"pytest-mock",
+ "pytest-rerunfailures",
"freezegun>=1.5.0",
"dash[testing]",
"chromedriver-autoinstaller>=0.6.4",
@@ -33,10 +34,14 @@ dependencies = [
"PyGithub",
"imutils",
"opencv-python",
- "pyhamcrest"
+ "pyhamcrest",
+ "gunicorn"
]
installer = "uv"
+[envs.default.env-vars]
+PYTHONPATH = "tests/e2e/vizro/dashboards/default:tests/tests_utils"
+
[envs.default.scripts]
example = "hatch run examples:example {args:scratch_dev}" # shortcut script to underlying example environment script.
lint = "pre-commit run {args} --all-files"
@@ -63,7 +68,12 @@ templates-check = ["python src/vizro/_themes/generate_plotly_templates.py --chec
# fix this, but we don't actually use `hatch run test` anywhere right now.
# See comments added in https://github.com/mckinsey/vizro/pull/444.
test = "pytest tests --headless {args}"
-test-e2e-component-library = "pytest tests/e2e/test_component_library.py --headless {args}"
+test-e2e-component-library = "pytest tests/e2e/component_library/test_component_library.py --headless {args}"
+test-e2e-vizro-dom-elements = [
+ "gunicorn dashboard:app -b 0.0.0.0:5002 -w 1 --timeout 90 &",
+ "tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh 127.0.0.1:5002 -t 30",
+ "pytest -vs --reruns 1 tests/e2e/vizro/test_dom_elements --headless {args}"
+]
test-integration = "pytest tests/integration --headless {args}"
test-js = "./tools/run_jest.sh {args}"
test-unit = "pytest tests/unit {args}"
diff --git a/vizro-core/tests/e2e/test_component_library.py b/vizro-core/tests/e2e/component_library/test_component_library.py
similarity index 98%
rename from vizro-core/tests/e2e/test_component_library.py
rename to vizro-core/tests/e2e/component_library/test_component_library.py
index 2eb2ce538..6d0a36153 100644
--- a/vizro-core/tests/e2e/test_component_library.py
+++ b/vizro-core/tests/e2e/component_library/test_component_library.py
@@ -1,7 +1,7 @@
import dash_bootstrap_components as dbc
import pandas as pd
from dash import Dash, html
-from e2e_asserts import assert_image_equal, make_screenshot_and_paths
+from e2e.asserts import assert_image_equal, make_screenshot_and_paths
from vizro.figures.library import kpi_card, kpi_card_reference
diff --git a/vizro-core/tests/e2e/vizro/custom_components/custom_actions/custom_actions.py b/vizro-core/tests/e2e/vizro/custom_components/custom_actions/custom_actions.py
new file mode 100644
index 000000000..a3b71d510
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/custom_components/custom_actions/custom_actions.py
@@ -0,0 +1,11 @@
+from typing import Optional
+
+from vizro.models.types import capture
+
+
+@capture("action")
+def my_custom_action(click_data: Optional[dict] = None):
+ """Custom action."""
+ if click_data:
+ return f'Scatter chart clicked data:\n### Species: "{click_data["points"][0]["customdata"][0]}"'
+ return "### No data clicked."
diff --git a/vizro-core/tests/e2e/vizro/custom_components/custom_charts/bar_custom.py b/vizro-core/tests/e2e/vizro/custom_components/custom_charts/bar_custom.py
new file mode 100644
index 000000000..39312f21e
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/custom_components/custom_charts/bar_custom.py
@@ -0,0 +1,10 @@
+import vizro.plotly.express as px
+from vizro.models.types import capture
+
+
+@capture("graph")
+def bar_with_highlight(data_frame, x, highlight_bar=None):
+ """Custom chart to test using DatePicker with Parameter."""
+ fig = px.bar(data_frame=data_frame, x=x)
+ fig["data"][0]["marker"]["color"] = ["orange" if c == highlight_bar else "blue" for c in fig["data"][0]["x"]]
+ return fig
diff --git a/vizro-core/tests/e2e/vizro/custom_components/custom_components/new_dropdown.py b/vizro-core/tests/e2e/vizro/custom_components/custom_components/new_dropdown.py
new file mode 100644
index 000000000..7d4c21721
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/custom_components/custom_components/new_dropdown.py
@@ -0,0 +1,57 @@
+from typing import Annotated, Literal, Optional, Union
+
+from dash import dcc, html
+from pydantic import AfterValidator, Field, PlainSerializer, PrivateAttr
+
+import vizro.models as vm
+from vizro.models import Action
+from vizro.models._action._actions_chain import _action_validator_factory
+from vizro.models._base import VizroBaseModel, _log_call
+
+# Case 2: Entirely new component (actually exists, but for ease of explanation chosen)
+SingleOptionType = Union[bool, float, str]
+MultiOptionType = Union[list[bool], list[float], list[str]]
+
+
+class NewDropdown(VizroBaseModel):
+ """Categorical single/multi-selector `Dropdown` to be provided to `Filter`."""
+
+ type: Literal["new-dropdown"] = "new-dropdown"
+ options: Optional[MultiOptionType] = Field(default=None, description="Possible options the user can select from")
+ value: Optional[Union[SingleOptionType, MultiOptionType]] = Field(
+ default=None, description="Options that are selected by default"
+ )
+ multi: bool = Field(default=True, description="Whether to allow selection of multiple values")
+ actions: Annotated[
+ list[Action],
+ AfterValidator(_action_validator_factory("value")),
+ PlainSerializer(lambda x: x[0].actions),
+ Field(default=[]),
+ ]
+ title: Optional[str] = Field(None, description="Title to be displayed")
+
+ # Component properties for actions and interactions
+ _input_property: str = PrivateAttr("value")
+
+ @_log_call
+ def build(self):
+ full_options = self.options
+
+ return html.Div(
+ [
+ html.P(self.title) if self.title else None,
+ dcc.Dropdown(
+ id=self.id,
+ options=full_options,
+ value=self.value or full_options[0],
+ multi=self.multi,
+ persistence=True,
+ clearable=False,
+ ),
+ ],
+ )
+
+
+# Important: Add new components to expected type - here the selector of the parent components
+vm.Filter.add_type("selector", NewDropdown)
+vm.Parameter.add_type("selector", NewDropdown)
diff --git a/vizro-core/tests/e2e/vizro/custom_components/custom_components/range_slider_non_cross.py b/vizro-core/tests/e2e/vizro/custom_components/custom_components/range_slider_non_cross.py
new file mode 100644
index 000000000..5faf32880
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/custom_components/custom_components/range_slider_non_cross.py
@@ -0,0 +1,24 @@
+from typing import Literal
+
+import vizro.models as vm
+
+
+# Case 1: Based on existing component
+class RangeSliderNonCross(vm.RangeSlider):
+ """Custom numeric multi-selector `RangeSliderNonCross` to be provided to `Filter`."""
+
+ type: Literal["other_range_slider"] = "other_range_slider"
+
+ def build(self):
+ range_slider_build_obj = super().build()
+ range_slider_build_obj[self.id].allowCross = False
+ range_slider_build_obj[self.id].tooltip = {
+ "always_visible": True,
+ "placement": "bottom",
+ }
+ return range_slider_build_obj
+
+
+# Important: Add new components to expected type - here the selector of the parent components
+vm.Filter.add_type("selector", RangeSliderNonCross)
+vm.Parameter.add_type("selector", RangeSliderNonCross)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/css/typography.css b/vizro-core/tests/e2e/vizro/dashboards/assets/css/typography.css
new file mode 100644
index 000000000..93358e790
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/assets/css/typography.css
@@ -0,0 +1,6 @@
+h1,
+h2,
+.heading-1-400,
+.heading-2-400 {
+ color: hotpink;
+}
diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/favicon.ico b/vizro-core/tests/e2e/vizro/dashboards/assets/favicon.ico
new file mode 100644
index 000000000..e61cdb6d9
Binary files /dev/null and b/vizro-core/tests/e2e/vizro/dashboards/assets/favicon.ico differ
diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/collections.svg b/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/collections.svg
new file mode 100644
index 000000000..62333018e
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/collections.svg
@@ -0,0 +1,3 @@
+
diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/features.svg b/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/features.svg
new file mode 100644
index 000000000..645c58733
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/features.svg
@@ -0,0 +1,3 @@
+
diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/hypotheses.svg b/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/hypotheses.svg
new file mode 100644
index 000000000..505d7c7cd
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/assets/images/icons/content/hypotheses.svg
@@ -0,0 +1,3 @@
+
diff --git a/vizro-core/tests/e2e/vizro/dashboards/assets/logo.jpeg b/vizro-core/tests/e2e/vizro/dashboards/assets/logo.jpeg
new file mode 100644
index 000000000..3a22f8f51
Binary files /dev/null and b/vizro-core/tests/e2e/vizro/dashboards/assets/logo.jpeg differ
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py
new file mode 100644
index 000000000..00f76b2f0
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/dashboard.py
@@ -0,0 +1,33 @@
+import e2e.vizro.constants as cnst
+from pages.ag_grid_page import ag_grid_page
+from pages.datepicker_page import datepicker_page
+from pages.filters_page import filters_page
+from pages.homepage import homepage
+from pages.kpi_indicators_page import kpi_indicators_page
+from pages.parameters_page import parameters_page
+
+import vizro.models as vm
+from vizro import Vizro
+
+dashboard = vm.Dashboard(
+ title="Vizro dashboard for integration testing",
+ pages=[homepage, filters_page, parameters_page, kpi_indicators_page, datepicker_page, ag_grid_page],
+ navigation=vm.Navigation(
+ pages={
+ cnst.GENERAL_ACCORDION: [
+ cnst.HOME_PAGE_ID,
+ cnst.FILTERS_PAGE,
+ cnst.PARAMETERS_PAGE,
+ cnst.KPI_INDICATORS_PAGE,
+ ],
+ cnst.DATEPICKER_ACCORDION: [cnst.DATEPICKER_PAGE],
+ cnst.AG_GRID_ACCORDION: [cnst.TABLE_AG_GRID_PAGE],
+ }
+ ),
+ theme="vizro_light",
+)
+
+app = Vizro(assets_folder="../assets").build(dashboard)
+
+if __name__ == "__main__":
+ app.run(debug=True)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/ag_grid_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/ag_grid_page.py
new file mode 100644
index 000000000..118aab5b1
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/ag_grid_page.py
@@ -0,0 +1,51 @@
+import e2e.vizro.constants as cnst
+
+import vizro.models as vm
+import vizro.plotly.express as px
+from vizro.tables import dash_ag_grid
+
+gapminder = px.data.gapminder()
+
+ag_grid_page = vm.Page(
+ title=cnst.TABLE_AG_GRID_PAGE,
+ components=[
+ vm.Container(
+ id=cnst.TABLE_AG_GRID_CONTAINER,
+ title=cnst.TABLE_AG_GRID_CONTAINER,
+ layout=vm.Layout(grid=[[0, 1]], col_gap="0px"),
+ components=[
+ vm.AgGrid(
+ id=cnst.TABLE_AG_GRID_ID,
+ title="Equal Title One",
+ figure=dash_ag_grid(data_frame=gapminder, dashGridOptions={"pagination": True}),
+ ),
+ vm.Graph(
+ id=cnst.BOX_AG_GRID_PAGE_ID,
+ figure=px.box(gapminder, x="continent", y="lifeExp", title="Equal Title One"),
+ ),
+ ],
+ )
+ ],
+ controls=[
+ vm.Filter(
+ column="year",
+ targets=[cnst.TABLE_AG_GRID_ID],
+ selector=vm.Dropdown(value=2007),
+ ),
+ vm.Filter(
+ column="continent",
+ targets=[cnst.TABLE_AG_GRID_ID],
+ selector=vm.RadioItems(options=["Europe", "Africa", "Americas"]),
+ ),
+ vm.Filter(
+ column="continent",
+ targets=[cnst.TABLE_AG_GRID_ID],
+ selector=vm.Checklist(options=["Asia", "Oceania"]),
+ ),
+ vm.Filter(
+ column="pop",
+ targets=[cnst.TABLE_AG_GRID_ID],
+ selector=vm.RangeSlider(step=1000000.0, min=1000000, max=10000000),
+ ),
+ ],
+)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/datepicker_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/datepicker_page.py
new file mode 100644
index 000000000..743026ecd
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/datepicker_page.py
@@ -0,0 +1,141 @@
+import e2e.vizro.constants as cnst
+import pandas as pd
+from flask_caching import Cache
+
+import vizro.models as vm
+import vizro.plotly.express as px
+from vizro.managers import data_manager
+from vizro.tables import dash_data_table
+
+datepicker_df = pd.DataFrame(
+ [
+ [
+ "2016-05-16 20:42:31",
+ "Male",
+ 35,
+ "$30,000 to $39,999",
+ "Employed for wages",
+ "mechanical drafter",
+ "Associate degree",
+ None,
+ ],
+ [
+ "2016-05-16",
+ "Male",
+ 21,
+ "$1 to $10,000",
+ "Out of work and looking for work",
+ "-",
+ "Some college, no degree",
+ "join clubs/socual clubs/meet ups",
+ ],
+ [
+ "2016-05-17",
+ "Male",
+ 22,
+ "$0",
+ "Out of work but not currently looking for work",
+ "unemployed, Some college",
+ "no degree",
+ "Other exercise",
+ ],
+ [
+ "2016-05-18",
+ "Male",
+ 19,
+ "$1 to $10,000",
+ "A student",
+ "student",
+ "Some college, no degree",
+ "Joined a gym/go to the gym",
+ ],
+ [
+ "2016-05-18",
+ "Male",
+ 23,
+ "$30,000 to $39,999",
+ "Employed for wages",
+ "Factory worker",
+ "High school graduate, diploma or the equivalent (for example: GED)",
+ None,
+ ],
+ [
+ "2016-05-19",
+ "Male",
+ 23,
+ "$30,000 to $39,999",
+ "Employed for wages",
+ "Factory worker",
+ "High school graduate, diploma or the equivalent (for example: GED)",
+ None,
+ ],
+ ],
+ columns=["time", "gender", "age", "income", "employment", "job_title", "edu_level", "improve_yourself_how"],
+)
+
+
+def load_datepicker_data():
+ datepicker_df["time"] = pd.to_datetime(datepicker_df["time"], format="mixed")
+ return datepicker_df
+
+
+data_manager.cache = Cache(config={"CACHE_TYPE": "FileSystemCache", "CACHE_DIR": "cache"})
+data_manager["datepicker_df"] = load_datepicker_data
+data_manager["datepicker_df"].timeout = 10
+
+
+datepicker_page = vm.Page(
+ title=cnst.DATEPICKER_PAGE,
+ id=cnst.DATEPICKER_PAGE,
+ layout=vm.Layout(grid=[[0, 1], [0, 1], [2, 3], [2, 3]]),
+ components=[
+ vm.Graph(
+ id=cnst.BAR_POP_RANGE_ID,
+ figure=px.bar(
+ "datepicker_df",
+ x="time",
+ y="age",
+ color="age",
+ ),
+ ),
+ vm.Graph(
+ id=cnst.BAR_POP_DATE_ID,
+ figure=px.bar(
+ "datepicker_df",
+ x="time",
+ y="age",
+ color="age",
+ ),
+ ),
+ vm.Table(
+ id=cnst.TABLE_POP_RANGE_ID,
+ title="Table Pop Range",
+ figure=dash_data_table(
+ data_frame="datepicker_df",
+ ),
+ ),
+ vm.Table(
+ id=cnst.TABLE_POP_DATE_ID,
+ title="Table Pop Date",
+ figure=dash_data_table(
+ data_frame="datepicker_df",
+ ),
+ ),
+ ],
+ controls=[
+ vm.Filter(
+ column="time",
+ selector=vm.DatePicker(
+ title="Pick a date range",
+ value=["2016-05-16", "2016-05-19"],
+ max="2016-06-01",
+ ),
+ targets=[cnst.TABLE_POP_RANGE_ID, cnst.BAR_POP_RANGE_ID],
+ ),
+ vm.Filter(
+ column="time",
+ selector=vm.DatePicker(title="Pick a date", range=False),
+ targets=[cnst.TABLE_POP_DATE_ID, cnst.BAR_POP_DATE_ID],
+ ),
+ ],
+)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/filters_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/filters_page.py
new file mode 100644
index 000000000..702f787e4
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/filters_page.py
@@ -0,0 +1,103 @@
+import e2e.vizro.constants as cnst
+
+import vizro.models as vm
+import vizro.plotly.express as px
+from vizro.actions import export_data
+
+iris = px.data.iris()
+
+filters_page = vm.Page(
+ title=cnst.FILTERS_PAGE,
+ components=[
+ vm.Tabs(
+ tabs=[
+ vm.Container(
+ id=cnst.FILTERS_TAB_CONTAINER,
+ title=cnst.FILTERS_TAB_CONTAINER,
+ components=[
+ vm.Container(
+ id=cnst.FILTERS_COMPONENTS_CONTAINER,
+ title=cnst.FILTERS_COMPONENTS_CONTAINER,
+ layout=vm.Layout(grid=[[0, 1], [0, 1], [0, 2]]),
+ components=[
+ vm.Graph(
+ id=cnst.SCATTER_GRAPH_ID,
+ figure=px.scatter(
+ iris,
+ x="sepal_length",
+ y="petal_width",
+ color="sepal_width",
+ ),
+ ),
+ vm.Card(
+ text="""
+ 
+
+ Leads to the home page on click.
+ """,
+ href="/",
+ ),
+ vm.Button(
+ text="Export data",
+ actions=[
+ vm.Action(
+ function=export_data(
+ targets=[cnst.SCATTER_GRAPH_ID],
+ file_format="csv",
+ )
+ ),
+ vm.Action(
+ function=export_data(
+ targets=[cnst.SCATTER_GRAPH_ID],
+ file_format="xlsx",
+ )
+ ),
+ ],
+ ),
+ ],
+ )
+ ],
+ )
+ ]
+ ),
+ vm.Graph(
+ id=cnst.BOX_GRAPH_ID,
+ figure=px.box(
+ iris,
+ x="sepal_length",
+ y="petal_width",
+ color="sepal_width",
+ ),
+ ),
+ ],
+ controls=[
+ vm.Filter(
+ column="species",
+ targets=[cnst.SCATTER_GRAPH_ID, cnst.BOX_GRAPH_ID],
+ ),
+ vm.Filter(
+ column="species",
+ targets=[cnst.SCATTER_GRAPH_ID, cnst.BOX_GRAPH_ID],
+ selector=vm.RadioItems(
+ id=cnst.RADIO_ITEMS_FILTER_FILTERS_PAGE, options=["setosa", "versicolor", "virginica"]
+ ),
+ ),
+ vm.Filter(
+ column="species",
+ targets=[cnst.SCATTER_GRAPH_ID],
+ selector=vm.Checklist(
+ id=cnst.CHECK_LIST_FILTER_FILTERS_PAGE, options=["setosa", "versicolor", "virginica"]
+ ),
+ ),
+ vm.Filter(
+ column="petal_width",
+ targets=[cnst.SCATTER_GRAPH_ID],
+ selector=vm.Slider(id=cnst.SLIDER_FILTER_FILTERS_PAGE, step=0.5),
+ ),
+ vm.Filter(
+ column="sepal_length",
+ targets=[cnst.SCATTER_GRAPH_ID, cnst.BOX_GRAPH_ID],
+ selector=vm.RangeSlider(id=cnst.RANGE_SLIDER_FILTER_FILTERS_PAGE, step=1.0),
+ ),
+ ],
+)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py
new file mode 100644
index 000000000..dadbcebad
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/homepage.py
@@ -0,0 +1,69 @@
+import e2e.vizro.constants as cnst
+
+import vizro.models as vm
+import vizro.plotly.express as px
+
+iris = px.data.iris()
+
+homepage = vm.Page(
+ title=cnst.HOME_PAGE,
+ id=cnst.HOME_PAGE_ID,
+ layout=vm.Layout(grid=[[0, 4], [1, 4], [2, 4], [3, 4]]),
+ components=[
+ vm.Card(
+ text="""
+ # This is an
tag
+ ## This is an tag
+ ###### This is an tag
+ \n
+ >
+ > Block quotes are used to highlight text.
+ >
+ \n
+ * Item 1
+ * Item 2
+ \n
+ *This text will be italic*
+ _This will also be italic_
+ **This text will be bold**
+ _You **can** combine them_
+ """,
+ ),
+ vm.Card(
+ text="""
+ 
+
+ Leads to the filters page on click.
+ """,
+ href=cnst.FILTERS_PAGE_PATH,
+ ),
+ vm.Card(
+ text="""
+ 
+
+ Leads to the datepicker page on click.
+ """,
+ href=cnst.DATEPICKER_PAGE_PATH,
+ ),
+ vm.Card(
+ text="""
+ 
+
+ Leads to the 404 page on click.
+ """,
+ href=cnst.PAGE_404_PATH,
+ ),
+ vm.Graph(
+ id=cnst.AREA_GRAPH_ID,
+ figure=px.area(
+ iris,
+ x="sepal_length",
+ y="petal_width",
+ color="sepal_width",
+ ),
+ ),
+ ],
+ controls=[
+ vm.Filter(column="species", targets=[cnst.AREA_GRAPH_ID]),
+ ],
+)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/kpi_indicators_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/kpi_indicators_page.py
new file mode 100644
index 000000000..e5de177bf
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/kpi_indicators_page.py
@@ -0,0 +1,103 @@
+import e2e.vizro.constants as cnst
+import pandas as pd
+
+import vizro.models as vm
+from vizro.figures import kpi_card, kpi_card_reference
+
+kpi_df = pd.DataFrame(
+ [[67434, 65553, "A"], [6434, 6553, "B"], [34, 53, "C"]],
+ columns=["Actual", "Reference", "Category"],
+)
+
+kpi_indicators_page = vm.Page(
+ title=cnst.KPI_INDICATORS_PAGE,
+ layout=vm.Layout(grid=[[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
+ components=[
+ # Style 1: Value Only
+ vm.Figure(
+ figure=kpi_card(
+ data_frame=kpi_df,
+ value_column="Actual",
+ title="Value I",
+ agg_func="sum",
+ )
+ ),
+ vm.Figure(
+ figure=kpi_card(
+ data_frame=kpi_df,
+ value_column="Actual",
+ title="Value II",
+ agg_func="mean",
+ )
+ ),
+ vm.Figure(
+ figure=kpi_card(
+ data_frame=kpi_df,
+ value_column="Actual",
+ title="Value III",
+ agg_func="median",
+ )
+ ),
+ # Style 2: Value and reference value
+ vm.Figure(
+ figure=kpi_card_reference(
+ data_frame=kpi_df,
+ value_column="Reference",
+ reference_column="Actual",
+ title="Ref. Value II",
+ agg_func="sum",
+ )
+ ),
+ vm.Figure(
+ figure=kpi_card_reference(
+ data_frame=kpi_df,
+ value_column="Actual",
+ reference_column="Reference",
+ title="Ref. Value I",
+ agg_func="sum",
+ )
+ ),
+ vm.Figure(
+ id="kpi-card-reverse-coloring",
+ figure=kpi_card_reference(
+ data_frame=kpi_df,
+ value_column="Actual",
+ reference_column="Reference",
+ title="Ref. Value III",
+ agg_func="median",
+ icon="shopping_cart",
+ ),
+ ),
+ # Style 3: Value and icon
+ vm.Figure(
+ figure=kpi_card(
+ data_frame=kpi_df,
+ value_column="Actual",
+ icon="shopping_cart",
+ title="Icon I",
+ agg_func="sum",
+ value_format="${value:.2f}",
+ )
+ ),
+ vm.Figure(
+ figure=kpi_card(
+ data_frame=kpi_df,
+ value_column="Actual",
+ icon="payment",
+ title="Icon II",
+ agg_func="mean",
+ value_format="{value:.0f}€",
+ )
+ ),
+ vm.Figure(
+ figure=kpi_card(
+ data_frame=kpi_df,
+ value_column="Actual",
+ icon="monitoring",
+ title="Icon III",
+ agg_func="median",
+ )
+ ),
+ ],
+ controls=[vm.Filter(column="Category")],
+)
diff --git a/vizro-core/tests/e2e/vizro/dashboards/default/pages/parameters_page.py b/vizro-core/tests/e2e/vizro/dashboards/default/pages/parameters_page.py
new file mode 100644
index 000000000..995e61bc5
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/dashboards/default/pages/parameters_page.py
@@ -0,0 +1,111 @@
+import e2e.vizro.constants as cnst
+
+import vizro.models as vm
+import vizro.plotly.express as px
+
+iris = px.data.iris()
+
+parameters_page = vm.Page(
+ title=cnst.PARAMETERS_PAGE,
+ path=cnst.PARAMETERS_PAGE_PATH,
+ components=[
+ vm.Tabs(
+ tabs=[
+ vm.Container(
+ id=cnst.PARAMETERS_TAB_CONTAINER,
+ title=cnst.PARAMETERS_TAB_CONTAINER,
+ components=[
+ vm.Tabs(
+ id=cnst.PARAMETERS_SUB_TAB_ID,
+ tabs=[
+ vm.Container(
+ id=cnst.PARAMETERS_SUB_TAB_CONTAINER_ONE,
+ title=cnst.PARAMETERS_SUB_TAB_CONTAINER_ONE,
+ components=[
+ vm.Graph(
+ id=cnst.BAR_GRAPH_ID,
+ figure=px.bar(
+ iris,
+ x="sepal_length",
+ y="petal_width",
+ color="species",
+ color_discrete_map={
+ "setosa": "black",
+ "versicolor": "pink",
+ },
+ ),
+ )
+ ],
+ ),
+ vm.Container(
+ id=cnst.PARAMETERS_SUB_TAB_CONTAINER_TWO,
+ title=cnst.PARAMETERS_SUB_TAB_CONTAINER_TWO,
+ components=[
+ vm.Graph(
+ id=cnst.HISTOGRAM_GRAPH_ID,
+ figure=px.histogram(
+ iris,
+ x="sepal_length",
+ y="petal_width",
+ color="species",
+ color_discrete_map={
+ "setosa": "black",
+ "versicolor": "pink",
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ )
+ ],
+ )
+ ]
+ ),
+ vm.Container(
+ id=cnst.PARAMETERS_CONTAINER,
+ title=cnst.PARAMETERS_CONTAINER,
+ components=[
+ vm.Graph(
+ id=cnst.BAR_GRAPH_ID_CONTAINER,
+ figure=px.bar(
+ iris,
+ x="sepal_length",
+ y="petal_width",
+ color="species",
+ color_discrete_map={
+ "setosa": "black",
+ "versicolor": "pink",
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ controls=[
+ vm.Parameter(
+ targets=[f"{cnst.HISTOGRAM_GRAPH_ID}.color_discrete_map.setosa"],
+ selector=vm.Dropdown(options=["NONE", "red", "blue"], multi=False, value="blue"),
+ ),
+ vm.Parameter(
+ targets=[f"{cnst.BAR_GRAPH_ID}.color_discrete_map.virginica"],
+ selector=vm.Dropdown(options=["NONE", "red", "blue"], multi=False, value="blue"),
+ ),
+ vm.Parameter(
+ targets=[f"{cnst.BAR_GRAPH_ID}.title", f"{cnst.HISTOGRAM_GRAPH_ID}.title"],
+ selector=vm.RadioItems(options=["red", "blue"], value="blue"),
+ ),
+ vm.Parameter(
+ targets=[f"{cnst.BAR_GRAPH_ID}.y"],
+ selector=vm.RadioItems(options=["petal_width", "petal_length"], value="petal_width"),
+ ),
+ vm.Parameter(
+ targets=[f"{cnst.BAR_GRAPH_ID}.opacity"],
+ selector=vm.Slider(min=0, max=1, value=0.2, step=0.2, title="Bubble opacity"),
+ ),
+ vm.Parameter(
+ targets=[f"{cnst.HISTOGRAM_GRAPH_ID}.range_x"],
+ selector=vm.RangeSlider(min=4, max=8, step=1.0, title="Range X Histogram"),
+ ),
+ ],
+)
diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/conftest.py b/vizro-core/tests/e2e/vizro/test_dom_elements/conftest.py
new file mode 100644
index 000000000..42588e888
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/test_dom_elements/conftest.py
@@ -0,0 +1,46 @@
+import e2e.vizro.constants as cnst
+import pytest
+from e2e.vizro.checkers import browser_console_warnings_checker
+from selenium.common import WebDriverException
+from selenium.webdriver.chrome.options import Options as ChromeOptions
+
+
+# dash_br_driver options hook
+def pytest_setup_options():
+ options = ChromeOptions()
+ options.add_argument("--headless")
+ options.add_argument("--window-size=1920,1080")
+ return options
+
+
+def make_teardown(dash_br):
+ # checking for browser console errors
+ try:
+ log_levels = [level for level in dash_br.get_logs() if level["level"] == "SEVERE" or "WARNING"]
+ if log_levels:
+ for log_level in log_levels:
+ browser_console_warnings_checker(log_level, log_levels)
+ except WebDriverException:
+ pass
+
+
+@pytest.fixture(autouse=True)
+def dash_br_driver(dash_br, request):
+ """Built-in driver from the dash library."""
+ port = request.param.get("port", cnst.DEFAULT_PORT) if hasattr(request, "param") else cnst.DEFAULT_PORT
+ path = request.param.get("path", "") if hasattr(request, "param") else ""
+ dash_br.server_url = f"http://127.0.0.1:{port}/{path}"
+
+
+@pytest.fixture(autouse=True)
+def teardown_method(request):
+ """Fixture checks log errors and quits the driver after each test."""
+ yield
+ for driver_name in [
+ "dash_br",
+ # TODO: uncomment for the full scope of tests
+ # "driver",
+ # "chromedriver_second_browser",
+ ]:
+ if (driver := request.node.funcargs.get(driver_name)) is not None:
+ make_teardown(driver)
diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_charts.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_charts.py
new file mode 100644
index 000000000..229281d1f
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_charts.py
@@ -0,0 +1,7 @@
+import e2e.vizro.constants as cnst
+
+
+def test_modebar(dash_br):
+ """Check that modebar element exist for the chart."""
+ dash_br.multiple_click(f"a[href='{cnst.FILTERS_PAGE_PATH}']", 1)
+ dash_br.wait_for_element(".modebar-container")
diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py
new file mode 100644
index 000000000..72483c16a
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_filters.py
@@ -0,0 +1,92 @@
+import e2e.vizro.constants as cnst
+import pytest
+from e2e.vizro.checkers import check_graph_is_loading, check_slider_value
+from e2e.vizro.navigation import page_select, select_dropdown_value
+from e2e.vizro.paths import categorical_components_value_path, slider_value_path
+from e2e.vizro.waiters import graph_load_waiter
+from hamcrest import assert_that, equal_to
+
+
+def test_dropdown(dash_br):
+ page_select(dash_br, page_path=cnst.FILTERS_PAGE_PATH, page_name=cnst.FILTERS_PAGE, graph_id=cnst.SCATTER_GRAPH_ID)
+ select_dropdown_value(dash_br, value=2)
+ check_graph_is_loading(dash_br, graph_id=cnst.SCATTER_GRAPH_ID)
+
+
+@pytest.mark.parametrize(
+ "filter_id",
+ [cnst.CHECK_LIST_FILTER_FILTERS_PAGE, cnst.RADIO_ITEMS_FILTER_FILTERS_PAGE],
+ ids=["checklist", "radio_items"],
+)
+def test_categorical_filters(dash_br, filter_id):
+ page_select(dash_br, page_path=cnst.FILTERS_PAGE_PATH, page_name=cnst.FILTERS_PAGE, graph_id=cnst.SCATTER_GRAPH_ID)
+ dash_br.multiple_click(categorical_components_value_path(elem_id=filter_id, value=2), 1)
+ check_graph_is_loading(dash_br, graph_id=cnst.SCATTER_GRAPH_ID)
+
+
+def test_slider(dash_br):
+ page_select(dash_br, page_path=cnst.FILTERS_PAGE_PATH, page_name=cnst.FILTERS_PAGE, graph_id=cnst.SCATTER_GRAPH_ID)
+ dash_br.multiple_click(slider_value_path(elem_id=cnst.SLIDER_FILTER_FILTERS_PAGE, value=2), 1)
+ check_graph_is_loading(dash_br, graph_id=cnst.SCATTER_GRAPH_ID)
+ check_slider_value(dash_br, expected_end_value="0.6", elem_id=cnst.SLIDER_FILTER_FILTERS_PAGE)
+
+
+@pytest.mark.xfail(reason="Should be fixed later in vizro by Petar")
+# Right now is failing with the next error:
+# AssertionError: Element number is '4', but expected number is '4.3'
+def test_range_slider(dash_br):
+ page_select(dash_br, page_path=cnst.FILTERS_PAGE_PATH, page_name=cnst.FILTERS_PAGE, graph_id=cnst.SCATTER_GRAPH_ID)
+ dash_br.multiple_click(slider_value_path(elem_id=cnst.RANGE_SLIDER_FILTER_FILTERS_PAGE, value=4), 1)
+ check_graph_is_loading(dash_br, graph_id=cnst.SCATTER_GRAPH_ID)
+ check_slider_value(
+ dash_br, elem_id=cnst.RANGE_SLIDER_FILTER_FILTERS_PAGE, expected_start_value="4.3", expected_end_value="7"
+ )
+
+
+def test_dropdown_homepage(dash_br):
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
+ select_dropdown_value(dash_br, value=2)
+ check_graph_is_loading(dash_br, cnst.AREA_GRAPH_ID)
+
+
+def test_dropdown_kpi_indicators_page(dash_br):
+ page_select(dash_br, page_path=cnst.KPI_INDICATORS_PAGE_PATH, page_name=cnst.KPI_INDICATORS_PAGE)
+ dash_br.wait_for_text_to_equal(".card-body", "73902")
+ values = dash_br.find_elements(".card-body")
+ values_text = [value.text for value in values]
+ assert_that(
+ actual_or_assertion=values_text,
+ matcher=equal_to(
+ [
+ "73902",
+ "24634.0",
+ "6434.0",
+ "72159",
+ "73902",
+ "6434.0",
+ "$73902.00",
+ "24634€",
+ "6434.0",
+ ]
+ ),
+ )
+ select_dropdown_value(dash_br, value=2)
+ dash_br.wait_for_text_to_equal(".card-body", "67434")
+ values = dash_br.find_elements(".card-body")
+ values_text = [value.text for value in values]
+ assert_that(
+ actual_or_assertion=values_text,
+ matcher=equal_to(
+ [
+ "67434",
+ "67434.0",
+ "67434.0",
+ "65553",
+ "67434",
+ "67434.0",
+ "$67434.00",
+ "67434€",
+ "67434.0",
+ ]
+ ),
+ )
diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_pages.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_pages.py
new file mode 100644
index 000000000..4395f0b4d
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_pages.py
@@ -0,0 +1,42 @@
+import e2e.vizro.constants as cnst
+import pytest
+from e2e.vizro.checkers import check_accordion_active
+from e2e.vizro.paths import nav_card_link_path, page_title_path
+from e2e.vizro.waiters import graph_load_waiter
+
+
+def test_pages(dash_br):
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
+ dash_br.wait_for_text_to_equal(page_title_path(), cnst.HOME_PAGE)
+ dash_br.multiple_click(nav_card_link_path(href=cnst.FILTERS_PAGE_PATH), 1)
+ graph_load_waiter(dash_br, graph_id=cnst.SCATTER_GRAPH_ID)
+ dash_br.wait_for_text_to_equal(page_title_path(), text=cnst.FILTERS_PAGE)
+ dash_br.multiple_click(nav_card_link_path(href=cnst.HOME_PAGE_PATH), 1)
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
+ dash_br.wait_for_text_to_equal(page_title_path(), cnst.HOME_PAGE)
+
+
+def test_active_accordion(dash_br):
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
+ dash_br.multiple_click(nav_card_link_path(href=cnst.DATEPICKER_PAGE_PATH), 1)
+ graph_load_waiter(dash_br, graph_id=cnst.BAR_POP_DATE_ID)
+ dash_br.wait_for_text_to_equal(page_title_path(), cnst.DATEPICKER_PAGE)
+ check_accordion_active(dash_br, accordion_name=cnst.DATEPICKER_ACCORDION.upper())
+
+
+def test_404_page(dash_br):
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
+ dash_br.multiple_click(nav_card_link_path(href=cnst.PAGE_404_PATH), 1)
+ dash_br.wait_for_text_to_equal("a[class='mt-4 btn btn-primary']", "Take me home")
+ dash_br.multiple_click("a[class='mt-4 btn btn-primary']", 1)
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
+ dash_br.wait_for_text_to_equal(page_title_path(), cnst.HOME_PAGE)
+
+
+@pytest.mark.parametrize(
+ "dash_br",
+ [{"port": cnst.DEFAULT_PORT, "path": "?unexpercted_param=parampampam"}],
+ indirect=["dash_br"],
+)
+def test_unexpected_query_parameters_page(dash_br):
+ graph_load_waiter(dash_br, graph_id=cnst.AREA_GRAPH_ID)
diff --git a/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py b/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py
new file mode 100644
index 000000000..2282321c0
--- /dev/null
+++ b/vizro-core/tests/e2e/vizro/test_dom_elements/test_themes.py
@@ -0,0 +1,106 @@
+import e2e.vizro.constants as cnst
+import pytest
+from e2e.vizro.checkers import (
+ check_ag_grid_theme_color,
+ check_graph_color,
+ check_theme_color,
+)
+from e2e.vizro.navigation import accordion_select, page_select
+from e2e.vizro.paths import tab_path, theme_toggle_path
+
+
+@pytest.mark.parametrize(
+ "dash_br, dashboard_id",
+ [({"port": cnst.DEFAULT_PORT}, cnst.DASHBOARD_DEFAULT)],
+ ids=[cnst.DASHBOARD_DEFAULT],
+ indirect=["dash_br"],
+)
+def test_themes(dash_br, dashboard_id):
+ page_select(dash_br, page_path=cnst.FILTERS_PAGE_PATH, page_name=cnst.FILTERS_PAGE, graph_id=cnst.SCATTER_GRAPH_ID)
+ if dashboard_id == cnst.DASHBOARD_DEFAULT:
+ check_graph_color(dash_br, style_background=cnst.STYLE_GREY, color=cnst.HEX_GREY)
+ check_theme_color(dash_br, color=cnst.THEME_LIGHT)
+ dash_br.multiple_click(theme_toggle_path(), 1)
+ check_graph_color(dash_br, style_background=cnst.STYLE_BLACK, color=cnst.HEX_BLACK)
+ check_theme_color(dash_br, color=cnst.THEME_DARK)
+ dash_br.multiple_click(theme_toggle_path(), 1)
+ check_graph_color(dash_br, style_background=cnst.STYLE_GREY, color=cnst.HEX_GREY)
+ check_theme_color(dash_br, color=cnst.THEME_LIGHT)
+ else:
+ check_graph_color(dash_br, style_background=cnst.STYLE_BLACK, color=cnst.HEX_BLACK)
+ check_theme_color(dash_br, color=cnst.THEME_DARK)
+ dash_br.multiple_click(theme_toggle_path(), 1)
+ check_graph_color(dash_br, style_background=cnst.STYLE_GREY, color=cnst.HEX_GREY)
+ check_theme_color(dash_br, color=cnst.THEME_LIGHT)
+ dash_br.multiple_click(theme_toggle_path(), 1)
+ check_graph_color(dash_br, style_background=cnst.STYLE_BLACK, color=cnst.HEX_BLACK)
+ check_theme_color(dash_br, color=cnst.THEME_DARK)
+
+
+@pytest.mark.parametrize(
+ "dash_br, dashboard_id",
+ [({"port": cnst.DEFAULT_PORT}, cnst.DASHBOARD_DEFAULT)],
+ ids=[cnst.DASHBOARD_DEFAULT],
+ indirect=["dash_br"],
+)
+def test_ag_grid_themes(dash_br, dashboard_id):
+ accordion_select(dash_br, accordion_name=cnst.AG_GRID_ACCORDION.upper(), accordion_number=3)
+ page_select(
+ dash_br,
+ page_path=cnst.TABLE_AG_GRID_PAGE_PATH,
+ page_name=cnst.TABLE_AG_GRID_PAGE,
+ graph_id=cnst.BOX_AG_GRID_PAGE_ID,
+ )
+ if dashboard_id == cnst.DASHBOARD_DEFAULT:
+ check_ag_grid_theme_color(dash_br, ag_grid_id=cnst.TABLE_AG_GRID_ID, color=cnst.AG_GRID_LIGHT)
+ dash_br.multiple_click(theme_toggle_path(), 1)
+ check_ag_grid_theme_color(dash_br, ag_grid_id=cnst.TABLE_AG_GRID_ID, color=cnst.AG_GRID_DARK)
+ else:
+ check_ag_grid_theme_color(dash_br, ag_grid_id=cnst.TABLE_AG_GRID_ID, color=cnst.AG_GRID_DARK)
+ dash_br.multiple_click(theme_toggle_path(), 1)
+ check_ag_grid_theme_color(dash_br, ag_grid_id=cnst.TABLE_AG_GRID_ID, color=cnst.AG_GRID_LIGHT)
+
+
+@pytest.mark.parametrize(
+ "dash_br, dashboard_id",
+ [({"port": cnst.DEFAULT_PORT}, cnst.DASHBOARD_DEFAULT)],
+ ids=[cnst.DASHBOARD_DEFAULT],
+ indirect=["dash_br"],
+)
+def test_themes_page_change(dash_br, dashboard_id):
+ page_select(
+ dash_br,
+ page_path=cnst.PARAMETERS_PAGE_PATH,
+ page_name=cnst.PARAMETERS_PAGE,
+ graph_id=cnst.BAR_GRAPH_ID,
+ )
+ dash_br.multiple_click(theme_toggle_path(), 1)
+
+ def _logic(style_background, graph_color, theme_color):
+ check_graph_color(dash_br, style_background=style_background, color=graph_color)
+ check_theme_color(dash_br, color=theme_color)
+ dash_br.multiple_click(tab_path(tab_id=cnst.PARAMETERS_SUB_TAB_ID, classname="nav-link"), 1)
+ check_graph_color(dash_br, style_background=style_background, color=graph_color)
+ page_select(
+ dash_br,
+ page_path=cnst.FILTERS_PAGE_PATH,
+ page_name=cnst.FILTERS_PAGE,
+ graph_id=cnst.SCATTER_GRAPH_ID,
+ )
+ page_select(
+ dash_br,
+ page_path=cnst.PARAMETERS_PAGE_PATH,
+ page_name=cnst.PARAMETERS_PAGE,
+ graph_id=cnst.BAR_GRAPH_ID,
+ )
+ dash_br.wait_for_text_to_equal(
+ tab_path(tab_id=cnst.PARAMETERS_SUB_TAB_ID, classname="active nav-link"),
+ cnst.PARAMETERS_SUB_TAB_CONTAINER_TWO,
+ )
+ check_graph_color(dash_br, style_background=style_background, color=graph_color)
+ check_theme_color(dash_br, color=theme_color)
+
+ if dashboard_id == cnst.DASHBOARD_DEFAULT:
+ _logic(style_background=cnst.STYLE_BLACK, graph_color=cnst.HEX_BLACK, theme_color=cnst.THEME_DARK)
+ else:
+ _logic(style_background=cnst.STYLE_BLACK, graph_color=cnst.HEX_GREY, theme_color=cnst.THEME_LIGHT)
diff --git a/vizro-core/tests/tests_utils/e2e_asserts.py b/vizro-core/tests/tests_utils/e2e/asserts.py
similarity index 96%
rename from vizro-core/tests/tests_utils/e2e_asserts.py
rename to vizro-core/tests/tests_utils/e2e/asserts.py
index cc977b69f..acdf9b94a 100644
--- a/vizro-core/tests/tests_utils/e2e_asserts.py
+++ b/vizro-core/tests/tests_utils/e2e/asserts.py
@@ -40,11 +40,11 @@ def _create_image_difference(expected_image, result_image):
return expected_image
-def make_screenshot_and_paths(browserdriver, request_node_name):
+def make_screenshot_and_paths(driver, request_node_name):
"""Creates image paths and makes screenshot during the test run."""
result_image_path = f"{request_node_name}_branch.png"
expected_image_path = f"tests/e2e/screenshots/{request_node_name.replace('test', 'main')}.png"
- browserdriver.save_screenshot(result_image_path)
+ driver.save_screenshot(result_image_path)
return result_image_path, expected_image_path
diff --git a/vizro-core/tests/tests_utils/e2e/vizro/checkers.py b/vizro-core/tests/tests_utils/e2e/vizro/checkers.py
new file mode 100644
index 000000000..ef0ab4e5c
--- /dev/null
+++ b/vizro-core/tests/tests_utils/e2e/vizro/checkers.py
@@ -0,0 +1,67 @@
+import e2e.vizro.constants as cnst
+from e2e.vizro.waiters import graph_load_waiter
+from hamcrest import any_of, assert_that, contains_string, equal_to
+from selenium.webdriver.support.color import Color
+
+
+def browser_console_warnings_checker(log_level, log_levels):
+ """Whitelist for browser console errors and its assert."""
+ assert_that(
+ log_level["message"],
+ any_of(
+ contains_string(cnst.INVALID_PROP_ERROR),
+ contains_string(cnst.REACT_NOT_RECOGNIZE_ERROR),
+ contains_string(cnst.SCROLL_ZOOM_ERROR),
+ contains_string(cnst.REACT_RENDERING_ERROR),
+ contains_string(cnst.UNMOUNT_COMPONENTS_ERROR),
+ contains_string(cnst.WILLMOUNT_RENAMED_WARNING),
+ contains_string(cnst.WILLRECEIVEPROPS_RENAMED_WARNING),
+ contains_string(cnst.READPIXELS_WARNING),
+ contains_string(cnst.WEBGL_WARNING),
+ ),
+ reason=f"Error outoput: {log_levels}",
+ )
+
+
+def check_graph_is_loading(driver, graph_id):
+ """Waiting for graph to start reloading."""
+ driver.wait_for_element(f"div[id='{graph_id}'][data-dash-is-loading='true']")
+ graph_load_waiter(driver, graph_id)
+
+
+def check_slider_value(driver, elem_id, expected_end_value, expected_start_value=None):
+ end_value = driver.find_element(f"input[id='{elem_id}_end_value']").get_attribute("value")
+ assert_that(
+ end_value,
+ equal_to(expected_end_value),
+ reason=f"Element number is '{end_value}', but expected number is '{expected_end_value}'",
+ )
+ if expected_start_value:
+ start_value = driver.find_element(f"input[id='{elem_id}_start_value']").get_attribute("value")
+ assert_that(
+ start_value,
+ equal_to(expected_start_value),
+ reason=f"Element number is '{start_value}', but expected number is '{expected_start_value}'",
+ )
+
+
+def check_accordion_active(driver, accordion_name):
+ driver.wait_for_text_to_equal("button[class='accordion-button']", accordion_name)
+
+
+def check_theme_color(driver, color):
+ driver.wait_for_element(f"html[data-bs-theme='{color}']")
+
+
+def check_ag_grid_theme_color(driver, ag_grid_id, color):
+ driver.wait_for_element(f"div[id='__input_{ag_grid_id}'][class='{color}']")
+
+
+def check_graph_color(driver, style_background, color):
+ rgb = driver.wait_for_element(f"svg[style='{style_background}']").value_of_css_property("background-color")
+ graph_color = Color.from_string(rgb).hex
+ assert_that(
+ graph_color,
+ equal_to(color),
+ reason=f"Graph color is '{graph_color}', but expected color is '{color}'",
+ )
diff --git a/vizro-core/tests/tests_utils/e2e/vizro/constants.py b/vizro-core/tests/tests_utils/e2e/vizro/constants.py
new file mode 100644
index 000000000..6aad7a55f
--- /dev/null
+++ b/vizro-core/tests/tests_utils/e2e/vizro/constants.py
@@ -0,0 +1,86 @@
+# Console errors and warnings
+
+INVALID_PROP_ERROR = "Invalid prop `persisted_props[0]` of value `on` supplied to `t`"
+REACT_NOT_RECOGNIZE_ERROR = "React does not recognize the `%s` prop on a DOM element"
+SCROLL_ZOOM_ERROR = "_scrollZoom"
+REACT_RENDERING_ERROR = "unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering"
+UNMOUNT_COMPONENTS_ERROR = "React state update on an unmounted component"
+WILLMOUNT_RENAMED_WARNING = "componentWillMount has been renamed"
+WILLRECEIVEPROPS_RENAMED_WARNING = "componentWillReceiveProps has been renamed"
+READPIXELS_WARNING = "GPU stall due to ReadPixels"
+WEBGL_WARNING = "WebGL" # https://issues.chromium.org/issues/40277080
+
+# Pages and its components
+
+HOME_PAGE = "homepage (cards + graph)"
+HOME_PAGE_PATH = "/"
+HOME_PAGE_ID = "hp"
+AREA_GRAPH_ID = "ArEa"
+
+FILTERS_PAGE = "filters page (tabs + containers)"
+FILTERS_PAGE_PATH = "/filters-page-tabs--containers"
+FILTERS_TAB_CONTAINER = "filters tab_container"
+FILTERS_COMPONENTS_CONTAINER = "filters components container"
+SCATTER_GRAPH_ID = "Scatter"
+BOX_GRAPH_ID = "box graph"
+RADIO_ITEMS_FILTER_FILTERS_PAGE = "radio-filters-page"
+CHECK_LIST_FILTER_FILTERS_PAGE = "check filters-page"
+SLIDER_FILTER_FILTERS_PAGE = "slider-filters-page"
+RANGE_SLIDER_FILTER_FILTERS_PAGE = "range slider-filters-page"
+
+PARAMETERS_PAGE = "parameters_p@ge! (tabs + containers)"
+PARAMETERS_PAGE_PATH = "/parameters_page"
+PARAMETERS_TAB_CONTAINER = "parameters-tab_container"
+PARAMETERS_SUB_TAB_ID = "sub tab params"
+PARAMETERS_SUB_TAB_CONTAINER_ONE = "parameters sub t@b container 1"
+PARAMETERS_SUB_TAB_CONTAINER_TWO = "parameters sub t@b container 2"
+PARAMETERS_CONTAINER = "parameters container"
+BAR_GRAPH_ID = "b@R-graph"
+BAR_GRAPH_ID_CONTAINER = "b@R-graph__container"
+HISTOGRAM_GRAPH_ID = "-histogram-graph--"
+
+KPI_INDICATORS_PAGE = "kpi indicators page"
+KPI_INDICATORS_PAGE_PATH = "/kpi-indicators-page"
+
+EXPORT_PAGE = "export page"
+EXPORT_PAGE_PATH = "/exportp"
+
+DATEPICKER_PAGE = "DATEpicker page"
+DATEPICKER_PAGE_PATH = "/datepicker-page"
+BAR_POP_RANGE_ID = "bar pop range"
+BAR_POP_DATE_ID = "bar pop date"
+TABLE_POP_RANGE_ID = "table pop range"
+TABLE_POP_DATE_ID = "table pop date"
+
+TABLE_AG_GRID_PAGE = "table AG grid page"
+TABLE_AG_GRID_PAGE_PATH = "/table-ag-grid-page"
+TABLE_AG_GRID_ID = "123_ag_grid_table"
+BOX_AG_GRID_PAGE_ID = "B@x on ag grid page"
+TABLE_AG_GRID_CONTAINER = "table_ag_grid_container"
+
+PAGE_404_PATH = "/404-page"
+
+# Accordion names
+
+GENERAL_ACCORDION = "generAl"
+DATEPICKER_ACCORDION = "DATEpicker"
+AG_GRID_ACCORDION = "AGgrid"
+
+# Ports
+
+DEFAULT_PORT = 5002
+
+# Dashboards
+
+DASHBOARD_DEFAULT = "dashboard_default_gunicorn"
+
+# Themes
+
+THEME_DARK = "dark"
+THEME_LIGHT = "light"
+HEX_BLACK = "#141721"
+HEX_GREY = "#ffffff"
+STYLE_BLACK = "background: rgb(20, 23, 33);"
+STYLE_GREY = "background: white;"
+AG_GRID_DARK = "ag-theme-quartz-dark ag-theme-vizro"
+AG_GRID_LIGHT = "ag-theme-quartz ag-theme-vizro"
diff --git a/vizro-core/tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh b/vizro-core/tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh
new file mode 100755
index 000000000..c661e34c9
--- /dev/null
+++ b/vizro-core/tests/tests_utils/e2e/vizro/dashboards/wait-for-it.sh
@@ -0,0 +1,204 @@
+#!/usr/bin/env bash
+
+#The MIT License (MIT)
+#Copyright (c) 2016 Giles Hall
+#
+#Permission is hereby granted, free of charge, to any person obtaining a copy of
+#this software and associated documentation files (the "Software"), to deal in
+#the Software without restriction, including without limitation the rights to
+#use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+#of the Software, and to permit persons to whom the Software is furnished to do
+#so, subject to the following conditions:
+#
+#The above copyright notice and this permission notice shall be included in all
+#copies or substantial portions of the Software.
+#
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+#SOFTWARE.
+
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
diff --git a/vizro-core/tests/tests_utils/e2e/vizro/navigation.py b/vizro-core/tests/tests_utils/e2e/vizro/navigation.py
new file mode 100644
index 000000000..a47541e32
--- /dev/null
+++ b/vizro-core/tests/tests_utils/e2e/vizro/navigation.py
@@ -0,0 +1,29 @@
+import time
+
+from e2e.vizro.checkers import check_accordion_active
+from e2e.vizro.paths import page_title_path
+from e2e.vizro.waiters import graph_load_waiter
+
+
+def accordion_select(driver, accordion_name, accordion_number):
+ """Selecting accordion and checking if it is active."""
+ driver.multiple_click(f"div[class='accordion accordion'] div:nth-of-type({accordion_number})", 1)
+ check_accordion_active(driver, accordion_name)
+ # to let accordion open
+ time.sleep(1)
+
+
+def page_select(driver, page_path, page_name, graph_id=None):
+ """Selecting page and checking if it has proper title."""
+ driver.wait_for_page()
+ driver.multiple_click(f"a[href='{page_path}']", 1)
+ driver.wait_for_text_to_equal(page_title_path(), page_name)
+ if graph_id:
+ graph_load_waiter(driver, graph_id)
+
+
+def select_dropdown_value(driver, value):
+ """Steps to select value in dropdown."""
+ driver.multiple_click(".Select-clear", 1)
+ driver.multiple_click(".Select-arrow", 1)
+ driver.multiple_click(f".ReactVirtualized__Grid__innerScrollContainer div:nth-of-type({value})", 1)
diff --git a/vizro-core/tests/tests_utils/e2e/vizro/paths.py b/vizro-core/tests/tests_utils/e2e/vizro/paths.py
new file mode 100644
index 000000000..5f9f8a724
--- /dev/null
+++ b/vizro-core/tests/tests_utils/e2e/vizro/paths.py
@@ -0,0 +1,28 @@
+def theme_toggle_path():
+ return "#theme-selector"
+
+
+# Navigation
+
+
+def tab_path(tab_id, classname):
+ return f"ul[id='{tab_id}'] a[class='{classname}']"
+
+
+# Components
+
+
+def page_title_path():
+ return "#page-title"
+
+
+def nav_card_link_path(href):
+ return f"a[href='{href}'][class='nav-link']"
+
+
+def slider_value_path(elem_id, value):
+ return f"div[id='{elem_id}'] div div span:nth-of-type({value})"
+
+
+def categorical_components_value_path(elem_id, value):
+ return f"div[id='{elem_id}'] div:nth-of-type({value}) input"
diff --git a/vizro-core/tests/tests_utils/e2e/vizro/waiters.py b/vizro-core/tests/tests_utils/e2e/vizro/waiters.py
new file mode 100644
index 000000000..8ac64d64a
--- /dev/null
+++ b/vizro-core/tests/tests_utils/e2e/vizro/waiters.py
@@ -0,0 +1,3 @@
+def graph_load_waiter(driver, graph_id):
+ """Waiting for graph's x-axis to appear."""
+ driver.wait_for_element(f"div[id='{graph_id}'] path[class='xtick ticks crisp']")