Skip to content

Commit 5ca1e2b

Browse files
committed
fix: improve maestro text tap targets
1 parent b2a39d2 commit 5ca1e2b

4 files changed

Lines changed: 150 additions & 47 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect, test } from 'vitest';
2+
import { pointForMaestroTapOnTarget } from '../runtime-geometry.ts';
3+
4+
test('pointForMaestroTapOnTarget biases broad iOS scroll-area text matches toward the visible label', () => {
5+
const point = pointForMaestroTapOnTarget(
6+
{
7+
node: {
8+
index: 5,
9+
ref: 'e5',
10+
type: 'scroll-area',
11+
label: 'Chat',
12+
rect: { x: 0, y: 117, width: 402, height: 48 },
13+
},
14+
rect: { x: 0, y: 117, width: 402, height: 48 },
15+
frame: { referenceWidth: 402, referenceHeight: 874 },
16+
},
17+
true,
18+
);
19+
20+
expect(point).toEqual({ x: 84, y: 141 });
21+
});
22+
23+
test('pointForMaestroTapOnTarget biases broad iOS other text matches at tab bar height', () => {
24+
const point = pointForMaestroTapOnTarget(
25+
{
26+
node: {
27+
index: 5,
28+
ref: 'e5',
29+
type: 'Other',
30+
label: 'Chat',
31+
rect: { x: 0, y: 117, width: 402, height: 48 },
32+
},
33+
rect: { x: 0, y: 117, width: 402, height: 48 },
34+
frame: { referenceWidth: 402, referenceHeight: 874 },
35+
},
36+
true,
37+
);
38+
39+
expect(point).toEqual({ x: 84, y: 141 });
40+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { expect, test } from 'vitest';
2+
import type { DaemonRequest, DaemonResponse } from '../../../daemon/types.ts';
3+
import type { ReplayVarScope } from '../../../replay/vars.ts';
4+
import type { SnapshotState } from '../../../utils/snapshot.ts';
5+
import { invokeMaestroTapOn } from '../runtime-interactions.ts';
6+
import { rememberMaestroSnapshot } from '../runtime-support.ts';
7+
8+
test('invokeMaestroTapOn resolves mutating taps from a fresh snapshot instead of stale assertion cache', async () => {
9+
const scope: ReplayVarScope = { values: {} };
10+
const selector =
11+
'label="Article by Gandalf" || text="Article by Gandalf" || id="Article by Gandalf"';
12+
rememberMaestroSnapshot(scope, staleLaunchSnapshot(), selector);
13+
14+
const clicks: string[][] = [];
15+
const response = await invokeMaestroTapOn({
16+
baseReq: {
17+
token: 'test',
18+
session: 'nav',
19+
flags: { platform: 'ios' },
20+
},
21+
positionals: [selector],
22+
scope,
23+
invoke: async (req: DaemonRequest): Promise<DaemonResponse> => {
24+
if (req.command === 'snapshot') return { ok: true, data: currentBreadcrumbSnapshot() };
25+
if (req.command === 'click') {
26+
clicks.push(req.positionals ?? []);
27+
return { ok: true, data: {} };
28+
}
29+
return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } };
30+
},
31+
});
32+
33+
expect(response.ok).toBe(true);
34+
expect(clicks).toEqual([['86', '89']]);
35+
});
36+
37+
function staleLaunchSnapshot(): SnapshotState {
38+
return {
39+
createdAt: Date.now(),
40+
nodes: [
41+
appNode(),
42+
windowNode(),
43+
{
44+
index: 2,
45+
ref: 'e3',
46+
type: 'Other',
47+
label: 'Article by Gandalf',
48+
depth: 2,
49+
rect: { x: 0, y: 0, width: 402, height: 48 },
50+
},
51+
],
52+
};
53+
}
54+
55+
function currentBreadcrumbSnapshot(): SnapshotState {
56+
return {
57+
createdAt: Date.now(),
58+
nodes: [
59+
appNode(),
60+
windowNode(),
61+
{
62+
index: 2,
63+
ref: 'e3',
64+
type: 'ScrollView',
65+
label: 'Article by Gandalf',
66+
depth: 4,
67+
parentIndex: 1,
68+
rect: { x: 0, y: 58.33333333333333, width: 402, height: 58.33333333333333 },
69+
},
70+
{
71+
index: 3,
72+
ref: 'e4',
73+
type: 'Cell',
74+
label: 'Article by Gandalf',
75+
depth: 5,
76+
parentIndex: 2,
77+
rect: { x: 8, y: 65.33333587646484, width: 155, height: 48 },
78+
},
79+
],
80+
};
81+
}
82+
83+
function appNode(): SnapshotState['nodes'][number] {
84+
return {
85+
index: 0,
86+
ref: 'e1',
87+
type: 'Application',
88+
label: 'React Navigation Example',
89+
depth: 0,
90+
rect: { x: 0, y: 0, width: 402, height: 874 },
91+
};
92+
}
93+
94+
function windowNode(): SnapshotState['nodes'][number] {
95+
return {
96+
index: 1,
97+
ref: 'e2',
98+
type: 'Window',
99+
depth: 1,
100+
parentIndex: 0,
101+
rect: { x: 0, y: 0, width: 402, height: 874 },
102+
};
103+
}

