test(app): bench dragging the review's edge

This commit is contained in:
Luke Parker
2026-11-04 12:30:09 +10:00
Unverified
parent d49b63142d
commit 4e5df802c7
2 changed files with 145 additions and 5 deletions
+105
View File
@@ -0,0 +1,105 @@
import type { Page } from "@playwright/test"
/** A divider to drag: its centre, how far it can travel each way (px), and the display's frame period (ms). */
export type Divider = { x: number; y: number; left: number; right: number; period: number }
type Leg = { seconds: number; at: (progress: number) => number }
const ease = (from: number, to: number, seconds: number): Leg => ({
seconds,
at: (progress) => from + ((to - from) * (1 - Math.cos(Math.PI * progress))) / 2,
})
// Whole cycles, so a jiggle ends where it began.
const jiggle = (around: number, amplitude: number, cycles: number, seconds: number): Leg => ({
seconds,
at: (progress) => around + amplitude * Math.sin(2 * Math.PI * cycles * progress),
})
/**
* How a hand drags a divider, as offsets from where the drag starts against time, scaled by the room each way: a slow
* stretch one way, a jiggle there, a fast throw the other way, a slow pull back, a small jiggle and home. 4.95 s.
*/
function legs(divider: Divider) {
const l = -divider.left
const r = divider.right
return [
ease(0, 0.8 * l, 1.2),
jiggle(0.8 * l, 0.15 * divider.left, 3, 0.75),
ease(0.8 * l, r, 0.6),
ease(r, l, 1.5),
ease(l, 0.4 * l, 0.4),
jiggle(0.4 * l, 0.1 * divider.left, 3, 0.5),
ease(0.4 * l, 0, 0.5),
]
}
/**
* Presses the divider, moves the pointer once per display frame along the drag above without letting go, and releases
* it where it started. strobe's driver has no pointer drag (`page.move` does not hold a button, and its `until:
* "frame"` waits two animation frames and a round trip per move), so Playwright's mouse drives the drag, from the
* runner, paced by the display's frame period on the wall clock. A frame the page misses is skipped, as a hand keeps
* moving while the screen lags.
*/
export async function dragDivider(mouse: Page["mouse"], divider: Divider) {
const path = legs(divider)
const total = path.reduce((sum, leg) => sum + leg.seconds, 0)
const offset = (seconds: number) => {
let left = seconds
for (const leg of path) {
if (left <= leg.seconds) return leg.at(left / leg.seconds)
left -= leg.seconds
}
return 0
}
await mouse.move(divider.x, divider.y)
await mouse.down()
const began = performance.now()
for (let frame = 1; ; frame++) {
const wait = began + frame * divider.period - performance.now()
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait))
frame = Math.max(frame, Math.floor((performance.now() - began) / divider.period))
const seconds = (performance.now() - began) / 1000
if (seconds >= total) break
await mouse.move(divider.x + offset(seconds), divider.y)
}
await mouse.move(divider.x, divider.y)
await mouse.up()
}
/**
* Reads, in the page, where the divider `selector` sits, the room its panel `panel` has to shrink to `min` and grow to
* the row `row` less `rest` (px), and the display's frame period over 30 frames. Only in setup: it reads layout.
*/
export function measureDivider(input: { selector: string; panel: string; row: string; min: number; rest: number }) {
return new Promise<Divider>((resolve) => {
const box = document.querySelector(input.selector)!.getBoundingClientRect()
const width = document.querySelector(input.panel)!.getBoundingClientRect().width
const row = document.querySelector(input.row)!.getBoundingClientRect().width
const times: number[] = []
const tick = (time: number) => {
times.push(time)
if (times.length < 31) return requestAnimationFrame(tick)
resolve({
x: box.left + box.width / 2,
y: box.top + box.height / 2,
left: Math.max(0, Math.min(360, width - input.min - 10)),
right: Math.max(0, Math.min(160, row - input.rest - width - 10)),
period: (times[30]! - times[0]!) / 30,
})
}
requestAnimationFrame(tick)
})
}
+40 -5
View File
@@ -1,7 +1,9 @@
// One long session (120 turns of markdown, code fences, tool calls and diffs) at normal speed.
import { bench, web } from "@hona/strobe"
import { editTools } from "./fixtures/diffs"
import { editTools, reviewDiffs } from "./fixtures/diffs"
import { dragDivider, measureDivider, type Divider } from "./fixtures/drag"
import { cached } from "./fixtures/mock"
import { openReview } from "./fixtures/review"
import { composer, mockTimelineSession, oldestPrompt, timeline } from "./fixtures/timeline"
const find = '[data-component="timeline-search-bar"] input'
@@ -13,25 +15,47 @@ const edit = '[data-scope="apply-patch"] [data-slot="accordion-trigger"]'
const diff = '[data-component="apply-patch-file-diff"] diffs-container >>> [data-line]'
const drag = "drag the review's edge"
// The divider between the session and the side panel, read once the review is open. The session panel keeps at least
// 400px, the side panel 320px and the 8px gap.
let divider: Divider | undefined
export default bench({
targets: {
web: web({
viewport: { width: 1366, height: 768 },
prepare: async ({ case: name, playwright, mock, url }) =>
mockTimelineSession(cached(playwright.page, mock), {
prepare: async ({ case: name, playwright, mock, url }) => ({
...(await mockTimelineSession(cached(playwright.page, mock), {
server: new URL(url).origin,
// The edits alone, so the case measures the diff, not the history around it.
historyTurns: name === "expand an edit" ? 0 : 120,
collapsedEdits: name === "expand an edit",
tools: name === "expand an edit" ? await editTools() : undefined,
}),
vcsDiff: name === drag ? reviewDiffs() : undefined,
})),
mouse: playwright.page.mouse,
}),
url: ({ prepared }) => prepared.path,
}),
},
async setup({ page }) {
async setup({ case: name, page }) {
await page.waitFor(timeline, { timeout: 60_000 })
await page.waitFor(composer)
if (name === drag) {
await openReview(page)
await page.idle()
divider = await page.eval(measureDivider, {
selector: '[data-slot="session-chat-panel"] > div > [data-component="resize-handle"]',
panel: '[data-slot="session-chat-panel"]',
row: "[data-pane-row]",
min: 400,
rest: 328,
})
}
await page.idle()
},
@@ -85,5 +109,16 @@ export default bench({
await page.idle()
},
},
// The divider beside the review (72 changed files) held for five seconds and dragged as a hand does, a pointer
// move every display frame: a slow stretch toward the session, jiggles, a fast throw toward the review and a slow
// pull back (fixtures/drag.ts). The timeline narrows and widens on every frame, so the rows in view rewrap; each
// frame should keep up with the display.
[drag]: {
primary: "frames.dropped.pct",
metrics: ["main.work", "main.layout", "main.style", "main.script", "frames.interval.p95"],
run: ({ prepared }) => dragDivider(prepared.mouse, divider!),
reset: ({ page }) => page.idle(),
},
},
})