@e2e-dev/mobile 0.12.0 → 0.12.1-nightly-20261014080557

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.build.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.12.0"}
1
+ {"version":"0.12.1-nightly-20261014080557"}
package/dist/engine.js CHANGED
@@ -29,21 +29,23 @@ export function buildEngine(surface) {
29
29
  dispose: (context) => surface.dispose(context),
30
30
  observe: (operation, options) => surface.observe(operation, options),
31
31
  locate: (expression, operation) => surface.locate(expression, operation),
32
- perform: (ref, action, operation) => surface.perform(ref, action, operation),
32
+ // The harness signal does not abort at an operation's budget, so each
33
+ // input enforces its own; see InputBudget in support.ts.
34
+ perform: (ref, action, operation) => surface.perform(ref, action, operation, surface.inputBudget(action.kind, operation.timeoutMs)),
33
35
  actions: DEVICE_ACTIONS,
34
- performAt: (point, action, operation) => surface.performAt(point, action, operation),
36
+ performAt: (point, action, operation) => surface.performAt(point, action, operation, surface.inputBudget(action.kind, operation.timeoutMs)),
35
37
  pointerActions: DEVICE_POINTER_ACTIONS,
36
38
  keyboard: {
37
- type: (text, keyboardOptions, operation) => surface.typeText(text, keyboardOptions, operation),
38
- press: (key, operation) => surface.pressFocusedKey(key, operation),
39
- dismiss: (operation) => surface.dismissKeyboard(operation.signal),
39
+ type: (text, keyboardOptions, operation) => surface.typeText(text, keyboardOptions, operation, surface.inputBudget('type', operation.timeoutMs)),
40
+ press: (key, operation) => surface.pressFocusedKey(key, operation, surface.inputBudget('press', operation.timeoutMs)),
41
+ dismiss: (operation) => surface.dismissKeyboard(operation.signal, surface.inputBudget('keyboard dismiss', operation.timeoutMs)),
40
42
  },
41
43
  validateApp: (app, info) => validateApp(app, info, surface.options.platform),
42
44
  // No `open`: a device app has no URL to open, so the runner serves
43
45
  // `app.open()` with `restart`, the fresh launch of the pinned app, which
44
46
  // is also the device's "recreate the context".
45
47
  session: {
46
- back: (operation) => surface.back(operation),
48
+ back: (operation) => surface.back(operation, surface.inputBudget('back', operation.timeoutMs)),
47
49
  restart: (operation) => surface.restart(operation),
48
50
  reset: (operation) => surface.reset(operation),
49
51
  },
package/dist/errors.d.ts CHANGED
@@ -14,8 +14,10 @@
14
14
  * | anything else, agent-device's own timeouts included | ENGINE_FAILURE with the text |
15
15
  *
16
16
  * `OPERATION_TIMEOUT` is never produced here. In the contract it means the
17
- * harness budget expired, which `raceAbort` already reports, and core
18
- * replaces its message with a bare `operation timed out`. A timeout
17
+ * operation budget expired: `InputBudget` (support.ts) reports it for an input
18
+ * the budget ran out before, and `ACTION_MAY_HAVE_COMMITTED` for one sent that
19
+ * did not answer in time. Core replaces the timeout's message with a bare
20
+ * `operation timed out`. A timeout
19
21
  * agent-device itself reports (a simulator boot, the runner connecting, an
20
22
  * app launch) is a device-side fault whose text is the diagnosis, so it
21
23
  * stays `ENGINE_FAILURE` and keeps its message and hint.
package/dist/errors.js CHANGED
@@ -14,8 +14,10 @@
14
14
  * | anything else, agent-device's own timeouts included | ENGINE_FAILURE with the text |
15
15
  *
16
16
  * `OPERATION_TIMEOUT` is never produced here. In the contract it means the
17
- * harness budget expired, which `raceAbort` already reports, and core
18
- * replaces its message with a bare `operation timed out`. A timeout
17
+ * operation budget expired: `InputBudget` (support.ts) reports it for an input
18
+ * the budget ran out before, and `ACTION_MAY_HAVE_COMMITTED` for one sent that
19
+ * did not answer in time. Core replaces the timeout's message with a bare
20
+ * `operation timed out`. A timeout
19
21
  * agent-device itself reports (a simulator boot, the runner connecting, an
20
22
  * app launch) is a device-side fault whose text is the diagnosis, so it
21
23
  * stays `ENGINE_FAILURE` and keeps its message and hint.
package/dist/support.d.ts CHANGED
@@ -12,6 +12,34 @@ export interface Rect {
12
12
  readonly height: number;
13
13
  }
14
14
  export declare function cancelled(text: string): EngineError;
15
+ /**
16
+ * One input operation's budget (`actionTimeout`, or the call's `timeout`),
17
+ * which the harness signal does not enforce. Only the device commands that
18
+ * send input run under it; reads stay unbounded, so a locator poll keeps its
19
+ * last snapshot and a target settling before a tap is relocated in full.
20
+ */
21
+ export declare class InputBudget {
22
+ private readonly label;
23
+ private readonly track;
24
+ private readonly endsAt;
25
+ private sent;
26
+ /**
27
+ * `track` keeps the device command itself in flight, past a timeout that
28
+ * abandons it, until it settles. The callers' own tracking settles with the
29
+ * race, so this is the registration the next attempt waits on.
30
+ */
31
+ constructor(label: string, timeoutMs: number, track: <T>(pending: Promise<T>) => Promise<T>);
32
+ /**
33
+ * Sends one input with what is left of the budget. With none left it sends
34
+ * nothing: `OPERATION_TIMEOUT`, or `ACTION_MAY_HAVE_COMMITTED` once an
35
+ * earlier input of this operation went out. A gesture that holds (`holdMs`)
36
+ * at least as long as the budget left could only be cut off mid-gesture, so
37
+ * it is refused unsent. An input that does not answer in time may have
38
+ * landed, so it is `ACTION_MAY_HAVE_COMMITTED` and the runner never repeats it.
39
+ */
40
+ send<T>(dispatch: () => Promise<T>, holdMs?: number): Promise<T>;
41
+ private maybeCommitted;
42
+ }
15
43
  export declare function invalidState(text: string): EngineError;
16
44
  export declare function notActionable(text: string): EngineError;
17
45
  export declare function unsupported(text: string): EngineError;
package/dist/support.js CHANGED
@@ -1,8 +1,60 @@
1
1
  /** Shared helpers for the agent-device engine: error constructors, filenames, PNG headers, gestures, the screen location and size. */
2
- import { EngineError } from 'e2e/engine';
2
+ import { EngineError, withTimeout } from 'e2e/engine';
3
3
  export function cancelled(text) {
4
4
  return new EngineError('CANCELLED', text, { retryable: false });
5
5
  }
6
+ /**
7
+ * The least time an input gets to answer once sent, however little budget
8
+ * was left: a tap that lands is then not reported as cut off.
9
+ */
10
+ const MIN_ANSWER_MS = 250;
11
+ /**
12
+ * One input operation's budget (`actionTimeout`, or the call's `timeout`),
13
+ * which the harness signal does not enforce. Only the device commands that
14
+ * send input run under it; reads stay unbounded, so a locator poll keeps its
15
+ * last snapshot and a target settling before a tap is relocated in full.
16
+ */
17
+ export class InputBudget {
18
+ label;
19
+ track;
20
+ endsAt;
21
+ sent = false;
22
+ /**
23
+ * `track` keeps the device command itself in flight, past a timeout that
24
+ * abandons it, until it settles. The callers' own tracking settles with the
25
+ * race, so this is the registration the next attempt waits on.
26
+ */
27
+ constructor(label, timeoutMs, track) {
28
+ this.label = label;
29
+ this.track = track;
30
+ this.endsAt = Date.now() + timeoutMs;
31
+ }
32
+ /**
33
+ * Sends one input with what is left of the budget. With none left it sends
34
+ * nothing: `OPERATION_TIMEOUT`, or `ACTION_MAY_HAVE_COMMITTED` once an
35
+ * earlier input of this operation went out. A gesture that holds (`holdMs`)
36
+ * at least as long as the budget left could only be cut off mid-gesture, so
37
+ * it is refused unsent. An input that does not answer in time may have
38
+ * landed, so it is `ACTION_MAY_HAVE_COMMITTED` and the runner never repeats it.
39
+ */
40
+ send(dispatch, holdMs = 0) {
41
+ const remaining = this.endsAt - Date.now();
42
+ if (remaining <= 0) {
43
+ return Promise.reject(this.sent ? this.maybeCommitted() : new EngineError('OPERATION_TIMEOUT', `${this.label} timed out before it was sent`, { retryable: false }));
44
+ }
45
+ if (holdMs >= remaining) {
46
+ return Promise.reject(new EngineError('NOT_ACTIONABLE', `${this.label} holds for ${holdMs} ms, but only ${Math.floor(remaining)} ms of its timeout are left; raise the timeout or shorten the hold`, { retryable: false }));
47
+ }
48
+ const pending = this.track(dispatch());
49
+ this.sent = true;
50
+ return withTimeout(pending, Math.max(remaining, MIN_ANSWER_MS), () => this.maybeCommitted());
51
+ }
52
+ maybeCommitted() {
53
+ return new EngineError('ACTION_MAY_HAVE_COMMITTED', `${this.label} timed out before the device answered; its input may have been dispatched`, {
54
+ retryable: false,
55
+ });
56
+ }
57
+ }
6
58
  export function invalidState(text) {
7
59
  return new EngineError('INVALID_STATE', text, { retryable: false });
8
60
  }
package/dist/surface.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  import { type EngineAttemptContext, type EngineCleanupContext, type EngineInitInfo, type EngineObserveOptions, type EngineSnapshot, type LocatorAction, type LocatorExpression, type NodeRef, type OperationContext, type PointerAction, type VideoSegment, type SemanticNode, type ViewportPoint } from 'e2e/engine';
15
15
  import { type AgentDeviceClient, type ClientFactory, type DevicePermission, type LaunchPermissions, type MobileOptions, type PermissionState } from './options.ts';
16
16
  import { DevicePool } from './pool.ts';
17
+ import { InputBudget } from './support.ts';
17
18
  /** Hinge poses `device.fold` accepts: `closed` lights the outer display, `half-open` and `open` the inner one. */
18
19
  export type FoldPose = 'closed' | 'half-open' | 'open';
19
20
  /** How `installApp` puts a build on the device. */
@@ -138,6 +139,8 @@ export declare class AgentDeviceSurface {
138
139
  * not yet arrived.
139
140
  */
140
141
  screenCommand<T>(label: string, run: (client: AgentDeviceClient) => Promise<T>, signal: AbortSignal): Promise<T>;
142
+ /** The budget one input runs under; its device commands stay in flight until they settle. */
143
+ inputBudget(label: string, timeoutMs: number): InputBudget;
141
144
  /** Registers one device command as in flight until it settles. */
142
145
  private track;
143
146
  /**
@@ -313,7 +316,7 @@ export declare class AgentDeviceSurface {
313
316
  * return, since the input lands late in the command's own round trip.
314
317
  */
315
318
  private markAction;
316
- perform(ref: NodeRef, action: LocatorAction, operation: OperationContext): Promise<void>;
319
+ perform(ref: NodeRef, action: LocatorAction, operation: OperationContext, input: InputBudget): Promise<void>;
317
320
  /**
318
321
  * Keys on a touch surface, in the contract's key grammar: `Enter` submits
319
322
  * through the soft keyboard, `Space` and any single character are typed
@@ -327,7 +330,7 @@ export declare class AgentDeviceSurface {
327
330
  * node's bounds are in, with no element resolved behind it. `settle` waits
328
331
  * for the UI to go quiet, as the node taps do.
329
332
  */
330
- performAt(point: ViewportPoint, action: PointerAction, operation: OperationContext): Promise<void>;
333
+ performAt(point: ViewportPoint, action: PointerAction, operation: OperationContext, input: InputBudget): Promise<void>;
331
334
  /**
332
335
  * Types into whatever holds focus through the device's text-input path;
333
336
  * agent-device's `type` lands on the focused field. `replace` is not a
@@ -336,13 +339,13 @@ export declare class AgentDeviceSurface {
336
339
  */
337
340
  typeText(text: string, options: {
338
341
  readonly replace: boolean;
339
- }, operation: OperationContext): Promise<void>;
342
+ }, operation: OperationContext, input: InputBudget): Promise<void>;
340
343
  /**
341
344
  * One key to the focused field, with the same reach as a node press: Enter
342
345
  * submits through the soft keyboard, Space and single characters are typed.
343
346
  * Modifiers and the other named keys have no event bus to land on.
344
347
  */
