diff --git a/apps/bench/src/__tests__/ag-grid-adapter.test.tsx b/apps/bench/src/__tests__/ag-grid-adapter.test.tsx index 950a01e..7591e91 100644 --- a/apps/bench/src/__tests__/ag-grid-adapter.test.tsx +++ b/apps/bench/src/__tests__/ag-grid-adapter.test.tsx @@ -2,6 +2,7 @@ import { render, waitFor } from "@testing-library/react"; import { describe, expect, test } from "vitest"; import { AgGridAdapter } from "../ag-grid-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("AgGridAdapter", () => { test("mounts and renders AG Grid public selectors", async () => { const { container } = render( @@ -28,4 +58,41 @@ describe("AgGridAdapter", () => { expect(container.querySelector(".ag-root-wrapper")).not.toBeNull(); }); }); + + test("publishes the post-filter row count, not the full dataset size", async () => { + // Mirror the bench: mount first, let the grid become ready, THEN apply the + // interaction plan. (The flushSync timing in the adapter is what makes the + // count land inside the bench's settle window in Chromium; this jsdom test + // guards the onFilterChanged wiring and that the count is published.) + const { container, rerender } = render( + , + ); + await waitFor(() => { + expect(container.querySelector(".ag-root-wrapper")).not.toBeNull(); + }); + + rerender( + , + ); + + // status === "running" matches 2 of 4 rows. Filtering is a pure + // client-side row-model operation in AG Grid (no layout required), so the + // displayed-row count must reflect the filter even in jsdom. + await waitFor(() => { + const section = container.querySelector( + '[data-benchmark-adapter="ag-grid"]', + ); + expect(section?.getAttribute("data-bench-result-row-count")).toBe("2"); + }); + }); }); diff --git a/apps/bench/src/ag-grid-adapter.tsx b/apps/bench/src/ag-grid-adapter.tsx index b90a753..838547e 100644 --- a/apps/bench/src/ag-grid-adapter.tsx +++ b/apps/bench/src/ag-grid-adapter.tsx @@ -1,10 +1,12 @@ -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { flushSync } from "react-dom"; import { AgGridReact } from "ag-grid-react"; import { AllCommunityModule, ModuleRegistry, themeQuartz, type ColDef, + type FilterChangedEvent, type GridApi, type GridReadyEvent, } from "ag-grid-community"; @@ -78,6 +80,12 @@ export function AgGridAdapter({ const apiRef = useRef(null); const onUpdateApiReadyRef = useRef(onUpdateApiReady); const onAutosizeReadyRef = useRef(onAutosizeReady); + // Post-filter displayed-row count, published as data-bench-result-row-count. + const [resultRowCount, setResultRowCount] = useState(dataset.rows.length); + // `onGridReady` fires asynchronously after mount; gating the interaction + // effect on this flag re-applies a filter/sort plan that is already present + // at mount once the grid API exists, rather than silently skipping it. + const [gridReady, setGridReady] = useState(false); useEffect(() => { onUpdateApiReadyRef.current = onUpdateApiReady; @@ -94,6 +102,7 @@ export function AgGridAdapter({ const onGridReady = (event: GridReadyEvent) => { apiRef.current = event.api; + setGridReady(true); const apply: ApplyBenchUpdates = (patches) => { const updates = patches.map((p) => ({ ...p })); event.api.applyTransaction({ update: updates }); @@ -142,13 +151,13 @@ export function AgGridAdapter({ } api.setFilterModel(model); } - }, [interactionPlan, runKey]); + }, [interactionPlan, runKey, gridReady]); return (
@@ -164,6 +173,17 @@ export function AgGridAdapter({ columnDefs={columnDefs} rowHeight={ROW_HEIGHT} onGridReady={onGridReady} + onFilterChanged={(event: FilterChangedEvent) => { + // AG Grid renders rows imperatively, so the bench's settle loop + // records result_row_count the moment the filtered rows paint. + // A normal setState re-render lands a few frames later — after + // settle — so the bench would capture the stale pre-filter count. + // flushSync forces the attribute update synchronously with this + // event, landing it inside the settle window. + flushSync(() => + setResultRowCount(event.api.getDisplayedRowCount()), + ); + }} getRowId={(params) => String((params.data as { id: unknown }).id)} />