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
67 changes: 67 additions & 0 deletions apps/bench/src/__tests__/ag-grid-adapter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ const dataset = {
],
};

// S2 ("wrap-auto-height") shape: some columns carry `wrap: true`, the rest
// `wrap: false`. Both kinds must be present in one dataset so a single mount
// proves the flags are gated on `wrap` rather than applied unconditionally.
const wrapDataset = {
columns: [
{ id: "plain", header: "Plain", wrap: false, widthPx: 140 },
{ id: "wrapped", header: "Wrapped", wrap: true, widthPx: 220 },
],
rows: [
{ id: "1", plain: "short", wrapped: "a much longer sentence that wraps" },
],
};

const statusDataset = {
columns: [
{ id: "id", header: "ID", wrap: false, widthPx: 80 },
Expand Down Expand Up @@ -61,6 +74,60 @@ describe("AgGridAdapter", () => {
});
});

test("carries the wrap colDef onto the right cells, and only those", async () => {
// READ THIS BEFORE TRUSTING THIS TEST. Everything asserted here is a
// *class or attribute* that AG Grid toggles straight off the colDef —
// `CellCtrl.applyStaticCssClasses` reads `column.isAutoHeight()` and
// `setWrapText` reads `colDef.wrapText`. jsdom has no layout engine, so it
// cannot tell whether any of it changed a pixel: `getBoundingClientRect()`
// returns zeros and `scrollHeight` is always 0. This test passed unchanged
// while AG Grid was laying every wrapped line out at 39px of leading and
// painting every wrapped row at the fixed 48px `rowHeight`.
//
// What it IS good for: catching a colDef that stopped being emitted, or
// being emitted for the wrong columns, cheaply and in the unit layer.
// The pixels are proved in `apps/bench/tests/ag-grid-wrap-auto-height.spec.ts`,
// which runs in real Chromium and fails if any of the three colDef fields
// below is dropped.
//
// AG Grid needs all three and they are independent: `wrapText` toggles
// `.ag-cell-wrap-text` (white-space: normal, overriding the base
// `.ag-cell { white-space: nowrap }`); `autoHeight` toggles
// `.ag-cell-auto-height` and enrolls the cell in row-height measurement;
// and `cellStyle` releases the line-height from the row height, which AG
// Grid's theme otherwise uses as the leading for every wrapped line.
const { container } = render(
<AgGridAdapter dataset={wrapDataset as never} runKey={0} />,
);

await waitFor(() => {
expect(
container.querySelector('.ag-cell[col-id="wrapped"]'),
).not.toBeNull();
});

const wrapped = container.querySelector<HTMLElement>(
'.ag-cell[col-id="wrapped"]',
);
expect(wrapped?.classList.contains("ag-cell-wrap-text")).toBe(true);
expect(wrapped?.classList.contains("ag-cell-auto-height")).toBe(true);
// `cellStyle` lands as an inline style, which is a DOM fact rather than a
// layout one, so jsdom can see it — it just cannot see what it does.
expect(wrapped?.style.lineHeight).toBe("1.5");

// The negative half is the load-bearing one: setting the flags
// unconditionally would pass the assertions above while silently changing
// every `wrapped_columns: 0` scenario (S1 etc.) out from under its
// baseline.
const plain = container.querySelector<HTMLElement>(
'.ag-cell[col-id="plain"]',
);
expect(plain).not.toBeNull();
expect(plain?.classList.contains("ag-cell-wrap-text")).toBe(false);
expect(plain?.classList.contains("ag-cell-auto-height")).toBe(false);
expect(plain?.style.lineHeight).toBe("");
});

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
Expand Down
128 changes: 128 additions & 0 deletions apps/bench/src/__tests__/bench-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2283,3 +2283,131 @@ describe("bench data update runtime", () => {
}
}, 20_000);
});

