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, }); });