@craft-native/ios 0.0.205 → 0.0.206

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/README.md CHANGED
@@ -172,6 +172,16 @@ the [Native STX guide](../../docs/guides/native-stx.md). On macOS,
172
172
  `bun packages/ios/scripts/test-native-render.ts` runs the renderer's unit and
173
173
  simulator UI tests.
174
174
 
175
+ `initSimulatorDriver` can also drive an installed release on a paired iPhone.
176
+ Pass `script: serializeSimulatorSteps(steps)` to bundle a journey in the test
177
+ target. `shot`, `tree` and `systemtree` then save native XCTest attachments;
178
+ checks appear in the Xcode test log. Generate the project with XcodeGen, sign
179
+ the test runner for your team and run it against the device. The installed app
180
+ under test is activated by its bundle identifier and is not rebuilt by the
181
+ driver. Export evidence with `xcrun xcresulttool export attachments --path
182
+ <result.xcresult> --output-path <evidence>`. Omitting `script` restores the
183
+ control-server protocol, required for the `net`, `delay` and `exec` hooks.
184
+
175
185
  The iOS host uses the shared tree-layout ABI for eligible definite-size,
176
186
  non-grid, non-wrapped containers and retains a local fallback for older
177
187
  runtimes or unsupported styles. Both paths implement wrapped-line and grid
package/dist/index.js CHANGED
@@ -102,6 +102,17 @@ function initSimulatorDriver(options) {
102
102
  if (previous !== content)
103
103
  writeFileSync(path, content);
104
104
  }
105
+ const script = join2(output, "UITests/journey.txt");
106
+ if (options.script === undefined)
107
+ rmSync(script, { force: true });
108
+ else {
109
+ let previous;
110
+ try {
111
+ previous = readFileSync(script, "utf8");
112
+ } catch {}
113
+ if (previous !== options.script)
114
+ writeFileSync(script, options.script);
115
+ }
105
116
  }
106
117
  function serializeSimulatorSteps(steps) {
107
118
  const number = (value) => {
package/dist/testing.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface SimulatorDriverOptions {
7
7
  deploymentTarget?: string;
8
8
  controlUrl?: string;
9
9
  tabs?: readonly string[];
10
+ /** Bundle a wire-protocol journey and save native XCTest attachments. No control server is needed on physical devices. */
11
+ script?: string;
10
12
  }
11
13
  /** Generate the reusable XCUITest shell from package templates. No app-owned Swift. */
12
14
  export declare function initSimulatorDriver(options: SimulatorDriverOptions): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.205",
3
+ "version": "0.0.206",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -348,6 +348,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
348
348
  }
349
349
  }
350
350
 