describe("scroll targets track a growing scroll extent", () => {
/**
* A grid with auto-height rows does not know its own height up front: AG Grid
* keeps the `rowHeight` option for every unmeasured row, so `scrollHeight`
* grows underneath the pass as cells are rendered and measured.
*
* Deriving all 36 targets from one initial `scrollHeight` therefore aims the
* whole run at a mostly-unmeasured model, and the pass covers a shrinking
* fraction of the dataset — while a grid that sizes rows up front covers all
* of it. That is a like-for-like break, and it only appeared once the
* comparator adapters started wrapping (#400).
*
* The fixture grows the content as it is scrolled, which is the shape of the
* real failure. For a fixed-height grid `scrollHeight` never moves and the
* fraction form is arithmetically identical to the old one, so this changes
* only the case the old form got wrong.
*/
test("aims at the live scroll extent, not the one sampled before measurement", async () => {
document.body.innerHTML = `
<div data-testid="root">
<div aria-label="AG Grid Community adapter">
<div class="ag-grid-viewport">
<div class="ag-row" data-row-index="0" data-row-height="60">
<div class="ag-cell">row 0</div>
</div>
<div class="ag-row" data-row-index="1" data-row-height="60">
<div class="ag-cell">row 1</div>
</div>
</div>
</div>
</div>
`;

const root = document.querySelector<HTMLElement>('[data-testid="root"]');
const viewport = root?.querySelector<HTMLElement>(".ag-grid-viewport");
const rows = [...root!.querySelectorAll<HTMLElement>(".ag-row")];
const OriginalPerformanceObserver = globalThis.PerformanceObserver;
const assignedTops: number[] = [];
const INITIAL_SCROLL_HEIGHT = 1_000;
const CLIENT_HEIGHT = 120;
const initialMaxScrollTop = INITIAL_SCROLL_HEIGHT - CLIENT_HEIGHT;
let scrollTop = 0;

expect(root).toBeTruthy();
expect(viewport).toBeTruthy();

Object.defineProperties(viewport!, {
clientTop: { value: 0, configurable: true },
clientHeight: { value: CLIENT_HEIGHT, configurable: true },
scrollHeight: {
configurable: true,
get() {
// Content grows as the run scrolls into it, the way measured
// auto-height rows grow a grid that had estimated them at 48px.
return Math.min(5_000, INITIAL_SCROLL_HEIGHT + scrollTop * 4);
},
},
scrollTop: {
configurable: true,
get() {
return scrollTop;
},
set(value: number) {
assignedTops.push(value);
scrollTop = value;
},
},
});
viewport!.getBoundingClientRect = () =>
createRect({ top: 0, bottom: CLIENT_HEIGHT });

let frame = 0;

Object.defineProperty(globalThis, "requestAnimationFrame", {
configurable: true,
value: (callback: FrameRequestCallback) => {
frame += 1;
callback(frame * 16);
return frame;
},
});
Object.defineProperty(globalThis, "PerformanceObserver", {
configurable: true,
value: class {
static supportedEntryTypes = ["longtask"];
observe() {}
disconnect() {}
},
});
Object.defineProperty(globalThis, "getComputedStyle", {
configurable: true,
value: () => ({
contain: "none",
containIntrinsicSize: "none",
contentVisibility: "visible",
overflowAnchor: "none",
overscrollBehavior: "contain",
}),
});

for (const [index, row] of rows.entries()) {
row.getBoundingClientRect = () =>
createRect({
top: index * 60 - viewport!.scrollTop,
bottom: (index + 1) * 60 - viewport!.scrollTop,
});
}

await measureBenchScrollRun(root!, "ag-grid");

Object.defineProperty(globalThis, "PerformanceObserver", {
configurable: true,
value: OriginalPerformanceObserver,
});

// The load-bearing assertion. Targets derived once from the initial extent
// can never exceed it, so this is exactly the number that separates the two
// implementations.
expect(Math.max(...assignedTops)).toBeGreaterThan(initialMaxScrollTop);

// And the run must still reach the true bottom, not merely overshoot the
// stale value — otherwise a partial fix would pass the assertion above.
expect(Math.max(...assignedTops)).toBeGreaterThanOrEqual(
viewport!.scrollHeight - CLIENT_HEIGHT - 1,
);
});
});
67 changes: 67 additions & 0 deletions apps/bench/src/__tests__/mui-adapter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { render, waitFor } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import { gridClasses } from "@mui/x-data-grid";

import { MuiAdapter } from "../mui-adapter";
import type { BenchInteractionPlan } from "../interaction-plan";
Expand All @@ -15,6 +16,20 @@ const dataset = {
],
};

// Mirrors an S2-shaped scenario: at least one column with `wrap: true`.
// `packages/scenario-data` sets `wrap: index < scenario.wrapped_columns`, so a
// wrapped prefix followed by unwrapped columns is the real shape.
const wrappedDataset = {
columns: [
{ id: "id", header: "ID", wrap: false, widthPx: 80 },
{ id: "notes", header: "Notes", wrap: true, widthPx: 220 },
],
rows: [
{ id: "1", notes: "Alpha beta gamma delta epsilon zeta eta theta" },
{ id: "2", notes: "Iota kappa lambda mu nu xi omicron pi rho sigma" },
],
};

const statusDataset = {
columns: [
{ id: "id", header: "ID", wrap: false, widthPx: 80 },
Expand Down Expand Up @@ -65,6 +80,58 @@ describe("MuiAdapter", () => {
});
});

// Both directions are load-bearing. A positive-only assertion would still
// pass if auto height were enabled unconditionally, which would silently
// re-baseline every fixed-height scenario (S1 etc., `wrapped_columns: 0`).
// Assertions read the computed style / class of the real rendered row and
// cell, not the props we passed, so they also catch MUI dropping the
// `row--dynamicHeight` whiteSpace override on a version bump.
describe.each([
{
label: "a dataset with a wrapped column",
data: wrappedDataset,
dynamicHeight: true,
whiteSpace: "normal",
heightVar: "auto",
},
{
label: "a dataset with no wrapped columns",
data: dataset,
dynamicHeight: false,
whiteSpace: "nowrap",
heightVar: "48px",
},
])("$label", ({ data, dynamicHeight, whiteSpace, heightVar }) => {
test(`renders rows with dynamicHeight=${String(dynamicHeight)}`, async () => {
const { container } = render(
<MuiAdapter dataset={data as never} runKey={0} />,
);

let row!: HTMLElement;
await waitFor(() => {
const found = container.querySelector<HTMLElement>(
".MuiDataGrid-row[data-id]",
);
expect(found).not.toBeNull();
row = found!;
});

expect(row.classList.contains(gridClasses["row--dynamicHeight"])).toBe(
dynamicHeight,
);
// The row's own height contract: `--height: auto` vs a pinned 48px.
expect(row.style.getPropertyValue("--height")).toBe(heightVar);

// The pixel that actually matters for the wedge: a cell that is
// allowed to wrap. MUI's default is `white-space: nowrap`; the
// `row--dynamicHeight > cell` rule overrides it to `initial`, which
// computes to `normal`. No `sx` override of our own is involved.
const cell = row.querySelector<HTMLElement>(".MuiDataGrid-cell");
expect(cell).not.toBeNull();
expect(getComputedStyle(cell!).whiteSpace).toBe(whiteSpace);
});
});

test("publishes the post-filter row count, not the full dataset size", async () => {
const { container, rerender } = render(
<MuiAdapter
Expand Down
Loading