@craft-native/ios 0.0.435 → 0.0.436

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.435",
3
+ "version": "0.0.436",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,22 @@ struct CraftNativeMutationFailure: Error, Equatable {
18
18
  struct CraftNativeMutationResult {
19
19
  let batchId: String
20
20
  let revision: Int
21
- let document: [String: Any]?
22
21
  let updatedNodeIds: [String]
23
22
  let affectedNodeIds: [String]
24
23
  let requiresFullRender: Bool
24
+ private let materialize: () -> [String: Any]?
25
+ /// The committed tree, built when asked for: a host that redraws only the
26
+ /// nodes a batch touched never needs the whole tree.
27
+ var document: [String: Any]? { materialize() }
28
+
29
+ init(batchId: String, revision: Int, document: @escaping () -> [String: Any]?, updatedNodeIds: [String], affectedNodeIds: [String], requiresFullRender: Bool) {
30
+ self.batchId = batchId
31
+ self.revision = revision
32
+ self.materialize = document
33
+ self.updatedNodeIds = updatedNodeIds
34
+ self.affectedNodeIds = affectedNodeIds
35
+ self.requiresFullRender = requiresFullRender
36
+ }
25
37
  }
26
38
 
27
39
  /// Validates and applies versioned native-tree mutations without touching UIKit.
@@ -91,6 +103,8 @@ final class CraftNativeMutationDocument {
91
103
  var updatedNodeIds: [String] = []
92
104
  var affectedNodeIds: [String] = []
93
105
  var requiresFullRender = false
106
+ // Only an operation that adds, moves or removes nodes can leave one unreachable.
107
+ var reshaped = false
94
108
  for (index, value) in operations.enumerated() {
95
109
  guard let operation = value as? [String: Any], let name = operation["op"] as? String else {
96
110
  throw CraftNativeMutationFailure(
@@ -99,6 +113,7 @@ final class CraftNativeMutationDocument {
99
113
  operationIndex: index
100
114
  )
101
115
  }
116
+ if name != "updateNode" { reshaped = true }
102
117
  do {
103
118
  switch name {
104
119
  case "createNode":
@@ -147,7 +162,7 @@ final class CraftNativeMutationDocument {
147
162
  guard let root = candidateNodes[candidateRoot], root.parent == nil else {
148
163
  throw CraftNativeMutationFailure("INVALID_TREE", "root node must not have a parent")
149
164
  }
150
- guard descendants(of: candidateRoot, nodes: candidateNodes).count + 1 == candidateNodes.count else {
165
+ guard !reshaped || descendants(of: candidateRoot, nodes: candidateNodes).count + 1 == candidateNodes.count else {
151
166
  throw CraftNativeMutationFailure("INVALID_TREE", "all nodes must be reachable from the root")
152
167
  }
153
168
  }
@@ -158,7 +173,7 @@ final class CraftNativeMutationDocument {
158
173
  return CraftNativeMutationResult(
159
174
  batchId: batchId,
160
175
  revision: nextRevision,
161
- document: candidateRoot.flatMap { materialize($0, nodes: candidateNodes) },
176
+ document: { [candidateRoot, candidateNodes, weak self] in candidateRoot.flatMap { self?.materialize($0, nodes: candidateNodes) } },
162
177
  updatedNodeIds: updatedNodeIds,
163
178
  affectedNodeIds: affectedNodeIds,
164
179
  requiresFullRender: requiresFullRender
@@ -285,8 +285,15 @@ final class DriverTests: XCTestCase {
285
285
  return (parts[0], parts.count > 1 ? Double(parts[1]) ?? 10 : 10)
286
286
  }
287
287
 
288
- /// A tap that never fails the run: an element covered a moment (the big
289
- /// timer coming up) is tapped where it is, and the check after says.
288
+ /// A switch reports "0" or "1". Any other value is not a switch state to
289
+ /// wait on, so the tap stands as it would for any other control.
290
+ func switchFlipped(from before: String?, to after: String?) -> Bool {
291
+ let states: Set<String> = ["0", "1"]
292
+ guard let before, let after, states.contains(before), states.contains(after) else { return true }
293
+ return before != after
294
+ }
295
+
296
+ /// Deliver one gesture; the command's observation determines success.
290
297
  func press(_ element: XCUIElement) {
291
298
  if element.isHittable { element.tap() }
292
299
  else { element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() }
@@ -438,8 +445,21 @@ final class DriverTests: XCTestCase {
438
445
  else { XCTFail("Element not found"); note("FAIL tapexact \(arg): NOT FOUND") }
439
446
  case "tapid":
440
447
  let target = identified(arg)
441
- if target.waitForExistence(timeout: 10) { press(target); note("tapid \(arg): ok") }
442
- else { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
448
+ if !target.waitForExistence(timeout: 10) { XCTFail("Element not found"); note("FAIL tapid \(arg): NOT FOUND") }
449
+ else if target.elementType == .switch {
450
+ // Observe one physical gesture. Retapping could conceal a
451
+ // renderer or hit-testing failure (HQ agreement job 7570).
452
+ let before = target.value as? String
453
+ press(target)
454
+ let settle = Date().addingTimeInterval(1.5)
455
+ var flipped = false
456
+ repeat {
457
+ flipped = switchFlipped(from: before, to: target.value as? String)
458
+ if !flipped { Thread.sleep(forTimeInterval: 0.25) }
459
+ } while !flipped && Date() < settle
460
+ check(flipped, "tapid \(arg) flips the switch")
461
+ }
462
+ else { press(target); note("tapid \(arg): ok") }
443
463
  case "longpressid":
444
464
  let target = identified(arg)
445
465
  guard target.waitForExistence(timeout: 10), target.isHittable else {