agent-device 0.4.2 → 0.5.0
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/README.md +2 -9
- package/dist/src/797.js +1 -1
- package/dist/src/bin.js +5 -5
- package/dist/src/daemon.js +16 -20
- package/package.json +7 -9
- package/skills/agent-device/SKILL.md +3 -6
- package/skills/agent-device/references/permissions.md +3 -15
- package/skills/agent-device/references/snapshot-refs.md +1 -4
- package/dist/bin/axsnapshot +0 -0
- package/ios-runner/AXSnapshot/Package.swift +0 -18
- package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
- package/src/__tests__/cli-close.test.ts +0 -155
- package/src/__tests__/cli-help.test.ts +0 -102
- package/src/bin.ts +0 -3
- package/src/cli.ts +0 -305
- package/src/core/__tests__/capabilities.test.ts +0 -75
- package/src/core/__tests__/dispatch-open.test.ts +0 -25
- package/src/core/__tests__/open-target.test.ts +0 -55
- package/src/core/capabilities.ts +0 -57
- package/src/core/dispatch.ts +0 -382
- package/src/core/open-target.ts +0 -27
- package/src/daemon/__tests__/device-ready.test.ts +0 -52
- package/src/daemon/__tests__/is-predicates.test.ts +0 -68
- package/src/daemon/__tests__/selectors.test.ts +0 -261
- package/src/daemon/__tests__/session-routing.test.ts +0 -108
- package/src/daemon/__tests__/session-selector.test.ts +0 -64
- package/src/daemon/__tests__/session-store.test.ts +0 -142
- package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
- package/src/daemon/action-utils.ts +0 -29
- package/src/daemon/context.ts +0 -48
- package/src/daemon/device-ready.ts +0 -155
- package/src/daemon/handlers/__tests__/find.test.ts +0 -99
- package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
- package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
- package/src/daemon/handlers/__tests__/session.test.ts +0 -820
- package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
- package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
- package/src/daemon/handlers/find.ts +0 -324
- package/src/daemon/handlers/interaction.ts +0 -550
- package/src/daemon/handlers/parse-utils.ts +0 -8
- package/src/daemon/handlers/record-trace.ts +0 -154
- package/src/daemon/handlers/session.ts +0 -1137
- package/src/daemon/handlers/snapshot.ts +0 -439
- package/src/daemon/is-predicates.ts +0 -46
- package/src/daemon/selectors.ts +0 -540
- package/src/daemon/session-routing.ts +0 -22
- package/src/daemon/session-selector.ts +0 -39
- package/src/daemon/session-store.ts +0 -296
- package/src/daemon/snapshot-processing.ts +0 -131
- package/src/daemon/types.ts +0 -56
- package/src/daemon-client.ts +0 -272
- package/src/daemon.ts +0 -295
- package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
- package/src/platforms/android/__tests__/index.test.ts +0 -274
- package/src/platforms/android/devices.ts +0 -196
- package/src/platforms/android/index.ts +0 -784
- package/src/platforms/android/ui-hierarchy.ts +0 -312
- package/src/platforms/boot-diagnostics.ts +0 -128
- package/src/platforms/ios/__tests__/index.test.ts +0 -312
- package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
- package/src/platforms/ios/apps.ts +0 -358
- package/src/platforms/ios/ax-snapshot.ts +0 -207
- package/src/platforms/ios/config.ts +0 -28
- package/src/platforms/ios/devicectl.ts +0 -134
- package/src/platforms/ios/devices.ts +0 -100
- package/src/platforms/ios/index.ts +0 -20
- package/src/platforms/ios/runner-client.ts +0 -994
- package/src/platforms/ios/simulator.ts +0 -164
- package/src/utils/__tests__/args.test.ts +0 -239
- package/src/utils/__tests__/daemon-client.test.ts +0 -95
- package/src/utils/__tests__/exec.test.ts +0 -16
- package/src/utils/__tests__/finders.test.ts +0 -34
- package/src/utils/__tests__/keyed-lock.test.ts +0 -55
- package/src/utils/__tests__/process-identity.test.ts +0 -33
- package/src/utils/__tests__/retry.test.ts +0 -44
- package/src/utils/args.ts +0 -239
- package/src/utils/command-schema.ts +0 -622
- package/src/utils/device.ts +0 -84
- package/src/utils/errors.ts +0 -35
- package/src/utils/exec.ts +0 -339
- package/src/utils/finders.ts +0 -101
- package/src/utils/interactive.ts +0 -4
- package/src/utils/interactors.ts +0 -173
- package/src/utils/keyed-lock.ts +0 -14
- package/src/utils/output.ts +0 -204
- package/src/utils/process-identity.ts +0 -100
- package/src/utils/retry.ts +0 -180
- package/src/utils/snapshot.ts +0 -64
- package/src/utils/timeouts.ts +0 -9
- package/src/utils/version.ts +0 -26
package/src/daemon/selectors.ts
DELETED
|
@@ -1,540 +0,0 @@
|
|
|
1
|
-
import { AppError } from '../utils/errors.ts';
|
|
2
|
-
import type { SnapshotNode, SnapshotState } from '../utils/snapshot.ts';
|
|
3
|
-
import { extractNodeText, isFillableType, normalizeType } from './snapshot-processing.ts';
|
|
4
|
-
import { uniqueStrings } from './action-utils.ts';
|
|
5
|
-
|
|
6
|
-
type SelectorKey =
|
|
7
|
-
| 'id'
|
|
8
|
-
| 'role'
|
|
9
|
-
| 'text'
|
|
10
|
-
| 'label'
|
|
11
|
-
| 'value'
|
|
12
|
-
| 'visible'
|
|
13
|
-
| 'hidden'
|
|
14
|
-
| 'editable'
|
|
15
|
-
| 'selected'
|
|
16
|
-
| 'enabled'
|
|
17
|
-
| 'hittable';
|
|
18
|
-
|
|
19
|
-
type SelectorTerm = {
|
|
20
|
-
key: SelectorKey;
|
|
21
|
-
value: string | boolean;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type Selector = {
|
|
25
|
-
raw: string;
|
|
26
|
-
terms: SelectorTerm[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type SelectorChain = {
|
|
30
|
-
raw: string;
|
|
31
|
-
selectors: Selector[];
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type SelectorDiagnostics = {
|
|
35
|
-
selector: string;
|
|
36
|
-
matches: number;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type SelectorResolution = {
|
|
40
|
-
node: SnapshotNode;
|
|
41
|
-
selector: Selector;
|
|
42
|
-
selectorIndex: number;
|
|
43
|
-
matches: number;
|
|
44
|
-
diagnostics: SelectorDiagnostics[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const TEXT_KEYS = new Set<SelectorKey>(['id', 'role', 'text', 'label', 'value']);
|
|
48
|
-
const BOOLEAN_KEYS = new Set<SelectorKey>([
|
|
49
|
-
'visible',
|
|
50
|
-
'hidden',
|
|
51
|
-
'editable',
|
|
52
|
-
'selected',
|
|
53
|
-
'enabled',
|
|
54
|
-
'hittable',
|
|
55
|
-
]);
|
|
56
|
-
const ALL_KEYS = new Set<SelectorKey>([...TEXT_KEYS, ...BOOLEAN_KEYS]);
|
|
57
|
-
|
|
58
|
-
export function parseSelectorChain(expression: string): SelectorChain {
|
|
59
|
-
const raw = expression.trim();
|
|
60
|
-
if (!raw) {
|
|
61
|
-
throw new AppError('INVALID_ARGS', 'Selector expression cannot be empty');
|
|
62
|
-
}
|
|
63
|
-
const segments = splitByFallback(raw);
|
|
64
|
-
if (segments.length === 0) {
|
|
65
|
-
throw new AppError('INVALID_ARGS', 'Selector expression cannot be empty');
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
raw,
|
|
69
|
-
selectors: segments.map((segment) => parseSelector(segment)),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function tryParseSelectorChain(expression: string): SelectorChain | null {
|
|
74
|
-
try {
|
|
75
|
-
return parseSelectorChain(expression);
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function resolveSelectorChain(
|
|
82
|
-
nodes: SnapshotState['nodes'],
|
|
83
|
-
chain: SelectorChain,
|
|
84
|
-
options: {
|
|
85
|
-
platform: 'ios' | 'android';
|
|
86
|
-
requireRect?: boolean;
|
|
87
|
-
requireUnique?: boolean;
|
|
88
|
-
disambiguateAmbiguous?: boolean;
|
|
89
|
-
},
|
|
90
|
-
): SelectorResolution | null {
|
|
91
|
-
const requireRect = options.requireRect ?? false;
|
|
92
|
-
const requireUnique = options.requireUnique ?? true;
|
|
93
|
-
const disambiguateAmbiguous = options.disambiguateAmbiguous ?? false;
|
|
94
|
-
const diagnostics: SelectorDiagnostics[] = [];
|
|
95
|
-
for (let i = 0; i < chain.selectors.length; i += 1) {
|
|
96
|
-
const selector = chain.selectors[i];
|
|
97
|
-
const summary = analyzeSelectorMatches(nodes, selector, {
|
|
98
|
-
platform: options.platform,
|
|
99
|
-
requireRect,
|
|
100
|
-
});
|
|
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
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
node: summary.firstNode,
|
|
117
|
-
selector,
|
|
118
|
-
selectorIndex: i,
|
|
119
|
-
matches: summary.count,
|
|
120
|
-
diagnostics,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function findSelectorChainMatch(
|
|
127
|
-
nodes: SnapshotState['nodes'],
|
|
128
|
-
chain: SelectorChain,
|
|
129
|
-
options: {
|
|
130
|
-
platform: 'ios' | 'android';
|
|
131
|
-
requireRect?: boolean;
|
|
132
|
-
},
|
|
133
|
-
): { selectorIndex: number; selector: Selector; matches: number; diagnostics: SelectorDiagnostics[] } | null {
|
|
134
|
-
const requireRect = options.requireRect ?? false;
|
|
135
|
-
const diagnostics: SelectorDiagnostics[] = [];
|
|
136
|
-
for (let i = 0; i < chain.selectors.length; i += 1) {
|
|
137
|
-
const selector = chain.selectors[i];
|
|
138
|
-
const matches = countSelectorMatchesOnly(nodes, selector, {
|
|
139
|
-
platform: options.platform,
|
|
140
|
-
requireRect,
|
|
141
|
-
});
|
|
142
|
-
diagnostics.push({ selector: selector.raw, matches });
|
|
143
|
-
if (matches > 0) {
|
|
144
|
-
return { selectorIndex: i, selector, matches, diagnostics };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function formatSelectorFailure(
|
|
151
|
-
chain: SelectorChain,
|
|
152
|
-
diagnostics: SelectorDiagnostics[],
|
|
153
|
-
options: { unique?: boolean },
|
|
154
|
-
): string {
|
|
155
|
-
const unique = options.unique ?? true;
|
|
156
|
-
if (diagnostics.length === 0) {
|
|
157
|
-
return `Selector did not match: ${chain.raw}`;
|
|
158
|
-
}
|
|
159
|
-
const summary = diagnostics.map((entry) => `${entry.selector} -> ${entry.matches}`).join(', ');
|
|
160
|
-
if (unique) {
|
|
161
|
-
return `Selector did not resolve uniquely (${summary})`;
|
|
162
|
-
}
|
|
163
|
-
return `Selector did not match (${summary})`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function isSelectorToken(token: string): boolean {
|
|
167
|
-
const trimmed = token.trim();
|
|
168
|
-
if (!trimmed) return false;
|
|
169
|
-
if (trimmed === '||') return true;
|
|
170
|
-
const equalsIdx = trimmed.indexOf('=');
|
|
171
|
-
if (equalsIdx !== -1) {
|
|
172
|
-
const key = trimmed.slice(0, equalsIdx).trim().toLowerCase() as SelectorKey;
|
|
173
|
-
return ALL_KEYS.has(key);
|
|
174
|
-
}
|
|
175
|
-
return ALL_KEYS.has(trimmed.toLowerCase() as SelectorKey);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function splitSelectorFromArgs(
|
|
179
|
-
args: string[],
|
|
180
|
-
options: { preferTrailingValue?: boolean } = {},
|
|
181
|
-
): { selectorExpression: string; rest: string[] } | null {
|
|
182
|
-
if (args.length === 0) return null;
|
|
183
|
-
const preferTrailingValue = options.preferTrailingValue ?? false;
|
|
184
|
-
let i = 0;
|
|
185
|
-
const boundaries: number[] = [];
|
|
186
|
-
while (i < args.length && isSelectorToken(args[i])) {
|
|
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
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const selectorExpression = args.slice(0, boundary).join(' ').trim();
|
|
205
|
-
if (!selectorExpression) return null;
|
|
206
|
-
return {
|
|
207
|
-
selectorExpression,
|
|
208
|
-
rest: args.slice(boundary),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
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
|
-
|
|
223
|
-
export function isNodeVisible(node: SnapshotNode): boolean {
|
|
224
|
-
if (node.hittable === true) return true;
|
|
225
|
-
if (!node.rect) return false;
|
|
226
|
-
return node.rect.width > 0 && node.rect.height > 0;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function isNodeEditable(node: SnapshotNode, platform: 'ios' | 'android'): boolean {
|
|
230
|
-
const type = node.type ?? '';
|
|
231
|
-
return isFillableType(type, platform) && node.enabled !== false;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function buildSelectorChainForNode(
|
|
235
|
-
node: SnapshotNode,
|
|
236
|
-
platform: 'ios' | 'android',
|
|
237
|
-
options: { action?: 'click' | 'fill' | 'get' } = {},
|
|
238
|
-
): string[] {
|
|
239
|
-
const chain: string[] = [];
|
|
240
|
-
const role = normalizeType(node.type ?? '');
|
|
241
|
-
const id = normalizeSelectorText(node.identifier);
|
|
242
|
-
const label = normalizeSelectorText(node.label);
|
|
243
|
-
const value = normalizeSelectorText(node.value);
|
|
244
|
-
const text = normalizeSelectorText(extractNodeText(node));
|
|
245
|
-
const requireEditable = options.action === 'fill';
|
|
246
|
-
|
|
247
|
-
if (id) {
|
|
248
|
-
chain.push(`id=${quoteSelectorValue(id)}`);
|
|
249
|
-
}
|
|
250
|
-
if (role && label) {
|
|
251
|
-
chain.push(
|
|
252
|
-
requireEditable
|
|
253
|
-
? `role=${quoteSelectorValue(role)} label=${quoteSelectorValue(label)} editable=true`
|
|
254
|
-
: `role=${quoteSelectorValue(role)} label=${quoteSelectorValue(label)}`,
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
if (label) {
|
|
258
|
-
chain.push(requireEditable ? `label=${quoteSelectorValue(label)} editable=true` : `label=${quoteSelectorValue(label)}`);
|
|
259
|
-
}
|
|
260
|
-
if (value) {
|
|
261
|
-
chain.push(requireEditable ? `value=${quoteSelectorValue(value)} editable=true` : `value=${quoteSelectorValue(value)}`);
|
|
262
|
-
}
|
|
263
|
-
if (text && text !== label && text !== value) {
|
|
264
|
-
chain.push(requireEditable ? `text=${quoteSelectorValue(text)} editable=true` : `text=${quoteSelectorValue(text)}`);
|
|
265
|
-
}
|
|
266
|
-
if (role && requireEditable && !chain.some((entry) => entry.includes('editable=true'))) {
|
|
267
|
-
chain.push(`role=${quoteSelectorValue(role)} editable=true`);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const deduped = uniqueStrings(chain);
|
|
271
|
-
if (deduped.length === 0 && role) {
|
|
272
|
-
deduped.push(requireEditable ? `role=${quoteSelectorValue(role)} editable=true` : `role=${quoteSelectorValue(role)}`);
|
|
273
|
-
}
|
|
274
|
-
if (deduped.length === 0) {
|
|
275
|
-
const visible = isNodeVisible(node);
|
|
276
|
-
if (visible) deduped.push('visible=true');
|
|
277
|
-
}
|
|
278
|
-
return deduped;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function parseSelector(segment: string): Selector {
|
|
282
|
-
const raw = segment.trim();
|
|
283
|
-
if (!raw) throw new AppError('INVALID_ARGS', 'Selector segment cannot be empty');
|
|
284
|
-
const tokens = tokenize(raw);
|
|
285
|
-
if (tokens.length === 0) {
|
|
286
|
-
throw new AppError('INVALID_ARGS', `Invalid selector segment: ${segment}`);
|
|
287
|
-
}
|
|
288
|
-
const terms = tokens.map(parseTerm);
|
|
289
|
-
return { raw, terms };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function parseTerm(token: string): SelectorTerm {
|
|
293
|
-
const normalized = token.trim();
|
|
294
|
-
if (!normalized) {
|
|
295
|
-
throw new AppError('INVALID_ARGS', 'Empty selector term');
|
|
296
|
-
}
|
|
297
|
-
const equalsIdx = normalized.indexOf('=');
|
|
298
|
-
if (equalsIdx === -1) {
|
|
299
|
-
const key = normalized.toLowerCase() as SelectorKey;
|
|
300
|
-
if (!BOOLEAN_KEYS.has(key)) {
|
|
301
|
-
throw new AppError('INVALID_ARGS', `Invalid selector term "${token}", expected key=value`);
|
|
302
|
-
}
|
|
303
|
-
return { key, value: true };
|
|
304
|
-
}
|
|
305
|
-
const keyRaw = normalized.slice(0, equalsIdx).trim().toLowerCase() as SelectorKey;
|
|
306
|
-
const valueRaw = normalized.slice(equalsIdx + 1).trim();
|
|
307
|
-
if (!ALL_KEYS.has(keyRaw)) {
|
|
308
|
-
throw new AppError('INVALID_ARGS', `Unknown selector key: ${keyRaw}`);
|
|
309
|
-
}
|
|
310
|
-
if (!valueRaw) {
|
|
311
|
-
throw new AppError('INVALID_ARGS', `Missing selector value for key: ${keyRaw}`);
|
|
312
|
-
}
|
|
313
|
-
if (BOOLEAN_KEYS.has(keyRaw)) {
|
|
314
|
-
const parsedBoolean = parseBoolean(valueRaw);
|
|
315
|
-
if (parsedBoolean === null) {
|
|
316
|
-
throw new AppError('INVALID_ARGS', `Invalid boolean value for ${keyRaw}: ${valueRaw}`);
|
|
317
|
-
}
|
|
318
|
-
return { key: keyRaw, value: parsedBoolean };
|
|
319
|
-
}
|
|
320
|
-
return { key: keyRaw, value: unquote(valueRaw) };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function matchesSelector(node: SnapshotNode, selector: Selector, platform: 'ios' | 'android'): boolean {
|
|
324
|
-
return selector.terms.every((term) => matchesTerm(node, term, platform));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function matchesTerm(node: SnapshotNode, term: SelectorTerm, platform: 'ios' | 'android'): boolean {
|
|
328
|
-
switch (term.key) {
|
|
329
|
-
case 'id':
|
|
330
|
-
return textEquals(node.identifier, String(term.value));
|
|
331
|
-
case 'role':
|
|
332
|
-
return roleEquals(node.type, String(term.value));
|
|
333
|
-
case 'label':
|
|
334
|
-
return textEquals(node.label, String(term.value));
|
|
335
|
-
case 'value':
|
|
336
|
-
return textEquals(node.value, String(term.value));
|
|
337
|
-
case 'text': {
|
|
338
|
-
const query = normalizeText(String(term.value));
|
|
339
|
-
return normalizeText(extractNodeText(node)) === query;
|
|
340
|
-
}
|
|
341
|
-
case 'visible':
|
|
342
|
-
return isNodeVisible(node) === Boolean(term.value);
|
|
343
|
-
case 'hidden':
|
|
344
|
-
return (!isNodeVisible(node)) === Boolean(term.value);
|
|
345
|
-
case 'editable':
|
|
346
|
-
return isNodeEditable(node, platform) === Boolean(term.value);
|
|
347
|
-
case 'selected':
|
|
348
|
-
return Boolean(node.selected === true) === Boolean(term.value);
|
|
349
|
-
case 'enabled':
|
|
350
|
-
return Boolean(node.enabled !== false) === Boolean(term.value);
|
|
351
|
-
case 'hittable':
|
|
352
|
-
return Boolean(node.hittable === true) === Boolean(term.value);
|
|
353
|
-
default:
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function splitByFallback(expression: string): string[] {
|
|
359
|
-
const segments: string[] = [];
|
|
360
|
-
let current = '';
|
|
361
|
-
let quote: '"' | "'" | null = null;
|
|
362
|
-
for (let i = 0; i < expression.length; i += 1) {
|
|
363
|
-
const ch = expression[i];
|
|
364
|
-
if ((ch === '"' || ch === "'") && !isEscapedQuote(expression, i)) {
|
|
365
|
-
if (!quote) {
|
|
366
|
-
quote = ch;
|
|
367
|
-
} else if (quote === ch) {
|
|
368
|
-
quote = null;
|
|
369
|
-
}
|
|
370
|
-
current += ch;
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
if (!quote && ch === '|' && expression[i + 1] === '|') {
|
|
374
|
-
const segment = current.trim();
|
|
375
|
-
if (!segment) {
|
|
376
|
-
throw new AppError('INVALID_ARGS', `Invalid selector fallback expression: ${expression}`);
|
|
377
|
-
}
|
|
378
|
-
segments.push(segment);
|
|
379
|
-
current = '';
|
|
380
|
-
i += 1;
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
current += ch;
|
|
384
|
-
}
|
|
385
|
-
const finalSegment = current.trim();
|
|
386
|
-
if (!finalSegment) {
|
|
387
|
-
throw new AppError('INVALID_ARGS', `Invalid selector fallback expression: ${expression}`);
|
|
388
|
-
}
|
|
389
|
-
segments.push(finalSegment);
|
|
390
|
-
return segments;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function tokenize(segment: string): string[] {
|
|
394
|
-
const tokens: string[] = [];
|
|
395
|
-
let current = '';
|
|
396
|
-
let quote: '"' | "'" | null = null;
|
|
397
|
-
for (let i = 0; i < segment.length; i += 1) {
|
|
398
|
-
const ch = segment[i];
|
|
399
|
-
if ((ch === '"' || ch === "'") && !isEscapedQuote(segment, i)) {
|
|
400
|
-
if (!quote) {
|
|
401
|
-
quote = ch;
|
|
402
|
-
} else if (quote === ch) {
|
|
403
|
-
quote = null;
|
|
404
|
-
}
|
|
405
|
-
current += ch;
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
if (!quote && /\s/.test(ch)) {
|
|
409
|
-
if (current.trim().length > 0) {
|
|
410
|
-
tokens.push(current.trim());
|
|
411
|
-
}
|
|
412
|
-
current = '';
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
current += ch;
|
|
416
|
-
}
|
|
417
|
-
if (quote) {
|
|
418
|
-
throw new AppError('INVALID_ARGS', `Unclosed quote in selector: ${segment}`);
|
|
419
|
-
}
|
|
420
|
-
if (current.trim().length > 0) {
|
|
421
|
-
tokens.push(current.trim());
|
|
422
|
-
}
|
|
423
|
-
return tokens;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function unquote(value: string): string {
|
|
427
|
-
const trimmed = value.trim();
|
|
428
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
429
|
-
return trimmed.slice(1, -1).replace(/\\(["'])/g, '$1');
|
|
430
|
-
}
|
|
431
|
-
return trimmed;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function parseBoolean(value: string): boolean | null {
|
|
435
|
-
const normalized = unquote(value).toLowerCase();
|
|
436
|
-
if (normalized === 'true') return true;
|
|
437
|
-
if (normalized === 'false') return false;
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function textEquals(value: string | undefined, query: string): boolean {
|
|
442
|
-
return normalizeText(value ?? '') === normalizeText(query);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function roleEquals(value: string | undefined, query: string): boolean {
|
|
446
|
-
return normalizeRole(value ?? '') === normalizeRole(query);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function normalizeText(value: string): string {
|
|
450
|
-
return value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function normalizeRole(value: string): string {
|
|
454
|
-
return normalizeType(value);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function quoteSelectorValue(value: string): string {
|
|
458
|
-
return JSON.stringify(value);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function normalizeSelectorText(value: string | undefined): string | null {
|
|
462
|
-
if (!value) return null;
|
|
463
|
-
const trimmed = value.trim();
|
|
464
|
-
if (!trimmed) return null;
|
|
465
|
-
return trimmed;
|
|
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
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { DaemonRequest } from './types.ts';
|
|
2
|
-
import { SessionStore } from './session-store.ts';
|
|
3
|
-
import type { CommandFlags } from '../core/dispatch.ts';
|
|
4
|
-
|
|
5
|
-
export function resolveEffectiveSessionName(
|
|
6
|
-
req: DaemonRequest,
|
|
7
|
-
sessionStore: SessionStore,
|
|
8
|
-
): string {
|
|
9
|
-
const requested = req.session || 'default';
|
|
10
|
-
if (hasExplicitSessionFlag(req)) return requested;
|
|
11
|
-
if (requested !== 'default') return requested;
|
|
12
|
-
if (sessionStore.has(requested)) return requested;
|
|
13
|
-
|
|
14
|
-
const sessions = sessionStore.toArray();
|
|
15
|
-
if (sessions.length === 1) return sessions[0].name;
|
|
16
|
-
return requested;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function hasExplicitSessionFlag(req: DaemonRequest): boolean {
|
|
20
|
-
const value = (req.flags as CommandFlags | undefined)?.session;
|
|
21
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
22
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { AppError } from '../utils/errors.ts';
|
|
2
|
-
import type { CommandFlags } from '../core/dispatch.ts';
|
|
3
|
-
import type { SessionState } from './types.ts';
|
|
4
|
-
|
|
5
|
-
export function assertSessionSelectorMatches(
|
|
6
|
-
session: SessionState,
|
|
7
|
-
flags?: CommandFlags,
|
|
8
|
-
): void {
|
|
9
|
-
if (!flags) return;
|
|
10
|
-
|
|
11
|
-
const mismatches: string[] = [];
|
|
12
|
-
const device = session.device;
|
|
13
|
-
|
|
14
|
-
if (flags.platform && flags.platform !== device.platform) {
|
|
15
|
-
mismatches.push(`--platform=${flags.platform}`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (flags.udid && (device.platform !== 'ios' || flags.udid !== device.id)) {
|
|
19
|
-
mismatches.push(`--udid=${flags.udid}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (flags.serial && (device.platform !== 'android' || flags.serial !== device.id)) {
|
|
23
|
-
mismatches.push(`--serial=${flags.serial}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (mismatches.length === 0) return;
|
|
27
|
-
|
|
28
|
-
throw new AppError(
|
|
29
|
-
'INVALID_ARGS',
|
|
30
|
-
`Session "${session.name}" is bound to ${describeDevice(session)} and cannot be used with ${mismatches.join(', ')}. Use a different --session name or close this session first.`,
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function describeDevice(session: SessionState): string {
|
|
35
|
-
const platform = session.device.platform;
|
|
36
|
-
const name = session.device.name.trim();
|
|
37
|
-
const id = session.device.id;
|
|
38
|
-
return `${platform} device "${name}" (${id})`;
|
|
39
|
-
}
|