Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions apps/bench/src/__tests__/tanstack-adapter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,107 @@ describe("TanstackAdapter column pinning", () => {
}
});
});

// The `group` interaction script's shape: `rowGroups: ["col_5"]` (an owner
// column, cardinality 4 at every scale) with an "avg" aggregate on every
// numeric column — pretable's adapter attaches those deliberately so the
// aggregation stage is costed (`applyGroupAggregates`), and a comparator that
// groups WITHOUT aggregating would measure less work and flatter itself.
// Two owners and a numeric column here, so groups and their means are both
// assertable.
const groupableDataset = {
columns: [
{ id: "col_0", header: "Message", wrap: false, widthPx: 140 },
{ id: "col_5", header: "Owner", wrap: false, widthPx: 140 },
{ id: "col_7", header: "Score", wrap: false, widthPx: 96 },
],
rows: [
{ id: "r1", col_0: "a", col_5: "text-core", col_7: 10 },
{ id: "r2", col_0: "b", col_5: "text-core", col_7: 30 },
{ id: "r3", col_0: "c", col_5: "layout-core", col_7: 50 },
{ id: "r4", col_0: "d", col_5: "layout-core", col_7: 70 },
],
};

function groupPlan(): BenchInteractionPlan {
return {
focusedRowId: "r2",
filters: {},
mode: "group",
probeColumnId: "col_5",
resultRowCount: 6,
rows: groupableDataset.rows as never,
rowGroups: ["col_5"],
selectedRowId: "r2",
sort: [],
};
}

