diff --git a/apps/bench/src/__tests__/bench-runtime.test.ts b/apps/bench/src/__tests__/bench-runtime.test.ts index c74de5df6..1f13762dc 100644 --- a/apps/bench/src/__tests__/bench-runtime.test.ts +++ b/apps/bench/src/__tests__/bench-runtime.test.ts @@ -1358,6 +1358,67 @@ describe("bench runtime", () => { expect(result.metrics.interaction_latency_ms).toBeGreaterThanOrEqual(0); }); + test("measureBenchKeySequenceRun waits for cells that arrive after the viewport", async () => { + // The viewport element attaches before the row model projects its first + // window. One settle frame is not enough for that, so the run used to fail + // for want of a body cell that was about to exist — which is how all three + // selection scripts aborted the comparative runset at zero rendered rows. + document.body.innerHTML = ` +
+
+
+ `; + const root = document.querySelector('[data-testid="root"]')!; + const viewport = root.querySelector( + "[data-pretable-scroll-viewport]", + )!; + const dispatched: string[] = []; + const MOUNT_FRAME = 30; + let frame = 0; + + const previousRaf = globalThis.requestAnimationFrame; + Object.defineProperty(globalThis, "requestAnimationFrame", { + configurable: true, + value: (callback: FrameRequestCallback) => { + frame += 1; + if (frame === MOUNT_FRAME) { + viewport.innerHTML = ` +
+
row 0
+
+ `; + viewport + .querySelector("[data-pretable-cell]")! + .addEventListener("keydown", (event) => { + dispatched.push((event as KeyboardEvent).key); + }); + } + callback(frame * 16); + return frame; + }, + }); + + try { + const result = await measureBenchKeySequenceRun( + root, + "pretable", + "keyboard-nav-row", + { key: "ArrowDown", shiftKey: false, count: 3, framesBetween: 1 }, + ); + + expect(result.notes).not.toContain( + "no body cell available for keyboard focus", + ); + expect(result.status).toBe("completed"); + expect(dispatched).toEqual(["ArrowDown", "ArrowDown", "ArrowDown"]); + } finally { + Object.defineProperty(globalThis, "requestAnimationFrame", { + configurable: true, + value: previousRaf, + }); + } + }); + test("measureBenchKeySequenceRun returns partial when no viewport is present", async () => { document.body.innerHTML = `
`; diff --git a/apps/bench/src/bench-runtime.ts b/apps/bench/src/bench-runtime.ts index d0506f326..e5375cf30 100644 --- a/apps/bench/src/bench-runtime.ts +++ b/apps/bench/src/bench-runtime.ts @@ -1966,8 +1966,13 @@ export async function measureBenchKeySequenceRun( }; } - // Allow the grid to settle and ensure focus is on a body cell. - await waitForAnimationFrame(); + // Allow the grid to settle and ensure focus is on a body cell. One frame is + // not settling: the viewport attaches before the row model projects its first + // window, so a single frame leaves the body empty and the run fails below for + // want of a cell that is about to exist. This is the same wait `scroll` and + // `initial` take (#334); all three selection scripts aborted the comparative + // runset at zero rendered rows without it. + await waitForRenderedRowBaseline(root, profile.rowSelector); const firstCell = viewport.querySelector( `${profile.cellSelector}[tabindex="0"]`, @@ -2095,7 +2100,10 @@ export async function measureBenchAutosizeRun( metrics: { dom_nodes_peak: root.querySelectorAll("*").length }, }; } - await waitForAnimationFrame(); + // Autosize measures the cost of fitting columns to their content, so the + // content has to be on screen first. One frame after mount it is not, and the + // run would time a fit over an empty body. + await waitForRenderedRowBaseline(root, profile.rowSelector); const start = performance.now(); await autosize(); await waitForAnimationFrame();