@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
|
@@ -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 =
|
|
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(
|
|
6178
|
+
list.setRefreshHandler(refreshHandler.map { [weak self] handler in
|
|
6151
6179
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
6152
|
-
}, refreshing:
|
|
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
|
-
///
|
|
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
|
-
|
|
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)
|