describe("TanstackAdapter row grouping", () => {
test("a group plan renders group rows, leaf rows, and computed aggregates", async () => {
const { container } = render(
<TanstackAdapter
dataset={groupableDataset as never}
runKey={0}
scriptName="group"
interactionPlan={groupPlan()}
/>,
);

await waitFor(() => {
expect(
container.querySelectorAll("[data-tanstack-group-row]").length,
).toBe(2);
});

// Group rows are rows to the harness: the settle signature and the row
// walk read the same attributes off them as off leaves.
const groupRows = [
...container.querySelectorAll<HTMLElement>("[data-tanstack-group-row]"),
];
for (const row of groupRows) {
expect(row.hasAttribute("data-tanstack-row")).toBe(true);
expect(row.getAttribute("data-row-id")).toBeTruthy();
expect(row.getAttribute("data-row-index")).toBeTruthy();
}

// All four leaves survive alongside the two groups...
expect(container.querySelectorAll("[data-tanstack-row]").length).toBe(6);
// ...and the published count is what the plan's arithmetic predicts
// (leaves + one group row per distinct key), or the settle detector
// refuses to complete the run against `plan.resultRowCount`.
expect(
container
.querySelector("[data-benchmark-adapter]")
?.getAttribute("data-bench-result-row-count"),
).toBe("6");

// The aggregation stage really ran: the group rows render the MEAN of
// their numeric column, which forces the computation inside the measured
// window exactly as pretable's formatAggregate does.
const texts = groupRows.map((row) => row.textContent ?? "");
expect(texts.some((t) => t.includes("20"))).toBe(true); // mean(10, 30)
expect(texts.some((t) => t.includes("60"))).toBe(true); // mean(50, 70)
});

test("no plan means no grouping — the render is the ungrouped one", async () => {
// The negative arm that protects every other scenario: registering the
// grouping features must be inert until a plan asks for groups.
const { container } = render(
<TanstackAdapter dataset={groupableDataset as never} runKey={0} />,
);

await waitFor(() => {
expect(container.querySelectorAll("[data-tanstack-row]").length).toBe(4);
});

expect(container.querySelectorAll("[data-tanstack-group-row]").length).toBe(
0,
);
expect(
container
.querySelector("[data-benchmark-adapter]")
?.getAttribute("data-bench-result-row-count"),
).toBe("4");
});
});
92 changes: 89 additions & 3 deletions apps/bench/src/tanstack-adapter.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { useEffect, useMemo, useRef, useState } from "react";
import {
aggregationFns,
columnFilteringFeature,
columnGroupingFeature,
columnPinningFeature,
columnSizingFeature,
createExpandedRowModel,
createFilteredRowModel,
createGroupedRowModel,
createSortedRowModel,
filterFns,
flexRender,
rowExpandingFeature,
rowAggregationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
Expand Down Expand Up @@ -37,15 +43,30 @@ const OVERSCAN = 4;
// unconditionally because `tableFeatures` is module scope — a scenario that
// pins nothing simply leaves `columnPinning.start` empty, and `getIsPinned()`
// returns false for every column, which is the pre-#413 render exactly.
// Grouping is registered the same way pinning is (#413): unconditionally at
// module scope, gated entirely by STATE. A scenario whose plan asks for no
// `rowGroups` leaves `grouping` empty, the grouped and expanded row models
// pass rows through untouched, and the render is byte-identical to before —
// the negative arm of the grouping test pins that. TanStack Table v9 ships
// all of this in the free package (`features/column-grouping`,
// `features/row-expanding`, `features/row-aggregation` in the installed
// 9.1.2); row grouping is only PAID in AG Grid (Enterprise) and MUI
// (Premium).
const tanstackFeatures = tableFeatures({
columnFilteringFeature,
columnGroupingFeature,
columnPinningFeature,
columnSizingFeature,
rowExpandingFeature,
rowAggregationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
groupedRowModel: createGroupedRowModel(),
sortedRowModel: createSortedRowModel(),
expandedRowModel: createExpandedRowModel(),
filterFns,
sortFns,
aggregationFns,
});

export interface TanstackAdapterProps {
Expand All @@ -66,13 +87,23 @@ function toColumnDef(
column: ScenarioColumn,
scriptName: string | undefined,
interactionMode: BenchInteractionPlan["mode"] | null,
sampleRow: ScenarioRow | undefined,
): ColumnDef<typeof tanstackFeatures, ScenarioRow> {
const def: ColumnDef<typeof tanstackFeatures, ScenarioRow> = {
id: column.id,
accessorKey: column.id,
header: column.header ?? column.id,
enableSorting: true,
enableColumnFilter: true,
// Every numeric column aggregates, mirroring pretable's
// `applyGroupAggregates` ("avg" there, `mean` here — the same fold): the
// grouping scripts deliberately cost the aggregation stage, and a
// comparator that groups without aggregating measures less work than the
// grid it is compared against. Inert until `grouping` state is non-empty,
// so no other scenario moves.
...(sampleRow !== undefined && typeof sampleRow[column.id] === "number"
? { aggregationFn: "mean" as const }
: {}),
// TanStack's default filterFn is "auto" which maps to includesString
// for strings. filter-metadata uses equals semantics in the bench
// plan (see interaction-plan.ts METADATA_FILTER), so set
Expand Down Expand Up @@ -166,8 +197,10 @@ export function TanstackAdapter({
const interactionMode = interactionPlan?.mode ?? null;
const columns = useMemo(
() =>
dataset.columns.map((c) => toColumnDef(c, scriptName, interactionMode)),
[dataset.columns, scriptName, interactionMode],
dataset.columns.map((c) =>
toColumnDef(c, scriptName, interactionMode, dataset.rows[0]),
),
[dataset.columns, scriptName, interactionMode, dataset.rows],
);

// The scenario's `pinned_left` columns, in dataset order. Empty for every
Expand All @@ -178,6 +211,17 @@ export function TanstackAdapter({
[dataset.columns],
);

// The `group` script's trigger IS the plan arriving (bench-app sets the
// interaction-plan override inside the measured window), so grouping is
// derived state: plan present with rowGroups -> grouped, otherwise not.
// `expanded: true` keeps every group open, which is the state the plan's
// `resultRowCount` arithmetic (leaves + one group row per key) describes.
const grouping = useMemo(
() =>
interactionPlan?.mode === "group" ? [...interactionPlan.rowGroups] : [],
[interactionPlan],
);

const table = useTable({
features: tanstackFeatures,
data,
Expand All @@ -186,7 +230,12 @@ export function TanstackAdapter({
// reason `sorting` is: `runKey` remounts the adapter per run and the pinned
// set is derived from the dataset, so it must follow a dataset swap rather
// than latch whatever the first render saw.
state: { sorting, columnPinning: { start: pinnedColumnIds, end: [] } },
state: {
sorting,
columnPinning: { start: pinnedColumnIds, end: [] },
grouping,
expanded: true,
},
onSortingChange: setSorting,
getRowId: (row) => String(row.id),
});
Expand Down Expand Up @@ -384,6 +433,7 @@ export function TanstackAdapter({
>
{virtualRows.map((vr) => {
const row = rows[vr.index];
const isGroupRow = row.getIsGrouped();
return (
<div
key={row.id}
Expand All @@ -392,6 +442,11 @@ export function TanstackAdapter({
// onto the dynamic-measurement path and move S1's numbers.
ref={hasWrappedColumns ? virtualizer.measureElement : undefined}
data-tanstack-row=""
// Group rows are rows to the harness — same id/index
// attributes, so the settle signature and the row walk treat
// them exactly as pretable's `data-pretable-group-row` rows
// are treated by its profile.
{...(isGroupRow ? { "data-tanstack-group-row": "" } : {})}
data-row-id={row.id}
data-row-index={String(vr.index)}
style={{
Expand All @@ -415,6 +470,37 @@ export function TanstackAdapter({
// wrapped branch mirrors that text model so the two grids
// lay the same string out under the same rules.
const wraps = wrappedColumnIds.has(cell.column.id);
// A group row's cells: the grouped column shows the key and
// member count; aggregated columns READ their value, which
// is what forces TanStack's lazy aggregation to actually
// compute inside the measured window — pretable's
// `formatAggregate` renders the same way. Everything else
// is blank, as in any grouped grid.
if (isGroupRow) {
const grouped = cell.getIsGrouped();
const aggregated =
!grouped && cell.column.columnDef.aggregationFn != null;
return (
<div
key={cell.id}
data-tanstack-cell=""
data-column-id={cell.column.id}
style={{
padding: "8px 10px",
fontWeight: grouped ? 700 : 400,
borderRight: "1px solid rgb(229 233 237)",
overflow: "hidden",
whiteSpace: "nowrap",
}}
>
{grouped
? `${String(cell.getValue() ?? "")} (${row.subRows.length})`
: aggregated
? String(cell.getValue() ?? "")
: ""}
</div>
);
}
// Read off TanStack rather than off the dataset: the feature
// owns the state, and `getStart("start")` is the running sum
// of the pinned widths before this column. See
Expand Down
70 changes: 53 additions & 17 deletions packages/bench-runner/src/__tests__/bench-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,24 +592,60 @@ describe("bench-runner contract", () => {
).toEqual({ ok: true });
}

// ...and rejected, with a reason, for every other adapter. Row grouping
// is AG Grid Enterprise / MUI X Premium and absent from TanStack, so
// these numbers are absolute, never comparative.
// `group` is COMPARATIVE against TanStack: v9 ships the grouping row
// model, aggregation and expansion in the free package, and the tanstack
// adapter registers them with aggregation parity. AG Grid and MUI stay
// excluded on tier (Enterprise / Premium respectively).
expect(
validateSupportedP0aRequest({
...baseRequest,
adapterId: "tanstack",
scenarioId: "S2",
scriptName: "group",
}),
).toEqual({ ok: true });
for (const adapterId of ["ag-grid", "mui"] as const) {
expect(
validateSupportedP0aRequest({
...baseRequest,
adapterId,
scenarioId: "S2",
scriptName: "group",
}),
).toEqual({
ok: false,
reason: expect.stringContaining("Enterprise"),
});
}
// `group-expand` stays pretable-only for a PLUMBING reason (bench-app's
// setup/trigger machinery), which the tanstack rejection must state —
// repeating the stale "absent from TanStack" claim here is exactly what
// this test previously did.
expect(
validateSupportedP0aRequest({
...baseRequest,
adapterId: "tanstack",
scenarioId: "S2",
scriptName: "group-expand",
}),
).toEqual({
ok: false,
reason: expect.stringContaining("plumbing"),
});
for (const adapterId of ["ag-grid", "mui"] as const) {
expect(
validateSupportedP0aRequest({
...baseRequest,
adapterId,
scenarioId: "S2",
scriptName: "group-expand",
}),
).toEqual({
ok: false,
reason: expect.stringContaining("adapter"),
});
}
for (const adapterId of ["ag-grid", "tanstack", "mui"] as const) {
for (const scriptName of ["group", "group-expand"] as const) {
expect(
validateSupportedP0aRequest({
...baseRequest,
adapterId,
scenarioId: "S2",
scriptName,
}),
).toEqual({
ok: false,
reason: expect.stringContaining("adapter"),
});
}

for (const scriptName of [
"group-updates",
"group-updates-stable-keys",
Expand Down
Loading