345
- pressFocusedKey(key: string, operation: OperationContext): Promise<void>;
348
+ pressFocusedKey(key: string, operation: OperationContext, input: InputBudget): Promise<void>;
346
349
  /**
347
350
  * Hides the soft keyboard, so a control it covered can be reached.
348
351
  * agent-device presses the keyboard's own dismiss key and refuses when the
@@ -351,7 +354,7 @@ export declare class AgentDeviceSurface {
351
354
  * makes that guess, the way a user does and Maestro's `hideKeyboard` does,
352
355
  * in {@link dismissKeyboardByGesture}.
353
356
  */
354
- dismissKeyboard(signal: AbortSignal): Promise<void>;
357
+ dismissKeyboard(signal: AbortSignal, input?: InputBudget): Promise<void>;
355
358
  /**
356
359
  * A keyboard with no dismiss key goes the way a user sends it away: a short
357
360
  * drag at the centre of the screen, which a scroll view turns into a
@@ -378,7 +381,7 @@ export declare class AgentDeviceSurface {
378
381
  * press, or the one given when no tip is showing.
379
382
  */
380
383
  private dismissKeyboardTip;
381
- back(operation: OperationContext): Promise<void>;
384
+ back(operation: OperationContext, input: InputBudget): Promise<void>;
382
385
  restart(operation: OperationContext): Promise<void>;
