snapdrive-ios 0.1.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/LICENSE +21 -0
- package/README.ja.md +95 -0
- package/README.md +95 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +265 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/command-executor.d.ts +15 -0
- package/dist/core/command-executor.d.ts.map +1 -0
- package/dist/core/command-executor.js +64 -0
- package/dist/core/command-executor.js.map +1 -0
- package/dist/core/element-finder.d.ts +81 -0
- package/dist/core/element-finder.d.ts.map +1 -0
- package/dist/core/element-finder.js +246 -0
- package/dist/core/element-finder.js.map +1 -0
- package/dist/core/idb-client.d.ts +68 -0
- package/dist/core/idb-client.d.ts.map +1 -0
- package/dist/core/idb-client.js +327 -0
- package/dist/core/idb-client.js.map +1 -0
- package/dist/core/image-differ.d.ts +55 -0
- package/dist/core/image-differ.d.ts.map +1 -0
- package/dist/core/image-differ.js +211 -0
- package/dist/core/image-differ.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +9 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/report-generator.d.ts +31 -0
- package/dist/core/report-generator.d.ts.map +1 -0
- package/dist/core/report-generator.js +675 -0
- package/dist/core/report-generator.js.map +1 -0
- package/dist/core/scenario-runner.d.ts +54 -0
- package/dist/core/scenario-runner.d.ts.map +1 -0
- package/dist/core/scenario-runner.js +701 -0
- package/dist/core/scenario-runner.js.map +1 -0
- package/dist/core/simctl-client.d.ts +64 -0
- package/dist/core/simctl-client.d.ts.map +1 -0
- package/dist/core/simctl-client.js +214 -0
- package/dist/core/simctl-client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/config.interface.d.ts +37 -0
- package/dist/interfaces/config.interface.d.ts.map +1 -0
- package/dist/interfaces/config.interface.js +14 -0
- package/dist/interfaces/config.interface.js.map +1 -0
- package/dist/interfaces/element.interface.d.ts +49 -0
- package/dist/interfaces/element.interface.d.ts.map +1 -0
- package/dist/interfaces/element.interface.js +5 -0
- package/dist/interfaces/element.interface.js.map +1 -0
- package/dist/interfaces/index.d.ts +7 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +7 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/scenario.interface.d.ts +101 -0
- package/dist/interfaces/scenario.interface.d.ts.map +1 -0
- package/dist/interfaces/scenario.interface.js +5 -0
- package/dist/interfaces/scenario.interface.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +943 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-finder.d.ts","sourceRoot":"","sources":["../../src/core/element-finder.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,KAAK,EACL,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAAC;IACrF,mBAAmB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAAC;IAC/F,UAAU,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAAC;IACnF,UAAU,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAAC;IACnF,eAAe,CACb,QAAQ,EAAE,oBAAoB,EAAE,EAChC,SAAS,EAAE,gBAAgB,GAC1B,oBAAoB,EAAE,CAAC;IAC1B,QAAQ,CACN,QAAQ,EAAE,oBAAoB,EAAE,EAChC,SAAS,EAAE,gBAAgB,EAC3B,YAAY,CAAC,EAAE,KAAK,GACnB,mBAAmB,CAAC;IACvB,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,KAAK,CAAC;IACrD,YAAY,CAAC,QAAQ,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,CAAC;IACzD,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CACjG;AAED,qBAAa,aAAc,YAAW,cAAc;IAClD;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAIpF;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAK9F;;OAEG;IACH,UAAU,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAKlF;;OAEG;IACH,UAAU,CAAC,QAAQ,EAAE,oBAAoB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAKlF;;OAEG;IACH,eAAe,CACb,QAAQ,EAAE,oBAAoB,EAAE,EAChC,SAAS,EAAE,gBAAgB,GAC1B,oBAAoB,EAAE;IAIzB;;OAEG;IACH,QAAQ,CACN,QAAQ,EAAE,oBAAoB,EAAE,EAChC,SAAS,EAAE,gBAAgB,EAC3B,YAAY,GAAE,KAA0B,GACvC,mBAAmB;IAwBtB;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,KAAK;IAOpD;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE;IAUxD;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA6CxB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAsBpB;;OAEG;IACH,OAAO,CAAC,YAAY;IAWpB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAOxB;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,EAAE,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAgFhG;AAED,eAAO,MAAM,aAAa,eAAsB,CAAC"}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element finder - search and locate UI elements
|
|
3
|
+
*/
|
|
4
|
+
export class ElementFinder {
|
|
5
|
+
/**
|
|
6
|
+
* Find elements with exact label match
|
|
7
|
+
*/
|
|
8
|
+
findByLabel(elements, label) {
|
|
9
|
+
return elements.filter((el) => el.label === label);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Find elements with partial label match
|
|
13
|
+
*/
|
|
14
|
+
findByLabelContains(elements, partial) {
|
|
15
|
+
const lowerPartial = partial.toLowerCase();
|
|
16
|
+
return elements.filter((el) => el.label?.toLowerCase().includes(lowerPartial));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Find elements by type
|
|
20
|
+
*/
|
|
21
|
+
findByType(elements, type) {
|
|
22
|
+
const lowerType = type.toLowerCase();
|
|
23
|
+
return elements.filter((el) => el.type?.toLowerCase() === lowerType);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Find elements by role
|
|
27
|
+
*/
|
|
28
|
+
findByRole(elements, role) {
|
|
29
|
+
const lowerRole = role.toLowerCase();
|
|
30
|
+
return elements.filter((el) => el.role?.toLowerCase().includes(lowerRole));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Find elements matching a predicate
|
|
34
|
+
*/
|
|
35
|
+
findByPredicate(elements, predicate) {
|
|
36
|
+
return elements.filter((el) => this.matchesPredicate(el, predicate));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Find the best matching element and return search result with tap coordinates
|
|
40
|
+
*/
|
|
41
|
+
findBest(elements, predicate, screenCenter = { x: 200, y: 400 }) {
|
|
42
|
+
const matches = this.findByPredicate(elements, predicate);
|
|
43
|
+
if (matches.length === 0) {
|
|
44
|
+
return {
|
|
45
|
+
found: false,
|
|
46
|
+
elements: [],
|
|
47
|
+
count: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Rank and sort matches
|
|
51
|
+
const ranked = this.rankElements(matches, screenCenter);
|
|
52
|
+
const best = ranked[0];
|
|
53
|
+
return {
|
|
54
|
+
found: true,
|
|
55
|
+
element: best,
|
|
56
|
+
elements: ranked,
|
|
57
|
+
count: ranked.length,
|
|
58
|
+
tapCoordinates: this.getCenterPoint(best),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calculate center point of element's frame
|
|
63
|
+
*/
|
|
64
|
+
getCenterPoint(element) {
|
|
65
|
+
return {
|
|
66
|
+
x: Math.round(element.frame.x + element.frame.width / 2),
|
|
67
|
+
y: Math.round(element.frame.y + element.frame.height / 2),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get all labels from elements (for debugging/suggestions)
|
|
72
|
+
*/
|
|
73
|
+
getAllLabels(elements) {
|
|
74
|
+
const labels = new Set();
|
|
75
|
+
for (const el of elements) {
|
|
76
|
+
if (el.label) {
|
|
77
|
+
labels.add(el.label);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Array.from(labels).sort();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check if element matches predicate
|
|
84
|
+
*/
|
|
85
|
+
matchesPredicate(el, pred) {
|
|
86
|
+
// Check label (exact match or regex)
|
|
87
|
+
if (pred.label !== undefined) {
|
|
88
|
+
if (pred.label instanceof RegExp) {
|
|
89
|
+
if (!el.label || !pred.label.test(el.label)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
if (el.label !== pred.label) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check labelContains
|
|
100
|
+
if (pred.labelContains !== undefined) {
|
|
101
|
+
if (!el.label?.toLowerCase().includes(pred.labelContains.toLowerCase())) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Check type
|
|
106
|
+
if (pred.type !== undefined) {
|
|
107
|
+
if (el.type?.toLowerCase() !== pred.type.toLowerCase()) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Check role
|
|
112
|
+
if (pred.role !== undefined) {
|
|
113
|
+
if (!el.role?.toLowerCase().includes(pred.role.toLowerCase())) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Check enabled
|
|
118
|
+
if (pred.enabled !== undefined) {
|
|
119
|
+
if (el.enabled !== pred.enabled) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Rank elements by priority:
|
|
127
|
+
* 1. Buttons and tappable elements first
|
|
128
|
+
* 2. Enabled elements before disabled
|
|
129
|
+
* 3. Closer to screen center
|
|
130
|
+
*/
|
|
131
|
+
rankElements(elements, screenCenter) {
|
|
132
|
+
return [...elements].sort((a, b) => {
|
|
133
|
+
// Priority 1: Button-like elements
|
|
134
|
+
const aIsButton = this.isButtonLike(a);
|
|
135
|
+
const bIsButton = this.isButtonLike(b);
|
|
136
|
+
if (aIsButton && !bIsButton)
|
|
137
|
+
return -1;
|
|
138
|
+
if (!aIsButton && bIsButton)
|
|
139
|
+
return 1;
|
|
140
|
+
// Priority 2: Enabled elements
|
|
141
|
+
if (a.enabled && !b.enabled)
|
|
142
|
+
return -1;
|
|
143
|
+
if (!a.enabled && b.enabled)
|
|
144
|
+
return 1;
|
|
145
|
+
// Priority 3: Closer to screen center
|
|
146
|
+
const aDistance = this.distanceToCenter(a, screenCenter);
|
|
147
|
+
const bDistance = this.distanceToCenter(b, screenCenter);
|
|
148
|
+
return aDistance - bDistance;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if element is button-like (more likely to be tappable)
|
|
153
|
+
*/
|
|
154
|
+
isButtonLike(el) {
|
|
155
|
+
const buttonTypes = ['button', 'link', 'tab', 'cell'];
|
|
156
|
+
const type = el.type?.toLowerCase() ?? '';
|
|
157
|
+
const role = el.role?.toLowerCase() ?? '';
|
|
158
|
+
return (buttonTypes.some((t) => type.includes(t) || role.includes(t)) ||
|
|
159
|
+
el.traits?.some((t) => t.toLowerCase().includes('button')) === true);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Calculate distance from element center to screen center
|
|
163
|
+
*/
|
|
164
|
+
distanceToCenter(el, screenCenter) {
|
|
165
|
+
const center = this.getCenterPoint(el);
|
|
166
|
+
const dx = center.x - screenCenter.x;
|
|
167
|
+
const dy = center.y - screenCenter.y;
|
|
168
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Find the best scroll region by analyzing element frames
|
|
172
|
+
* Returns center point of the largest container that likely contains scrollable content
|
|
173
|
+
*/
|
|
174
|
+
findScrollRegion(elements) {
|
|
175
|
+
// Default fallback
|
|
176
|
+
const defaultCenter = { centerX: 200, centerY: 400 };
|
|
177
|
+
if (elements.length === 0) {
|
|
178
|
+
return defaultCenter;
|
|
179
|
+
}
|
|
180
|
+
// Find container elements (Group, etc.) that could be scroll containers
|
|
181
|
+
const containerTypes = ['group', 'axgroup', 'scrollview', 'tableview', 'collectionview', 'list'];
|
|
182
|
+
const containers = elements.filter((el) => {
|
|
183
|
+
const type = el.type?.toLowerCase() ?? '';
|
|
184
|
+
const role = el.role?.toLowerCase() ?? '';
|
|
185
|
+
return containerTypes.some(t => type.includes(t) || role.includes(t));
|
|
186
|
+
});
|
|
187
|
+
// If we found containers, find the largest one that's not at the very top
|
|
188
|
+
// (to avoid navigation bars which are usually at y < 100)
|
|
189
|
+
let bestContainer = null;
|
|
190
|
+
let maxArea = 0;
|
|
191
|
+
const candidateContainers = containers.length > 0 ? containers : elements;
|
|
192
|
+
for (const el of candidateContainers) {
|
|
193
|
+
const frame = el.frame;
|
|
194
|
+
if (!frame)
|
|
195
|
+
continue;
|
|
196
|
+
// Skip elements that are likely navigation bars (at very top)
|
|
197
|
+
if (frame.y < 50 && frame.height < 100)
|
|
198
|
+
continue;
|
|
199
|
+
// Skip elements that are likely tab bars (at very bottom, small height)
|
|
200
|
+
if (frame.y > 700 && frame.height < 100)
|
|
201
|
+
continue;
|
|
202
|
+
// Skip very small elements
|
|
203
|
+
if (frame.width < 100 || frame.height < 100)
|
|
204
|
+
continue;
|
|
205
|
+
const area = frame.width * frame.height;
|
|
206
|
+
if (area > maxArea) {
|
|
207
|
+
maxArea = area;
|
|
208
|
+
bestContainer = el;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (bestContainer && bestContainer.frame) {
|
|
212
|
+
const frame = bestContainer.frame;
|
|
213
|
+
return {
|
|
214
|
+
centerX: Math.round(frame.x + frame.width / 2),
|
|
215
|
+
centerY: Math.round(frame.y + frame.height / 2),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Fallback: calculate center from all element bounds
|
|
219
|
+
let minY = Infinity, maxY = 0;
|
|
220
|
+
let avgX = 0;
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const el of elements) {
|
|
223
|
+
if (!el.frame)
|
|
224
|
+
continue;
|
|
225
|
+
const frame = el.frame;
|
|
226
|
+
// Skip likely navigation/tab bars
|
|
227
|
+
if (frame.y < 50 && frame.height < 100)
|
|
228
|
+
continue;
|
|
229
|
+
if (frame.y > 700 && frame.height < 100)
|
|
230
|
+
continue;
|
|
231
|
+
minY = Math.min(minY, frame.y);
|
|
232
|
+
maxY = Math.max(maxY, frame.y + frame.height);
|
|
233
|
+
avgX += frame.x + frame.width / 2;
|
|
234
|
+
count++;
|
|
235
|
+
}
|
|
236
|
+
if (count > 0) {
|
|
237
|
+
return {
|
|
238
|
+
centerX: Math.round(avgX / count),
|
|
239
|
+
centerY: Math.round((minY + maxY) / 2),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return defaultCenter;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export const elementFinder = new ElementFinder();
|
|
246
|
+
//# sourceMappingURL=element-finder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-finder.js","sourceRoot":"","sources":["../../src/core/element-finder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* idb (iOS Development Bridge) client wrapper
|
|
3
|
+
* Wraps Facebook's idb CLI for UI automation
|
|
4
|
+
*/
|
|
5
|
+
import type { AccessibilityElement, UITree } from '../interfaces/element.interface.js';
|
|
6
|
+
import type { DeviceButton } from '../interfaces/config.interface.js';
|
|
7
|
+
import { type ICommandExecutor } from './command-executor.js';
|
|
8
|
+
import { type ILogger } from '../utils/logger.js';
|
|
9
|
+
export interface TapOptions {
|
|
10
|
+
duration?: number;
|
|
11
|
+
deviceUdid?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SwipeOptions {
|
|
14
|
+
delta?: number;
|
|
15
|
+
duration?: number;
|
|
16
|
+
deviceUdid?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IDBClientOptions {
|
|
19
|
+
deviceUdid?: string;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface IIDBClient {
|
|
23
|
+
tap(x: number, y: number, options?: TapOptions): Promise<void>;
|
|
24
|
+
swipe(startX: number, startY: number, endX: number, endY: number, options?: SwipeOptions): Promise<void>;
|
|
25
|
+
typeText(text: string, deviceUdid?: string): Promise<void>;
|
|
26
|
+
describeAll(deviceUdid?: string): Promise<UITree>;
|
|
27
|
+
describePoint(x: number, y: number, deviceUdid?: string): Promise<AccessibilityElement | null>;
|
|
28
|
+
pressButton(button: DeviceButton, duration?: number, deviceUdid?: string): Promise<void>;
|
|
29
|
+
keyEvent(keyCode: number, duration?: number, deviceUdid?: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare class IDBClient implements IIDBClient {
|
|
32
|
+
private executor;
|
|
33
|
+
private logger;
|
|
34
|
+
private defaultUdid?;
|
|
35
|
+
private timeoutMs;
|
|
36
|
+
private connectedUdids;
|
|
37
|
+
constructor(options?: IDBClientOptions, executor?: ICommandExecutor, logger?: ILogger);
|
|
38
|
+
/**
|
|
39
|
+
* Ensure idb is connected to the target device
|
|
40
|
+
*/
|
|
41
|
+
private ensureConnected;
|
|
42
|
+
private buildArgs;
|
|
43
|
+
tap(x: number, y: number, options?: TapOptions): Promise<void>;
|
|
44
|
+
swipe(startX: number, startY: number, endX: number, endY: number, options?: SwipeOptions): Promise<void>;
|
|
45
|
+
typeText(text: string, deviceUdid?: string): Promise<void>;
|
|
46
|
+
describeAll(deviceUdid?: string): Promise<UITree>;
|
|
47
|
+
describePoint(x: number, y: number, deviceUdid?: string): Promise<AccessibilityElement | null>;
|
|
48
|
+
pressButton(button: DeviceButton, duration?: number, deviceUdid?: string): Promise<void>;
|
|
49
|
+
keyEvent(keyCode: number, duration?: number, deviceUdid?: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Parse idb describe-all output into AccessibilityElement array
|
|
52
|
+
* Handles both JSON array and newline-delimited JSON formats
|
|
53
|
+
*/
|
|
54
|
+
private parseDescribeOutput;
|
|
55
|
+
/**
|
|
56
|
+
* Normalize element from idb output to our interface
|
|
57
|
+
* Handles different field naming conventions defensively
|
|
58
|
+
*/
|
|
59
|
+
private normalizeElement;
|
|
60
|
+
private getString;
|
|
61
|
+
private getBoolean;
|
|
62
|
+
private getStringArray;
|
|
63
|
+
private parseFrame;
|
|
64
|
+
private parseAXFrameString;
|
|
65
|
+
private isValidFrame;
|
|
66
|
+
}
|
|
67
|
+
export declare const idbClient: IDBClient;
|
|
68
|
+
//# sourceMappingURL=idb-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idb-client.d.ts","sourceRoot":"","sources":["../../src/core/idb-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* idb (iOS Development Bridge) client wrapper
|
|
3
|
+
* Wraps Facebook's idb CLI for UI automation
|
|
4
|
+
*/
|
|
5
|
+
import { CommandExecutor } from './command-executor.js';
|
|
6
|
+
import { Logger } from '../utils/logger.js';
|
|
7
|
+
export class IDBClient {
|
|
8
|
+
executor;
|
|
9
|
+
logger;
|
|
10
|
+
defaultUdid;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
connectedUdids = new Set();
|
|
13
|
+
constructor(options = {}, executor, logger) {
|
|
14
|
+
this.executor = executor ?? new CommandExecutor();
|
|
15
|
+
this.logger = logger ?? new Logger('idb-client');
|
|
16
|
+
this.defaultUdid = options.deviceUdid;
|
|
17
|
+
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ensure idb is connected to the target device
|
|
21
|
+
*/
|
|
22
|
+
async ensureConnected(deviceUdid) {
|
|
23
|
+
// Get target UDID
|
|
24
|
+
let udid = deviceUdid ?? this.defaultUdid;
|
|
25
|
+
if (!udid) {
|
|
26
|
+
// Try to find booted device
|
|
27
|
+
const result = await this.executor.execute('xcrun', ['simctl', 'list', 'devices', 'booted', '-j'], { timeoutMs: 10000 });
|
|
28
|
+
if (result.exitCode === 0) {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(result.stdout);
|
|
31
|
+
for (const deviceList of Object.values(data.devices ?? {})) {
|
|
32
|
+
for (const device of deviceList) {
|
|
33
|
+
if (device.state === 'Booted' && device.udid) {
|
|
34
|
+
udid = device.udid;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (udid)
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore parse error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!udid) {
|
|
48
|
+
throw new Error('No simulator UDID specified and no booted simulator found');
|
|
49
|
+
}
|
|
50
|
+
// Connect if not already connected
|
|
51
|
+
if (!this.connectedUdids.has(udid)) {
|
|
52
|
+
this.logger.debug(`Connecting idb to device: ${udid}`);
|
|
53
|
+
const connectResult = await this.executor.execute('idb', ['connect', udid], { timeoutMs: 10000 });
|
|
54
|
+
if (connectResult.exitCode !== 0) {
|
|
55
|
+
throw new Error(`Failed to connect idb to device ${udid}: ${connectResult.stderr}`);
|
|
56
|
+
}
|
|
57
|
+
this.connectedUdids.add(udid);
|
|
58
|
+
this.logger.info(`Connected idb to device: ${udid}`);
|
|
59
|
+
}
|
|
60
|
+
return udid;
|
|
61
|
+
}
|
|
62
|
+
buildArgs(baseArgs) {
|
|
63
|
+
// No --udid flag needed after idb connect
|
|
64
|
+
return baseArgs;
|
|
65
|
+
}
|
|
66
|
+
async tap(x, y, options = {}) {
|
|
67
|
+
await this.ensureConnected(options.deviceUdid);
|
|
68
|
+
const args = this.buildArgs(['ui', 'tap', String(Math.round(x)), String(Math.round(y))]);
|
|
69
|
+
if (options.duration && options.duration > 0) {
|
|
70
|
+
args.push('--duration', String(options.duration));
|
|
71
|
+
}
|
|
72
|
+
const result = await this.executor.execute('idb', args, {
|
|
73
|
+
timeoutMs: this.timeoutMs,
|
|
74
|
+
});
|
|
75
|
+
if (result.exitCode !== 0) {
|
|
76
|
+
throw new Error(`idb tap failed: ${result.stderr}`);
|
|
77
|
+
}
|
|
78
|
+
this.logger.debug(`Tapped at (${x}, ${y})`);
|
|
79
|
+
}
|
|
80
|
+
async swipe(startX, startY, endX, endY, options = {}) {
|
|
81
|
+
await this.ensureConnected(options.deviceUdid);
|
|
82
|
+
const args = this.buildArgs([
|
|
83
|
+
'ui',
|
|
84
|
+
'swipe',
|
|
85
|
+
String(Math.round(startX)),
|
|
86
|
+
String(Math.round(startY)),
|
|
87
|
+
String(Math.round(endX)),
|
|
88
|
+
String(Math.round(endY)),
|
|
89
|
+
]);
|
|
90
|
+
if (options.delta) {
|
|
91
|
+
args.push('--delta', String(options.delta));
|
|
92
|
+
}
|
|
93
|
+
if (options.duration) {
|
|
94
|
+
args.push('--duration', String(options.duration));
|
|
95
|
+
}
|
|
96
|
+
const result = await this.executor.execute('idb', args, {
|
|
97
|
+
timeoutMs: this.timeoutMs,
|
|
98
|
+
});
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
throw new Error(`idb swipe failed: ${result.stderr}`);
|
|
101
|
+
}
|
|
102
|
+
this.logger.debug(`Swiped from (${startX}, ${startY}) to (${endX}, ${endY})`);
|
|
103
|
+
}
|
|
104
|
+
async typeText(text, deviceUdid) {
|
|
105
|
+
await this.ensureConnected(deviceUdid);
|
|
106
|
+
const args = this.buildArgs(['ui', 'text', text]);
|
|
107
|
+
const result = await this.executor.execute('idb', args, {
|
|
108
|
+
timeoutMs: this.timeoutMs,
|
|
109
|
+
});
|
|
110
|
+
if (result.exitCode !== 0) {
|
|
111
|
+
throw new Error(`idb text failed: ${result.stderr}`);
|
|
112
|
+
}
|
|
113
|
+
this.logger.debug(`Typed text: "${text.slice(0, 20)}${text.length > 20 ? '...' : ''}"`);
|
|
114
|
+
}
|
|
115
|
+
async describeAll(deviceUdid) {
|
|
116
|
+
await this.ensureConnected(deviceUdid);
|
|
117
|
+
const args = this.buildArgs(['ui', 'describe-all']);
|
|
118
|
+
const result = await this.executor.execute('idb', args, {
|
|
119
|
+
timeoutMs: this.timeoutMs,
|
|
120
|
+
});
|
|
121
|
+
if (result.exitCode !== 0) {
|
|
122
|
+
throw new Error(`idb describe-all failed: ${result.stderr}`);
|
|
123
|
+
}
|
|
124
|
+
const elements = this.parseDescribeOutput(result.stdout);
|
|
125
|
+
return {
|
|
126
|
+
elements,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async describePoint(x, y, deviceUdid) {
|
|
131
|
+
await this.ensureConnected(deviceUdid);
|
|
132
|
+
const args = this.buildArgs([
|
|
133
|
+
'ui',
|
|
134
|
+
'describe-point',
|
|
135
|
+
String(Math.round(x)),
|
|
136
|
+
String(Math.round(y)),
|
|
137
|
+
]);
|
|
138
|
+
const result = await this.executor.execute('idb', args, {
|
|
139
|
+
timeoutMs: this.timeoutMs,
|
|
140
|
+
});
|
|
141
|
+
if (result.exitCode !== 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(result.stdout);
|
|
146
|
+
return this.normalizeElement(parsed);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async pressButton(button, duration, deviceUdid) {
|
|
153
|
+
await this.ensureConnected(deviceUdid);
|
|
154
|
+
const buttonMap = {
|
|
155
|
+
HOME: 'HOME',
|
|
156
|
+
LOCK: 'LOCK',
|
|
157
|
+
SIDE_BUTTON: 'SIDE_BUTTON',
|
|
158
|
+
SIRI: 'SIRI',
|
|
159
|
+
APPLE_PAY: 'APPLE_PAY',
|
|
160
|
+
};
|
|
161
|
+
const args = this.buildArgs(['ui', 'button', buttonMap[button]]);
|
|
162
|
+
if (duration) {
|
|
163
|
+
args.push('--duration', String(duration));
|
|
164
|
+
}
|
|
165
|
+
const result = await this.executor.execute('idb', args, {
|
|
166
|
+
timeoutMs: this.timeoutMs,
|
|
167
|
+
});
|
|
168
|
+
if (result.exitCode !== 0) {
|
|
169
|
+
throw new Error(`idb button press failed: ${result.stderr}`);
|
|
170
|
+
}
|
|
171
|
+
this.logger.debug(`Pressed button: ${button}`);
|
|
172
|
+
}
|
|
173
|
+
async keyEvent(keyCode, duration, deviceUdid) {
|
|
174
|
+
await this.ensureConnected(deviceUdid);
|
|
175
|
+
const args = this.buildArgs(['ui', 'key', String(keyCode)]);
|
|
176
|
+
if (duration) {
|
|
177
|
+
args.push('--duration', String(duration));
|
|
178
|
+
}
|
|
179
|
+
const result = await this.executor.execute('idb', args, {
|
|
180
|
+
timeoutMs: this.timeoutMs,
|
|
181
|
+
});
|
|
182
|
+
if (result.exitCode !== 0) {
|
|
183
|
+
throw new Error(`idb key event failed: ${result.stderr}`);
|
|
184
|
+
}
|
|
185
|
+
this.logger.debug(`Sent key event: ${keyCode}`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Parse idb describe-all output into AccessibilityElement array
|
|
189
|
+
* Handles both JSON array and newline-delimited JSON formats
|
|
190
|
+
*/
|
|
191
|
+
parseDescribeOutput(output) {
|
|
192
|
+
const trimmed = output.trim();
|
|
193
|
+
if (!trimmed) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
// Try parsing as JSON array first
|
|
198
|
+
const parsed = JSON.parse(trimmed);
|
|
199
|
+
if (Array.isArray(parsed)) {
|
|
200
|
+
return parsed.map((el) => this.normalizeElement(el)).filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
// Single object
|
|
203
|
+
const normalized = this.normalizeElement(parsed);
|
|
204
|
+
return normalized ? [normalized] : [];
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Try parsing as newline-delimited JSON
|
|
208
|
+
const elements = [];
|
|
209
|
+
for (const line of trimmed.split('\n')) {
|
|
210
|
+
const lineTrimmed = line.trim();
|
|
211
|
+
if (!lineTrimmed)
|
|
212
|
+
continue;
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(lineTrimmed);
|
|
215
|
+
const normalized = this.normalizeElement(parsed);
|
|
216
|
+
if (normalized) {
|
|
217
|
+
elements.push(normalized);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
this.logger.debug(`Failed to parse line: ${lineTrimmed.slice(0, 50)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return elements;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Normalize element from idb output to our interface
|
|
229
|
+
* Handles different field naming conventions defensively
|
|
230
|
+
*/
|
|
231
|
+
normalizeElement(raw) {
|
|
232
|
+
if (!raw || typeof raw !== 'object') {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
// Parse frame - handle multiple formats
|
|
236
|
+
const frame = this.parseFrame(raw);
|
|
237
|
+
if (!frame || !this.isValidFrame(frame)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
label: this.getString(raw, ['AXLabel', 'label', 'title', 'name']),
|
|
242
|
+
value: this.getString(raw, ['AXValue', 'value']),
|
|
243
|
+
type: this.getString(raw, ['type', 'AXType', 'element_type']),
|
|
244
|
+
role: this.getString(raw, ['role', 'AXRole']),
|
|
245
|
+
roleDescription: this.getString(raw, ['role_description', 'AXRoleDescription']),
|
|
246
|
+
identifier: this.getString(raw, ['AXUniqueId', 'identifier', 'accessibilityIdentifier']),
|
|
247
|
+
frame,
|
|
248
|
+
enabled: this.getBoolean(raw, ['enabled', 'AXEnabled'], true),
|
|
249
|
+
traits: this.getStringArray(raw, ['traits', 'AXTraits']),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
getString(obj, keys) {
|
|
253
|
+
for (const key of keys) {
|
|
254
|
+
const val = obj[key];
|
|
255
|
+
if (typeof val === 'string' && val.length > 0) {
|
|
256
|
+
return val;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
getBoolean(obj, keys, defaultVal) {
|
|
262
|
+
for (const key of keys) {
|
|
263
|
+
const val = obj[key];
|
|
264
|
+
if (typeof val === 'boolean') {
|
|
265
|
+
return val;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return defaultVal;
|
|
269
|
+
}
|
|
270
|
+
getStringArray(obj, keys) {
|
|
271
|
+
for (const key of keys) {
|
|
272
|
+
const val = obj[key];
|
|
273
|
+
if (Array.isArray(val)) {
|
|
274
|
+
return val.filter((v) => typeof v === 'string');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
parseFrame(obj) {
|
|
280
|
+
// Format 1: {frame: {x, y, width, height}}
|
|
281
|
+
const frameObj = obj['frame'];
|
|
282
|
+
if (frameObj && typeof frameObj === 'object') {
|
|
283
|
+
const f = frameObj;
|
|
284
|
+
if (typeof f['x'] === 'number' &&
|
|
285
|
+
typeof f['y'] === 'number' &&
|
|
286
|
+
typeof f['width'] === 'number' &&
|
|
287
|
+
typeof f['height'] === 'number') {
|
|
288
|
+
return {
|
|
289
|
+
x: f['x'],
|
|
290
|
+
y: f['y'],
|
|
291
|
+
width: f['width'],
|
|
292
|
+
height: f['height'],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Format 2: AXFrame string "{{x, y}, {width, height}}"
|
|
297
|
+
const axFrame = obj['AXFrame'];
|
|
298
|
+
if (typeof axFrame === 'string') {
|
|
299
|
+
return this.parseAXFrameString(axFrame);
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
parseAXFrameString(frameStr) {
|
|
304
|
+
// Format: "{{123.0, 456.0}, {100.0, 50.0}}"
|
|
305
|
+
try {
|
|
306
|
+
const cleaned = frameStr.replace(/\s/g, '').replace(/[{}]/g, '');
|
|
307
|
+
const nums = cleaned.split(',').map(Number);
|
|
308
|
+
if (nums.length === 4 && nums.every((n) => !isNaN(n))) {
|
|
309
|
+
return {
|
|
310
|
+
x: nums[0],
|
|
311
|
+
y: nums[1],
|
|
312
|
+
width: nums[2],
|
|
313
|
+
height: nums[3],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Fall through to return null
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
isValidFrame(frame) {
|
|
323
|
+
return frame.width > 0 && frame.height > 0 && frame.x >= 0 && frame.y >= 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export const idbClient = new IDBClient();
|
|
327
|
+
//# sourceMappingURL=idb-client.js.map
|