Skip to content

Commit 2f7369d

Browse files
committed
refactor: move touch commands to platform runtime
1 parent 2964477 commit 2f7369d

99 files changed

Lines changed: 2936 additions & 2240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/contracts/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@
300300
"types": "./src/type-text-runtime.ts",
301301
"default": "./src/type-text-runtime.ts"
302302
},
303+
"./touch-runtime": {
304+
"types": "./src/touch-runtime.ts",
305+
"default": "./src/touch-runtime.ts"
306+
},
303307
"./viewport-runtime": {
304308
"types": "./src/viewport-runtime.ts",
305309
"default": "./src/viewport-runtime.ts"

packages/contracts/src/facades/interaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414
resolveClickButton,
1515
} from '../click-button.ts';
1616
export type { ClickButton } from '../click-button.ts';
17+
export type { PressPointOptions } from '../interactor-types.ts';
1718
export type { ClipboardCommandResult } from '../clipboard.ts';
1819
export { COORDINATE_GESTURE_KINDS, GESTURE_KINDS, readGesturePayload } from '../gesture-input.ts';
1920
export type {

packages/contracts/src/facades/platform.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export {
7272
providerRuntimeOwner,
7373
runtimeOwnerKey,
7474
sameRuntimeOwner,
75+
whenAdmitted,
7576
} from '../platform-runtime.ts';
7677
export type {
7778
BoundDeviceRuntime,
@@ -252,13 +253,30 @@ export type {
252253
export {
253254
bootTargetHeadlessUse,
254255
bootTargetUse,
256+
capturedFillUse,
257+
capturedHoverUse,
258+
capturedLongPressUse,
259+
capturedTapUse,
260+
clickRuntimeUses,
255261
deviceBootRuntimeUses,
262+
fillPointUse,
263+
fillRuntimeUses,
264+
hoverPointUse,
265+
hoverRuntimeUses,
266+
longPressPointUse,
267+
longPressRuntimeUses,
268+
pressRuntimeUses,
256269
resolveDeviceReadinessRuntimePlan,
270+
resolveTouchRuntimePlan,
257271
appStateRuntimeUses,
258272
appStateUse,
259273
shutdownTargetUse,
274+
tapPointUse,
275+
} from '../platform-runtime-operations.ts';
276+
export type {
277+
DeviceReadinessRuntimePlan,
278+
TouchRuntimePlan,
260279
} from '../platform-runtime-operations.ts';
261-
export type { DeviceReadinessRuntimePlan } from '../platform-runtime-operations.ts';
262280
export {
263281
bindLocalScreenshotInteractor,
264282
bindProviderScreenshotInteractor,
@@ -316,6 +334,25 @@ export {
316334
bindProviderTypeTextInteractor,
317335
typeTextRuntimeOperationFacts,
318336
} from '../type-text-runtime.ts';
337+
export {
338+
bindLocalTouchInteractor,
339+
bindProviderTouchInteractor,
340+
HOVER_UNAVAILABLE_HINT,
341+
pressJitter,
342+
touchRuntimeOperationFacts,
343+
} from '../touch-runtime.ts';
344+
export type {
345+
FillPointInput,
346+
FillRefInput,
347+
HoverPointInput,
348+
HoverRefInput,
349+
LongPressPointInput,
350+
TapElementSelectorInput,
351+
TapPointInput,
352+
TapRefInput,
353+
TouchRuntimeOperationFacts,
354+
TouchRuntimeOperations,
355+
} from '../touch-runtime.ts';
319356
export type {
320357
LocalTypeTextInteractorResolver,
321358
ProviderTypeTextInteractorResolver,

packages/contracts/src/interaction-guarantees.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,8 @@ export const INTERACTION_DISPATCH_PATHS: Record<InteractionPathId, InteractionPa
298298
},
299299
'direct-ios-selector': {
300300
description:
301-
'Simple press selectors on iOS are sent to the XCTest runner, which queries and taps natively without a daemon tree capture. Fill deliberately resolves through the runtime tree so AX-hostile text inputs carry typed target evidence into coordinate entry.',
302-
commands: ['press'],
301+
'Simple press and click selectors use the bound owner preferred tapElementSelector operation when its exact facts advertise it, querying and tapping without a daemon tree capture. Fill deliberately resolves through the runtime tree so AX-hostile text inputs carry typed target evidence into coordinate entry.',
302+
commands: ['press', 'click'],
303303
guarantees: {
304304
disambiguation: {
305305
kind: 'delegated',
@@ -366,7 +366,7 @@ export const INTERACTION_DISPATCH_PATHS: Record<InteractionPathId, InteractionPa
366366
// 2026-07-04 while designing the #1088 retirement experiment, which this
367367
// finding dissolved: there is no iOS runner round trip to retire.
368368
description:
369-
'click @ref / fill @ref / hover @ref dispatch to backend.tapTarget/fillTarget/hoverTarget (web provider clickRef/fillRef/hoverRef only; no mobile backend implements these) without runtime resolution when no non-default options are set. A zero-round-trip preflight (preflightNativeRefInteraction) runs the shared guards against the stored session snapshot node first; no snapshot / no usable rect makes the preflight a no-op.',
369+
'click @ref / fill @ref / hover @ref dispatch through the bound touch operation, whose web runtime owner selects clickRef/fillRef/hoverRef internally; no mobile owner advertises native-ref support. The route bypasses runtime resolution when no non-default options are set. A zero-round-trip preflight (preflightNativeRefInteraction) runs the shared guards against the stored session snapshot node first; no snapshot / no usable rect makes the preflight a no-op.',
370370
commands: ['click', 'fill', 'hover'],
371371
guarantees: {
372372
disambiguation: {

packages/contracts/src/interactor-types.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ export type ElementSelectorTapOptions = {
6565
expectedPoint?: Point;
6666
};
6767

68+
export type PressPointOptions = Readonly<{
69+
button: 'primary' | 'secondary' | 'middle';
70+
count: number;
71+
intervalMs: number;
72+
holdMs: number;
73+
jitterPx: number;
74+
doubleTap: boolean;
75+
surface?: SessionSurface;
76+
}>;
77+
6878
/**
6979
* Legacy success text retained for compatibility when the XCTest runner used
7080
* the Maestro non-hittable coordinate fallback. Usage itself is carried by
@@ -126,7 +136,7 @@ export type TypeTextBackendResult = {
126136
* to enter text unwitnessed.
127137
*
128138
* Closed set: the cloud interactor is its only producer and the boundary that
129-
* narrows it (readFillBackendResult, src/core/dispatch-interactions.ts) drops a
139+
* narrows it (readFillBackendResult in the touch handler) drops a
130140
* value it cannot name.
131141
*/
132142
export const CLOUD_TEXT_ENTRY_READINESS = ['focused-element', 'keyboard-shown'] as const;
@@ -252,6 +262,15 @@ export type Interactor = {
252262
openDevice(): Promise<void>;
253263
close(app: string): Promise<void>;
254264
tap(x: number, y: number): Promise<Record<string, unknown> | void>;
265+
/** Complete point-press semantics for owners with fused series, alternate buttons, or surfaces. */
266+
pressPoint?(point: Point, options: PressPointOptions): Promise<Record<string, unknown> | void>;
267+
/** Alternate mouse buttons for owners that otherwise use the shared point-press series. */
268+
alternateClick?(
269+
point: Point,
270+
button: 'secondary' | 'middle',
271+
): Promise<Record<string, unknown> | void>;
272+
/** Owner-native ref routes; currently the managed web runtime is their only local owner. */
273+
tapRef?(ref: string): Promise<Record<string, unknown> | void>;
255274
tapElementSelector?(selector: ElementSelectorTapOptions): Promise<Record<string, unknown> | void>;
256275
doubleTap(x: number, y: number): Promise<Record<string, unknown> | void>;
257276
longPress(x: number, y: number, durationMs?: number): Promise<Record<string, unknown> | void>;
@@ -261,20 +280,17 @@ export type Interactor = {
261280
* and leave it undefined, which the `hover` command reports as unsupported.
262281
*/
263282
hover?(x: number, y: number): Promise<Record<string, unknown> | void>;
283+
hoverRef?(ref: string): Promise<Record<string, unknown> | void>;
264284
focus(x: number, y: number): Promise<Record<string, unknown> | void>;
265285
type(text: string, delayMs?: number): Promise<TypeTextBackendResult | void>;
266-
fillElementSelector?(
267-
selector: ElementSelectorTapOptions,
268-
text: string,
269-
delayMs?: number,
270-
): Promise<Record<string, unknown> | void>;
271286
fill(
272287
x: number,
273288
y: number,
274289
text: string,
275290
delayMs?: number,
276291
options?: { allowNonHittableCoordinateFallback?: boolean },
277292
): Promise<Record<string, unknown> | void>;
293+
fillRef?(ref: string, text: string, delayMs?: number): Promise<Record<string, unknown> | void>;
278294
scroll(
279295
direction: ScrollDirection,
280296
options?: ScrollExecutionOptions,

packages/contracts/src/platform-runtime-operations.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { HomeRuntimeOperations } from './home-runtime.ts';
2424
import type { OrientationRuntimeOperations } from './orientation-runtime.ts';
2525
import type { TvRemoteRuntimeOperations } from './tv-remote-runtime.ts';
2626
import type { KeyboardRuntimeOperations } from './keyboard-runtime.ts';
27+
import type { TouchRuntimeOperations } from './touch-runtime.ts';
2728
import type {
2829
DeviceReadinessRuntimeHost,
2930
DeviceReadinessRuntimeOperations,
@@ -65,6 +66,7 @@ export type PlatformRuntimeOperations = AppLogRuntimeOperations &
6566
OrientationRuntimeOperations &
6667
TvRemoteRuntimeOperations &
6768
KeyboardRuntimeOperations &
69+
TouchRuntimeOperations &
6870
DeviceReadinessRuntimeOperations &
6971
DeviceShutdownRuntimeOperations &
7072
ApplicationLifecycleRuntimeOperations;
@@ -93,6 +95,81 @@ export const tvRemoteRuntimeUse = defineUse({ required: ['tvRemote'] });
9395
export const keyboardStatusUse = defineUse({ required: ['keyboardStatus'] });
9496
export const keyboardDismissUse = defineUse({ required: ['keyboardDismiss'] });
9597
export const keyboardEnterUse = defineUse({ required: ['keyboardEnter'] });
98+
export const tapPointUse = defineUse({ required: ['tapPoint'] });
99+
export const capturedTapUse = defineUse({
100+
required: ['captureSnapshot', 'tapPoint'],
101+
preferred: ['tapElementSelector', 'tapRef'],
102+
});
103+
export const longPressPointUse = defineUse({ required: ['longPressPoint'] });
104+
export const capturedLongPressUse = defineUse({
105+
required: ['captureSnapshot', 'longPressPoint'],
106+
});
107+
export const hoverPointUse = defineUse({ required: ['hoverPoint'] });
108+
export const capturedHoverUse = defineUse({
109+
required: ['captureSnapshot', 'hoverPoint'],
110+
preferred: ['hoverRef'],
111+
});
112+
export const fillPointUse = defineUse({ required: ['fillPoint'] });
113+
export const capturedFillUse = defineUse({
114+
required: ['captureSnapshot', 'fillPoint'],
115+
preferred: ['fillRef'],
116+
});
117+
118+
export const clickRuntimeUses = Object.freeze([tapPointUse, capturedTapUse] as const);
119+
export const pressRuntimeUses = clickRuntimeUses;
120+
export const longPressRuntimeUses = Object.freeze([
121+
longPressPointUse,
122+
capturedLongPressUse,
123+
] as const);
124+
export const hoverRuntimeUses = Object.freeze([hoverPointUse, capturedHoverUse] as const);
125+
export const fillRuntimeUses = Object.freeze([fillPointUse, capturedFillUse] as const);
126+
127+
export type TouchRuntimePlan =
128+
| Readonly<{ kind: 'tap-point'; operation: 'tapPoint'; use: typeof tapPointUse }>
129+
| Readonly<{ kind: 'captured-tap'; operation: 'tapPoint'; use: typeof capturedTapUse }>
130+
| Readonly<{
131+
kind: 'long-press-point';
132+
operation: 'longPressPoint';
133+
use: typeof longPressPointUse;
134+
}>
135+
| Readonly<{
136+
kind: 'captured-long-press';
137+
operation: 'longPressPoint';
138+
use: typeof capturedLongPressUse;
139+
}>
140+
| Readonly<{ kind: 'hover-point'; operation: 'hoverPoint'; use: typeof hoverPointUse }>
141+
| Readonly<{
142+
kind: 'captured-hover';
143+
operation: 'hoverPoint';
144+
use: typeof capturedHoverUse;
145+
}>
146+
| Readonly<{ kind: 'fill-point'; operation: 'fillPoint'; use: typeof fillPointUse }>
147+
| Readonly<{ kind: 'captured-fill'; operation: 'fillPoint'; use: typeof capturedFillUse }>;
148+
149+
export function resolveTouchRuntimePlan(
150+
command: 'click' | 'press' | 'fill' | 'longpress' | 'hover',
151+
requiresCapture: boolean,
152+
): TouchRuntimePlan {
153+
switch (command) {
154+
case 'click':
155+
case 'press':
156+
return requiresCapture
157+
? { kind: 'captured-tap', operation: 'tapPoint', use: capturedTapUse }
158+
: { kind: 'tap-point', operation: 'tapPoint', use: tapPointUse };
159+
case 'fill':
160+
return requiresCapture
161+
? { kind: 'captured-fill', operation: 'fillPoint', use: capturedFillUse }
162+
: { kind: 'fill-point', operation: 'fillPoint', use: fillPointUse };
163+
case 'longpress':
164+
return requiresCapture
165+
? { kind: 'captured-long-press', operation: 'longPressPoint', use: capturedLongPressUse }
166+
: { kind: 'long-press-point', operation: 'longPressPoint', use: longPressPointUse };
167+
case 'hover':
168+
return requiresCapture
169+
? { kind: 'captured-hover', operation: 'hoverPoint', use: capturedHoverUse }
170+
: { kind: 'hover-point', operation: 'hoverPoint', use: hoverPointUse };
171+
}
172+
}
96173
const captureSnapshotWithCustomActionsUse = defineUse({
97174
required: ['captureSnapshot', 'captureSnapshotWithCustomActions'],
98175
});

packages/contracts/src/platform-runtime-unavailable.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ test('generic unavailable binding preserves exact provider ownership and mode',
3333
viewport: { available: false, reason: 'unsupported-platform-leaf' },
3434
focus: { available: false, reason: 'unsupported-provider-mode' },
3535
typeText: { available: false, reason: 'unsupported-provider-mode' },
36+
touch: { available: false, reason: 'unsupported-provider-mode' },
3637
elementText: { available: false, reason: 'unsupported-provider-mode' },
3738
back: { available: false, reason: 'unsupported-provider-mode' },
3839
home: { available: false, reason: 'unsupported-provider-mode' },

packages/contracts/src/platform-runtime-unavailable.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { homeRuntimeOperationFacts } from './home-runtime.ts';
2222
import { orientationRuntimeOperationFacts } from './orientation-runtime.ts';
2323
import { tvRemoteRuntimeOperationFacts } from './tv-remote-runtime.ts';
2424
import { keyboardRuntimeOperationFacts } from './keyboard-runtime.ts';
25+
import { touchRuntimeOperationFacts } from './touch-runtime.ts';
2526

2627
/**
2728
* A runtime-contract helper for provider ownership gaps. It never assigns lifecycle semantics:
@@ -39,6 +40,7 @@ export type UnavailablePlatformRuntimeFacts = Readonly<{
3940
viewport: RuntimeOperationUnavailability;
4041
focus: RuntimeOperationUnavailability;
4142
typeText: RuntimeOperationUnavailability;
43+
touch: RuntimeOperationUnavailability;
4244
elementText: RuntimeOperationUnavailability;
4345
back: RuntimeOperationUnavailability;
4446
home: RuntimeOperationUnavailability;
@@ -92,6 +94,7 @@ export function createUnavailablePlatformRuntimeFacts(
9294
viewport,
9395
focus,
9496
typeText,
97+
touch,
9598
elementText,
9699
back,
97100
home,
@@ -141,6 +144,16 @@ export function createUnavailablePlatformRuntimeFacts(
141144
...viewportRuntimeOperationFacts({ setViewport: viewport }),
142145
...focusRuntimeOperationFacts({ focus }),
143146
...typeTextRuntimeOperationFacts({ type: typeText }),
147+
...touchRuntimeOperationFacts({
148+
tap: touch,
149+
tapRef: touch,
150+
longPress: touch,
151+
hover: touch,
152+
hoverRef: touch,
153+
fill: touch,
154+
fillRef: touch,
155+
tapElementSelector: touch,
156+
}),
144157
...elementTextRuntimeOperationFacts({ readTextAtPoint: elementText }),
145158
...backRuntimeOperationFacts({ back }),
146159
...homeRuntimeOperationFacts({ home }),
@@ -182,6 +195,7 @@ function freezeUnavailableFacts(
182195
// exact kinds, and one that cannot must say why rather than inherit a transport gap.
183196
focus: Object.freeze({ ...unavailable.focus }),
184197
typeText: Object.freeze({ ...unavailable.typeText }),
198+
touch: Object.freeze({ ...unavailable.touch }),
185199
readiness: orNetwork(unavailable.readiness),
186200
shutdown: orNetwork(unavailable.shutdown),
187201
elementText: Object.freeze({ ...unavailable.elementText }),

packages/contracts/src/platform-runtime.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,11 @@ function unsupportedRuntimeOperation(key: string, fact: RuntimeOperationUnavaila
313313
hint: fact.hint,
314314
});
315315
}
316+
317+
/** Include an operation binding only when the owning runtime's published fact admits it. */
318+
export function whenAdmitted<T extends object>(
319+
fact: RuntimeOperationFact,
320+
build: () => T,
321+
): T | Record<string, never> {
322+
return fact.available ? build() : {};
323+
}

packages/contracts/src/scroll-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function honoredScrollDurationMs(
7272

7373
/**
7474
* `scroll` — the generic-route result built by `buildDispatchedScrollResult`
75-
* (src/core/dispatch-interactions.ts): the resolved direction, the edge-pass
75+
* (src/core/dispatch-scroll.ts): the resolved direction, the edge-pass
7676
* bookkeeping for `top`/`bottom` scrolls, the honored distance/timing echo,
7777
* and the success message. Platform leaves add gesture-plan coordinates
7878
* (`x1`/`y1`/`x2`/`y2`, reference frame) on top; the output schema stays

0 commit comments

Comments
 (0)