Compare commits

...
Author SHA1 Message Date
Kit Langton 69e9cc80a4 fix(simulation): remove artificial click pacing 2026-09-03 22:49:11 -04:00
3 changed files with 104 additions and 1 deletions
+42
View File
@@ -0,0 +1,42 @@
import { BoxRenderable, TextRenderable } from "@opentui/core"
import { Effect } from "effect"
import { SimulationActions } from "../src/frontend/actions"
import { SimulationRenderer } from "../src/frontend/renderer"
// One warmup, seven measured batches. Real mouse dispatch and rendering included.
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const renderer = yield* SimulationRenderer.create({})
const label = new TextRenderable(renderer, { content: "Clicks: 0" })
let clicks = 0
const button = new BoxRenderable(renderer, {
width: 20,
height: 1,
onMouseUp: () => {
label.content = `Clicks: ${++clicks}`
},
})
button.add(label)
renderer.root.add(button)
const harness = SimulationActions.createHarness(renderer)
yield* Effect.promise(() => harness.renderOnce())
const samples: number[] = []
for (let batch = 0; batch < 8; batch++) {
const start = performance.now()
for (let index = 0; index < 20; index++) {
yield* SimulationActions.execute(harness, { type: "ui.click", target: button.num, x: 1, y: 0 })
if (!harness.screen().includes(`Clicks: ${clicks}`)) throw new Error("click returned before painting")
if (harness.mockMouse.getPressedButtons().length) throw new Error("click left a button held")
}
if (clicks !== (batch + 1) * 20) throw new Error("lost a native click")
if (batch > 0) samples.push((performance.now() - start) / 20)
}
const median = samples.toSorted((a, b) => a - b)[3]
if (median === undefined) throw new Error("missing benchmark samples")
const mad = samples.map((value) => Math.abs(value - median)).sort((a, b) => a - b)[3]
console.log(JSON.stringify({ metric: "simulation_click_ms", median, mad, samples, clicks }))
console.log(`METRIC simulation_click_ms=${median.toFixed(3)}`)
}),
),
)
+4 -1
View File
@@ -233,7 +233,10 @@ export const execute = Effect.fn("SimulationActions.execute")(function* (harness
)
return yield* Effect.fail(new Error("click position must be within the target element"))
SimulationRenderer.recordPointer(harness.renderer, "click", target.screenX + action.x, target.screenY + action.y)
yield* Effect.tryPromise(() => harness.mockMouse.click(target.screenX + action.x, target.screenY + action.y))
// Opt out of test-helper pacing; retain its down/up ordering and the render below.
yield* Effect.tryPromise(() =>
harness.mockMouse.click(target.screenX + action.x, target.screenY + action.y, undefined, { delayMs: 0 }),
)
break
}
case "ui.resize":
+58
View File
@@ -103,6 +103,64 @@ test("clicks a target at relative coordinates through descendant text", async ()
)
})
test("unpaced clicks preserve native event order, button release and rendered state", async () => {
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const renderer = yield* SimulationRenderer.create({})
const events: string[] = []
const label = new TextRenderable(renderer, { content: "Not clicked" })
let clicks = 0
const button = new BoxRenderable(renderer, {
id: "click-order",
width: 20,
height: 1,
onMouseDown: () => {
events.push("down")
queueMicrotask(() => events.push("down microtask"))
},
onMouseUp: () => {
events.push("up")
label.content = `Clicks: ${++clicks}`
},
})
button.add(label)
renderer.root.add(button)
const harness = createHarness(renderer)
yield* Effect.promise(() => harness.renderOnce())
for (let index = 0; index < 5; index++) {
const result = yield* execute(harness, { type: "ui.click", target: button.num, x: 1, y: 0 })
expect(clicks).toBe(index + 1)
expect(events.splice(0)).toEqual(["down", "down microtask", "up"])
expect(harness.mockMouse.getPressedButtons()).toEqual([])
expect(harness.screen()).toContain(`Clicks: ${index + 1}`)
expect(result).toEqual(state(harness))
}
}),
),
)
})
test("unpaced clicks retain native double-click text selection", async () => {
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const renderer = yield* SimulationRenderer.create({})
const text = new TextRenderable(renderer, { content: "alpha beta", selectable: true })
renderer.root.add(text)
const harness = createHarness(renderer)
yield* Effect.promise(() => harness.renderOnce())
yield* execute(harness, { type: "ui.click", target: text.num, x: 1, y: 0 })
yield* execute(harness, { type: "ui.click", target: text.num, x: 1, y: 0 })
expect(renderer.getSelection()?.getSelectedText()).toBe("alpha")
expect(harness.mockMouse.getPressedButtons()).toEqual([])
}),
),
)
})
test("mouse input drives native hover, drag, buttons and scrolling at absolute coordinates", async () => {
await Effect.runPromise(
Effect.scoped(