383
386
  /** Clears the pinned app's persisted state and relaunches it: the device equivalent of a fresh context. */
384
387
  reset(operation: OperationContext): Promise<void>;
package/dist/surface.js CHANGED
@@ -25,7 +25,7 @@ import { deviceLabel, pinnedApp } from './bindings.js';
25
25
  import { assertAppId } from './links.js';
26
26
  import { DevicePool, deviceSelection } from './pool.js';
27
27
  import { recordLease, travelledLease } from './provider.js';
28
- import { invalidState, notActionable, logicalScreenSize, readPngSize, sanitizeFilename, screenLocation, swipeWithin, unsupported, } from './support.js';
28
+ import { invalidState, notActionable, logicalScreenSize, readPngSize, sanitizeFilename, screenLocation, swipeWithin, unsupported, InputBudget, } from './support.js';
29
29
  /** Why nothing is pinned with `appPath` alone: the install is the suite's. */
30
30
  const UNINSTALLED_BUILD = "the build the target's `app.appPath` names installed first with `device.installApp()`";
31
31
  /** Platform element types that are the soft keyboard or one of its keys, as agent-device names them. */
@@ -99,6 +99,10 @@ function depthBelow(entry, ancestor) {
99
99
  }
100
100
  return hops;
101
101
  }
