@thelacanians/vue-native-cli 0.4.12 → 0.4.14

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.
Files changed (70) hide show
  1. package/dist/cli.js +6 -1
  2. package/native/android/.editorconfig +25 -0
  3. package/native/android/VueNativeCore/build.gradle.kts +25 -1
  4. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSPolyfills.kt +17 -10
  5. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSRuntime.kt +5 -5
  6. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +13 -13
  7. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/ComponentRegistry.kt +27 -27
  8. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActionSheetFactory.kt +6 -4
  9. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActivityIndicatorFactory.kt +1 -1
  10. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VAlertDialogFactory.kt +24 -12
  11. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VButtonFactory.kt +5 -2
  12. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VImageFactory.kt +7 -7
  13. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VInputFactory.kt +12 -12
  14. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VKeyboardAvoidingFactory.kt +0 -1
  15. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +5 -2
  16. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VModalFactory.kt +5 -2
  17. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPickerFactory.kt +3 -2
  18. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPressableFactory.kt +5 -3
  19. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRootFactory.kt +5 -2
  20. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +5 -2
  21. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSectionListFactory.kt +5 -2
  22. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSegmentedControlFactory.kt +3 -3
  23. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VStatusBarFactory.kt +3 -3
  24. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSwitchFactory.kt +0 -1
  25. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +9 -3
  26. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VWebViewFactory.kt +7 -5
  27. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/NativeComponentFactory.kt +5 -2
  28. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/GestureHelper.kt +4 -1
  29. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AnimationModule.kt +77 -21
  30. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AsyncStorageModule.kt +20 -5
  31. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BackgroundTaskModule.kt +12 -3
  32. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BiometryModule.kt +5 -2
  33. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BluetoothModule.kt +88 -23
  34. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CalendarModule.kt +24 -11
  35. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ClipboardModule.kt +7 -2
  36. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ContactsModule.kt +24 -12
  37. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DeviceInfoModule.kt +14 -11
  38. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/FileSystemModule.kt +79 -24
  39. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeolocationModule.kt +10 -7
  40. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HapticsModule.kt +5 -5
  41. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HttpModule.kt +17 -8
  42. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/IAPModule.kt +20 -5
  43. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/KeyboardModule.kt +4 -1
  44. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/LinkingModule.kt +12 -3
  45. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NetworkModule.kt +4 -1
  46. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NotificationsModule.kt +24 -6
  47. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/OTAModule.kt +13 -5
  48. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PerformanceModule.kt +8 -2
  49. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PermissionsModule.kt +17 -8
  50. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SecureStorageModule.kt +20 -5
  51. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SensorsModule.kt +16 -4
  52. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ShareModule.kt +6 -3
  53. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SocialAuthModule.kt +4 -2
  54. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/WebSocketModule.kt +26 -8
  55. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Styling/StyleEngine.kt +127 -84
  56. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Tags.kt +26 -26
  57. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/VueNativeActivity.kt +1 -1
  58. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentRegistryTest.kt +173 -0
  59. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +436 -0
  60. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleRegistryTest.kt +251 -0
  61. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/StyleEngineTest.kt +482 -0
  62. package/native/android/build.gradle.kts +1 -0
  63. package/native/ios/.swiftlint.yml +62 -0
  64. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/JSPolyfills.swift +15 -2
  65. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +4 -1
  66. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentRegistryTests.swift +237 -0
  67. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeBridgeOperationTests.swift +398 -0
  68. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleRegistryTests.swift +203 -0
  69. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/StyleEngineTests.swift +381 -0
  70. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ #if canImport(UIKit)
