From bdd7b55ea265e165dd10ecb1823dedcd3fbe7967 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 5 Jun 2026 15:49:50 -0700 Subject: [PATCH] fix(bench): MUI + TanStack comparators publish post-filter row count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B2 follow-up #5 residual. Since PR #140 surfaces comparator evidence in the H7/H8 filter hypotheses, the comparators' hardcoded `data-bench-result-row-count` (always dataset.rows.length = 3000) is now visibly misleading. - TanStack: publish `table.getRowModel().rows.length` (the post-filter, post-sort model) instead of `data.length` (full dataset). - MUI: track `gridFilteredTopLevelRowCountSelector` via a `filteredRowsSet` subscription instead of the static row state. Verified in Chromium (S2/hypothesis): both now report filter-metadata → 750, filter-text → 500 (matching pretable), and stay 3000 on sort. Adds jsdom regression tests asserting the post-filter count for each. AG Grid is intentionally NOT changed here: neither a synchronous getDisplayedRowCount() read nor onModelUpdated/onFilterChanged updates the count in Chromium (works in jsdom), and it stays exactly 3000 rather than dropping — suggesting setFilterModel may not reduce the client-side row model in the real bench. Tracked as a separate investigation. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/bench/src/__tests__/mui-adapter.test.tsx | 60 +++++++++++++++++ .../src/__tests__/tanstack-adapter.test.tsx | 64 +++++++++++++++++++ apps/bench/src/mui-adapter.tsx | 26 +++++++- apps/bench/src/tanstack-adapter.tsx | 5 +- 4 files changed, 152 insertions(+), 3 deletions(-) diff --git a/apps/bench/src/__tests__/mui-adapter.test.tsx b/apps/bench/src/__tests__/mui-adapter.test.tsx index 66df618..0239e4f 100644 --- a/apps/bench/src/__tests__/mui-adapter.test.tsx +++ b/apps/bench/src/__tests__/mui-adapter.test.tsx @@ -2,6 +2,7 @@ import { render, waitFor } from "@testing-library/react"; import { describe, expect, test } from "vitest"; import { MuiAdapter } from "../mui-adapter"; +import type { BenchInteractionPlan } from "../interaction-plan"; const dataset = { columns: [ @@ -14,6 +15,35 @@ const dataset = { ], }; +const statusDataset = { + columns: [ + { id: "id", header: "ID", wrap: false, widthPx: 80 }, + { id: "status", header: "Status", wrap: false, widthPx: 160 }, + ], + rows: [ + { id: "1", status: "running" }, + { id: "2", status: "stopped" }, + { id: "3", status: "running" }, + { id: "4", status: "idle" }, + ], +}; + +function filterPlan( + mode: "filter-metadata" | "filter-text", + filters: Record, +): BenchInteractionPlan { + return { + focusedRowId: null, + filters, + mode, + probeColumnId: Object.keys(filters)[0] ?? "", + resultRowCount: 0, + rows: [], + selectedRowId: null, + sort: null, + }; +} + describe("MuiAdapter", () => { test("mounts and renders MUI DataGrid public selectors", async () => { const { container } = render( @@ -33,4 +63,34 @@ describe("MuiAdapter", () => { expect(container.querySelector(".MuiDataGrid-root")).not.toBeNull(); }); }); + + test("publishes the post-filter row count, not the full dataset size", async () => { + const { container, rerender } = render( + , + ); + await waitFor(() => { + expect(container.querySelector(".MuiDataGrid-root")).not.toBeNull(); + }); + + rerender( + , + ); + + // status === "running" matches 2 of 4 rows. The published count is sourced + // from the grid's filtered-row selector, not the full dataset size. + await waitFor(() => { + const section = container.querySelector('[data-benchmark-adapter="mui"]'); + expect(section?.getAttribute("data-bench-result-row-count")).toBe("2"); + }); + }); }); diff --git a/apps/bench/src/__tests__/tanstack-adapter.test.tsx b/apps/bench/src/__tests__/tanstack-adapter.test.tsx index a10832e..c011b3b 100644 --- a/apps/bench/src/__tests__/tanstack-adapter.test.tsx +++ b/apps/bench/src/__tests__/tanstack-adapter.test.tsx @@ -2,6 +2,7 @@ import { render, waitFor } from "@testing-library/react"; import { beforeAll, describe, expect, test } from "vitest"; import { TanstackAdapter } from "../tanstack-adapter"; +import type { BenchInteractionPlan } from "../interaction-plan"; const dataset = { columns: [ @@ -14,6 +15,35 @@ const dataset = { ], }; +const statusDataset = { + columns: [ + { id: "id", header: "ID", wrap: false, widthPx: 80 }, + { id: "status", header: "Status", wrap: false, widthPx: 160 }, + ], + rows: [ + { id: "1", status: "running" }, + { id: "2", status: "stopped" }, + { id: "3", status: "running" }, + { id: "4", status: "idle" }, + ], +}; + +function filterPlan( + mode: "filter-metadata" | "filter-text", + filters: Record, +): BenchInteractionPlan { + return { + focusedRowId: null, + filters, + mode, + probeColumnId: Object.keys(filters)[0] ?? "", + resultRowCount: 0, + rows: [], + selectedRowId: null, + sort: null, + }; +} + beforeAll(() => { // jsdom doesn't ship ResizeObserver and reports zero offsetWidth / // offsetHeight for every element. @tanstack/react-virtual reads @@ -67,4 +97,38 @@ describe("TanstackAdapter", () => { ).toBeGreaterThan(0); }); }); + + test("publishes the post-filter row count, not the full dataset size", async () => { + const { container, rerender } = render( + , + ); + await waitFor(() => { + expect( + container.querySelector("[data-pretable-bench-tanstack-viewport]"), + ).not.toBeNull(); + }); + + rerender( + , + ); + + // status === "running" matches 2 of 4 rows. The published count comes from + // table.getRowModel().rows (post-filter), not the full dataset size. + await waitFor(() => { + const section = container.querySelector( + '[data-benchmark-adapter="tanstack"]', + ); + expect(section?.getAttribute("data-bench-result-row-count")).toBe("2"); + }); + }); }); diff --git a/apps/bench/src/mui-adapter.tsx b/apps/bench/src/mui-adapter.tsx index f78c047..dc0637c 100644 --- a/apps/bench/src/mui-adapter.tsx +++ b/apps/bench/src/mui-adapter.tsx @@ -1,5 +1,10 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { DataGrid, useGridApiRef, type GridColDef } from "@mui/x-data-grid"; +import { + DataGrid, + gridFilteredTopLevelRowCountSelector, + useGridApiRef, + type GridColDef, +} from "@mui/x-data-grid"; import type { ScenarioColumn, @@ -76,12 +81,29 @@ export function MuiAdapter({ onAutosizeReadyRef.current = onAutosizeReady; const [rows, setRows] = useState(() => dataset.rows.slice()); + // Post-interaction visible-row count. `rows` above is always the full + // dataset (DataGrid filters internally), so the published count is sourced + // from the grid's filtered-row selector instead — keeping it in sync with + // what the grid actually shows after a filter. + const [resultRowCount, setResultRowCount] = useState(dataset.rows.length); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect -- runKey reset setRows(dataset.rows.slice()); }, [dataset.rows, runKey]); + useEffect(() => { + const api = apiRef.current; + if (!api) return; + // Seed immediately (covers a filter already applied before subscribe), + // then track every filter change. `filteredRowsSet` fires after the grid + // recomputes its filtered model. + const sync = () => + setResultRowCount(gridFilteredTopLevelRowCountSelector(apiRef)); + sync(); + return api.subscribeEvent("filteredRowsSet", sync); + }, [apiRef, runKey, dataset.rows.length]); + const columns = useMemo( () => dataset.columns.map((c) => toColDef(c, scriptName)), [dataset.columns, scriptName], @@ -146,7 +168,7 @@ export function MuiAdapter({
diff --git a/apps/bench/src/tanstack-adapter.tsx b/apps/bench/src/tanstack-adapter.tsx index bb7b1a5..20c510a 100644 --- a/apps/bench/src/tanstack-adapter.tsx +++ b/apps/bench/src/tanstack-adapter.tsx @@ -188,7 +188,10 @@ export function TanstackAdapter({