102
+ /** Sends an input under its operation's budget, or directly from a fixture call, which has none. */
103
+ function sendInput(input, dispatch) {
104
+ return input === undefined ? dispatch() : input.send(dispatch);
105
+ }
102
106
  function sleep(ms, signal) {
103
107
  return new Promise((resolve) => {
104
108
  const timer = setTimeout(done, ms);
@@ -276,6 +280,10 @@ export class AgentDeviceSurface {
276
280
  this.markAction(before);
277
281
  return result;
278
282
  }
283
+ /** The budget one input runs under; its device commands stay in flight until they settle. */
284
+ inputBudget(label, timeoutMs) {
285
+ return new InputBudget(label, timeoutMs, (pending) => this.track(pending));
286
+ }
279
287
  /** Registers one device command as in flight until it settles. */
280
288
  track(pending) {
281
289
  this.inflight.add(pending);
@@ -853,7 +861,7 @@ export class AgentDeviceSurface {
853
861
  this.indexBeforeAction = before;
854
862
  this.lastActionAt = Date.now();
855
863
  }
856
- async perform(ref, action, operation) {
864
+ async perform(ref, action, operation, input) {
857
865
  // A test's step verifies its outcome with `expect`, so it never waits for
858
866
  // the screen to settle afterwards; it only lets a control that came with
859
867
  // the last action finish arriving. The agent reads the screen right after
@@ -872,7 +880,7 @@ export class AgentDeviceSurface {
872
880
  if (action.kind !== 'swipe')
873
881
  throw notActionable(`the screen root takes swipe only, not ${action.kind}; act on a node`);
874
882
  const before = this.latestIndex;
875
- await this.command('swipe', (client) => client.interactions.scroll({ direction: action.direction, ...settle }), operation.signal);
883
+ await this.command('swipe', (client) => input.send(() => client.interactions.scroll({ direction: action.direction, ...settle })), operation.signal);
876
884
  this.markAction(before);
877
885
  return;
878
886
  }
@@ -889,26 +897,26 @@ export class AgentDeviceSurface {
889
897
  const target = deterministic ? await this.settled(entry, operation) : entry;
890
898
  switch (action.kind) {
891
899
  case 'tap':
892
- return client.interactions.press({ ...this.actionTarget(target, true), ...settle });
900
+ return input.send(() => client.interactions.press({ ...this.actionTarget(target, true), ...settle }));
893
901
  case 'focus':
894
902
  // A touch surface focuses by tapping, and a tap on anything but an
895
903
  // editable field activates it; focus is offered for fields only.
896
904
  if (target.node.role !== 'textbox') {
897
905
  throw unsupported(`agent-device can only focus editable fields; node ${target.id} is ${target.node.role ?? 'unknown'}`);
898
906
  }
899
- return client.interactions.press({ ...this.actionTarget(target), ...settle });
907
+ return input.send(() => client.interactions.press({ ...this.actionTarget(target), ...settle }));
900
908
  case 'doubleTap':
901
- return client.interactions.press({ ...this.actionTarget(target), count: 2, ...settle });
909
+ return input.send(() => client.interactions.press({ ...this.actionTarget(target), count: 2, ...settle }));
902
910
  case 'longPress':
903
- return client.interactions.longPress({ ...this.actionTarget(target), ...settle, durationMs: action.durationMs ?? DEFAULT_LONG_PRESS_MS });
911
+ return input.send(() => client.interactions.longPress({ ...this.actionTarget(target), ...settle, durationMs: action.durationMs ?? DEFAULT_LONG_PRESS_MS }), action.durationMs ?? DEFAULT_LONG_PRESS_MS);
904
912
  case 'hover':
905
- return client.interactions.hover(this.actionTarget(target));
913
+ return input.send(() => client.interactions.hover(this.actionTarget(target)));
906
914
  case 'fill':
907
915
  // oxlint-disable-next-line unicorn/no-array-fill-with-reference-type -- agent-device fill, not Array#fill
908
- return client.interactions.fill({ ...this.actionTarget(target), text: action.value, ...settle });
916
+ return input.send(() => client.interactions.fill({ ...this.actionTarget(target), text: action.value, ...settle }));
909
917
  case 'clear':
910
918
  // oxlint-disable-next-line unicorn/no-array-fill-with-reference-type -- agent-device fill, not Array#fill
911
- return client.interactions.fill({ ...this.actionTarget(target), text: '', ...settle });
919
+ return input.send(() => client.interactions.fill({ ...this.actionTarget(target), text: '', ...settle }));
912
920
  case 'check':
913
921
  case 'uncheck': {
914
922
  const wanted = action.kind === 'check';
@@ -920,22 +928,22 @@ export class AgentDeviceSurface {
920
928
  }
921
929
  if (checked === wanted)
922
930
  return undefined;
923
- return client.interactions.press({ ...this.actionTarget(target, true), ...settle });
931
+ return input.send(() => client.interactions.press({ ...this.actionTarget(target, true), ...settle }));
924
932
  }
925
933
  case 'press':
926
- return this.pressKey(client, target, action.key, settle);
934
+ return this.pressKey(client, target, action.key, settle, input);
927
935
  case 'swipe': {
928
936
  const rect = target.node.rect;
929
937
  if (rect === undefined)
930
938
  throw notActionable(`node ${target.id} has no bounds to swipe within`);
931
- return client.interactions.swipe(swipeWithin(rect, action.direction, action.momentum));
939
+ return input.send(() => client.interactions.swipe(swipeWithin(rect, action.direction, action.momentum)));
932
940
  }
933
941
  case 'dragTo': {
934
942
  const destination = this.resolveRef(action.target);
935
- return client.interactions.drag({
943
+ return input.send(() => client.interactions.drag({
936
944
  source: this.actionTarget(target).ref,
937
945
  destination: this.actionTarget(destination).ref,
938
- });
946
+ }));
939
947
  }
940
948
  case 'scrollIntoView':
941
949
  case 'selectOption':
@@ -959,7 +967,7 @@ export class AgentDeviceSurface {
959
967
  * modifiers and the other named keys (`Escape`, `Tab`, `Backspace`, the
960
968
  * arrows) have nothing to land on and are refused.
961
969
  */
962
- async pressKey(client, entry, key, settle) {
970
+ async pressKey(client, entry, key, settle, input) {
963
971
  const parsed = parseKey(key);
964
972
  if (parsed === undefined) {
965
973
  throw unsupported(`"${key}" is not a key: press takes one key in the form [Modifier+]...Key, a named key (${KEY_NAMES.join(', ')}) or one character`);
@@ -968,14 +976,14 @@ export class AgentDeviceSurface {
968
976
  throw unsupported(`agent-device cannot hold ${parsed.modifiers.join('+')} on a device surface; press the key alone`);
969
977
  }
970
978
  if (parsed.key.kind === 'named' && parsed.key.name === 'Enter') {
971
- return client.command.keyboard({ action: 'enter' });
979
+ return input.send(() => client.command.keyboard({ action: 'enter' }));
972
980
  }
973
981
  if (parsed.key.kind === 'char' || parsed.key.name === 'Space') {
974
982
  const text = parsed.key.kind === 'char' ? parsed.key.char : ' ';
975
983
  if (entry.node.role === 'textbox' && entry.node.states?.focused !== true) {
976
- await client.interactions.press({ ...this.actionTarget(entry), ...settle });
984
+ await input.send(() => client.interactions.press({ ...this.actionTarget(entry), ...settle }));
977
985
  }
978
- return client.interactions.type({ text });
986
+ return input.send(() => client.interactions.type({ text }));
979
987
  }
980
988
  throw unsupported(`agent-device cannot press ${parsed.key.name} on a device surface; only Enter, Space, and single characters reach the soft keyboard`);
981
989
  }
@@ -984,10 +992,11 @@ export class AgentDeviceSurface {
984
992
  * node's bounds are in, with no element resolved behind it. `settle` waits
985
993
  * for the UI to go quiet, as the node taps do.
986
994
  */
987
- async performAt(point, action, operation) {
995
+ async performAt(point, action, operation, input) {
988
996
  const settle = operation.origin === 'test' ? {} : this.settleOptions;
989
997
  const before = this.latestIndex;
990
- await this.command(`${action.kind} at point`, (client) => pointerInteraction(client, point, action, settle), operation.signal);
998
+ const holdMs = action.kind === 'longPress' ? (action.durationMs ?? DEFAULT_LONG_PRESS_MS) : action.kind === 'swipeTo' ? action.durationMs : undefined;
999
+ await this.command(`${action.kind} at point`, (client) => input.send(() => pointerInteraction(client, point, action, settle), holdMs), operation.signal);
991
1000
  this.markAction(before);
992
1001
  }
993
1002
  /**
@@ -996,12 +1005,12 @@ export class AgentDeviceSurface {
996
1005
  * device primitive without a target, so it is refused rather than faked:
997
1006
  * the model clears a listed field by filling it by id.
998
1007
  */
999
- async typeText(text, options, operation) {
1008
+ async typeText(text, options, operation, input) {
1000
1009
  if (options.replace) {
1001
1010
  throw unsupported('agent-device cannot clear the focused field without a node; type into a listed field by id to replace its value');
1002
1011
  }
1003
1012
  const before = this.latestIndex;
1004
- await this.command('keyboard.type', (client) => client.interactions.type({ text }), operation.signal);
1013
+ await this.command('keyboard.type', (client) => input.send(() => client.interactions.type({ text })), operation.signal);
1005
1014
  this.markAction(before);
1006
1015
  }
1007
1016
  /**
@@ -1009,7 +1018,7 @@ export class AgentDeviceSurface {
1009
1018
  * submits through the soft keyboard, Space and single characters are typed.
1010
1019
  * Modifiers and the other named keys have no event bus to land on.
1011
1020
  */
1012
- async pressFocusedKey(key, operation) {
1021
+ async pressFocusedKey(key, operation, input) {
1013
1022
  const parsed = parseKey(key);
1014
1023
  if (parsed === undefined) {
1015
1024
  throw unsupported(`"${key}" is not a key: press takes one key in the form [Modifier+]...Key, a named key (${KEY_NAMES.join(', ')}) or one character`);
@@ -1019,11 +1028,11 @@ export class AgentDeviceSurface {
1019
1028
  }
1020
1029
  const before = this.latestIndex;
1021
1030
  if (parsed.key.kind === 'named' && parsed.key.name === 'Enter') {
1022
- await this.command('keyboard.press', (client) => client.command.keyboard({ action: 'enter' }), operation.signal);
1031
+ await this.command('keyboard.press', (client) => input.send(() => client.command.keyboard({ action: 'enter' })), operation.signal);
1023
1032
  }
1024
1033
  else if (parsed.key.kind === 'char' || parsed.key.name === 'Space') {
1025
1034
  const text = parsed.key.kind === 'char' ? parsed.key.char : ' ';
1026
- await this.command('keyboard.press', (client) => client.interactions.type({ text }), operation.signal);
1035
+ await this.command('keyboard.press', (client) => input.send(() => client.interactions.type({ text })), operation.signal);
1027
1036
  }
1028
1037
  else {
1029
1038
  throw unsupported(`agent-device cannot press ${parsed.key.name} on a device surface; only Enter, Space, and single characters reach the soft keyboard`);
@@ -1038,15 +1047,15 @@ export class AgentDeviceSurface {
1038
1047
  * makes that guess, the way a user does and Maestro's `hideKeyboard` does,
1039
1048
  * in {@link dismissKeyboardByGesture}.
1040
1049
  */
1041
- async dismissKeyboard(signal) {
1050
+ async dismissKeyboard(signal, input) {
1042
1051
  const before = this.latestIndex;
1043
1052
  try {
1044
- await this.command('keyboard.dismiss', (client) => client.command.keyboard({ action: 'dismiss' }), signal);
1053
+ await this.command('keyboard.dismiss', (client) => sendInput(input, () => client.command.keyboard({ action: 'dismiss' })), signal);
1045
1054
  }
1046
1055
  catch (cause) {
1047
1056
  if (!(cause instanceof EngineError && cause.code === 'UNSUPPORTED_CAPABILITY'))
1048
1057
  throw cause;
1049
- await this.dismissKeyboardByGesture(signal);
1058
+ await this.dismissKeyboardByGesture(signal, input);
1050
1059
  }
1051
1060
  this.markAction(before);
1052
1061
  }
@@ -1068,8 +1077,8 @@ export class AgentDeviceSurface {
1068
1077
  * look is the observation's own capture, sparse-tree retries included: a
1069
1078
  * one-node tree has no keys and would pass for a dismissed keyboard.
1070
1079
  */
1071
- async dismissKeyboardByGesture(signal) {
1072
- let raw = await this.dismissKeyboardTip(await this.snapshot(signal, false), signal);
1080
+ async dismissKeyboardByGesture(signal, input) {
1081
+ let raw = await this.dismissKeyboardTip(await this.snapshot(signal, false), signal, input);
1073
1082
  if (!keyboardShowing(raw))
1074
1083
  return;
1075
1084
  const projected = projectSnapshot(raw.nodes ?? [], { mintId: () => 'keyboard', viewport: raw.viewport });
@@ -1080,7 +1089,7 @@ export class AgentDeviceSurface {
1080
1089
  { x: centre.x, y: centre.y - viewport.height * KEYBOARD_DRAG_RATIO },
1081
1090
  ];
1082
1091
  for (const to of drags) {
1083
- await this.command('keyboard.dismiss', (client) => client.interactions.swipe({ from: centre, to }), signal);
1092
+ await this.command('keyboard.dismiss', (client) => sendInput(input, () => client.interactions.swipe({ from: centre, to })), signal);
1084
1093
  raw = await this.snapshot(signal, false);
1085
1094
  if (!keyboardShowing(raw))
1086
1095
  return;
@@ -1093,7 +1102,7 @@ export class AgentDeviceSurface {
1093
1102
  * Maestro's `hideKeyboard` presses it too. Answers the capture after the
1094
1103
  * press, or the one given when no tip is showing.
1095
1104
  */
1096
- async dismissKeyboardTip(raw, signal) {
1105
+ async dismissKeyboardTip(raw, signal, input) {
1097
1106
  const nodes = raw.nodes ?? [];
1098
1107
  const tip = nodes.find((node) => (node.label ?? node.value ?? '').startsWith('Speed up your typing'));
1099
1108
  if (tip?.rect === undefined)
@@ -1102,13 +1111,13 @@ export class AgentDeviceSurface {
1102
1111
  const button = nodes.find((node) => node.label === 'Continue' && node.rect !== undefined && node.rect.y > tipTop);
1103
1112
  if (button?.rect === undefined)
1104
1113
  return raw;
1105
- await this.command('keyboard.dismiss', (client) => client.interactions.press(centreOf(button.rect)), signal);
1114
+ await this.command('keyboard.dismiss', (client) => sendInput(input, () => client.interactions.press(centreOf(button.rect))), signal);
1106
1115
  return await this.snapshot(signal, false);
1107
1116
  }
1108
- async back(operation) {
1117
+ async back(operation, input) {
1109
1118
  const settle = operation.origin === 'test' ? {} : this.settleOptions;
1110
1119
  const before = this.latestIndex;
1111
- await this.command('back', (client) => client.command.back({ ...settle }), operation.signal);
1120
+ await this.command('back', (client) => input.send(() => client.command.back({ ...settle })), operation.signal);
1112
1121
  this.markAction(before);
1113
1122
  }
1114
1123
  async restart(operation) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e2e-dev/mobile",
3
- "version": "0.12.0",
3
+ "version": "0.12.1-nightly-20261014080557",
4
4
  "description": "Mobile engine (iOS and Android) for e2e agentic end-to-end testing, built on agent-device",
5
5
  "keywords": [
6
6
  "e2e",
@@ -57,7 +57,7 @@
57
57
  },
58
58
  "peerDependencies": {
59
59
  "ai": "^7.0.0",
60
- "e2e": ">=0.15.0 <1"
60
+ "e2e": "0.21.0-nightly-20261014080557"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "ai": {
@@ -68,7 +68,7 @@
68
68
  "@types/node": "26.6.2",
69
69
  "@types/pngjs": "6.0.5",
70
70
  "ai": "7.0.107",
71
- "e2e": "0.20.0",
71
+ "e2e": "0.21.0-nightly-20261014080557",
72
72
  "typescript": "7.0.2",
73
73
  "vitest": "5.0.1"
74
74
  },