From 05e8b4575a9ba34dcf93a92e242a2183c79627b6 Mon Sep 17 00:00:00 2001 From: Thomas Bierhance Date: Sat, 27 Jun 2026 12:41:01 +0200 Subject: [PATCH 1/3] Test for processUnpinnedColumns --- tests/assets/dashAgGridFunctions.js | 10 ++++++ tests/test_column_pinning.py | 49 +++++++++++++++++++++++++++++ tests/utils.py | 39 ++++++++++++++++++++--- 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 tests/test_column_pinning.py diff --git a/tests/assets/dashAgGridFunctions.js b/tests/assets/dashAgGridFunctions.js index a5bbcc36..e528c916 100644 --- a/tests/assets/dashAgGridFunctions.js +++ b/tests/assets/dashAgGridFunctions.js @@ -433,6 +433,16 @@ dagfuncs.dateFilterComparator = (filterLocalDateAtMidnight, cellValue) => { // END test_custom_filter.py +// FOR test_column_pinning.py +dagfuncs.unpinAllButFirstColumn = (params) => { + const {columns, viewportWidth, api, context} = params; + // columns contains the columns AgGrid would like to unpin, but we can override that by returning + // a different set of columns. In this case, we will unpin all columns except the first column + allButFirstColumn = api.getColumns().filter((col, index) => index > 0 && col.isPinned()); + return allButFirstColumn; +} +// END test_column_pinning.py + // FOR test_quick_filter.py dagfuncs.quickFilterMatcher = (quickFilterParts, rowQuickFilterAggregateText) => { return quickFilterParts.every(part => rowQuickFilterAggregateText.match(part)); diff --git a/tests/test_column_pinning.py b/tests/test_column_pinning.py new file mode 100644 index 00000000..6a1b23af --- /dev/null +++ b/tests/test_column_pinning.py @@ -0,0 +1,49 @@ +import dash_ag_grid as dag +from dash import Dash, html, dcc +from . import utils +import uuid + + +def test_cd001_process_unpinned_columns(dash_duo): + """ Test that the processUnpinnedColumns function is called when the available viewport space is exceeded and the right most columns are unpinned.""" + + column_count = 10 + row_count = 10 + rowData = [ + {f"COL_{col_idx}": uuid.uuid4().hex for col_idx in range(column_count)} for _ in range(row_count) + ] + + app = Dash(__name__) + columnDefs = [ + { + "field": col, + "pinned": "left" + } + for col in rowData[0].keys() + ] + + app.layout = html.Div( + [ + dcc.Markdown( + "This grid uses a javascript function to make sure," + " that the right most columns are unpinned when the available viewport space is exceeded." + ), + dag.AgGrid( + columnDefs=columnDefs, + rowData=rowData, + id="grid", + dashGridOptions={ + "processUnpinnedColumns": {"function": "unpinAllButFirstColumn(params)"}, + } + ), + ], + style={"margin": 20}, + ) + dash_duo.start_server(app) + + grid = utils.Grid(dash_duo, "grid") + + grid.wait_for_pinned_column(col_id="COL_0", pin_state="left") + grid.wait_for_pinned_column(col_id="COL_1", pin_state="scrolling") + grid.wait_for_pinned_column(col_id="COL_4", pin_state="scrolling") + diff --git a/tests/utils.py b/tests/utils.py index e2634aca..feae68eb 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,5 @@ +from typing import Literal + from selenium.webdriver.common.action_chains import ActionChains from dash.testing.wait import until @@ -44,11 +46,40 @@ def _wait_for_count(self, selector, expected, description): 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_pinned_column_count(expected, pin_state="left") + + def _header_class_for_pin_state(self, pin_state: Literal["left", "right", "scrolling"]): + """Return the appropriate header class for the given pin state.""" + if pin_state == "scrolling": + return "ag-header-viewport" + elif pin_state == "left": + return "ag-pinned-left-header" + elif pin_state == "right": + return "ag-pinned-right-header" + else: + raise ValueError(f"Invalid pin_state: {pin_state}") + + def wait_for_pinned_column_count(self, expected_count, pin_state: Literal["left", "right", "scrolling"]): + """Wait for the number of columns in the specified pin state to match the expected count.""" + header_class = self._header_class_for_pin_state(pin_state) self._wait_for_count( - f'#{self.id} .ag-pinned-left-header [aria-rowindex="1"] .ag-header-cell', - expected, - "pinned_cols", + f'#{self.id} .{header_class} [aria-rowindex="1"] .ag-header-cell', + expected_count, + f"pinned_cols '{pin_state}'", + ) + + def wait_for_pinned_column( + self, + col_id: str, + pin_state: Literal["left", "right", "scrolling"], + ) -> None: + """Wait for a column to be in the specified pin state.""" + header_class = self._header_class_for_pin_state(pin_state) + + self._wait_for_count( + f'#{self.id} .{header_class} [aria-rowindex="1"] .ag-header-cell[col-id="{col_id}"]', + 1, + f"column '{col_id}' pinned '{pin_state}'", ) def wait_for_viewport_cols(self, expected): From b6803fd1efb008789db23ab98c7f3ff535c284fe Mon Sep 17 00:00:00 2001 From: Thomas Bierhance Date: Sat, 27 Jun 2026 12:41:52 +0200 Subject: [PATCH 2/3] Add processUnpinnedColumns --- src/lib/utils/propCategories.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/utils/propCategories.js b/src/lib/utils/propCategories.js index 60f819ac..8a577d84 100644 --- a/src/lib/utils/propCategories.js +++ b/src/lib/utils/propCategories.js @@ -58,6 +58,9 @@ export const GRID_MAYBE_FUNCTIONS = { sendToClipboard: 1, processDataFromClipboard: 1, + // Columns + processUnpinnedColumns: 1, + // Exporting getCustomContentBelowRow: 1, shouldRowBeSkipped: 1, From 1de19eee8a6deea4441303eb7e64063e7bc9b2fb Mon Sep 17 00:00:00 2001 From: Thomas Bierhance Date: Sat, 27 Jun 2026 12:56:00 +0200 Subject: [PATCH 3/3] Linter fixes --- tests/assets/dashAgGridFunctions.js | 5 ++--- tests/test_column_pinning.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/assets/dashAgGridFunctions.js b/tests/assets/dashAgGridFunctions.js index e528c916..9258f82e 100644 --- a/tests/assets/dashAgGridFunctions.js +++ b/tests/assets/dashAgGridFunctions.js @@ -435,11 +435,10 @@ dagfuncs.dateFilterComparator = (filterLocalDateAtMidnight, cellValue) => { // FOR test_column_pinning.py dagfuncs.unpinAllButFirstColumn = (params) => { - const {columns, viewportWidth, api, context} = params; + const {api} = params; // columns contains the columns AgGrid would like to unpin, but we can override that by returning // a different set of columns. In this case, we will unpin all columns except the first column - allButFirstColumn = api.getColumns().filter((col, index) => index > 0 && col.isPinned()); - return allButFirstColumn; + return api.getColumns().filter((col, index) => index > 0 && col.isPinned()); } // END test_column_pinning.py diff --git a/tests/test_column_pinning.py b/tests/test_column_pinning.py index 6a1b23af..f91eb20f 100644 --- a/tests/test_column_pinning.py +++ b/tests/test_column_pinning.py @@ -9,17 +9,17 @@ def test_cd001_process_unpinned_columns(dash_duo): column_count = 10 row_count = 10 - rowData = [ + row_data = [ {f"COL_{col_idx}": uuid.uuid4().hex for col_idx in range(column_count)} for _ in range(row_count) ] app = Dash(__name__) - columnDefs = [ + column_defs = [ { "field": col, "pinned": "left" } - for col in rowData[0].keys() + for col in row_data[0].keys() ] app.layout = html.Div( @@ -29,8 +29,8 @@ def test_cd001_process_unpinned_columns(dash_duo): " that the right most columns are unpinned when the available viewport space is exceeded." ), dag.AgGrid( - columnDefs=columnDefs, - rowData=rowData, + columnDefs=column_defs, + rowData=row_data, id="grid", dashGridOptions={ "processUnpinnedColumns": {"function": "unpinAllButFirstColumn(params)"},