src/compat/maestro/runtime-geometry.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Rect, SnapshotNode } from '../../utils/snapshot.ts';
2+
import { normalizeType } from '../../utils/snapshot-processing.ts';
23
import type { MaestroSnapshotTarget } from './runtime-targets.ts';
34

45
const MAESTRO_GEOMETRY_POLICY = {
@@ -10,7 +11,7 @@ const MAESTRO_GEOMETRY_POLICY = {
1011
},
1112
largeTextContainerBias: {
1213
minWidth: 120,
13-
minHeight: 70,
14+
minHeight: 40,
1415
width: 168,
1516
height: 48,
1617
},
@@ -108,14 +109,13 @@ function shouldBiasMaestroVisibleTextTap(
108109
rect: Rect,
109110
): boolean {
110111
if (!isVisibleTextSelector) return false;
111-
if (
112-
rect.height < MAESTRO_GEOMETRY_POLICY.largeTextContainerBias.minHeight ||
113-
rect.width < MAESTRO_GEOMETRY_POLICY.largeTextContainerBias.minWidth
114-
) {
112+
if (rect.width < MAESTRO_GEOMETRY_POLICY.largeTextContainerBias.minWidth) {
115113
return false;
116114
}
117-
const type = node.type?.toLowerCase();
118-
return type === 'cell' || type === 'other' || type === 'scrollview';
115+
const type = normalizeType(node.type ?? '');
116+
const scrollableTextContainer = type === 'scrollview' || type === 'scroll-area';
117+
if (rect.height < MAESTRO_GEOMETRY_POLICY.largeTextContainerBias.minHeight) return false;
118+
return type === 'cell' || type === 'other' || scrollableTextContainer;
119119
}
120120

121121
function interiorCoordinate(origin: number, size: number): number {

src/compat/maestro/runtime-interactions.ts

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { sleep } from '../../utils/timeouts.ts';
55
import { pointForMaestroTapOnTarget, swipeCoordinatesFromTarget } from './runtime-geometry.ts';
66
import {
77
captureMaestroRawSnapshot,
8-
consumeMaestroSnapshot,
98
errorResponse,
109
readCachedMaestroReferenceFrame,
1110
readSnapshotState,
@@ -471,14 +470,6 @@ async function resolveMaestroSnapshotTarget(
471470
commandLabel: string,
472471
resolutionOptions: { promoteTapTarget: boolean },
473472
): Promise<{ ok: true; target: MaestroSnapshotTarget } | { ok: false; response: DaemonResponse }> {
474-
const cachedTarget = resolveCachedMaestroSnapshotTarget(
475-
params,
476-
selector,
477-
options,
478-
resolutionOptions,
479-
);
480-
if (cachedTarget.ok) return cachedTarget;
481-
482473
const snapshotResponse = await captureMaestroRawSnapshot(params);
483474
if (!snapshotResponse.ok) return { ok: false, response: snapshotResponse };
484475

@@ -543,37 +534,6 @@ async function resolveMaestroSnapshotTarget(
543534
};
544535
}
545536

546-
function resolveCachedMaestroSnapshotTarget(
547-
params: {
548-
baseReq: ReplayBaseRequest;
549-
scope?: ReplayVarScope;
550-
},
551-
selector: string,
552-
options: MaestroTapOnOptions,
553-
resolutionOptions: { promoteTapTarget: boolean },
554-
): { ok: true; target: MaestroSnapshotTarget } | { ok: false } {
555-
const cached = consumeMaestroSnapshot(params.scope, selector);
556-
if (!cached) return { ok: false };
557-
const resolution = resolveMaestroNodeFromSnapshot(
558-
cached.snapshot,
559-
selector,
560-
options,
561-
readMaestroSelectorPlatform(params.baseReq.flags),
562-
cached.frame,
563-
resolutionOptions,
564-
);
565-
return resolution.ok
566-
? {
567-
ok: true,
568-
target: {
569-
node: resolution.node,
570-
rect: resolution.rect,
571-
frame: cached.frame,
572-
},
573-
}
574-
: { ok: false };
575-
}
576-
577537
function readMaestroTapOnOptions(
578538
rawOptions: string | undefined,
579539
): { ok: true; value: MaestroTapOnOptions | null } | { ok: false; response: DaemonResponse } {

0 commit comments

Comments
 (0)