diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..40be081f --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,38 @@ +version: 2.1 + +orbs: + browser-tools: circleci/browser-tools@1.4.0 + +jobs: + test: + docker: + - image: cimg/python:3.9.9-node + auth: + username: dashautomation + password: $DASH_PAT_DOCKERHUB + steps: + - checkout + - browser-tools/install-chrome + - browser-tools/install-chromedriver + - run: + name: Install Python deps + command: | + python -m venv venv && . venv/bin/activate + pip install --upgrade pip wheel + pip install -r tests/requirements.txt + - run: + name: Build package + command: | + . venv/bin/activate + npm ci + npm run build + - run: + name: Run tests + command: | + . venv/bin/activate + pytest --headless + +workflows: + run-tests: + jobs: + - test diff --git a/pytest.ini b/pytest.ini index b6302a60..8b16d3d9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,6 @@ [pytest] +junit_family = xunit1 + testpaths = tests/ addopts = -rsxX -vv log_format = %(asctime)s | %(levelname)s | %(name)s:%(lineno)d | %(message)s diff --git a/tests/test_add_remove_columns.py b/tests/examples/add_remove_columns.py similarity index 100% rename from tests/test_add_remove_columns.py rename to tests/examples/add_remove_columns.py diff --git a/tests/test_add_remove_update_rows.py b/tests/examples/add_remove_update_rows.py similarity index 100% rename from tests/test_add_remove_update_rows.py rename to tests/examples/add_remove_update_rows.py diff --git a/tests/assets/dashAgGridComponentFunctions.js b/tests/examples/assets/dashAgGridComponentFunctions.js similarity index 100% rename from tests/assets/dashAgGridComponentFunctions.js rename to tests/examples/assets/dashAgGridComponentFunctions.js diff --git a/tests/assets/dashAgGridFunctions.js b/tests/examples/assets/dashAgGridFunctions.js similarity index 100% rename from tests/assets/dashAgGridFunctions.js rename to tests/examples/assets/dashAgGridFunctions.js diff --git a/tests/persist_row_groups.py b/tests/examples/persist_row_groups.py similarity index 100% rename from tests/persist_row_groups.py rename to tests/examples/persist_row_groups.py diff --git a/tests/selections_complex.py b/tests/examples/selections_complex.py similarity index 100% rename from tests/selections_complex.py rename to tests/examples/selections_complex.py diff --git a/tests/selections_complex_alternate.py b/tests/examples/selections_complex_alternate.py similarity index 100% rename from tests/selections_complex_alternate.py rename to tests/examples/selections_complex_alternate.py diff --git a/tests/test_column_drag.py b/tests/test_column_drag.py new file mode 100644 index 00000000..b107cbde --- /dev/null +++ b/tests/test_column_drag.py @@ -0,0 +1,50 @@ +from dash import Dash, html +from dash_ag_grid import AgGrid +import plotly.express as px + +from . import utils + + +df = px.data.election() +default_display_cols = ["district_id", "district", "winner"] + + +def test_cd001_drag_columns(dash_duo): + app = Dash() + app.layout = html.Div([ + AgGrid( + id="grid", + rowData=df.to_dict("records"), + columnDefs=[ + {"headerName": col.capitalize(), "field": col} + for col in default_display_cols + ], + ) + ]) + + dash_duo.start_server(app) + + grid = utils.Grid(dash_duo, "grid") + + grid.wait_for_all_header_texts(["District_id", "District", "Winner"]) + grid.wait_for_pinned_cols(0) + grid.wait_for_viewport_cols(3) + + grid.drag_col(2, 0) # last column first but not pinned + + grid.wait_for_all_header_texts(["Winner", "District_id", "District"]) + grid.wait_for_pinned_cols(0) + grid.wait_for_viewport_cols(3) + + grid.pin_col(1) # middle column pinned + + grid.wait_for_all_header_texts(["District_id", "Winner", "District"]) + grid.wait_for_pinned_cols(1) + grid.wait_for_viewport_cols(2) + + # pin first non-pinned column by dragging it to its own left edge + grid.pin_col(1, 1) + + grid.wait_for_all_header_texts(["District_id", "Winner", "District"]) + grid.wait_for_pinned_cols(2) + grid.wait_for_viewport_cols(1) diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 00000000..d7978cb5 --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,35 @@ +from dash import Dash, html +from dash_ag_grid import AgGrid +import plotly.express as px + +from . import utils + + +df = px.data.election() +default_display_cols = ["district_id", "district", "winner"] + + +def test_fi001_floating_filter(dash_duo): + app = Dash() + app.layout = html.Div([ + AgGrid( + id="grid", + rowData=df.to_dict("records"), + columnDefs=[ + {"headerName": col.capitalize(), "field": col} + for col in default_display_cols + ], + defaultColDef={"filter": True, "floatingFilter": True} + ) + ]) + + dash_duo.start_server(app) + + grid = utils.Grid(dash_duo, "grid") + + grid.wait_for_cell_text(0, 1, "101-Bois-de-Liesse") + + grid.set_filter(0, "12") + + grid.wait_for_cell_text(0, 1, "112-DeLorimier") + grid.wait_for_rendered_rows(5) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..3b9bfc84 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,98 @@ +from selenium.webdriver.common.action_chains import ActionChains + +from dash.testing.wait import until +from dash.testing.errors import TestingTimeoutError + +# we use zero-based columns, but aria colindex is one-based +# so we need to add 1 in a lot of places + +class Grid: + def __init__(self, dash_duo, grid_id): + self.dash_duo = dash_duo + self.id = grid_id + + def get_header_cell(self, col): + return self.dash_duo.find_element( + f'#{self.id} [aria-rowindex="1"] .ag-header-cell[aria-colindex="{col + 1}"]' + ) + + def wait_for_header_text(self, col, expected): + self.dash_duo.wait_for_text_to_equal( + f'#{self.id} [aria-rowindex="1"] .ag-header-cell[aria-colindex="{col + 1}"] .ag-header-cell-text', + expected + ) + + def wait_for_all_header_texts(self, expected): + for col, val in enumerate(expected): + self.wait_for_header_text(col, val) + cols = len(self.dash_duo.find_elements(f'#{self.id} [aria-rowindex="1"] .ag-header-cell')) + assert cols == len(expected) + + def _wait_for_count(self, selector, expected, description): + try: + until(lambda: len(self.dash_duo.find_elements(selector)) == expected, timeout=3) + except TestingTimeoutError: + els = self.dash_duo.find_elements(selector) + raise ValueError(f"found {len(els)} {description}, expected {expected}") + + + def wait_for_pinned_cols(self, expected): + # TODO: is there a pinned right? + self._wait_for_count( + f'#{self.id} .ag-pinned-left-header [aria-rowindex="1"] .ag-header-cell', + expected, + "pinned_cols" + ) + + def wait_for_viewport_cols(self, expected): + self._wait_for_count( + f'#{self.id} .ag-header-viewport [aria-rowindex="1"] .ag-header-cell', + expected, + "viewport_cols" + ) + + def drag_col(self, from_index, to_index): + from_col = self.get_header_cell(from_index) + to_col = self.get_header_cell(to_index) + ( + ActionChains(self.dash_duo.driver) + .move_to_element(from_col) + .click_and_hold() + .move_to_location( + to_col.location["x"] + to_col.size["width"] * 0.8, + to_col.location["y"] + to_col.size["height"] * 0.5 + ) + .pause(0.5) + .release() + ).perform() + + def pin_col(self, col, pinned_cols=0): + from_col = self.get_header_cell(col) + pin_col = self.get_header_cell(pinned_cols) + ( + ActionChains(self.dash_duo.driver) + .move_to_element(from_col) + .click_and_hold() + .move_to_location( + pin_col.location["x"] + pin_col.size["width"] * 0.1, + pin_col.location["y"] + pin_col.size["height"] * 0.5 + ) + .pause(1) + .release() + ).perform() + + def wait_for_rendered_rows(self, expected): + self._wait_for_count(f"#{self.id} .ag-row", expected, "rendered rows") + + def wait_for_cell_text(self, row, col, expected): + self.dash_duo.wait_for_text_to_equal( + f'#{self.id} .ag-row[row-index="{row}"] .ag-cell[aria-colindex="{col + 1}"]', + expected + ) + + def set_filter(self, col, val): + filter_input = self.dash_duo.find_element( + f'#{self.id} .ag-floating-filter[aria-colindex="{col + 1}"] input' + ) + self.dash_duo.clear_input(filter_input) + filter_input.send_keys(val)