@craft-native/ios 0.0.449 → 0.0.464

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.
@@ -61,6 +61,8 @@ final class DriverTests: XCTestCase {
61
61
  let tabs: [String] = {{TABS}}
62
62
  let tabIdentifiers: [String: String] = {{TAB_IDENTIFIERS}}
63
63
  private var bundledJourney = false
64
+ private var lastTappedInput: XCUIElement?
65
+ private var lastBooleanTap: [String: Any]?
64
66
 
65
67
  func get(_ path: String, timeout: Double = 15) -> Data? {
66
68
  var result: Data?
@@ -245,6 +247,9 @@ final class DriverTests: XCTestCase {
245
247
  /// nor any descendant has keyboard focus". A later tap lands once the app
246
248
  /// is idle again.
247
249
  func focus(_ target: XCUIElement, seconds: Double = 15) -> Bool {
250
+ // Retapping an already focused field can collapse its selection. A
251
+ // caller that just selected text must type into that same selection.
252
+ if hasKeyboardFocus(target) { return true }
248
253
  let end = Date().addingTimeInterval(seconds)
249
254
  repeat {
250
255
  target.tap()
@@ -290,24 +295,39 @@ final class DriverTests: XCTestCase {
290
295
  return (parts[0], parts.count > 1 ? Double(parts[1]) ?? 10 : 10)
291
296
  }
292
297
 
293
- /// A switch reports "0" or "1". Any other value is not a switch state to
294
- /// wait on, so the tap stands as it would for any other control.
295
298
  /// The switch answers `/exec` with 200 whatever the hook did, its first
296
299
  /// word the hook's exit code: only `exit 0` is a hook that worked.
297
300
  func hookSucceeded(_ answer: String) -> Bool {
298
- answer.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("exit 0")
301
+ // get() already requires HTTP success. Custom control servers may
302
+ // return a message or no body. Preserve the legacy shell protocol's
303
+ // explicit failures, without accepting "exit 01" as a zero status.
304
+ let body = answer.trimmingCharacters(in: .whitespacesAndNewlines)
305
+ if body == "no such hook" { return false }
306
+ let fields = body.split(whereSeparator: { $0.isWhitespace })
307
+ guard fields.first == "exit" else { return true }
308
+ return fields.count >= 2 && fields[1] == "0"
299
309
  }
300
310
 
311
+ /// UIKit and Craft's solid toggle both publish "0" or "1", regardless
312
+ /// of the accessibility element type that XCTest assigns to them.
301
313
  func switchFlipped(from before: String?, to after: String?) -> Bool {
302
314
  let states: Set<String> = ["0", "1"]
303
- guard let before, let after, states.contains(before), states.contains(after) else { return true }
315
+ // A lost accessibility value is missing evidence, not a successful tap.
316
+ guard let before, let after, states.contains(before), states.contains(after) else { return false }
304
317
  return before != after
305
318
  }
306
319
 
307
320
  /// Deliver one gesture; the command's observation determines success.
308
321
  func press(_ element: XCUIElement) {
322
+ // Resolve this before activation: a menu item can disappear as soon
323
+ // as its gesture completes (for example Select All).
324
+ let input = [.textField, .secureTextField, .textView, .searchField].contains(element.elementType)
325
+ ? element : nil
309
326
  if element.isHittable { element.tap() }
310
327
  else { element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() }
328
+ // A tap on a read-only text view need not open the keyboard. Only a
329
+ // following type command observes whether this target took focus.
330
+ lastTappedInput = input
311
331
  }
312
332
 
313
333
  func answerOpenPrompt() {
@@ -343,6 +363,11 @@ final class DriverTests: XCTestCase {
343
363
  let parts = line.split(separator: " ", maxSplits: 1).map(String.init)
344
364
  let command = parts[0]
345
365
  let arg = parts.count > 1 ? parts[1] : ""
366
+ // Context changes can move focus without going through press().
367
+ if ["launch", "activate", "terminate", "home", "background", "openurl", "universallink",
368
+ "back", "return", "tab", "tapxy", "edge", "notifications", "systemtap", "longpressid", "exec"].contains(command) {
369
+ lastTappedInput = nil
370
+ }
346
371
  if bundledJourney && ["net", "delay", "exec"].contains(command) {
347
372
  check(false, "\(command) requires a control-server journey")
348
373
  return
@@ -460,12 +485,15 @@ final class DriverTests: XCTestCase {
460
485
  if let target = poll(10, { exact(arg) }) { press(target); note("tapexact \(arg): ok") }
461
486
  else { XCTFail("Element not found"); note("FAIL tapexact \(arg): NOT FOUND") }
462
487
  case "tapid":
488
+ lastBooleanTap = nil
463
489
  let target = identified(arg)
464
490
  if !target.waitForExistence(timeout: 10) { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
465
- else if target.elementType == .switch {
491
+ else if target.elementType == .switch || ["0", "1"].contains(target.value as? String ?? "") {
466
492
  // Observe one physical gesture. Retapping could conceal a
467
493
  // renderer or hit-testing failure (HQ agreement job 7570).
468
494
  let before = target.value as? String
495
+ let enabledBefore = target.isEnabled
496
+ let frameBefore = target.frame
469
497
  press(target)
470
498
  let settle = Date().addingTimeInterval(1.5)
471
499
  var flipped = false
@@ -473,6 +501,15 @@ final class DriverTests: XCTestCase {
473
501
  flipped = switchFlipped(from: before, to: target.value as? String)
474
502
  if !flipped { Thread.sleep(forTimeInterval: 0.25) }
475
503
  } while !flipped && Date() < settle
504
+ let observation: [String: Any] = [
505
+ "id": arg, "gestures": 1, "flipped": flipped,
506
+ "enabledBefore": enabledBefore, "enabledAfter": target.isEnabled,
507
+ "valueBefore": before ?? "unknown", "valueAfter": target.value as? String ?? "unknown",
508
+ "frameBefore": [frameBefore.origin.x, frameBefore.origin.y, frameBefore.width, frameBefore.height],
509
+ ]
510
+ lastBooleanTap = observation
511
+ if let data = try? JSONSerialization.data(withJSONObject: observation, options: [.sortedKeys]),
512
+ let json = String(data: data, encoding: .utf8) { note("BOOLEAN_TAP \(json)") }
476
513
  check(flipped, "tapid \(arg) flips the switch")
477
514
  }
478
515
  else { press(target); note("tapid \(arg): ok") }
@@ -482,6 +519,12 @@ final class DriverTests: XCTestCase {
482
519
  check(false, "longpressid \(arg) is hittable")
483
520
  return
484
521
  }
522
+ if [.textField, .secureTextField, .textView].contains(target.elementType) {
523
+ guard focus(target) else {
524
+ check(false, "longpressid \(arg) takes keyboard focus")
525
+ return
526
+ }
527
+ }
485
528
  target.press(forDuration: 1.2)
486
529
  note("longpressid \(arg): ok")
487
530
  case "holdtab":
@@ -580,9 +623,20 @@ final class DriverTests: XCTestCase {
580
623
  start.press(forDuration: 0.05, thenDragTo: app.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5)))
581
624
  note("back: edge swipe")
582
625
  }
583
- case "type": app.typeText(arg)
626
+ case "type":
627
+ let target = lastTappedInput ?? app
628
+ lastTappedInput = nil
629
+ // A returned gesture is not proof of keyboard focus. Observe the
630
+ // one gesture before typing, including WebKit/cold-keyboard focus
631
+ // delivery. Never retap and conceal a dropped input event.
632
+ guard poll(15, { self.hasKeyboardFocus(target) ? target : nil }) != nil else {
633
+ check(false, "type into \(target.label) takes keyboard focus")
634
+ return
635
+ }
636
+ app.typeText(arg)
584
637
  case "return": app.typeText(XCUIKeyboardKey.return.rawValue); note("return")
585
638
  case "typeid":
639
+ lastTappedInput = nil
586
640
  let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
587
641
  let target = identified(idParts[0])
588
642
  if target.waitForExistence(timeout: 10) {
@@ -611,6 +665,7 @@ final class DriverTests: XCTestCase {
611
665
  let target = XCUIApplication(bundleIdentifier: bundle)
612
666
  check(target.wait(for: .runningForeground, timeout: seconds), "expectapp \(bundle)")
613
667
  case "field":
668
+ lastTappedInput = nil
614
669
  let fieldParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
615
670
  let index = Int(fieldParts[0]) ?? 0
616
671
  let fields = app.textFields.allElementsBoundByIndex + app.secureTextFields.allElementsBoundByIndex