From 5ad2b39e6fe55dfe9bb3cd8031831cb67074b872 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 12 Aug 2026 23:51:01 +0530 Subject: [PATCH] fix(spec): carry the container on an authored scroll serializeAction sent the container's own point as both endpoints, so an authored Scroll({in, direction}) reached the driver as a drag from a point to itself and did nothing, on the seeded arm. The wire now carries the selector and leaves the drag to the runner, which sizes it from the container's bounds and has always had tested support for it that nothing could produce. No rng runs in the serializer, which lowers an already-drawn action, so the draw stream does not move. Builtin scrolls compute both endpoints and their bytes are unchanged. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX --- pkg/spec/src/runtime-entry.ts | 28 +++++++++------ pkg/spec/test/runtime-entry.test.ts | 56 ++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/pkg/spec/src/runtime-entry.ts b/pkg/spec/src/runtime-entry.ts index 23114d9..c1f0f87 100644 --- a/pkg/spec/src/runtime-entry.ts +++ b/pkg/spec/src/runtime-entry.ts @@ -29,6 +29,7 @@ export type SerializedAction = toX: number; toY: number; durationMillis: number; + selector?: string; } | { kind: "PressKey"; key: string } | { kind: "Wait"; durationMillis: number }; @@ -90,20 +91,27 @@ export function serializeAction(action: ActionDescriptor | null): SerializedActi }; } case "Scroll": { - const from = pointOf(action.from) ?? pointOf(action.in); - if (!from) return null; - // The builtin generator pre-computes `to`; an author Scroll without it - // collapses to a zero-length gesture the runner re-derives from bounds. - const to = pointOf(action.to) ?? from; - return { + // The builtin generator pre-computes the whole gesture. An author names + // the container instead and leaves the drag to the runner, which sizes it + // from that container's bounds: sending the container's own point as both + // endpoints would be a drag from a point to itself, which the runner + // executes as written, and sending no container at all would scroll + // whatever else is on screen. + const from = pointOf(action.from); + const to = pointOf(action.to); + const gesture = + from && to + ? { fromX: from.x, fromY: from.y, toX: to.x, toY: to.y } + : { fromX: 0, fromY: 0, toX: 0, toY: 0 }; + const out: SerializedAction = { kind: "Scroll", direction: action.direction, - fromX: from.x, - fromY: from.y, - toX: to.x, - toY: to.y, + ...gesture, durationMillis: DEFAULT_SWIPE_DURATION, }; + const container = pointOf(action.in); + if (container?.selector) out.selector = container.selector; + return out; } case "PressKey": return { kind: "PressKey", key: action.key }; diff --git a/pkg/spec/test/runtime-entry.test.ts b/pkg/spec/test/runtime-entry.test.ts index d80ca2c..6a36473 100644 --- a/pkg/spec/test/runtime-entry.test.ts +++ b/pkg/spec/test/runtime-entry.test.ts @@ -75,14 +75,60 @@ test("Swipe defaults durationMillis to 250", () => { assert.equal(action.durationMillis, 250); }); -test("Scroll emits direction plus from/to point and duration", () => { +// An author Scroll names a container and leaves the drag to the runner, which +// sizes it from that container's bounds. Sending the container's own point as +// both endpoints would be a drag from a point to itself, which the runner takes +// at face value and dispatches as a zero-length gesture. +test("an author Scroll carries no endpoints", () => { assert.deepEqual(serializeAction({ kind: "Scroll", direction: "down", in: POINT }), { kind: "Scroll", direction: "down", - fromX: 12, - fromY: 34, - toX: 12, - toY: 34, + fromX: 0, + fromY: 0, + toX: 0, + toY: 0, + durationMillis: 250, + }); +}); + +test("an author Scroll names the container it scrolls", () => { + assert.deepEqual(serializeAction({ kind: "Scroll", direction: "down", in: "id:list" }), { + kind: "Scroll", + direction: "down", + fromX: 0, + fromY: 0, + toX: 0, + toY: 0, + durationMillis: 250, + selector: "id:list", + }); + assert.deepEqual( + serializeAction({ + kind: "Scroll", + direction: "down", + in: { x: 1, y: 2, selector: "id:list" }, + }), + { + kind: "Scroll", + direction: "down", + fromX: 0, + fromY: 0, + toX: 0, + toY: 0, + durationMillis: 250, + selector: "id:list", + }, + ); +}); + +test("a Scroll with no container scrolls the screen rather than dropping", () => { + assert.deepEqual(serializeAction({ kind: "Scroll", direction: "up" }), { + kind: "Scroll", + direction: "up", + fromX: 0, + fromY: 0, + toX: 0, + toY: 0, durationMillis: 250, }); });