agent-device 0.3.2 → 0.3.4
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/src/daemon.js +15 -15
- package/package.json +1 -1
- package/src/daemon/__tests__/selectors.test.ts +133 -0
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +140 -0
- package/src/daemon/handlers/interaction.ts +6 -3
- package/src/daemon/handlers/session.ts +71 -6
- package/src/daemon/selectors.ts +137 -20
- package/src/daemon/snapshot-processing.ts +5 -1
package/src/daemon/selectors.ts
CHANGED
|
@@ -85,25 +85,38 @@ export function resolveSelectorChain(
|
|
|
85
85
|
platform: 'ios' | 'android';
|
|
86
86
|
requireRect?: boolean;
|
|
87
87
|
requireUnique?: boolean;
|
|
88
|
+
disambiguateAmbiguous?: boolean;
|
|
88
89
|
},
|
|
89
90
|
): SelectorResolution | null {
|
|
90
91
|
const requireRect = options.requireRect ?? false;
|
|
91
92
|
const requireUnique = options.requireUnique ?? true;
|
|
93
|
+
const disambiguateAmbiguous = options.disambiguateAmbiguous ?? false;
|
|
92
94
|
const diagnostics: SelectorDiagnostics[] = [];
|
|
93
95
|
for (let i = 0; i < chain.selectors.length; i += 1) {
|
|
94
96
|
const selector = chain.selectors[i];
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const summary = analyzeSelectorMatches(nodes, selector, {
|
|
98
|
+
platform: options.platform,
|
|
99
|
+
requireRect,
|
|
98
100
|
});
|
|
99
|
-
diagnostics.push({ selector: selector.raw, matches:
|
|
100
|
-
if (
|
|
101
|
-
if (requireUnique &&
|
|
101
|
+
diagnostics.push({ selector: selector.raw, matches: summary.count });
|
|
102
|
+
if (summary.count === 0 || !summary.firstNode) continue;
|
|
103
|
+
if (requireUnique && summary.count !== 1) {
|
|
104
|
+
if (!disambiguateAmbiguous) continue;
|
|
105
|
+
const disambiguatedNode = summary.disambiguated;
|
|
106
|
+
if (!disambiguatedNode) continue;
|
|
107
|
+
return {
|
|
108
|
+
node: disambiguatedNode,
|
|
109
|
+
selector,
|
|
110
|
+
selectorIndex: i,
|
|
111
|
+
matches: summary.count,
|
|
112
|
+
diagnostics,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
102
115
|
return {
|
|
103
|
-
node:
|
|
116
|
+
node: summary.firstNode,
|
|
104
117
|
selector,
|
|
105
118
|
selectorIndex: i,
|
|
106
|
-
matches:
|
|
119
|
+
matches: summary.count,
|
|
107
120
|
diagnostics,
|
|
108
121
|
};
|
|
109
122
|
}
|
|
@@ -122,13 +135,13 @@ export function findSelectorChainMatch(
|
|
|
122
135
|
const diagnostics: SelectorDiagnostics[] = [];
|
|
123
136
|
for (let i = 0; i < chain.selectors.length; i += 1) {
|
|
124
137
|
const selector = chain.selectors[i];
|
|
125
|
-
const matches = nodes
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
const matches = countSelectorMatchesOnly(nodes, selector, {
|
|
139
|
+
platform: options.platform,
|
|
140
|
+
requireRect,
|
|
128
141
|
});
|
|
129
|
-
diagnostics.push({ selector: selector.raw, matches
|
|
130
|
-
if (matches
|
|
131
|
-
return { selectorIndex: i, selector, matches
|
|
142
|
+
diagnostics.push({ selector: selector.raw, matches });
|
|
143
|
+
if (matches > 0) {
|
|
144
|
+
return { selectorIndex: i, selector, matches, diagnostics };
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
return null;
|
|
@@ -162,21 +175,51 @@ export function isSelectorToken(token: string): boolean {
|
|
|
162
175
|
return ALL_KEYS.has(trimmed.toLowerCase() as SelectorKey);
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
export function splitSelectorFromArgs(
|
|
178
|
+
export function splitSelectorFromArgs(
|
|
179
|
+
args: string[],
|
|
180
|
+
options: { preferTrailingValue?: boolean } = {},
|
|
181
|
+
): { selectorExpression: string; rest: string[] } | null {
|
|
166
182
|
if (args.length === 0) return null;
|
|
183
|
+
const preferTrailingValue = options.preferTrailingValue ?? false;
|
|
167
184
|
let i = 0;
|
|
185
|
+
const boundaries: number[] = [];
|
|
168
186
|
while (i < args.length && isSelectorToken(args[i])) {
|
|
169
187
|
i += 1;
|
|
188
|
+
const candidate = args.slice(0, i).join(' ').trim();
|
|
189
|
+
if (!candidate) continue;
|
|
190
|
+
if (tryParseSelectorChain(candidate)) {
|
|
191
|
+
boundaries.push(i);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (boundaries.length === 0) return null;
|
|
195
|
+
let boundary = boundaries[boundaries.length - 1];
|
|
196
|
+
if (preferTrailingValue) {
|
|
197
|
+
for (let j = boundaries.length - 1; j >= 0; j -= 1) {
|
|
198
|
+
if (boundaries[j] < args.length) {
|
|
199
|
+
boundary = boundaries[j];
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
170
203
|
}
|
|
171
|
-
|
|
172
|
-
const selectorExpression = args.slice(0, i).join(' ').trim();
|
|
204
|
+
const selectorExpression = args.slice(0, boundary).join(' ').trim();
|
|
173
205
|
if (!selectorExpression) return null;
|
|
174
206
|
return {
|
|
175
207
|
selectorExpression,
|
|
176
|
-
rest: args.slice(
|
|
208
|
+
rest: args.slice(boundary),
|
|
177
209
|
};
|
|
178
210
|
}
|
|
179
211
|
|
|
212
|
+
export function splitIsSelectorArgs(positionals: string[]): {
|
|
213
|
+
predicate: string;
|
|
214
|
+
split: { selectorExpression: string; rest: string[] } | null;
|
|
215
|
+
} {
|
|
216
|
+
const predicate = positionals[0] ?? '';
|
|
217
|
+
const split = splitSelectorFromArgs(positionals.slice(1), {
|
|
218
|
+
preferTrailingValue: predicate === 'text',
|
|
219
|
+
});
|
|
220
|
+
return { predicate, split };
|
|
221
|
+
}
|
|
222
|
+
|
|
180
223
|
export function isNodeVisible(node: SnapshotNode): boolean {
|
|
181
224
|
if (node.hittable === true) return true;
|
|
182
225
|
if (!node.rect) return false;
|
|
@@ -318,7 +361,7 @@ function splitByFallback(expression: string): string[] {
|
|
|
318
361
|
let quote: '"' | "'" | null = null;
|
|
319
362
|
for (let i = 0; i < expression.length; i += 1) {
|
|
320
363
|
const ch = expression[i];
|
|
321
|
-
if ((ch === '"' || ch === "'") && expression
|
|
364
|
+
if ((ch === '"' || ch === "'") && !isEscapedQuote(expression, i)) {
|
|
322
365
|
if (!quote) {
|
|
323
366
|
quote = ch;
|
|
324
367
|
} else if (quote === ch) {
|
|
@@ -353,7 +396,7 @@ function tokenize(segment: string): string[] {
|
|
|
353
396
|
let quote: '"' | "'" | null = null;
|
|
354
397
|
for (let i = 0; i < segment.length; i += 1) {
|
|
355
398
|
const ch = segment[i];
|
|
356
|
-
if ((ch === '"' || ch === "'") && segment
|
|
399
|
+
if ((ch === '"' || ch === "'") && !isEscapedQuote(segment, i)) {
|
|
357
400
|
if (!quote) {
|
|
358
401
|
quote = ch;
|
|
359
402
|
} else if (quote === ch) {
|
|
@@ -421,3 +464,77 @@ function normalizeSelectorText(value: string | undefined): string | null {
|
|
|
421
464
|
if (!trimmed) return null;
|
|
422
465
|
return trimmed;
|
|
423
466
|
}
|
|
467
|
+
|
|
468
|
+
function analyzeSelectorMatches(
|
|
469
|
+
nodes: SnapshotState['nodes'],
|
|
470
|
+
selector: Selector,
|
|
471
|
+
options: { platform: 'ios' | 'android'; requireRect: boolean },
|
|
472
|
+
): { count: number; firstNode: SnapshotNode | null; disambiguated: SnapshotNode | null } {
|
|
473
|
+
let count = 0;
|
|
474
|
+
let firstNode: SnapshotNode | null = null;
|
|
475
|
+
let best: SnapshotNode | null = null;
|
|
476
|
+
let tie = false;
|
|
477
|
+
for (const node of nodes) {
|
|
478
|
+
if (options.requireRect && !node.rect) continue;
|
|
479
|
+
if (!matchesSelector(node, selector, options.platform)) continue;
|
|
480
|
+
count += 1;
|
|
481
|
+
if (!firstNode) {
|
|
482
|
+
firstNode = node;
|
|
483
|
+
}
|
|
484
|
+
if (!best) {
|
|
485
|
+
best = node;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const comparison = compareDisambiguationCandidates(node, best);
|
|
489
|
+
if (comparison > 0) {
|
|
490
|
+
best = node;
|
|
491
|
+
tie = false;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (comparison === 0) {
|
|
495
|
+
tie = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
count,
|
|
500
|
+
firstNode,
|
|
501
|
+
disambiguated: tie ? null : best,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function countSelectorMatchesOnly(
|
|
506
|
+
nodes: SnapshotState['nodes'],
|
|
507
|
+
selector: Selector,
|
|
508
|
+
options: { platform: 'ios' | 'android'; requireRect: boolean },
|
|
509
|
+
): number {
|
|
510
|
+
let count = 0;
|
|
511
|
+
for (const node of nodes) {
|
|
512
|
+
if (options.requireRect && !node.rect) continue;
|
|
513
|
+
if (!matchesSelector(node, selector, options.platform)) continue;
|
|
514
|
+
count += 1;
|
|
515
|
+
}
|
|
516
|
+
return count;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function compareDisambiguationCandidates(a: SnapshotNode, b: SnapshotNode): number {
|
|
520
|
+
const depthA = a.depth ?? 0;
|
|
521
|
+
const depthB = b.depth ?? 0;
|
|
522
|
+
if (depthA !== depthB) return depthA > depthB ? 1 : -1;
|
|
523
|
+
const areaA = areaOfNode(a);
|
|
524
|
+
const areaB = areaOfNode(b);
|
|
525
|
+
if (areaA !== areaB) return areaA < areaB ? 1 : -1;
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function areaOfNode(node: SnapshotNode): number {
|
|
530
|
+
if (!node.rect) return Number.POSITIVE_INFINITY;
|
|
531
|
+
return node.rect.width * node.rect.height;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isEscapedQuote(source: string, index: number): boolean {
|
|
535
|
+
let backslashCount = 0;
|
|
536
|
+
for (let i = index - 1; i >= 0 && source[i] === '\\'; i -= 1) {
|
|
537
|
+
backslashCount += 1;
|
|
538
|
+
}
|
|
539
|
+
return backslashCount % 2 === 1;
|
|
540
|
+
}
|
|
@@ -78,10 +78,14 @@ export function pruneGroupNodes(nodes: RawSnapshotNode[]): RawSnapshotNode[] {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function normalizeType(type: string): string {
|
|
81
|
-
let value = type.replace(/XCUIElementType/gi, '').toLowerCase();
|
|
81
|
+
let value = type.trim().replace(/XCUIElementType/gi, '').toLowerCase();
|
|
82
82
|
if (value.startsWith('ax')) {
|
|
83
83
|
value = value.replace(/^ax/, '');
|
|
84
84
|
}
|
|
85
|
+
const lastSeparator = Math.max(value.lastIndexOf('.'), value.lastIndexOf('/'));
|
|
86
|
+
if (lastSeparator !== -1) {
|
|
87
|
+
value = value.slice(lastSeparator + 1);
|
|
88
|
+
}
|
|
85
89
|
return value;
|
|
86
90
|
}
|
|
87
91
|
|