Skip to content

Commit dc87290

Browse files
committed
fix: improve React Navigation Maestro reliability
1 parent b7ca4fb commit dc87290

53 files changed

Lines changed: 1849 additions & 135 deletions

Some content is hidden

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

android-snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ private CaptureResult captureXml(
351351
AccessibilityNodeInfo root = automation.getRootInActiveWindow();
352352
try {
353353
if (root != null) {
354-
appendNode(xml, root, 0, 0, maxDepth, maxNodes, stats);
354+
appendNode(xml, root, 0, 0, maxDepth, maxNodes, stats, null);
355355
windowCount = 1;
356356
}
357357
captureMode = "active-window";
@@ -439,7 +439,15 @@ private static int appendInteractiveWindowRoots(
439439
}
440440
StringBuilder windowXml = new StringBuilder();
441441
CaptureStats windowStats = stats.copy();
442-
appendNode(windowXml, root, windowCount, 0, maxDepth, maxNodes, windowStats);
442+
appendNode(
443+
windowXml,
444+
root,
445+
windowCount,
446+
0,
447+
maxDepth,
448+
maxNodes,
449+
windowStats,
450+
readWindowMetadata(window, windowCount));
443451
xml.append(windowXml);
444452
stats.copyFrom(windowStats);
445453
windowCount += 1;
@@ -482,7 +490,8 @@ private static void appendNode(
482490
int depth,
483491
int maxDepth,
484492
int maxNodes,
485-
CaptureStats stats) {
493+
CaptureStats stats,
494+
WindowMetadata windowMetadata) {
486495
if (stats.nodeCount >= maxNodes) {
487496
stats.truncated = true;
488497
return;
@@ -495,11 +504,15 @@ private static void appendNode(
495504
// without affecting current snapshot semantics; add fields back here when TS starts reading
496505
// them.
497506
appendAttribute(xml, "index", Integer.toString(nodeIndex));
507+
if (windowMetadata != null) {
508+
appendWindowMetadata(xml, windowMetadata);
509+
}
498510
appendNonEmptyAttribute(xml, "text", node.getText());
499511
appendNonEmptyAttribute(xml, "resource-id", node.getViewIdResourceName());
500512
appendAttribute(xml, "class", node.getClassName());
501513
appendNonEmptyAttribute(xml, "package", node.getPackageName());
502514
appendNonEmptyAttribute(xml, "content-desc", node.getContentDescription());
515+
appendAttribute(xml, "visible-to-user", Boolean.toString(node.isVisibleToUser()));
503516
appendTrueAttribute(xml, "clickable", node.isClickable());
504517
appendAttribute(xml, "enabled", Boolean.toString(node.isEnabled()));
505518
appendTrueAttribute(xml, "focusable", node.isFocusable());
@@ -550,7 +563,7 @@ private static void appendNode(
550563
continue;
551564
}
552565
try {
553-
appendNode(xml, child, index, depth + 1, maxDepth, maxNodes, stats);
566+
appendNode(xml, child, index, depth + 1, maxDepth, maxNodes, stats, null);
554567
} finally {
555568
child.recycle();
556569
}
@@ -571,6 +584,32 @@ private static void appendTrueAttribute(StringBuilder xml, String name, boolean
571584
}
572585
}
573586

587+
private static void appendWindowMetadata(StringBuilder xml, WindowMetadata metadata) {
588+
appendAttribute(xml, "window-index", Integer.toString(metadata.index));
589+
appendAttribute(xml, "window-type", Integer.toString(metadata.type));
590+
appendAttribute(xml, "window-layer", Integer.toString(metadata.layer));
591+
appendAttribute(xml, "window-active", Boolean.toString(metadata.active));
592+
appendAttribute(xml, "window-focused", Boolean.toString(metadata.focused));
593+
appendAttribute(
594+
xml,
595+
"window-bounds",
596+
String.format(
597+
Locale.ROOT,
598+
"[%d,%d][%d,%d]",
599+
metadata.bounds.left,
600+
metadata.bounds.top,
601+
metadata.bounds.right,
602+
metadata.bounds.bottom));
603+
}
604+
605+
@SuppressWarnings("deprecation")
606+
private static WindowMetadata readWindowMetadata(AccessibilityWindowInfo window, int index) {
607+
Rect bounds = new Rect();
608+
window.getBoundsInScreen(bounds);
609+
return new WindowMetadata(
610+
index, window.getType(), window.getLayer(), window.isActive(), window.isFocused(), bounds);
611+
}
612+
574613
private static void appendAttribute(StringBuilder xml, String name, CharSequence value) {
575614
String stringValue = value == null ? "" : value.toString();
576615
xml.append(' ');
@@ -702,4 +741,22 @@ private static final class CaptureResult {
702741
this.truncated = truncated;
703742
}
704743
}
744+
745+
private static final class WindowMetadata {
746+
final int index;
747+
final int type;
748+
final int layer;
749+
final boolean active;
750+
final boolean focused;
751+
final Rect bounds;
752+
753+
WindowMetadata(int index, int type, int layer, boolean active, boolean focused, Rect bounds) {
754+
this.index = index;
755+
this.type = type;
756+
this.layer = layer;
757+
this.active = active;
758+
this.focused = focused;
759+
this.bounds = bounds;
760+
}
761+
}
705762
}

src/__tests__/cli-grammar.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,13 @@ test('settings grammar owns positional parsing for CLI commands', () => {
7979
assert.equal(location.state, 'set');
8080
assert.equal(location.latitude, 37.3349);
8181
assert.equal(location.longitude, -122.009);
82+
83+
const cleanUserData = readInputFromCli('settings', ['clean-user-data', 'com.example.app'], {
84+
...BASE_FLAGS,
85+
platform: 'android',
86+
});
87+
assert.equal(cleanUserData.platform, 'android');
88+
assert.equal(cleanUserData.setting, 'clean-user-data');
89+
assert.equal(cleanUserData.state, 'clear');
90+
assert.equal(cleanUserData.app, 'com.example.app');
8291
});

src/__tests__/runtime-interactions.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,36 @@ test('runtime directional swipe uses the visible viewport instead of off-screen
589589
assert.deepEqual(calls, [{ from: { x: 50, y: 50 }, to: { x: 25, y: 50 } }]);
590590
});
591591

592+
test('runtime gesture swipe presets use stable viewport lanes', async () => {
593+
const calls: unknown[] = [];
594+
const device = createInteractionDevice(snapshotWithOffscreenContent(), {
595+
platform: 'android',
596+
swipe: async (_context, from, to, options) => {
597+
calls.push({ from, to, durationMs: options?.durationMs });
598+
},
599+
});
600+
601+
const pageSwipe = await device.interactions.swipe({
602+
preset: 'left',
603+
durationMs: 300,
604+
session: 'default',
605+
});
606+
const edgeSwipe = await device.interactions.swipe({
607+
preset: 'right-edge',
608+
durationMs: 350,
609+
session: 'default',
610+
});
611+
612+
assert.deepEqual(pageSwipe.from, { x: 90, y: 65 });
613+
assert.deepEqual(pageSwipe.to, { x: 10, y: 65 });
614+
assert.deepEqual(edgeSwipe.from, { x: 8, y: 50 });
615+
assert.deepEqual(edgeSwipe.to, { x: 85, y: 50 });
616+
assert.deepEqual(calls, [
617+
{ from: { x: 90, y: 65 }, to: { x: 10, y: 65 }, durationMs: 300 },
618+
{ from: { x: 8, y: 50 }, to: { x: 85, y: 50 }, durationMs: 350 },
619+
]);
620+
});
621+
592622
test('runtime viewport gestures reject inspect-only macOS surfaces', async () => {
593623
for (const surface of ['desktop', 'menubar'] as const) {
594624
const device = createInteractionDevice(selectorSnapshot(), {

src/client-types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,11 @@ export type FlingOptions = ClientCommandBaseOptions & {
595595
durationMs?: number;
596596
};
597597

598+
export type SwipeGestureOptions = ClientCommandBaseOptions & {
599+
preset: 'left' | 'right' | 'left-edge' | 'right-edge';
600+
durationMs?: number;
601+
};
602+
598603
export type FocusOptions = ClientCommandBaseOptions & {
599604
x: number;
600605
y: number;
@@ -763,6 +768,11 @@ export type PermissionTarget =
763768
| 'input-monitoring';
764769

765770
export type SettingsUpdateOptions =
771+
| (ClientCommandBaseOptions & {
772+
setting: 'clean-user-data';
773+
state: 'clear';
774+
app?: string;
775+
})
766776
| (ClientCommandBaseOptions & {
767777
setting: 'wifi' | 'airplane' | 'location';
768778
state: 'on' | 'off';
@@ -915,6 +925,7 @@ export type AgentDeviceClient = {
915925
swipe: (options: SwipeOptions) => Promise<CommandRequestResult>;
916926
pan: (options: PanOptions) => Promise<CommandRequestResult>;
917927
fling: (options: FlingOptions) => Promise<CommandRequestResult>;
928+
swipeGesture: (options: SwipeGestureOptions) => Promise<CommandRequestResult>;
918929
focus: (options: FocusOptions) => Promise<CommandRequestResult>;
919930
type: (options: TypeTextOptions) => Promise<CommandRequestResult>;
920931
fill: (options: FillOptions) => Promise<CommandRequestResult>;

src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ export function createAgentDeviceClient(
270270
swipe: async (options) => await executeCommand('swipe', options),
271271
pan: async (options) => await executeCommand('gesture-pan', options),
272272
fling: async (options) => await executeCommand('gesture-fling', options),
273+
swipeGesture: async (options) => await executeCommand('gesture-swipe', options),
273274
focus: async (options) => await executeCommand('focus', options),
274275
type: async (options) => await executeCommand('type', options),
275276
fill: async (options) => await executeCommand('fill', options),

src/command-catalog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const LOCAL_CLI_COMMANDS = {
6565
session: 'session',
6666
} as const;
6767

68-
const GESTURE_SUBCOMMANDS = ['pan', 'fling', 'pinch', 'rotate', 'transform'] as const;
68+
const GESTURE_SUBCOMMANDS = ['pan', 'fling', 'swipe', 'pinch', 'rotate', 'transform'] as const;
6969
export const GESTURE_SUBCOMMAND_ERROR = `gesture requires one of: ${GESTURE_SUBCOMMANDS.join(', ')}`;
7070

7171
export type PublicCommandName = (typeof PUBLIC_COMMANDS)[keyof typeof PUBLIC_COMMANDS];

src/commands/cli-grammar/capture.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,17 @@ function readSettingsOptionsFromPositionals(
231231
mode: readPermissionMode(positionals[3]),
232232
};
233233
}
234+
if (setting === 'clean-user-data') {
235+
const app = state === 'clear' ? positionals[2] : state;
236+
return { ...base, setting, state: 'clear', app };
237+
}
234238
throw new AppError('INVALID_ARGS', 'Invalid settings arguments.');
235239
}
236240

237241
function settingsPositionals(input: SettingsUpdateOptions): string[] {
242+
if (input.setting === 'clean-user-data') {
243+
return [input.setting, ...optionalString(input.app)];
244+
}
238245
if (input.setting === 'location' && input.state === 'set') {
239246
return [input.setting, input.state, String(input.latitude), String(input.longitude)];
240247
}

src/commands/cli-grammar/gesture.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const gestureDaemonWriters = {
2121
'gesture-fling': direct(PUBLIC_COMMANDS.gesture, (input) =>
2222
flingPositionals(input as FlingOptions),
2323
),
24+
'gesture-swipe': direct(PUBLIC_COMMANDS.gesture, swipePresetPositionals),
2425
'gesture-pinch': direct(PUBLIC_COMMANDS.gesture, pinchPositionals),
2526
'gesture-rotate': direct(PUBLIC_COMMANDS.gesture, (input) =>
2627
rotateGesturePositionals(input as RotateGestureOptions),
@@ -49,6 +50,8 @@ function gesturePositionals(input: CommandInput): string[] {
4950
...optionalNumber(input.distance),
5051
...optionalNumber(input.durationMs),
5152
];
53+
case 'swipe':
54+
return swipePresetPositionals(input);
5255
case 'pinch':
5356
return [
5457
'pinch',
@@ -78,11 +81,19 @@ function gesturePositionals(input: CommandInput): string[] {
7881
default:
7982
throw new AppError(
8083
'INVALID_ARGS',
81-
'gesture requires pan, fling, pinch, rotate, or transform',
84+
'gesture requires pan, fling, swipe, pinch, rotate, or transform',
8285
);
8386
}
8487
}
8588

89+
function swipePresetPositionals(input: CommandInput): string[] {
90+
return [
91+
'swipe',
92+
requiredDaemonString(input.preset, 'gesture swipe requires preset'),
93+
...optionalNumber(input.durationMs),
94+
];
95+
}
96+
8697
function panPositionals(input: CommandInput): string[] {
8798
return [
8899
'pan',
@@ -153,6 +164,13 @@ function gestureInputFromCli(positionals: string[], flags: CliFlags): Record<str
153164
distance: optionalCliNumber(args[3]),
154165
durationMs: optionalCliNumber(args[4]),
155166
};
167+
case 'swipe':
168+
return {
169+
...common,
170+
kind: subcommand,
171+
preset: args[0],
172+
durationMs: optionalCliNumber(args[1]),
173+
};
156174
case 'pinch':
157175
return {
158176
...common,
@@ -187,7 +205,7 @@ function gestureInputFromCli(positionals: string[], flags: CliFlags): Record<str
187205
default:
188206
throw new AppError(
189207
'INVALID_ARGS',
190-
'gesture requires pan, fling, pinch, rotate, or transform',
208+
'gesture requires pan, fling, swipe, pinch, rotate, or transform',
191209
);
192210
}
193211
}

src/commands/command-projection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const NON_BATCH_COMMAND_NAMES = [
3838
'batch',
3939
'gesture-pan',
4040
'gesture-fling',
41+
'gesture-swipe',
4142
'gesture-pinch',
4243
'gesture-rotate',
4344
'gesture-transform',

src/commands/interaction-command-contracts.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
LongPressOptions,
1313
RotateGestureOptions,
1414
ScrollOptions,
15+
SwipeGestureOptions,
1516
SwipeOptions,
1617
TransformGestureOptions,
1718
TypeTextOptions,
@@ -49,8 +50,9 @@ import {
4950
import { defineFieldCommand } from './field-command-contract.ts';
5051

5152
const CLICK_BUTTON_VALUES = ['primary', 'secondary', 'middle'] as const;
52-
const GESTURE_KIND_VALUES = ['pan', 'fling', 'pinch', 'rotate', 'transform'] as const;
53+
const GESTURE_KIND_VALUES = ['pan', 'fling', 'swipe', 'pinch', 'rotate', 'transform'] as const;
5354
const GESTURE_DIRECTION_VALUES = ['up', 'down', 'left', 'right'] as const;
55+
const GESTURE_SWIPE_PRESET_VALUES = ['left', 'right', 'left-edge', 'right-edge'] as const;
5456
const FIND_ACTION_VALUES = [
5557
'click',
5658
'focus',
@@ -149,6 +151,7 @@ const findFields = {
149151
const gestureFields = {
150152
kind: requiredField(enumField(GESTURE_KIND_VALUES, 'Gesture variant.')),
151153
direction: enumField(GESTURE_DIRECTION_VALUES, 'Fling direction.'),
154+
preset: enumField(GESTURE_SWIPE_PRESET_VALUES, 'Swipe preset.'),
152155
origin: pointField('Gesture origin point.'),
153156
delta: pointField('Movement delta for pan or transform gestures.'),
154157
distance: integerField('Fling distance.', { min: 0 }),
@@ -179,6 +182,12 @@ type FlingInput = CommonCommandInput & {
179182
durationMs?: number;
180183
};
181184

185+
type SwipeGestureInput = CommonCommandInput & {
186+
kind: 'swipe';
187+
preset: 'left' | 'right' | 'left-edge' | 'right-edge';
188+
durationMs?: number;
189+
};
190+
182191
type PinchInput = CommonCommandInput & {
183192
kind: 'pinch';
184193
scale: number;
@@ -201,7 +210,13 @@ type TransformInput = CommonCommandInput & {
201210
durationMs?: number;
202211
};
203212

204-
type GestureInput = PanInput | FlingInput | PinchInput | RotateInput | TransformInput;
213+
type GestureInput =
214+
| PanInput
215+
| FlingInput
216+
| SwipeGestureInput
217+
| PinchInput
218+
| RotateInput
219+
| TransformInput;
205220

206221
export const interactionCommandDefinitions = [
207222
defineCommand({
@@ -269,6 +284,8 @@ export const interactionCommandDefinitions = [
269284
return await client.interactions.pan(toPanOptions(input));
270285
case 'fling':
271286
return await client.interactions.fling(toFlingOptions(input));
287+
case 'swipe':
288+
return await client.interactions.swipeGesture(toSwipeGestureOptions(input));
272289
case 'pinch':
273290
return await client.interactions.pinch(toPinchOptions(input));
274291
case 'rotate':
@@ -303,6 +320,14 @@ function readGestureInput(input: unknown): GestureInput {
303320
durationMs: optionalInteger(record, 'durationMs', { min: 0 }),
304321
};
305322
}
323+
if (kind === 'swipe') {
324+
return {
325+
...common,
326+
kind,
327+
preset: requiredEnum(record, 'preset', GESTURE_SWIPE_PRESET_VALUES),
328+
durationMs: optionalInteger(record, 'durationMs', { min: 0 }),
329+
};
330+
}
306331
if (kind === 'pinch') {
307332
return {
308333
...common,
@@ -404,6 +429,14 @@ function toFlingOptions(input: FlingInput): FlingOptions {
404429
};
405430
}
406431

432+
function toSwipeGestureOptions(input: SwipeGestureInput): SwipeGestureOptions {
433+
return {
434+
...commonToClientOptions(input),
435+
preset: input.preset,
436+
durationMs: input.durationMs,
437+
};
438+
}
439+
407440
function toPinchOptions(input: PinchInput): PinchOptions {
408441
return {
409442
...commonToClientOptions(input),

0 commit comments

Comments
 (0)