2
+ import XCTest
3
+ import UIKit
4
+ @testable import VueNativeCore
5
+
6
+ @MainActor
7
+ final class ComponentRegistryTests: XCTestCase {
8
+
9
+ // MARK: - Properties
10
+
11
+ private var registry: ComponentRegistry!
12
+
13
+ // MARK: - Setup / Teardown
14
+
15
+ override func setUp() {
16
+ super.setUp()
17
+ registry = ComponentRegistry.shared
18
+ }
19
+
20
+ override func tearDown() {
21
+ registry = nil
22
+ super.tearDown()
23
+ }
24
+
25
+ // MARK: - All Built-in Component Types Registered
26
+
27
+ func testAllBuiltInComponentTypesRegistered() {
28
+ let expectedTypes = [
29
+ "VView", "VText", "VButton", "VInput", "VSwitch",
30
+ "VActivityIndicator", "VScrollView", "VImage",
31
+ "VKeyboardAvoiding", "VSafeArea", "VSlider", "VList",
32
+ "VModal", "VAlertDialog", "VStatusBar", "VWebView",
33
+ "VProgressBar", "VPicker", "VSegmentedControl", "VActionSheet",
34
+ "VRefreshControl", "VPressable", "VSectionList",
35
+ "VCheckbox", "VRadio", "VDropdown", "VVideo",
36
+ "__ROOT__",
37
+ ]
38
+
39
+ for type in expectedTypes {
40
+ let view = registry.createView(type: type)
41
+ XCTAssertNotNil(view, "createView should return non-nil for registered type '\(type)'")
42
+ }
43
+ }
44
+
45
+ // MARK: - Unknown Type Returns Nil
46
+
47
+ func testUnknownTypeReturnsNil() {
48
+ let view = registry.createView(type: "NonExistentComponent")
49
+ XCTAssertNil(view, "createView should return nil for unknown component types")
50
+ }
51
+
52
+ // MARK: - Specific View Type Assertions
53
+
54
+ func testVTextCreatesUILabel() {
55
+ let view = registry.createView(type: "VText")
56
+ XCTAssertNotNil(view, "VText should create a view")
57
+ XCTAssertTrue(view is UILabel, "VText should create a UILabel, got \(type(of: view!))")
58
+ }
59
+
60
+ func testVViewCreatesUIView() {
61
+ let view = registry.createView(type: "VView")
62
+ XCTAssertNotNil(view, "VView should create a view")
63
+ // VView creates a plain UIView (not a subclass)
64
+ XCTAssertNotNil(view, "VView should create a UIView")
65
+ }
66
+
67
+ func testVSwitchCreatesUISwitch() {
68
+ let view = registry.createView(type: "VSwitch")
69
+ XCTAssertNotNil(view, "VSwitch should create a view")
70
+ XCTAssertTrue(view is UISwitch, "VSwitch should create a UISwitch, got \(type(of: view!))")
71
+ }
72
+
73
+ func testVImageCreatesUIImageView() {
74
+ let view = registry.createView(type: "VImage")
75
+ XCTAssertNotNil(view, "VImage should create a view")
76
+ XCTAssertTrue(view is UIImageView, "VImage should create a UIImageView, got \(type(of: view!))")
77
+ }
78
+
79
+ // MARK: - Factory Stored on Created View
80
+
81
+ func testFactoryStoredOnCreatedView() {
82
+ let view = registry.createView(type: "VView")!
83
+ let factory = ComponentRegistry.factory(for: view)
84
+ XCTAssertNotNil(factory, "Factory should be stored on created view via associated object")
85
+ }
86
+
87
+ func testFactoryForTypeRetrieval() {
88
+ let factory = registry.factory(for: "VText")
89
+ XCTAssertNotNil(factory, "factory(for:) should return the registered factory for 'VText'")
90
+ }
91
+
92
+ func testFactoryForUnknownType() {
93
+ let factory = registry.factory(for: "NonExistent")
94
+ XCTAssertNil(factory, "factory(for:) should return nil for unregistered types")
95
+ }
96
+
97
+ // MARK: - Custom Factory Registration
98
+
99
+ func testRegisterCustomFactory() {
100
+ let customFactory = StubFactory()
101
+ registry.register("CustomComponent", factory: customFactory)
102
+
103
+ let view = registry.createView(type: "CustomComponent")
104
+ XCTAssertNotNil(view, "createView should return a view for the custom-registered type")
105
+ XCTAssertTrue(customFactory.createViewCalled, "Custom factory's createView should have been called")
106
+
107
+ // Clean up to avoid polluting singleton state
108
+ registry.unregister("CustomComponent")
109
+ }
110
+
111
+ // MARK: - Unregister
112
+
113
+ func testUnregisterRemovesFactory() {
114
+ let customFactory = StubFactory()
115
+ registry.register("TempComponent", factory: customFactory)
116
+
117
+ // Verify it's registered
118
+ XCTAssertNotNil(registry.createView(type: "TempComponent"), "Should exist before unregister")
119
+
120
+ registry.unregister("TempComponent")
121
+
122
+ let view = registry.createView(type: "TempComponent")
123
+ XCTAssertNil(view, "createView should return nil after unregistering")
124
+ }
125
+
126
+ // MARK: - updateProp Dispatches to Factory
127
+
128
+ func testUpdatePropDispatchesToFactory() {
129
+ let customFactory = StubFactory()
130
+ registry.register("PropTestComponent", factory: customFactory)
131
+
132
+ let view = registry.createView(type: "PropTestComponent")!
133
+ registry.updateProp(view: view, key: "testKey", value: "testValue")
134
+
135
+ XCTAssertTrue(customFactory.updatePropCalled, "updateProp should dispatch to the factory")
136
+ XCTAssertEqual(customFactory.lastPropKey, "testKey", "The correct key should be passed")
137
+ XCTAssertEqual(customFactory.lastPropValue as? String, "testValue", "The correct value should be passed")
138
+
139
+ // Clean up
140
+ registry.unregister("PropTestComponent")
141
+ }
142
+
143
+ // MARK: - addEventListener Dispatches to Factory
144
+
145
+ func testAddEventListenerDispatchesToFactory() {
146
+ let customFactory = StubFactory()
147
+ registry.register("EventTestComponent", factory: customFactory)
148
+
149
+ let view = registry.createView(type: "EventTestComponent")!
150
+ registry.addEventListener(view: view, event: "press") { _ in }
151
+
152
+ XCTAssertTrue(customFactory.addEventListenerCalled, "addEventListener should dispatch to the factory")
153
+ XCTAssertEqual(customFactory.lastEventName, "press", "The correct event name should be passed")
154
+
155
+ // Clean up
156
+ registry.unregister("EventTestComponent")
157
+ }
158
+
159
+ // MARK: - removeEventListener Dispatches to Factory
160
+
161
+ func testRemoveEventListenerDispatchesToFactory() {
162
+ let customFactory = StubFactory()
163
+ registry.register("RemoveEventTestComponent", factory: customFactory)
164
+
165
+ let view = registry.createView(type: "RemoveEventTestComponent")!
166
+ registry.removeEventListener(view: view, event: "press")
167
+
168
+ XCTAssertTrue(customFactory.removeEventListenerCalled, "removeEventListener should dispatch to the factory")
169
+
170
+ // Clean up
171
+ registry.unregister("RemoveEventTestComponent")
172
+ }
173
+
174
+ // MARK: - Factory for Plain UIView Returns Nil
175
+
176
+ func testFactoryForPlainUIViewReturnsNil() {
177
+ let plainView = UIView()
178
+ let factory = ComponentRegistry.factory(for: plainView)
179
+ XCTAssertNil(factory, "Factory should be nil for views not created by the registry")
180
+ }
181
+
182
+ // MARK: - Register Overwrites Existing
183
+
184
+ func testRegisterOverwritesExisting() {
185
+ let factory1 = StubFactory()
186
+ let factory2 = StubFactory()
187
+
188
+ registry.register("OverwriteTest", factory: factory1)
189
+ registry.register("OverwriteTest", factory: factory2)
190
+
191
+ let view = registry.createView(type: "OverwriteTest")
192
+ XCTAssertNotNil(view, "Should still create a view after overwrite")
193
+ XCTAssertTrue(factory2.createViewCalled, "Second factory's createView should be called")
194
+ XCTAssertFalse(factory1.createViewCalled, "First factory's createView should NOT be called")
195
+
196
+ // Clean up
197
+ registry.unregister("OverwriteTest")
198
+ }
199
+ }
200
+
201
+ // MARK: - StubFactory
202
+
203
+ /// A test-only factory that records method calls for verification.
204
+ @MainActor
205
+ private final class StubFactory: NativeComponentFactory {
206
+
207
+ var createViewCalled = false
208
+ var updatePropCalled = false
209
+ var addEventListenerCalled = false
210
+ var removeEventListenerCalled = false
211
+ var lastPropKey: String?
212
+ var lastPropValue: Any?
213
+ var lastEventName: String?
214
+
215
+ func createView() -> UIView {
216
+ createViewCalled = true
217
+ let view = UIView()
218
+ _ = view.flex
219
+ return view
220
+ }
221
+
222
+ func updateProp(view: UIView, key: String, value: Any?) {
223
+ updatePropCalled = true
224
+ lastPropKey = key
225
+ lastPropValue = value
226
+ }
227
+
228
+ func addEventListener(view: UIView, event: String, handler: @escaping (Any?) -> Void) {
229
+ addEventListenerCalled = true
230
+ lastEventName = event
231
+ }
232
+
233
+ func removeEventListener(view: UIView, event: String) {
234
+ removeEventListenerCalled = true
235
+ }
236
+ }
237
+ #endif
@@ -0,0 +1,398 @@
1
+ #if canImport(UIKit)
2
+ import XCTest
3
+ import UIKit
4
+ @testable import VueNativeCore
5
+
6
+ @MainActor
7
+ final class NativeBridgeOperationTests: XCTestCase {
8
+
9
+ // MARK: - Properties
10
+
11
+ private var bridge: NativeBridge!
12
+
13
+ // MARK: - Setup / Teardown
14
+
15
+ override func setUp() {
16
+ super.setUp()
17
+ bridge = NativeBridge.shared
18
+ // Reset the bridge state synchronously on main thread.
19
+ // Since reset() dispatches async to main and tests already run on main,
20
+ // we clear state by calling processOperations to remove views, then
21
+ // drain the run loop to ensure reset completes.
22
+ bridge.reset()
23
+ RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
24
+ }
25
+
26
+ override func tearDown() {
27
+ bridge.reset()
28
+ RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
29
+ bridge = nil
30
+ super.tearDown()
31
+ }
32
+
33
+ // MARK: - Helpers
34
+
35
+ /// Process a single operation for convenience.
36
+ private func processOp(_ op: String, args: [Any]) {
37
+ bridge.processOperations([["op": op, "args": args]])
38
+ }
39
+
40
+ /// Process multiple operations in a single batch.
41
+ private func processBatch(_ operations: [[String: Any]]) {
42
+ bridge.processOperations(operations)
43
+ }
44
+
45
+ // MARK: - create Tests
46
+
47
+ func testCreateRegistersView() {
48
+ let initialCount = bridge.registeredViewCount
49
+ processOp("create", args: [1, "VView"])
50
+
51
+ let view = bridge.view(forNodeId: 1)
52
+ XCTAssertNotNil(view, "create should register a view for the given node ID")
53
+ XCTAssertEqual(
54
+ bridge.registeredViewCount,
55
+ initialCount + 1,
56
+ "registeredViewCount should increase by 1"
57
+ )
58
+ }
59
+
60
+ func testCreateWithDifferentComponentTypes() {
61
+ processOp("create", args: [10, "VText"])
62
+ let textView = bridge.view(forNodeId: 10)
63
+ XCTAssertNotNil(textView, "create should work with VText")
64
+ XCTAssertTrue(textView is UILabel, "VText should create a UILabel")
65
+
66
+ processOp("create", args: [11, "VSwitch"])
67
+ let switchView = bridge.view(forNodeId: 11)
68
+ XCTAssertNotNil(switchView, "create should work with VSwitch")
69
+ XCTAssertTrue(switchView is UISwitch, "VSwitch should create a UISwitch")
70
+ }
71
+
72
+ // MARK: - createText Tests
73
+
74
+ func testCreateTextCreatesLabelWithText() {
75
+ processOp("createText", args: [2, "Hello"])
76
+
77
+ let view = bridge.view(forNodeId: 2)
78
+ XCTAssertNotNil(view, "createText should register a view")
79
+ XCTAssertTrue(view is UILabel, "createText should create a UILabel")
80
+
81
+ if let label = view as? UILabel {
82
+ XCTAssertEqual(label.text, "Hello", "UILabel text should be 'Hello'")
83
+ }
84
+ }
85
+
86
+ func testCreateTextWithEmptyString() {
87
+ processOp("createText", args: [3, ""])
88
+
89
+ let view = bridge.view(forNodeId: 3)
90
+ XCTAssertNotNil(view, "createText should work with empty string")
91
+ if let label = view as? UILabel {
92
+ XCTAssertEqual(label.text, "", "UILabel text should be empty string")
93
+ }
94
+ }
95
+
96
+ // MARK: - appendChild Tests
97
+
98
+ func testAppendChildAddsSubview() {
99
+ processOp("create", args: [1, "VView"])
100
+ processOp("create", args: [2, "VView"])
101
+ processOp("appendChild", args: [1, 2])
102
+
103
+ let parent = bridge.view(forNodeId: 1)!
104
+ let child = bridge.view(forNodeId: 2)!
105
+
106
+ XCTAssertTrue(
107
+ child.isDescendant(of: parent),
108
+ "Child should be a descendant of parent after appendChild"
109
+ )
110
+ }
111
+
112
+ func testAppendChildMultipleChildren() {
113
+ processOp("create", args: [1, "VView"])
114
+ processOp("create", args: [2, "VView"])
115
+ processOp("create", args: [3, "VView"])
116
+ processOp("appendChild", args: [1, 2])
117
+ processOp("appendChild", args: [1, 3])
118
+
119
+ let parent = bridge.view(forNodeId: 1)!
120
+ let child1 = bridge.view(forNodeId: 2)!
121
+ let child2 = bridge.view(forNodeId: 3)!
122
+
123
+ XCTAssertTrue(child1.isDescendant(of: parent), "First child should be in parent")
124
+ XCTAssertTrue(child2.isDescendant(of: parent), "Second child should be in parent")
125
+ }
126
+
127
+ // MARK: - removeChild Tests
128
+
129
+ func testRemoveChildRemovesFromParent() {
130
+ processOp("create", args: [1, "VView"])
131
+ processOp("create", args: [2, "VView"])
132
+ processOp("appendChild", args: [1, 2])
133
+
134
+ let parent = bridge.view(forNodeId: 1)!
135
+ let child = bridge.view(forNodeId: 2)!
136
+
137
+ XCTAssertTrue(child.isDescendant(of: parent), "Child should be in parent before removal")
138
+
139
+ let countBefore = bridge.registeredViewCount
140
+ processOp("removeChild", args: [2])
141
+
142
+ XCTAssertFalse(child.isDescendant(of: parent), "Child should be removed from parent")
143
+ XCTAssertNil(bridge.view(forNodeId: 2), "Removed child should no longer be in registry")
144
+ XCTAssertEqual(
145
+ bridge.registeredViewCount,
146
+ countBefore - 1,
147
+ "registeredViewCount should decrease by 1"
148
+ )
149
+ }
150
+
151
+ // MARK: - updateProp Tests
152
+
153
+ func testUpdatePropAppliesStyle() {
154
+ processOp("create", args: [1, "VView"])
155
+ processOp("updateProp", args: [1, "backgroundColor", "#ff0000"])
156
+
157
+ let view = bridge.view(forNodeId: 1)!
158
+ XCTAssertNotNil(view.backgroundColor, "backgroundColor should be set via updateProp")
159
+
160
+ var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
161
+ view.backgroundColor!.getRed(&r, green: &g, blue: &b, alpha: &a)
162
+ XCTAssertEqual(r, 1.0, accuracy: 0.01, "Red should be 1.0")
163
+ }
164
+
165
+ // MARK: - updateStyle Tests
166
+
167
+ func testUpdateStyleAppliesStyles() {
168
+ processOp("create", args: [1, "VView"])
169
+ processOp("updateStyle", args: [1, ["opacity": 0.5]])
170
+
171
+ let view = bridge.view(forNodeId: 1)!
172
+ XCTAssertEqual(view.alpha, 0.5, accuracy: 0.001, "alpha should be 0.5 after updateStyle")
173
+ }
174
+
175
+ func testUpdateStyleMultipleProperties() {
176
+ processOp("create", args: [1, "VView"])
177
+ processOp("updateStyle", args: [1, [
178
+ "opacity": 0.7,
179
+ "borderRadius": 12.0,
180
+ ]])
181
+
182
+ let view = bridge.view(forNodeId: 1)!
183
+ XCTAssertEqual(view.alpha, 0.7, accuracy: 0.001, "alpha should be 0.7")
184
+ XCTAssertEqual(view.layer.cornerRadius, 12.0, accuracy: 0.001, "cornerRadius should be 12")
185
+ }
186
+
187
+ // MARK: - setText Tests
188
+
189
+ func testSetTextUpdatesLabel() {
190
+ processOp("createText", args: [2, "Hello"])
191
+ processOp("setText", args: [2, "World"])
192
+
193
+ let label = bridge.view(forNodeId: 2) as? UILabel
194
+ XCTAssertNotNil(label, "View should be a UILabel")
195
+ XCTAssertEqual(label?.text, "World", "setText should update the label text to 'World'")
196
+ }
197
+
198
+ // MARK: - addEventListener Tests
199
+
200
+ func testAddEventListenerRegistersHandler() {
201
+ processOp("create", args: [1, "VView"])
202
+ processOp("addEventListener", args: [1, "press"])
203
+
204
+ // After addEventListener, the view should have a gesture recognizer
205
+ let view = bridge.view(forNodeId: 1)!
206
+ let hasTapRecognizer = view.gestureRecognizers?.contains(where: { $0 is UITapGestureRecognizer }) ?? false
207
+ XCTAssertTrue(hasTapRecognizer, "View should have a tap gesture recognizer after addEventListener for 'press'")
208
+ XCTAssertTrue(view.isUserInteractionEnabled, "User interaction should be enabled")
209
+ }
210
+
211
+ // MARK: - insertBefore Tests
212
+
213
+ func testInsertBeforeOrdersCorrectly() {
214
+ processOp("create", args: [1, "VView"])
215
+ processOp("create", args: [2, "VView"])
216
+ processOp("create", args: [3, "VView"])
217
+
218
+ // Append child2 first
219
+ processOp("appendChild", args: [1, 2])
220
+ // Insert child3 before child2
221
+ processOp("insertBefore", args: [1, 3, 2])
222
+
223
+ let parent = bridge.view(forNodeId: 1)!
224
+ let child2 = bridge.view(forNodeId: 2)!
225
+ let child3 = bridge.view(forNodeId: 3)!
226
+
227
+ XCTAssertTrue(child2.isDescendant(of: parent), "child2 should be in parent")
228
+ XCTAssertTrue(child3.isDescendant(of: parent), "child3 should be in parent")
229
+
230
+ // child3 should come before child2 in the subview order
231
+ if let idx2 = parent.subviews.firstIndex(of: child2),
232
+ let idx3 = parent.subviews.firstIndex(of: child3) {
233
+ XCTAssertLessThan(idx3, idx2, "child3 should be before child2 in subview order")
234
+ } else {
235
+ XCTFail("Both children should be found in parent's subviews")
236
+ }
237
+ }
238
+
239
+ // MARK: - Batch Operations Tests
240
+
241
+ func testMultipleOperationsInBatch() {
242
+ let operations: [[String: Any]] = [
243
+ ["op": "create", "args": [1, "VView"]],
244
+ ["op": "create", "args": [2, "VView"]],
245
+ ["op": "appendChild", "args": [1, 2]],
246
+ ["op": "updateStyle", "args": [2, ["opacity": 0.8]]],
247
+ ]
248
+
249
+ processBatch(operations)
250
+
251
+ let parent = bridge.view(forNodeId: 1)
252
+ let child = bridge.view(forNodeId: 2)
253
+
254
+ XCTAssertNotNil(parent, "Parent should be created")
255
+ XCTAssertNotNil(child, "Child should be created")
256
+ XCTAssertTrue(child!.isDescendant(of: parent!), "Child should be in parent")
257
+ XCTAssertEqual(child!.alpha, 0.8, accuracy: 0.001, "Child opacity should be 0.8")
258
+ }
259
+
260
+ // MARK: - Unknown Operation Tests
261
+
262
+ func testUnknownOperationDoesNotCrash() {
263
+ let operations: [[String: Any]] = [
264
+ ["op": "unknownOperation", "args": [1, 2, 3]],
265
+ ]
266
+ // Should not crash
267
+ processBatch(operations)
268
+ }
269
+
270
+ // MARK: - Invalid Args Tests
271
+
272
+ func testInvalidArgsDoesNotCrash() {
273
+ // Missing args key
274
+ let operations1: [[String: Any]] = [
275
+ ["op": "create"],
276
+ ]
277
+ processBatch(operations1)
278
+
279
+ // Args with wrong types
280
+ let operations2: [[String: Any]] = [
281
+ ["op": "create", "args": ["notAnInt", 123]],
282
+ ]
283
+ processBatch(operations2)
284
+
285
+ // Empty args
286
+ let operations3: [[String: Any]] = [
287
+ ["op": "create", "args": []],
288
+ ]
289
+ processBatch(operations3)
290
+ }
291
+
292
+ // MARK: - Reset Tests
293
+
294
+ func testResetClearsAllState() {
295
+ processOp("create", args: [100, "VView"])
296
+ processOp("create", args: [101, "VText"])
297
+ processOp("createText", args: [102, "Test"])
298
+
299
+ XCTAssertGreaterThanOrEqual(
300
+ bridge.registeredViewCount, 3,
301
+ "Should have at least 3 registered views before reset"
302
+ )
303
+
304
+ bridge.reset()
305
+ // Drain the main run loop to let the async reset block execute
306
+ RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
307
+
308
+ XCTAssertEqual(bridge.registeredViewCount, 0, "registeredViewCount should be 0 after reset")
309
+ XCTAssertNil(bridge.view(forNodeId: 100), "View 100 should be nil after reset")
310
+ XCTAssertNil(bridge.view(forNodeId: 101), "View 101 should be nil after reset")
311
+ XCTAssertNil(bridge.view(forNodeId: 102), "View 102 should be nil after reset")
312
+ }
313
+
314
+ // MARK: - removeEventListener Tests
315
+
316
+ func testRemoveEventListenerRemovesHandler() {
317
+ processOp("create", args: [1, "VView"])
318
+ processOp("addEventListener", args: [1, "press"])
319
+
320
+ let view = bridge.view(forNodeId: 1)!
321
+ let tapCountBefore = view.gestureRecognizers?.filter { $0 is UITapGestureRecognizer }.count ?? 0
322
+ XCTAssertGreaterThan(tapCountBefore, 0, "Should have tap recognizer before removal")
323
+
324
+ processOp("removeEventListener", args: [1, "press"])
325
+
326
+ let tapCountAfter = view.gestureRecognizers?.filter { $0 is UITapGestureRecognizer }.count ?? 0
327
+ XCTAssertEqual(tapCountAfter, 0, "Tap recognizer should be removed after removeEventListener")
328
+ }
329
+
330
+ // MARK: - Node ID Types
331
+
332
+ func testCreateWithDoubleNodeId() {
333
+ // JSON numbers from JSONSerialization may come as Double
334
+ processOp("create", args: [5.0, "VView"])
335
+ let view = bridge.view(forNodeId: 5)
336
+ XCTAssertNotNil(view, "create should handle Double node IDs (JSON deserialization)")
337
+ }
338
+
339
+ // MARK: - create Unknown Component Type
340
+
341
+ func testCreateUnknownComponentType() {
342
+ // Creating a view with an unknown type should not crash
343
+ processOp("create", args: [999, "UnknownWidget"])
344
+ let view = bridge.view(forNodeId: 999)
345
+ XCTAssertNil(view, "Unknown component type should not create a view")
346
+ }
347
+
348
+ // MARK: - removeChild Cleans Up Descendants
349
+
350
+ func testRemoveChildCleansUpDescendants() {
351
+ // Build a tree: root(1) -> parent(2) -> child(3)
352
+ processOp("create", args: [1, "VView"])
353
+ processOp("create", args: [2, "VView"])
354
+ processOp("create", args: [3, "VView"])
355
+ processOp("appendChild", args: [1, 2])
356
+ processOp("appendChild", args: [2, 3])
357
+
358
+ // Verify both exist
359
+ XCTAssertNotNil(bridge.view(forNodeId: 2), "Parent should exist")
360
+ XCTAssertNotNil(bridge.view(forNodeId: 3), "Child should exist")
361
+
362
+ // Remove parent (2), which should also clean up child (3)
363
+ processOp("removeChild", args: [2])
364
+
365
+ XCTAssertNil(bridge.view(forNodeId: 2), "Removed parent should be cleaned up")
366
+ XCTAssertNil(bridge.view(forNodeId: 3), "Descendant should be cleaned up recursively")
367
+ }
368
+
369
+ // MARK: - setElementText Tests
370
+
371
+ func testSetElementTextUpdatesLabel() {
372
+ processOp("createText", args: [5, "Original"])
373
+ processOp("setElementText", args: [5, "Updated"])
374
+
375
+ let label = bridge.view(forNodeId: 5) as? UILabel
376
+ XCTAssertEqual(label?.text, "Updated", "setElementText should update the label text")
377
+ }
378
+
379
+ // MARK: - Operations on Non-Existent Nodes
380
+
381
+ func testOperationsOnNonExistentNodesDoNotCrash() {
382
+ // All of these should silently fail without crashing
383
+ processOp("appendChild", args: [999, 998])
384
+ processOp("removeChild", args: [999])
385
+ processOp("updateProp", args: [999, "backgroundColor", "#ff0000"])
386
+ processOp("updateStyle", args: [999, ["opacity": 0.5]])
387
+ processOp("setText", args: [999, "test"])
388
+ processOp("addEventListener", args: [999, "press"])
389
+ processOp("insertBefore", args: [999, 998, 997])
390
+ }
391
+
392
+ // MARK: - Empty Batch
393
+
394
+ func testEmptyBatchDoesNotCrash() {
395
+ processBatch([])
396
+ }
397
+ }
398
+ #endif