@thelacanians/vue-native-cli 0.4.11 → 0.4.13
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/cli.js +1 -1
- package/native/android/.editorconfig +25 -0
- package/native/android/VueNativeCore/build.gradle.kts +25 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSPolyfills.kt +17 -10
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/JSRuntime.kt +5 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +13 -13
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/ComponentRegistry.kt +27 -27
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActionSheetFactory.kt +6 -4
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VActivityIndicatorFactory.kt +1 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VAlertDialogFactory.kt +24 -12
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VButtonFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VImageFactory.kt +7 -7
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VInputFactory.kt +12 -12
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VKeyboardAvoidingFactory.kt +0 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VModalFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPickerFactory.kt +3 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VPressableFactory.kt +5 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VRootFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSectionListFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSegmentedControlFactory.kt +3 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VStatusBarFactory.kt +3 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSwitchFactory.kt +0 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +9 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VWebViewFactory.kt +7 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/NativeComponentFactory.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/GestureHelper.kt +4 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AnimationModule.kt +77 -21
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/AsyncStorageModule.kt +20 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BackgroundTaskModule.kt +12 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BiometryModule.kt +5 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/BluetoothModule.kt +88 -23
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/CalendarModule.kt +24 -11
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ClipboardModule.kt +7 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ContactsModule.kt +24 -12
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/DeviceInfoModule.kt +14 -11
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/FileSystemModule.kt +79 -24
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeolocationModule.kt +10 -7
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HapticsModule.kt +5 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/HttpModule.kt +17 -8
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/IAPModule.kt +20 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/KeyboardModule.kt +4 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/LinkingModule.kt +12 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NetworkModule.kt +4 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NotificationsModule.kt +24 -6
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/OTAModule.kt +13 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PerformanceModule.kt +8 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/PermissionsModule.kt +17 -8
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SecureStorageModule.kt +20 -5
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SensorsModule.kt +16 -4
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/ShareModule.kt +6 -3
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/SocialAuthModule.kt +4 -2
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/WebSocketModule.kt +26 -8
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Styling/StyleEngine.kt +127 -84
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Tags.kt +26 -26
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/VueNativeActivity.kt +1 -1
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentRegistryTest.kt +173 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +436 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleRegistryTest.kt +251 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/StyleEngineTest.kt +482 -0
- package/native/android/build.gradle.kts +1 -0
- package/native/ios/.swiftlint.yml +62 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +4 -1
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentRegistryTests.swift +237 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeBridgeOperationTests.swift +398 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleRegistryTests.swift +203 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/StyleEngineTests.swift +381 -0
- 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
|