feat(spec): serialize selector-only string targets for the runner to re-resolve

This commit is contained in:
pj committed 2026-06-01 16:58:59 +05:30
1 parent 1a9d82711f
commit 50cb9e98cb
2 files changed
+23 -7

No files matched your search

+12 -5
View File
@@ -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<string, unknown>;
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;
+11 -2
View File
@@ -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);
});