@ulpi/browse 1.4.4 → 2.3.1
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 +191 -21
- package/README.md +186 -12
- package/bin/browse-android-app.apk +0 -0
- package/bin/browse-android.apk +0 -0
- package/bin/browse-ax +0 -0
- package/bin/browse-ios-runner/BrowseRunnerApp/BrowseRunnerApp.swift +382 -0
- package/bin/browse-ios-runner/BrowseRunnerApp/RunnerStatusStore.swift +135 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/ActionHandler.swift +277 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/BrowseRunnerUITests.swift +60 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/Models.swift +78 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/RunnerServer.swift +246 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/ScreenshotHandler.swift +35 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/StateHandler.swift +82 -0
- package/bin/browse-ios-runner/BrowseRunnerUITests/TreeBuilder.swift +323 -0
- package/bin/browse-ios-runner/build.sh +81 -0
- package/bin/browse-ios-runner/project.yml +47 -0
- package/browse-ios-runner/BrowseRunnerApp/BrowseRunnerApp.swift +382 -0
- package/browse-ios-runner/BrowseRunnerApp/RunnerStatusStore.swift +135 -0
- package/browse-ios-runner/BrowseRunnerUITests/ActionHandler.swift +277 -0
- package/browse-ios-runner/BrowseRunnerUITests/BrowseRunnerUITests.swift +60 -0
- package/browse-ios-runner/BrowseRunnerUITests/Models.swift +78 -0
- package/browse-ios-runner/BrowseRunnerUITests/RunnerServer.swift +246 -0
- package/browse-ios-runner/BrowseRunnerUITests/ScreenshotHandler.swift +35 -0
- package/browse-ios-runner/BrowseRunnerUITests/StateHandler.swift +82 -0
- package/browse-ios-runner/BrowseRunnerUITests/TreeBuilder.swift +323 -0
- package/browse-ios-runner/README.md +194 -0
- package/browse-ios-runner/build.sh +81 -0
- package/browse-ios-runner/project.yml +47 -0
- package/dist/browse.cjs +26595 -13648
- package/package.json +15 -5
- package/skill/SKILL.md +33 -0
- package/skill/references/commands.md +18 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// MARK: - State Handler
|
|
4
|
+
|
|
5
|
+
/// Captures lightweight state from the running application for action-context probes.
|
|
6
|
+
///
|
|
7
|
+
/// Produces an `IOSState` matching the TypeScript `IOSState` type in `protocol.ts`.
|
|
8
|
+
enum StateHandler {
|
|
9
|
+
|
|
10
|
+
/// Capture the current state of the application.
|
|
11
|
+
///
|
|
12
|
+
/// - Parameters:
|
|
13
|
+
/// - app: The XCUIApplication to inspect.
|
|
14
|
+
/// - bundleId: The target app's bundle identifier.
|
|
15
|
+
/// - Returns: An `IOSState` reflecting the current screen.
|
|
16
|
+
static func captureState(app: XCUIApplication, bundleId: String) -> IOSState {
|
|
17
|
+
let screenTitle = resolveScreenTitle(app: app)
|
|
18
|
+
let elementCount = TreeBuilder.countElements(in: app)
|
|
19
|
+
let alertPresent = app.alerts.count > 0 || app.sheets.count > 0
|
|
20
|
+
let keyboardVisible = app.keyboards.count > 0
|
|
21
|
+
let orientation = currentOrientation()
|
|
22
|
+
let statusBarTime = resolveStatusBarTime(app: app)
|
|
23
|
+
|
|
24
|
+
return IOSState(
|
|
25
|
+
bundleId: bundleId,
|
|
26
|
+
screenTitle: screenTitle,
|
|
27
|
+
elementCount: elementCount,
|
|
28
|
+
alertPresent: alertPresent,
|
|
29
|
+
keyboardVisible: keyboardVisible,
|
|
30
|
+
orientation: orientation,
|
|
31
|
+
statusBarTime: statusBarTime
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Private
|
|
36
|
+
|
|
37
|
+
/// Attempt to find the current screen title.
|
|
38
|
+
/// Checks navigation bars first, then falls back to the app title.
|
|
39
|
+
private static func resolveScreenTitle(app: XCUIApplication) -> String {
|
|
40
|
+
// Check navigation bars for a title
|
|
41
|
+
let navBars = app.navigationBars
|
|
42
|
+
if navBars.count > 0 {
|
|
43
|
+
let firstBar = navBars.element(boundBy: 0)
|
|
44
|
+
let barIdentifier = firstBar.identifier
|
|
45
|
+
if !barIdentifier.isEmpty {
|
|
46
|
+
return barIdentifier
|
|
47
|
+
}
|
|
48
|
+
// Try to find a static text child that acts as the title
|
|
49
|
+
let texts = firstBar.staticTexts
|
|
50
|
+
if texts.count > 0 {
|
|
51
|
+
let titleText = texts.element(boundBy: 0).label
|
|
52
|
+
if !titleText.isEmpty {
|
|
53
|
+
return titleText
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fall back to the app label
|
|
59
|
+
return app.label
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Get the current device orientation as a string.
|
|
63
|
+
private static func currentOrientation() -> String {
|
|
64
|
+
let orientation = XCUIDevice.shared.orientation
|
|
65
|
+
switch orientation {
|
|
66
|
+
case .landscapeLeft, .landscapeRight:
|
|
67
|
+
return "landscape"
|
|
68
|
+
default:
|
|
69
|
+
return "portrait"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Attempt to read the status bar time.
|
|
74
|
+
/// On iOS simulators, the status bar time can be read from the status bar element.
|
|
75
|
+
private static func resolveStatusBarTime(app: XCUIApplication) -> String {
|
|
76
|
+
// The status bar is accessible via the springboard, not the app.
|
|
77
|
+
// As a fallback, return the current time.
|
|
78
|
+
let formatter = DateFormatter()
|
|
79
|
+
formatter.dateFormat = "h:mm a"
|
|
80
|
+
return formatter.string(from: Date())
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// MARK: - Tree Builder
|
|
4
|
+
|
|
5
|
+
/// Walks the XCUIApplication element hierarchy and builds a `RawIOSNode` tree.
|
|
6
|
+
///
|
|
7
|
+
/// Uses XCUIElement's accessibility properties to map each element to the JSON
|
|
8
|
+
/// format expected by `src/app/ios/protocol.ts`. The tree is depth-first,
|
|
9
|
+
/// with children ordered by index within each element type group.
|
|
10
|
+
enum TreeBuilder {
|
|
11
|
+
|
|
12
|
+
/// Build the full accessibility tree for the given application.
|
|
13
|
+
/// Uses snapshot().dictionaryRepresentation for a fast single-IPC-call tree fetch
|
|
14
|
+
/// (same technique as Maestro). Falls back to element-by-element walk on failure.
|
|
15
|
+
static func buildTree(from app: XCUIApplication) -> RawIOSNode {
|
|
16
|
+
do {
|
|
17
|
+
let snapshot = try app.snapshot()
|
|
18
|
+
let dict = snapshot.dictionaryRepresentation
|
|
19
|
+
return nodeFromSnapshot(dict)
|
|
20
|
+
} catch {
|
|
21
|
+
NSLog("[BrowseRunner] Snapshot API failed (\(error.localizedDescription)), falling back to element walk")
|
|
22
|
+
return walkElement(app)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Convert a snapshot dictionary to a RawIOSNode recursively.
|
|
27
|
+
private static func nodeFromSnapshot(_ dict: [XCUIElement.AttributeName: Any]) -> RawIOSNode {
|
|
28
|
+
let elementType = (dict[.elementType] as? Int).flatMap { mapElementType($0) } ?? "other"
|
|
29
|
+
let label = dict[.label] as? String ?? ""
|
|
30
|
+
let value = dict[.value] as? String ?? ""
|
|
31
|
+
let identifier = dict[.identifier] as? String ?? ""
|
|
32
|
+
let placeholderValue = dict[.placeholderValue] as? String ?? ""
|
|
33
|
+
let isEnabled = dict[.enabled] as? Bool ?? true
|
|
34
|
+
let hasFocus = dict[.hasFocus] as? Bool ?? false
|
|
35
|
+
let isSelected = dict[.selected] as? Bool ?? false
|
|
36
|
+
|
|
37
|
+
// Frame from snapshot
|
|
38
|
+
var frame = NodeFrame(x: 0, y: 0, width: 0, height: 0)
|
|
39
|
+
if let frameDict = dict[.frame] as? [String: Double] {
|
|
40
|
+
frame = NodeFrame(
|
|
41
|
+
x: frameDict["X"] ?? 0,
|
|
42
|
+
y: frameDict["Y"] ?? 0,
|
|
43
|
+
width: frameDict["Width"] ?? 0,
|
|
44
|
+
height: frameDict["Height"] ?? 0
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Traits
|
|
49
|
+
var traits: [String] = []
|
|
50
|
+
if let traitValue = dict[XCUIElement.AttributeName(rawValue: "traits")] as? UInt64 {
|
|
51
|
+
traits = mapSnapshotTraits(traitValue)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Children
|
|
55
|
+
var children: [RawIOSNode] = []
|
|
56
|
+
if let childDicts = dict[.children] as? [[XCUIElement.AttributeName: Any]] {
|
|
57
|
+
children = childDicts.map { nodeFromSnapshot($0) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return RawIOSNode(
|
|
61
|
+
elementType: elementType,
|
|
62
|
+
identifier: identifier,
|
|
63
|
+
label: label,
|
|
64
|
+
value: value,
|
|
65
|
+
placeholderValue: placeholderValue,
|
|
66
|
+
frame: frame,
|
|
67
|
+
isEnabled: isEnabled,
|
|
68
|
+
isSelected: isSelected,
|
|
69
|
+
hasFocus: hasFocus,
|
|
70
|
+
traits: traits,
|
|
71
|
+
children: children
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Map XCUIElement.ElementType raw value to string.
|
|
76
|
+
private static func mapElementType(_ rawValue: Int) -> String {
|
|
77
|
+
// These values match XCUIElement.ElementType rawValue
|
|
78
|
+
switch rawValue {
|
|
79
|
+
case 0: return "any"
|
|
80
|
+
case 1: return "other"
|
|
81
|
+
case 2: return "application"
|
|
82
|
+
case 3: return "group"
|
|
83
|
+
case 4: return "window"
|
|
84
|
+
case 5: return "sheet"
|
|
85
|
+
case 6: return "drawer"
|
|
86
|
+
case 7: return "alert"
|
|
87
|
+
case 8: return "dialog"
|
|
88
|
+
case 9: return "button"
|
|
89
|
+
case 10: return "radioButton"
|
|
90
|
+
case 11: return "radioGroup"
|
|
91
|
+
case 12: return "checkBox"
|
|
92
|
+
case 13: return "disclosureTriangle"
|
|
93
|
+
case 14: return "popUpButton"
|
|
94
|
+
case 15: return "comboBox"
|
|
95
|
+
case 16: return "menuButton"
|
|
96
|
+
case 17: return "toolbarButton"
|
|
97
|
+
case 18: return "popover"
|
|
98
|
+
case 19: return "keyboard"
|
|
99
|
+
case 20: return "key"
|
|
100
|
+
case 21: return "navigationBar"
|
|
101
|
+
case 22: return "tabBar"
|
|
102
|
+
case 23: return "tabGroup"
|
|
103
|
+
case 24: return "toolbar"
|
|
104
|
+
case 25: return "statusBar"
|
|
105
|
+
case 26: return "table"
|
|
106
|
+
case 27: return "tableRow"
|
|
107
|
+
case 28: return "tableColumn"
|
|
108
|
+
case 29: return "outline"
|
|
109
|
+
case 30: return "outlineRow"
|
|
110
|
+
case 31: return "browser"
|
|
111
|
+
case 32: return "collectionView"
|
|
112
|
+
case 33: return "slider"
|
|
113
|
+
case 34: return "pageIndicator"
|
|
114
|
+
case 35: return "progressIndicator"
|
|
115
|
+
case 36: return "activityIndicator"
|
|
116
|
+
case 37: return "segmentedControl"
|
|
117
|
+
case 38: return "picker"
|
|
118
|
+
case 39: return "pickerWheel"
|
|
119
|
+
case 40: return "switch"
|
|
120
|
+
case 41: return "toggle"
|
|
121
|
+
case 42: return "link"
|
|
122
|
+
case 43: return "image"
|
|
123
|
+
case 44: return "icon"
|
|
124
|
+
case 45: return "searchField"
|
|
125
|
+
case 46: return "scrollView"
|
|
126
|
+
case 47: return "scrollBar"
|
|
127
|
+
case 48: return "staticText"
|
|
128
|
+
case 49: return "textField"
|
|
129
|
+
case 50: return "secureTextField"
|
|
130
|
+
case 51: return "datePicker"
|
|
131
|
+
case 52: return "textView"
|
|
132
|
+
case 53: return "menu"
|
|
133
|
+
case 54: return "menuItem"
|
|
134
|
+
case 55: return "menuBar"
|
|
135
|
+
case 56: return "menuBarItem"
|
|
136
|
+
case 57: return "map"
|
|
137
|
+
case 58: return "webView"
|
|
138
|
+
case 59: return "incrementArrow"
|
|
139
|
+
case 60: return "decrementArrow"
|
|
140
|
+
case 61: return "timeline"
|
|
141
|
+
case 62: return "ratingIndicator"
|
|
142
|
+
case 63: return "valueIndicator"
|
|
143
|
+
case 64: return "splitGroup"
|
|
144
|
+
case 65: return "splitter"
|
|
145
|
+
case 66: return "relevanceIndicator"
|
|
146
|
+
case 67: return "colorWell"
|
|
147
|
+
case 68: return "helpTag"
|
|
148
|
+
case 69: return "matte"
|
|
149
|
+
case 70: return "dockItem"
|
|
150
|
+
case 71: return "ruler"
|
|
151
|
+
case 72: return "rulerMarker"
|
|
152
|
+
case 73: return "grid"
|
|
153
|
+
case 74: return "levelIndicator"
|
|
154
|
+
case 75: return "cell"
|
|
155
|
+
case 76: return "layoutArea"
|
|
156
|
+
case 77: return "layoutItem"
|
|
157
|
+
case 78: return "handle"
|
|
158
|
+
case 79: return "stepper"
|
|
159
|
+
case 80: return "tab"
|
|
160
|
+
default: return "other"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Map snapshot trait bitmask to string array.
|
|
165
|
+
private static func mapSnapshotTraits(_ value: UInt64) -> [String] {
|
|
166
|
+
var traits: [String] = []
|
|
167
|
+
if value & (1 << 0) != 0 { traits.append("button") }
|
|
168
|
+
if value & (1 << 1) != 0 { traits.append("link") }
|
|
169
|
+
if value & (1 << 2) != 0 { traits.append("image") }
|
|
170
|
+
if value & (1 << 3) != 0 { traits.append("selected") }
|
|
171
|
+
if value & (1 << 4) != 0 { traits.append("playsSound") }
|
|
172
|
+
if value & (1 << 5) != 0 { traits.append("keyboardKey") }
|
|
173
|
+
if value & (1 << 6) != 0 { traits.append("staticText") }
|
|
174
|
+
if value & (1 << 7) != 0 { traits.append("summaryElement") }
|
|
175
|
+
if value & (1 << 8) != 0 { traits.append("notEnabled") }
|
|
176
|
+
if value & (1 << 9) != 0 { traits.append("updatesFrequently") }
|
|
177
|
+
if value & (1 << 12) != 0 { traits.append("searchField") }
|
|
178
|
+
if value & (1 << 13) != 0 { traits.append("startsMediaSession") }
|
|
179
|
+
if value & (1 << 14) != 0 { traits.append("adjustable") }
|
|
180
|
+
if value & (1 << 15) != 0 { traits.append("allowsDirectInteraction") }
|
|
181
|
+
if value & (1 << 16) != 0 { traits.append("causesPageTurn") }
|
|
182
|
+
if value & (1 << 17) != 0 { traits.append("tabBar") }
|
|
183
|
+
return traits
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Count the total number of elements in the tree rooted at the given element.
|
|
187
|
+
/// Uses a lightweight query to avoid rebuilding the full node tree.
|
|
188
|
+
static func countElements(in app: XCUIApplication) -> Int {
|
|
189
|
+
return app.descendants(matching: .any).count
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Resolve an element at a given tree path (array of child indices).
|
|
193
|
+
///
|
|
194
|
+
/// The path `[0]` is the root (application). `[0, 2]` is the third child
|
|
195
|
+
/// of the root. This matches how the bridge's `convertTree` assigns paths.
|
|
196
|
+
static func resolveElement(in app: XCUIApplication, path: [Int]) -> XCUIElement? {
|
|
197
|
+
guard !path.isEmpty else { return nil }
|
|
198
|
+
|
|
199
|
+
// Path[0] is always 0 (the root = app itself).
|
|
200
|
+
// Subsequent indices are child positions.
|
|
201
|
+
var current: XCUIElement = app
|
|
202
|
+
for childIndex in path.dropFirst() {
|
|
203
|
+
let children = current.children(matching: .any)
|
|
204
|
+
guard childIndex >= 0, childIndex < children.count else {
|
|
205
|
+
return nil
|
|
206
|
+
}
|
|
207
|
+
current = children.element(boundBy: childIndex)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return current
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// MARK: - Private
|
|
214
|
+
|
|
215
|
+
/// Recursively walk an XCUIElement and its children.
|
|
216
|
+
private static func walkElement(_ element: XCUIElement) -> RawIOSNode {
|
|
217
|
+
let children = element.children(matching: .any)
|
|
218
|
+
var childNodes: [RawIOSNode] = []
|
|
219
|
+
for i in 0..<children.count {
|
|
220
|
+
let child = children.element(boundBy: i)
|
|
221
|
+
// Only include elements that exist in the hierarchy.
|
|
222
|
+
// Skip elements that are not hittable and have no label/identifier
|
|
223
|
+
// to reduce noise in the tree.
|
|
224
|
+
if child.exists {
|
|
225
|
+
childNodes.append(walkElement(child))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let frame = element.frame
|
|
230
|
+
let nodeFrame = NodeFrame(
|
|
231
|
+
x: Double(frame.origin.x),
|
|
232
|
+
y: Double(frame.origin.y),
|
|
233
|
+
width: Double(frame.size.width),
|
|
234
|
+
height: Double(frame.size.height)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return RawIOSNode(
|
|
238
|
+
elementType: mapElementType(element.elementType),
|
|
239
|
+
identifier: element.identifier,
|
|
240
|
+
label: element.label,
|
|
241
|
+
value: stringValue(of: element),
|
|
242
|
+
placeholderValue: element.placeholderValue ?? "",
|
|
243
|
+
frame: nodeFrame,
|
|
244
|
+
isEnabled: element.isEnabled,
|
|
245
|
+
isSelected: element.isSelected,
|
|
246
|
+
hasFocus: element.hasFocus,
|
|
247
|
+
traits: mapTraits(element.elementType),
|
|
248
|
+
children: childNodes
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Extract the string value from an element.
|
|
253
|
+
/// XCUIElement.value is `Any?`, so we coerce to String.
|
|
254
|
+
private static func stringValue(of element: XCUIElement) -> String {
|
|
255
|
+
guard let val = element.value else { return "" }
|
|
256
|
+
if let str = val as? String { return str }
|
|
257
|
+
if let num = val as? NSNumber { return num.stringValue }
|
|
258
|
+
return String(describing: val)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Map XCUIElement.ElementType to the string name expected by protocol.ts.
|
|
262
|
+
/// These names match the keys in `IOS_ROLE_MAP` on the TypeScript side.
|
|
263
|
+
private static func mapElementType(_ type: XCUIElement.ElementType) -> String {
|
|
264
|
+
switch type {
|
|
265
|
+
case .application: return "application"
|
|
266
|
+
case .button: return "button"
|
|
267
|
+
case .staticText: return "staticText"
|
|
268
|
+
case .textField: return "textField"
|
|
269
|
+
case .secureTextField: return "secureTextField"
|
|
270
|
+
case .textView: return "textView"
|
|
271
|
+
case .image: return "image"
|
|
272
|
+
case .switch: return "switch"
|
|
273
|
+
case .toggle: return "toggle"
|
|
274
|
+
case .slider: return "slider"
|
|
275
|
+
case .stepper: return "stepper"
|
|
276
|
+
case .picker: return "picker"
|
|
277
|
+
case .segmentedControl: return "segmentedControl"
|
|
278
|
+
case .link: return "link"
|
|
279
|
+
case .cell: return "cell"
|
|
280
|
+
case .table: return "table"
|
|
281
|
+
case .collectionView: return "collectionView"
|
|
282
|
+
case .scrollView: return "scrollView"
|
|
283
|
+
case .navigationBar: return "navigationBar"
|
|
284
|
+
case .toolbar: return "toolbar"
|
|
285
|
+
case .tabBar: return "tabBar"
|
|
286
|
+
case .alert: return "alert"
|
|
287
|
+
case .sheet: return "sheet"
|
|
288
|
+
case .popover: return "popover"
|
|
289
|
+
case .window: return "window"
|
|
290
|
+
case .webView: return "webView"
|
|
291
|
+
case .map: return "map"
|
|
292
|
+
case .group: return "group"
|
|
293
|
+
case .icon: return "icon"
|
|
294
|
+
case .searchField: return "searchField"
|
|
295
|
+
case .activityIndicator: return "activityIndicator"
|
|
296
|
+
case .progressIndicator: return "progressIndicator"
|
|
297
|
+
case .menu: return "menu"
|
|
298
|
+
case .menuItem: return "menuItem"
|
|
299
|
+
case .tab: return "tab"
|
|
300
|
+
case .key: return "key"
|
|
301
|
+
case .keyboard: return "keyboard"
|
|
302
|
+
case .other: return "other"
|
|
303
|
+
default: return "other"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// Derive trait names from the element type.
|
|
308
|
+
/// On iOS, XCUIElement doesn't directly expose UIAccessibilityTraits,
|
|
309
|
+
/// so we infer traits from the element type.
|
|
310
|
+
private static func mapTraits(_ type: XCUIElement.ElementType) -> [String] {
|
|
311
|
+
switch type {
|
|
312
|
+
case .button: return ["button"]
|
|
313
|
+
case .link: return ["link"]
|
|
314
|
+
case .staticText: return ["staticText"]
|
|
315
|
+
case .image: return ["image"]
|
|
316
|
+
case .searchField: return ["searchField"]
|
|
317
|
+
case .switch, .toggle: return ["button"]
|
|
318
|
+
case .slider: return ["adjustable"]
|
|
319
|
+
case .tab: return ["button", "tab"]
|
|
320
|
+
default: return []
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# build.sh — Build the BrowseRunner XCUITest bundle.
|
|
5
|
+
#
|
|
6
|
+
# Prerequisites:
|
|
7
|
+
# - Xcode installed (xcrun, xcodebuild)
|
|
8
|
+
# - xcodegen installed: brew install xcodegen
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# cd browse-ios-runner && ./build.sh
|
|
12
|
+
# ./build.sh --install # Also install to booted simulator
|
|
13
|
+
# ./build.sh --install <UDID> # Install to specific simulator
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
16
|
+
cd "$SCRIPT_DIR"
|
|
17
|
+
|
|
18
|
+
DERIVED_DATA=".build"
|
|
19
|
+
SCHEME="BrowseRunnerUITests"
|
|
20
|
+
SDK="iphonesimulator"
|
|
21
|
+
CONFIGURATION="Debug"
|
|
22
|
+
|
|
23
|
+
# ── Step 1: Generate Xcode project from project.yml ──
|
|
24
|
+
|
|
25
|
+
if ! command -v xcodegen &> /dev/null; then
|
|
26
|
+
echo "Error: xcodegen not found. Install with: brew install xcodegen"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
echo "==> Generating Xcode project..."
|
|
31
|
+
xcodegen generate --spec project.yml
|
|
32
|
+
|
|
33
|
+
# ── Step 2: Build the UI test bundle ──
|
|
34
|
+
|
|
35
|
+
echo "==> Building BrowseRunner..."
|
|
36
|
+
xcodebuild \
|
|
37
|
+
-project BrowseRunner.xcodeproj \
|
|
38
|
+
-scheme BrowseRunnerApp \
|
|
39
|
+
-sdk "$SDK" \
|
|
40
|
+
-configuration "$CONFIGURATION" \
|
|
41
|
+
-derivedDataPath "$DERIVED_DATA" \
|
|
42
|
+
-destination "generic/platform=iOS Simulator" \
|
|
43
|
+
build-for-testing \
|
|
44
|
+
CODE_SIGN_IDENTITY="" \
|
|
45
|
+
CODE_SIGNING_ALLOWED=NO \
|
|
46
|
+
2>&1 | tail -20
|
|
47
|
+
|
|
48
|
+
BUILD_DIR="$DERIVED_DATA/Build/Products/$CONFIGURATION-$SDK"
|
|
49
|
+
APP_PATH="$BUILD_DIR/BrowseRunnerApp.app"
|
|
50
|
+
TEST_RUNNER="$BUILD_DIR/BrowseRunnerUITests-Runner.app"
|
|
51
|
+
|
|
52
|
+
if [ ! -d "$APP_PATH" ]; then
|
|
53
|
+
echo "Error: Build failed — $APP_PATH not found"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "==> Build successful"
|
|
58
|
+
echo " App: $APP_PATH"
|
|
59
|
+
echo " Test runner: $TEST_RUNNER"
|
|
60
|
+
|
|
61
|
+
# ── Step 3: Optional install ──
|
|
62
|
+
|
|
63
|
+
if [[ "${1:-}" == "--install" ]]; then
|
|
64
|
+
UDID="${2:-booted}"
|
|
65
|
+
echo "==> Installing to simulator ($UDID)..."
|
|
66
|
+
xcrun simctl install "$UDID" "$APP_PATH"
|
|
67
|
+
if [ -d "$TEST_RUNNER" ]; then
|
|
68
|
+
xcrun simctl install "$UDID" "$TEST_RUNNER"
|
|
69
|
+
fi
|
|
70
|
+
echo "==> Installed successfully"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
echo ""
|
|
74
|
+
echo "To run the test server in a booted simulator:"
|
|
75
|
+
echo " xcodebuild \\"
|
|
76
|
+
echo " -project BrowseRunner.xcodeproj \\"
|
|
77
|
+
echo " -scheme BrowseRunnerUITests \\"
|
|
78
|
+
echo " -sdk iphonesimulator \\"
|
|
79
|
+
echo " -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \\"
|
|
80
|
+
echo " -derivedDataPath .build \\"
|
|
81
|
+
echo " test-without-building"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: BrowseRunner
|
|
2
|
+
options:
|
|
3
|
+
bundleIdPrefix: io.ulpi
|
|
4
|
+
deploymentTarget:
|
|
5
|
+
iOS: "16.0"
|
|
6
|
+
createIntermediateGroups: true
|
|
7
|
+
|
|
8
|
+
settings:
|
|
9
|
+
base:
|
|
10
|
+
SWIFT_VERSION: "5"
|
|
11
|
+
|
|
12
|
+
packages:
|
|
13
|
+
FlyingFox:
|
|
14
|
+
url: https://github.com/swhitty/FlyingFox
|
|
15
|
+
exactVersion: "0.22.0"
|
|
16
|
+
|
|
17
|
+
targets:
|
|
18
|
+
BrowseRunnerApp:
|
|
19
|
+
type: application
|
|
20
|
+
platform: iOS
|
|
21
|
+
sources:
|
|
22
|
+
- path: BrowseRunnerApp
|
|
23
|
+
settings:
|
|
24
|
+
base:
|
|
25
|
+
PRODUCT_BUNDLE_IDENTIFIER: io.ulpi.browse-ios-runner
|
|
26
|
+
CODE_SIGN_IDENTITY: ""
|
|
27
|
+
CODE_SIGNING_ALLOWED: "NO"
|
|
28
|
+
GENERATE_INFOPLIST_FILE: "YES"
|
|
29
|
+
MARKETING_VERSION: "2.2.0"
|
|
30
|
+
CURRENT_PROJECT_VERSION: "1"
|
|
31
|
+
INFOPLIST_KEY_NSAppTransportSecurity_NSAllowsLocalNetworking: YES
|
|
32
|
+
|
|
33
|
+
BrowseRunnerUITests:
|
|
34
|
+
type: bundle.ui-testing
|
|
35
|
+
platform: iOS
|
|
36
|
+
sources:
|
|
37
|
+
- path: BrowseRunnerUITests
|
|
38
|
+
dependencies:
|
|
39
|
+
- target: BrowseRunnerApp
|
|
40
|
+
- package: FlyingFox
|
|
41
|
+
settings:
|
|
42
|
+
base:
|
|
43
|
+
PRODUCT_BUNDLE_IDENTIFIER: io.ulpi.browse-ios-runner-uitests
|
|
44
|
+
CODE_SIGN_IDENTITY: ""
|
|
45
|
+
CODE_SIGNING_ALLOWED: "NO"
|
|
46
|
+
GENERATE_INFOPLIST_FILE: "YES"
|
|
47
|
+
TEST_TARGET_NAME: BrowseRunnerApp
|