diff --git a/pkg/spec/src/runtime-entry.ts b/pkg/spec/src/runtime-entry.ts index 0f7e1e7..bc5fc83 100644 --- a/pkg/spec/src/runtime-entry.ts +++ b/pkg/spec/src/runtime-entry.ts @@ -34,16 +34,23 @@ export type SerializedAction = const DEFAULT_SWIPE_DURATION = 250; -// pointOf resolves a target to {x, y, selector?}. Builtins already supply a -// Point; author targets that carry numeric x/y (or a resolved element handle) -// collapse the same way. A string/selector target the host could not resolve -// returns undefined so the action is dropped. +// pointOf resolves a target to {x, y, selector?}. Builtins and resolved ax +// elements carry numeric x/y; a bare selector string carries no geometry, so it +// serializes with (0, 0) and the selector, leaving the native runner to +// re-resolve coordinates by id/text. A target with neither shape (null, an +// unrecognized object) returns undefined so the action is dropped. function pointOf(target: unknown): (Point & { selector?: string }) | undefined { + if (typeof target === "string") { + return target.length > 0 ? { x: 0, y: 0, selector: target } : undefined; + } if (!target || typeof target !== "object") return undefined; const obj = target as Record; if (typeof obj.x === "number" && typeof obj.y === "number") { const point: Point & { selector?: string } = { x: obj.x, y: obj.y }; - if (typeof obj.selector === "string") point.selector = obj.selector; + // The picker's builtin candidates carry `selector`; a goja ax element + // carries it under the runtime tag. Either lets the runner re-resolve. + const selector = obj.selector ?? obj.__sanderlingSelector; + if (typeof selector === "string" && selector.length > 0) point.selector = selector; return point; } return undefined; diff --git a/pkg/spec/test/runtime-entry.test.ts b/pkg/spec/test/runtime-entry.test.ts index f517ec7..d80ca2c 100644 --- a/pkg/spec/test/runtime-entry.test.ts +++ b/pkg/spec/test/runtime-entry.test.ts @@ -119,8 +119,17 @@ test("builtin Scroll carries the pre-computed to endpoint", () => { ); }); -test("an unresolved (string) target drops the action", () => { - assert.equal(serializeAction({ kind: "Tap", on: "id:never-resolved" }), null); +test("a string target serializes selector-only for the runner to re-resolve", () => { + assert.deepEqual(serializeAction({ kind: "Tap", on: "id:save" }), { + kind: "Tap", + x: 0, + y: 0, + selector: "id:save", + }); +}); + +test("an empty target or null action is dropped", () => { + assert.equal(serializeAction({ kind: "Tap", on: "" }), null); assert.equal(serializeAction(null), null); });