351
+ func setRefreshAppearance(_ props: [String: Any]) {
352
+ nativeRefreshControl.tintColor = (props["tintColor"] as? String).flatMap { UIColor(hex: $0) }
353
+ }
354
+
351
355
  @objc private func refreshTriggered() {
352
356
  refreshHandler?()
353
357
  DispatchQueue.main.async { [weak self] in
@@ -3318,6 +3318,7 @@ private final class CraftNativeSvgView: UIView {
3318
3318
  var linejoin: String?
3319
3319
  var dashes: [CGFloat]?
3320
3320
  var vectorEffect: String?
3321
+ var fontWeight: String?
3321
3322
  var dominantBaseline: String?
3322
3323
 
3323
3324
  func merged(_ node: [String: Any]) -> Paint {
@@ -3330,6 +3331,11 @@ private final class CraftNativeSvgView: UIView {
3330
3331
  let value = CGFloat(truncating: number)
3331
3332
  return value.isFinite ? value : nil
3332
3333
  }
3334
+ func string(_ raw: Any?) -> String? {
3335
+ if let value = raw as? String { return value }
3336
+ if let number = raw as? NSNumber { return number.stringValue }
3337
+ return nil
3338
+ }
3333
3339
  if let color = style["color"] as? String { next.color = color }
3334
3340
  if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
3335
3341
  if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
@@ -3346,6 +3352,7 @@ private final class CraftNativeSvgView: UIView {
3346
3352
  if let join = value("strokeLinejoin") as? String { next.linejoin = join }
3347
3353
  if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
3348
3354
  if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
3355
+ if let fontWeight = string(value("fontWeight")) { next.fontWeight = fontWeight }
3349
3356
  if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
3350
3357
  return next
3351
3358
  }
@@ -3446,7 +3453,14 @@ private final class CraftNativeSvgView: UIView {
3446
3453
  let props = node["props"] as? [String: Any] ?? [:]
3447
3454
  let style = node["style"] as? [String: Any] ?? [:]
3448
3455
  let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
3449
- let weight: UIFont.Weight = (props["fontWeight"] as? String ?? style["fontWeight"] as? String).map { ["500": .medium, "600": .semibold, "700": .bold, "bold": .bold][$0] ?? .regular } ?? .regular
3456
+ let weight: UIFont.Weight = {
3457
+ switch paint.fontWeight?.lowercased() {
3458
+ case "500": return .medium
3459
+ case "600": return .semibold
3460
+ case "bold", "700", "800", "900": return .bold
3461
+ default: return .regular
3462
+ }
3463
+ }()
3450
3464
  let font = UIFont.systemFont(ofSize: size, weight: weight)
3451
3465
  let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
3452
3466
  let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
@@ -5779,6 +5793,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5779
5793
  updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
5780
5794
  case "ImageBackground":
5781
5795
  let background = result as! CraftNativeImageBackgroundView
5796
+ let backgroundID = ObjectIdentifier(background)
5797
+ if let tint = color(style["tintColor"]) {
5798
+ tintedImages.insert(backgroundID)
5799
+ background.tintColor = tint
5800
+ } else {
5801
+ tintedImages.remove(backgroundID)
5802
+ background.tintColor = nil
5803
+ }
5804
+ if let current = background.image { background.image = imageForDisplay(current, view: background) }
5782
5805
  let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
5783
5806
  background.contentMode = imageContentMode(requestedResizeMode)
5784
5807
  background.clipsToBounds = background.contentMode == .scaleAspectFill
@@ -6139,6 +6162,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6139
6162
  let owner = parent.protocolId ?? path
6140
6163
  flatListOwners = flatListOwners.filter { $0.value != owner }
6141
6164
  for child in children { registerFlatListOwnership(child, owner: owner) }
6165
+ let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
6166
+ let propRefreshEvents = propRefresh["events"] as? [String: String] ?? [:]
6167
+ let refreshHandler = nonEmptyHandler(propRefreshEvents["onRefresh"])
6168
+ ?? nonEmptyHandler(events["onRefresh"])
6169
+ let refreshing = (propRefresh["refreshing"] ?? props["refreshing"]) as? Bool == true
6142
6170
  list.bounces = props["bounces"] as? Bool ?? true
6143
6171
  list.isScrollEnabled = props["scrollEnabled"] as? Bool != false
6144
6172
  list.onFittingHeightChanged = { [weak list] in
@@ -6147,9 +6175,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6147
6175
  }
6148
6176
  list.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
6149
6177
  list.setContentContainerStyle(props["contentContainerStyle"] as? [String: Any])
6150
- list.setRefreshHandler(nonEmptyHandler(events["onRefresh"]).map { [weak self] handler in
6178
+ list.setRefreshHandler(refreshHandler.map { [weak self] handler in
6151
6179
  { self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
6152
- }, refreshing: props["refreshing"] as? Bool == true)
6180
+ }, refreshing: refreshing)
6181
+ list.setRefreshAppearance(propRefresh)
6153
6182
  list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
6154
6183
  list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
6155
6184
  { [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
@@ -42,11 +42,14 @@ import XCTest
42
42
  /// ifid <id> <command…> runs the command only when that element is there (a screen shown the first time)
43
43
  /// log <text>
44
44
  ///
45
- /// Checks log `PASS …` / `FAIL …` lines into driver.log; matrix.sh counts them.
45
+ /// With a bundled journey.txt, evidence is kept as native XCTest attachments
46
+ /// and PASS/FAIL lines appear in the test log. Control-server journeys also
47
+ /// write driver.log; matrix.sh counts those checks.
46
48
  final class DriverTests: XCTestCase {
47
49
  let control = ProcessInfo.processInfo.environment["CRAFT_TEST_CONTROL"] ?? {{CONTROL_URL}}
48
50
  let app = XCUIApplication(bundleIdentifier: {{BUNDLE_ID}})
49
51
  let tabs: [String] = {{TABS}}
52
+ private var bundledJourney = false
50
53
 
51
54
  func get(_ path: String, timeout: Double = 15) -> Data? {
52
55
  var result: Data?
@@ -57,6 +60,15 @@ final class DriverTests: XCTestCase {
57
60
  }
58
61
 
59
62
  func post(_ path: String, _ body: Data) {
63
+ if bundledJourney {
64
+ guard path.hasPrefix("/save?"),
65
+ let name = URLComponents(string: path)?.queryItems?.first(where: { $0.name == "name" })?.value else { return }
66
+ let attachment = XCTAttachment(data: body, uniformTypeIdentifier: name.hasSuffix(".png") ? "public.png" : "public.plain-text")
67
+ attachment.name = name
68
+ attachment.lifetime = .keepAlways
69
+ add(attachment)
70
+ return
71
+ }
60
72
  var request = URLRequest(url: URL(string: control + path)!)
61
73
  request.httpMethod = "POST"
62
74
  request.httpBody = body
@@ -67,7 +79,7 @@ final class DriverTests: XCTestCase {
67
79
 
68
80
  func note(_ text: String) {
69
81
  print("DRIVER \(text)")
70
- post("/note", Data(text.utf8))
82
+ if !bundledJourney { post("/note", Data(text.utf8)) }
71
83
  }
72
84
 
73
85
  func check(_ ok: Bool, _ what: String) {
@@ -156,7 +168,14 @@ final class DriverTests: XCTestCase {
156
168
 
157
169
  func testScript() throws {
158
170
  continueAfterFailure = true
159
- guard let data = get("/script"), let script = String(data: data, encoding: .utf8) else {
171
+ let data: Data?
172
+ if let resource = Bundle(for: DriverTests.self).url(forResource: "journey", withExtension: "txt") {
173
+ bundledJourney = true
174
+ data = try Data(contentsOf: resource)
175
+ } else {
176
+ data = get("/script")
177
+ }
178
+ guard let data, let script = String(data: data, encoding: .utf8) else {
160
179
  XCTFail("no script")
161
180
  return
162
181
  }
@@ -171,6 +190,10 @@ final class DriverTests: XCTestCase {
171
190
  let parts = line.split(separator: " ", maxSplits: 1).map(String.init)
172
191
  let command = parts[0]
173
192
  let arg = parts.count > 1 ? parts[1] : ""
193
+ if bundledJourney && ["net", "delay", "exec"].contains(command) {
194
+ check(false, "\(command) requires a control-server journey")
195
+ return
196
+ }
174
197
  switch command {
175
198
  case "ifid":
176
199
  let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)