craft-native 0.0.205 → 0.0.207
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/CraftNativeRefreshableFlatList.kt.template +7 -0
- package/dist/android/templates/MainActivityNative.kt.template +253 -166
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +5 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +11 -0
- package/dist/ios/src/testing.d.ts +2 -0
- package/dist/ios/templates/CraftHybrid.swift +3 -1
- package/dist/ios/templates/CraftNativeFlatList.swift +4 -0
- package/dist/ios/templates/CraftNativeScreen.swift +45 -4
- package/dist/ios/templates/testing/DriverTests.swift.template +26 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.207";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -527,6 +527,8 @@ export interface SvgPaintProps extends BaseProps {
|
|
|
527
527
|
vectorEffect?: 'none' | 'non-scaling-stroke';
|
|
528
528
|
color?: string;
|
|
529
529
|
opacity?: number;
|
|
530
|
+
/** Font weight inherited by descendant native SVG text. */
|
|
531
|
+
fontWeight?: string | number;
|
|
530
532
|
}
|
|
531
533
|
/** Root native SVG drawing surface. */
|
|
532
534
|
export interface SvgProps extends SvgPaintProps {
|
|
@@ -559,7 +561,6 @@ export interface SvgTextProps extends SvgPaintProps {
|
|
|
559
561
|
x?: number;
|
|
560
562
|
y?: number;
|
|
561
563
|
fontSize?: number;
|
|
562
|
-
fontWeight?: string | number;
|
|
563
564
|
textAnchor?: 'start' | 'middle' | 'end' | string;
|
|
564
565
|
dominantBaseline?: string;
|
|
565
566
|
}
|
|
@@ -679,6 +680,7 @@ export interface TextInputProps extends BaseProps {
|
|
|
679
680
|
export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
|
|
680
681
|
export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
|
|
681
682
|
export interface SwitchProps extends BaseProps {
|
|
683
|
+
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
682
684
|
value?: boolean;
|
|
683
685
|
onValueChange?: (value: boolean) => void;
|
|
684
686
|
onChange?: (value: boolean) => void;
|
|
@@ -691,6 +693,7 @@ export interface SwitchProps extends BaseProps {
|
|
|
691
693
|
ios_backgroundColor?: string;
|
|
692
694
|
}
|
|
693
695
|
export interface CheckboxProps extends BaseProps {
|
|
696
|
+
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
694
697
|
value?: boolean;
|
|
695
698
|
checked?: boolean;
|
|
696
699
|
indeterminate?: boolean;
|
|
@@ -715,6 +718,7 @@ export interface SliderProps extends BaseProps {
|
|
|
715
718
|
}
|
|
716
719
|
export interface PickerOption {
|
|
717
720
|
label: string;
|
|
721
|
+
/** String or finite number; other bridge values are ignored by native hosts. */
|
|
718
722
|
value: string | number;
|
|
719
723
|
}
|
|
720
724
|
export interface PickerProps extends BaseProps {
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/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) => {
|
|
@@ -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;
|
|
@@ -854,9 +854,11 @@ final class CraftHybrid: NSObject {
|
|
|
854
854
|
let show: (CraftHybridController) -> Void = { [weak self] controller in
|
|
855
855
|
if native { controller.showNative(path: path, replace: false, fromLink: true) } else { self?.openOutside(url.absoluteString) }
|
|
856
856
|
}
|
|
857
|
-
guard let controller else {
|
|
857
|
+
guard let controller, controller.isViewLoaded else {
|
|
858
858
|
// Before the shell: a hybrid app's page has it; an all-native app
|
|
859
859
|
// opens it once the shell is up (a link that launched the app).
|
|
860
|
+
// Construction assigns controller before viewDidLoad makes the
|
|
861
|
+
// navigation stacks; a link in that interval must wait too.
|
|
860
862
|
guard !hasWeb else { return false }
|
|
861
863
|
untilShell.append(show)
|
|
862
864
|
return true
|
|
@@ -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
|
|
@@ -21,6 +21,13 @@ private func number(_ value: Any?) -> CGFloat? {
|
|
|
21
21
|
return value.isFinite ? value : nil
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
private func craftNativeFontWeight(_ value: Any?) -> String? {
|
|
25
|
+
if let value = value as? String { return value }
|
|
26
|
+
guard !(value is Bool), let number = value as? NSNumber, number.doubleValue.isFinite else { return nil }
|
|
27
|
+
let integer = number.doubleValue.rounded()
|
|
28
|
+
return integer == number.doubleValue ? String(Int(integer)) : nil
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
25
32
|
if let value = value as? String, !value.isEmpty { return value }
|
|
26
33
|
if let value = value as? NSNumber, value.doubleValue.isFinite { return String(value.doubleValue) }
|
|
@@ -1228,6 +1235,11 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1228
1235
|
return MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta))
|
|
1229
1236
|
}
|
|
1230
1237
|
|
|
1238
|
+
private func nonEmptyHandler(_ handler: String?) -> String? {
|
|
1239
|
+
guard let handler, !handler.isEmpty else { return nil }
|
|
1240
|
+
return handler
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1231
1243
|
private static func color(_ value: String?) -> UIColor {
|
|
1232
1244
|
switch value?.lowercased() {
|
|
1233
1245
|
case "red": return .systemRed
|
|
@@ -3318,6 +3330,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3318
3330
|
var linejoin: String?
|
|
3319
3331
|
var dashes: [CGFloat]?
|
|
3320
3332
|
var vectorEffect: String?
|
|
3333
|
+
var fontWeight: String?
|
|
3321
3334
|
var dominantBaseline: String?
|
|
3322
3335
|
|
|
3323
3336
|
func merged(_ node: [String: Any]) -> Paint {
|
|
@@ -3330,6 +3343,11 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3330
3343
|
let value = CGFloat(truncating: number)
|
|
3331
3344
|
return value.isFinite ? value : nil
|
|
3332
3345
|
}
|
|
3346
|
+
func string(_ raw: Any?) -> String? {
|
|
3347
|
+
if let value = raw as? String { return value }
|
|
3348
|
+
if let number = raw as? NSNumber { return number.stringValue }
|
|
3349
|
+
return nil
|
|
3350
|
+
}
|
|
3333
3351
|
if let color = style["color"] as? String { next.color = color }
|
|
3334
3352
|
if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
|
|
3335
3353
|
if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
|
|
@@ -3346,6 +3364,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3346
3364
|
if let join = value("strokeLinejoin") as? String { next.linejoin = join }
|
|
3347
3365
|
if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
|
|
3348
3366
|
if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
|
|
3367
|
+
if let fontWeight = string(value("fontWeight")) { next.fontWeight = fontWeight }
|
|
3349
3368
|
if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
|
|
3350
3369
|
return next
|
|
3351
3370
|
}
|
|
@@ -3446,7 +3465,14 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3446
3465
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
3447
3466
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
3448
3467
|
let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
|
|
3449
|
-
let weight: UIFont.Weight =
|
|
3468
|
+
let weight: UIFont.Weight = {
|
|
3469
|
+
switch paint.fontWeight?.lowercased() {
|
|
3470
|
+
case "500": return .medium
|
|
3471
|
+
case "600": return .semibold
|
|
3472
|
+
case "bold", "700", "800", "900": return .bold
|
|
3473
|
+
default: return .regular
|
|
3474
|
+
}
|
|
3475
|
+
}()
|
|
3450
3476
|
let font = UIFont.systemFont(ofSize: size, weight: weight)
|
|
3451
3477
|
let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
|
|
3452
3478
|
let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
|
|
@@ -5779,6 +5805,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5779
5805
|
updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
|
|
5780
5806
|
case "ImageBackground":
|
|
5781
5807
|
let background = result as! CraftNativeImageBackgroundView
|
|
5808
|
+
let backgroundID = ObjectIdentifier(background)
|
|
5809
|
+
if let tint = color(style["tintColor"]) {
|
|
5810
|
+
tintedImages.insert(backgroundID)
|
|
5811
|
+
background.tintColor = tint
|
|
5812
|
+
} else {
|
|
5813
|
+
tintedImages.remove(backgroundID)
|
|
5814
|
+
background.tintColor = nil
|
|
5815
|
+
}
|
|
5816
|
+
if let current = background.image { background.image = imageForDisplay(current, view: background) }
|
|
5782
5817
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
5783
5818
|
background.contentMode = imageContentMode(requestedResizeMode)
|
|
5784
5819
|
background.clipsToBounds = background.contentMode == .scaleAspectFill
|
|
@@ -6139,6 +6174,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6139
6174
|
let owner = parent.protocolId ?? path
|
|
6140
6175
|
flatListOwners = flatListOwners.filter { $0.value != owner }
|
|
6141
6176
|
for child in children { registerFlatListOwnership(child, owner: owner) }
|
|
6177
|
+
let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
|
|
6178
|
+
let propRefreshEvents = propRefresh["events"] as? [String: String] ?? [:]
|
|
6179
|
+
let refreshHandler = nonEmptyHandler(propRefreshEvents["onRefresh"])
|
|
6180
|
+
?? nonEmptyHandler(events["onRefresh"])
|
|
6181
|
+
let refreshing = (propRefresh["refreshing"] ?? props["refreshing"]) as? Bool == true
|
|
6142
6182
|
list.bounces = props["bounces"] as? Bool ?? true
|
|
6143
6183
|
list.isScrollEnabled = props["scrollEnabled"] as? Bool != false
|
|
6144
6184
|
list.onFittingHeightChanged = { [weak list] in
|
|
@@ -6147,9 +6187,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6147
6187
|
}
|
|
6148
6188
|
list.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
|
|
6149
6189
|
list.setContentContainerStyle(props["contentContainerStyle"] as? [String: Any])
|
|
6150
|
-
list.setRefreshHandler(
|
|
6190
|
+
list.setRefreshHandler(refreshHandler.map { [weak self] handler in
|
|
6151
6191
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
6152
|
-
}, refreshing:
|
|
6192
|
+
}, refreshing: refreshing)
|
|
6193
|
+
list.setRefreshAppearance(propRefresh)
|
|
6153
6194
|
list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
|
|
6154
6195
|
list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
|
|
6155
6196
|
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
@@ -6581,7 +6622,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6581
6622
|
(icon as? CraftNativeIconView)?.defaultBox = box == nil && fontSize == nil ? 20 : nil
|
|
6582
6623
|
let pointSize = fontSize ?? (box ?? 20) * 0.84
|
|
6583
6624
|
let weight: UIImage.SymbolWeight
|
|
6584
|
-
switch style["fontWeight"]
|
|
6625
|
+
switch craftNativeFontWeight(style["fontWeight"]) {
|
|
6585
6626
|
case "100": weight = .ultraLight
|
|
6586
6627
|
case "200": weight = .thin
|
|
6587
6628
|
case "300": weight = .light
|
|
@@ -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)
|