craft-native 0.0.156 → 0.0.160
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/android/templates/CraftNative.kt.template +24 -1
- package/dist/android/templates/CraftNativeCapabilities.kt.template +309 -4
- package/dist/android/templates/MainActivityNative.kt.template +606 -48
- package/dist/api/mobile.d.ts +3 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +23 -10
- package/dist/ios/src/testing.d.ts +7 -1
- package/dist/ios/templates/CraftHealth.swift +11 -0
- package/dist/ios/templates/CraftHybrid.swift +19 -0
- package/dist/ios/templates/CraftNativeScreen.swift +411 -46
- package/dist/ios/templates/testing/DriverTests.swift.template +161 -145
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ import XCTest
|
|
|
26
26
|
/// expectexact <label> [| secs] a button or text whose label is exactly this
|
|
27
27
|
/// absent <text> logs PASS when no element contains text
|
|
28
28
|
/// expectid <id> [| secs] / absentid <id> the same by accessibility identifier (testID)
|
|
29
|
+
/// labelid <id> <text> that element's label is exactly text (a count, a status)
|
|
29
30
|
/// tabbar shown|hidden the native tab bar (Today, Calendar, …) can be tapped, or not
|
|
30
31
|
/// selected <tab> the tab bar's selected button is this one
|
|
31
32
|
/// tab <name> tap a tab bar button
|
|
@@ -34,6 +35,7 @@ import XCTest
|
|
|
34
35
|
/// audit <name> accessibility audit (element descriptions, Dynamic Type, contrast); logs each issue
|
|
35
36
|
/// unlabelled logs every hittable button without a label (FAIL if any)
|
|
36
37
|
/// dismiss [label] a system prompt's button (default Not Now: "Save Password?")
|
|
38
|
+
/// ifid <id> <command…> runs the command only when that element is there (a screen shown the first time)
|
|
37
39
|
/// log <text>
|
|
38
40
|
///
|
|
39
41
|
/// Checks log `PASS …` / `FAIL …` lines into driver.log; matrix.sh counts them.
|
|
@@ -153,153 +155,167 @@ final class DriverTests: XCTestCase {
|
|
|
153
155
|
for raw in script.split(separator: "\n") {
|
|
154
156
|
let line = raw.trimmingCharacters(in: .whitespaces)
|
|
155
157
|
if line.isEmpty || line.hasPrefix("#") { continue }
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
case "
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
while !done && Date() < end {
|
|
296
|
-
if let button = candidates.first(where: { $0.exists && $0.isHittable }) { button.tap(); done = true }
|
|
297
|
-
else { Thread.sleep(forTimeInterval: 0.5) }
|
|
158
|
+
run(line)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func run(_ line: String) {
|
|
163
|
+
let parts = line.split(separator: " ", maxSplits: 1).map(String.init)
|
|
164
|
+
let command = parts[0]
|
|
165
|
+
let arg = parts.count > 1 ? parts[1] : ""
|
|
166
|
+
switch command {
|
|
167
|
+
case "ifid":
|
|
168
|
+
let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
169
|
+
if rest.count == 2, app.descendants(matching: .any)[rest[0]].waitForExistence(timeout: 3) { note("ifid \(rest[0]): shown"); run(rest[1]) }
|
|
170
|
+
else { note("ifid \(rest.first ?? ""): not shown") }
|
|
171
|
+
case "launch": app.launch()
|
|
172
|
+
case "activate": app.activate()
|
|
173
|
+
case "terminate": app.terminate()
|
|
174
|
+
case "home": XCUIDevice.shared.press(.home)
|
|
175
|
+
case "background":
|
|
176
|
+
XCUIDevice.shared.press(.home)
|
|
177
|
+
Thread.sleep(forTimeInterval: Double(arg) ?? 3)
|
|
178
|
+
app.activate()
|
|
179
|
+
case "wait": Thread.sleep(forTimeInterval: Double(arg) ?? 1)
|
|
180
|
+
case "net": _ = get("/\(arg)"); note("net \(arg)")
|
|
181
|
+
case "delay": _ = get("/delay?ms=\(arg)"); note("delay \(arg)ms")
|
|
182
|
+
case "exec":
|
|
183
|
+
let out = get("/exec?name=\(arg)", timeout: 180).flatMap { String(data: $0, encoding: .utf8) } ?? "no answer"
|
|
184
|
+
note("exec \(arg): \(out.trimmingCharacters(in: .whitespacesAndNewlines))")
|
|
185
|
+
case "openurl":
|
|
186
|
+
if let url = URL(string: arg) {
|
|
187
|
+
XCUIDevice.shared.system.open(url)
|
|
188
|
+
answerOpenPrompt()
|
|
189
|
+
note("openurl \(arg)")
|
|
190
|
+
}
|
|
191
|
+
case "shot":
|
|
192
|
+
let shot = XCUIScreen.main.screenshot()
|
|
193
|
+
post("/save?name=\(arg).png", shot.pngRepresentation)
|
|
194
|
+
case "tree":
|
|
195
|
+
post("/save?name=\(arg).txt", Data(app.debugDescription.utf8))
|
|
196
|
+
case "tap":
|
|
197
|
+
if let target = waitFor(arg, 10) { press(target); note("tap \(arg): ok") }
|
|
198
|
+
else { XCTFail("Element not found"); note("FAIL tap \(arg): NOT FOUND") }
|
|
199
|
+
case "tapif":
|
|
200
|
+
if let target = element(containing: arg), target.isHittable { press(target); note("tapif \(arg): tapped") }
|
|
201
|
+
else { note("tapif \(arg): not shown") }
|
|
202
|
+
case "tapexact":
|
|
203
|
+
if let target = poll(10, { exact(arg) }) { press(target); note("tapexact \(arg): ok") }
|
|
204
|
+
else { XCTFail("Element not found"); note("FAIL tapexact \(arg): NOT FOUND") }
|
|
205
|
+
case "tapid":
|
|
206
|
+
let target = app.descendants(matching: .any)[arg]
|
|
207
|
+
if target.waitForExistence(timeout: 10) { press(target); note("tapid \(arg): ok") }
|
|
208
|
+
else { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
|
|
209
|
+
case "tapxy":
|
|
210
|
+
let xy = arg.split(separator: " ").compactMap { Double($0) }
|
|
211
|
+
if xy.count == 2 && xy.allSatisfy({ $0 >= 0 && $0 <= 1 }) {
|
|
212
|
+
app.coordinate(withNormalizedOffset: CGVector(dx: xy[0], dy: xy[1])).tap()
|
|
213
|
+
} else { XCTFail("tapxy needs two coordinates between 0 and 1") }
|
|
214
|
+
case "back":
|
|
215
|
+
let bar = app.navigationBars.buttons.firstMatch
|
|
216
|
+
if bar.exists && bar.isHittable { bar.tap(); note("back: bar button") }
|
|
217
|
+
else {
|
|
218
|
+
let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.02, dy: 0.5))
|
|
219
|
+
start.press(forDuration: 0.05, thenDragTo: app.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5)))
|
|
220
|
+
note("back: edge swipe")
|
|
221
|
+
}
|
|
222
|
+
case "type": app.typeText(arg)
|
|
223
|
+
case "return": app.typeText(XCUIKeyboardKey.return.rawValue); note("return")
|
|
224
|
+
case "typeid":
|
|
225
|
+
let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
226
|
+
let target = app.descendants(matching: .any)[idParts[0]]
|
|
227
|
+
if target.waitForExistence(timeout: 10) {
|
|
228
|
+
target.tap()
|
|
229
|
+
Thread.sleep(forTimeInterval: 0.8)
|
|
230
|
+
app.typeText(idParts.count > 1 ? idParts[1] : "")
|
|
231
|
+
note("typeid \(idParts[0]): ok")
|
|
232
|
+
} else { XCTFail("Element not found"); note("FAIL typeid \(idParts[0]): NOT FOUND") }
|
|
233
|
+
case "field":
|
|
234
|
+
let fieldParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
235
|
+
let index = Int(fieldParts[0]) ?? 0
|
|
236
|
+
let fields = app.textFields.allElementsBoundByIndex + app.secureTextFields.allElementsBoundByIndex
|
|
237
|
+
if index >= 0 && index < fields.count {
|
|
238
|
+
fields[index].tap()
|
|
239
|
+
Thread.sleep(forTimeInterval: 0.6)
|
|
240
|
+
app.typeText(fieldParts.count > 1 ? fieldParts[1] : "")
|
|
241
|
+
note("field \(index): ok")
|
|
242
|
+
} else { XCTFail("Element not found"); note("FAIL field \(index): NOT FOUND (\(fields.count) fields)") }
|
|
243
|
+
case "expect":
|
|
244
|
+
let (text, seconds) = split(arg)
|
|
245
|
+
check(waitFor(text, seconds) != nil, "expect \(text)")
|
|
246
|
+
case "expectexact":
|
|
247
|
+
let (text, seconds) = split(arg)
|
|
248
|
+
check(poll(seconds, { exact(text) }) != nil, "expectexact \(text)")
|
|
249
|
+
case "expectid":
|
|
250
|
+
let (id, seconds) = split(arg)
|
|
251
|
+
check(app.descendants(matching: .any)[id].waitForExistence(timeout: seconds), "expectid \(id)")
|
|
252
|
+
case "labelid":
|
|
253
|
+
let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
254
|
+
let target = app.descendants(matching: .any)[idParts[0]]
|
|
255
|
+
let want = idParts.count > 1 ? idParts[1] : ""
|
|
256
|
+
let seen = target.waitForExistence(timeout: 5) ? target.label : "(missing)"
|
|
257
|
+
check(seen == want, "labelid \(idParts[0]) \(want) (saw \(seen))")
|
|
258
|
+
case "absentid":
|
|
259
|
+
check(!app.descendants(matching: .any)[arg].exists, "absentid \(arg)")
|
|
260
|
+
case "absent":
|
|
261
|
+
check(element(containing: arg) == nil, "absent \(arg)")
|
|
262
|
+
case "tabbar":
|
|
263
|
+
let want = arg != "hidden"
|
|
264
|
+
let end = Date().addingTimeInterval(5)
|
|
265
|
+
var shown = tabBarShown()
|
|
266
|
+
while shown != want && Date() < end { Thread.sleep(forTimeInterval: 0.5); shown = tabBarShown() }
|
|
267
|
+
check(shown == want, "tabbar \(arg)")
|
|
268
|
+
case "selected":
|
|
269
|
+
check(poll(5, { tabButton(arg).flatMap { $0.isSelected ? $0 : nil } }) != nil, "selected \(arg)")
|
|
270
|
+
case "tab":
|
|
271
|
+
if let button = poll(10, { tabButton(arg) }) { button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap(); note("tab \(arg): ok") }
|
|
272
|
+
else { XCTFail("Element not found"); note("FAIL tab \(arg): NOT FOUND") }
|
|
273
|
+
case "scrollto":
|
|
274
|
+
var found = element(containing: arg)
|
|
275
|
+
var swipes = 0
|
|
276
|
+
while (found == nil || !(found!.isHittable)) && swipes < 8 {
|
|
277
|
+
app.swipeUp()
|
|
278
|
+
swipes += 1
|
|
279
|
+
found = element(containing: arg)
|
|
280
|
+
}
|
|
281
|
+
check(found != nil && found!.isHittable, "scrollto \(arg)")
|
|
282
|
+
case "swipe":
|
|
283
|
+
switch arg {
|
|
284
|
+
case "up": app.swipeUp()
|
|
285
|
+
case "down": app.swipeDown()
|
|
286
|
+
case "left": app.swipeLeft()
|
|
287
|
+
default: app.swipeRight()
|
|
288
|
+
}
|
|
289
|
+
case "audit":
|
|
290
|
+
var issues: [String] = []
|
|
291
|
+
do {
|
|
292
|
+
try app.performAccessibilityAudit(for: [.sufficientElementDescription, .dynamicType, .contrast, .hitRegion]) { issue in
|
|
293
|
+
let label = issue.element?.label ?? ""
|
|
294
|
+
let id = issue.element?.identifier ?? ""
|
|
295
|
+
issues.append("\(issue.auditType.rawValue) \(issue.compactDescription) [\(label)|\(id)]")
|
|
296
|
+
return true
|
|
298
297
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
} catch { issues.append("audit error \(error)") }
|
|
299
|
+
post("/save?name=audit-\(arg).txt", Data(issues.joined(separator: "\n").utf8))
|
|
300
|
+
note("AUDIT \(arg): \(issues.count) issue(s)")
|
|
301
|
+
case "unlabelled":
|
|
302
|
+
let buttons = app.buttons.allElementsBoundByIndex.filter { $0.exists && $0.isHittable && $0.label.trimmingCharacters(in: .whitespaces).isEmpty }
|
|
303
|
+
let where_ = buttons.prefix(5).map { "\(Int($0.frame.midX)),\(Int($0.frame.midY)) id=\($0.identifier)" }.joined(separator: "; ")
|
|
304
|
+
check(buttons.isEmpty, "unlabelled buttons \(arg) \(buttons.count) \(where_)")
|
|
305
|
+
case "dismiss":
|
|
306
|
+
// A system prompt over the app ("Save Password?" → Not Now), wherever iOS shows it.
|
|
307
|
+
let label = arg.isEmpty ? "Not Now" : arg
|
|
308
|
+
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
|
|
309
|
+
let candidates = [app.buttons[label], springboard.buttons[label]]
|
|
310
|
+
let end = Date().addingTimeInterval(6)
|
|
311
|
+
var done = false
|
|
312
|
+
while !done && Date() < end {
|
|
313
|
+
if let button = candidates.first(where: { $0.exists && $0.isHittable }) { button.tap(); done = true }
|
|
314
|
+
else { Thread.sleep(forTimeInterval: 0.5) }
|
|
302
315
|
}
|
|
316
|
+
note("dismiss \(label): \(done ? "tapped" : "none shown")")
|
|
317
|
+
case "log": note(arg)
|
|
318
|
+
default: XCTFail("Unknown command \(command)"); note("FAIL unknown command \(command)")
|
|
303
319
|
}
|
|
304
320
|
}
|
|
305
321
|
}
|