@thelacanians/vue-native-cli 0.4.15 → 0.6.2

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 (116) hide show
  1. package/dist/cli.js +329 -15
  2. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +118 -0
  3. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
  4. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
  5. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
  6. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
  7. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
  8. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
  9. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
  10. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
  11. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
  12. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
  13. package/native/android/gradle.properties +1 -0
  14. package/native/android/gradlew +1 -1
  15. package/native/ios/VueNativeCore/Package.swift +1 -1
  16. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +1 -0
  17. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +143 -5
  18. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
  19. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
  20. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
  21. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
  22. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
  23. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
  24. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
  25. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
  26. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
  27. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
  28. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
  29. package/native/macos/VueNativeMacOS/Package.swift +34 -0
  30. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
  31. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
  32. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
  33. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
  34. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
  35. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
  36. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
  37. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
  38. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
  39. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
  40. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
  41. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
  42. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
  43. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
  44. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
  45. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
  46. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
  47. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
  48. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
  49. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
  50. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
  51. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
  52. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
  53. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
  54. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
  55. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
  56. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
  57. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
  58. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
  59. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
  60. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
  61. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
  62. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
  63. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
  64. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
  65. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
  66. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
  67. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
  68. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
  69. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
  70. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
  71. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
  72. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
  73. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
  74. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
  75. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
  76. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
  77. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
  78. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
  79. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
  80. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
  81. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
  82. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
  83. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
  84. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
  85. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
  86. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
  87. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
  88. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
  89. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
  90. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
  91. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
  92. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
  93. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
  94. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
  95. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
  96. package/native/shared/VueNativeShared/AGENTS.md +129 -0
  97. package/native/shared/VueNativeShared/Package.swift +14 -0
  98. package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
  99. package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
  100. package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
  101. package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
  102. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
  103. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
  104. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
  105. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
  106. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
  107. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
  108. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
  109. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
  110. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
  111. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
  112. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
  113. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
  114. package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
  115. package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
  116. package/package.json +8 -2
@@ -0,0 +1,183 @@
1
+ package com.vuenative.core
2
+
3
+ import android.content.Context
4
+ import android.widget.Button
5
+ import android.widget.FrameLayout
6
+ import android.widget.LinearLayout
7
+ import android.widget.ScrollView
8
+ import android.widget.TextView
9
+ import androidx.appcompat.app.AppCompatActivity
10
+ import androidx.test.core.app.ApplicationProvider
11
+ import org.junit.Assert.assertEquals
12
+ import org.junit.Assert.assertNotNull
13
+ import org.junit.Assert.assertNull
14
+ import org.junit.Assert.assertTrue
15
+ import org.junit.Before
16
+ import org.junit.Test
17
+ import org.junit.runner.RunWith
18
+ import org.robolectric.Robolectric
19
+ import org.robolectric.RobolectricTestRunner
20
+ import org.robolectric.annotation.Config
21
+
22
+ @RunWith(RobolectricTestRunner::class)
23
+ @Config(sdk = [34])
24
+ class ErrorOverlayViewTest {
25
+
26
+ private lateinit var context: Context
27
+
28
+ @Before
29
+ fun setUp() {
30
+ context = ApplicationProvider.getApplicationContext()
31
+ }
32
+
33
+ private fun createActivity(): AppCompatActivity {
34
+ val controller = Robolectric.buildActivity(AppCompatActivity::class.java)
35
+ val activity = controller.get()
36
+ activity.setTheme(androidx.appcompat.R.style.Theme_AppCompat)
37
+ controller.create().start().resume()
38
+ return activity
39
+ }
40
+
41
+ // -------------------------------------------------------------------------
42
+ // ErrorOverlayView is a singleton object
43
+ // -------------------------------------------------------------------------
44
+
45
+ @Test
46
+ fun testIsSingletonObject() {
47
+ val ref1 = ErrorOverlayView
48
+ val ref2 = ErrorOverlayView
49
+ assertTrue("ErrorOverlayView should be a singleton object", ref1 === ref2)
50
+ }
51
+
52
+ // -------------------------------------------------------------------------
53
+ // show() with non-activity context does not crash
54
+ // -------------------------------------------------------------------------
55
+
56
+ @Test
57
+ fun testShowWithNonActivityContext() {
58
+ // Application context is not an AppCompatActivity, so show() should return early
59
+ ErrorOverlayView.show(context, "Test error")
60
+ // Should not crash
61
+ assertTrue(true)
62
+ }
63
+
64
+ // -------------------------------------------------------------------------
65
+ // show() with Activity creates overlay
66
+ // -------------------------------------------------------------------------
67
+
68
+ @Test
69
+ fun testShowWithActivity() {
70
+ val activity = createActivity()
71
+
72
+ ErrorOverlayView.show(activity, "Runtime error: undefined is not a function")
73
+
74
+ val decorView = activity.window.decorView as? android.view.ViewGroup
75
+ assertNotNull("decorView should not be null", decorView)
76
+
77
+ val overlay = decorView?.findViewWithTag<FrameLayout>("vue_native_error_overlay")
78
+ assertNotNull("Overlay should be added to decorView", overlay)
79
+ }
80
+
81
+ // -------------------------------------------------------------------------
82
+ // Overlay contains title, error text, and dismiss button
83
+ // -------------------------------------------------------------------------
84
+
85
+ @Test
86
+ fun testOverlayContents() {
87
+ val activity = createActivity()
88
+
89
+ val errorMessage = "TypeError: Cannot read property 'value' of null"
90
+ ErrorOverlayView.show(activity, errorMessage)
91
+
92
+ val decorView = activity.window.decorView as android.view.ViewGroup
93
+ val overlay = decorView.findViewWithTag<FrameLayout>("vue_native_error_overlay")!!
94
+
95
+ // The overlay contains a card (LinearLayout)
96
+ val card = overlay.getChildAt(0) as LinearLayout
97
+
98
+ // Card should have 3 children: title, scroll (with error text), dismiss button
99
+ assertEquals("Card should have 3 children", 3, card.childCount)
100
+
101
+ // Title
102
+ val title = card.getChildAt(0) as TextView
103
+ assertEquals("Vue Native JS Error", title.text.toString())
104
+
105
+ // Scroll -> error text
106
+ val scroll = card.getChildAt(1) as ScrollView
107
+ val errorText = scroll.getChildAt(0) as TextView
108
+ assertEquals(errorMessage, errorText.text.toString())
109
+
110
+ // Dismiss button
111
+ val dismiss = card.getChildAt(2) as Button
112
+ assertEquals("Dismiss", dismiss.text.toString())
113
+ }
114
+
115
+ // -------------------------------------------------------------------------
116
+ // Dismiss button removes overlay
117
+ // -------------------------------------------------------------------------
118
+
119
+ @Test
120
+ fun testDismissRemovesOverlay() {
121
+ val activity = createActivity()
122
+
123
+ ErrorOverlayView.show(activity, "Some error")
124
+
125
+ val decorView = activity.window.decorView as android.view.ViewGroup
126
+ var overlay = decorView.findViewWithTag<FrameLayout>("vue_native_error_overlay")
127
+ assertNotNull("Overlay should exist", overlay)
128
+
129
+ // Find and click the dismiss button
130
+ val card = overlay!!.getChildAt(0) as LinearLayout
131
+ val dismiss = card.getChildAt(2) as Button
132
+ dismiss.performClick()
133
+
134
+ overlay = decorView.findViewWithTag<FrameLayout>("vue_native_error_overlay")
135
+ assertNull("Overlay should be removed after dismiss", overlay)
136
+ }
137
+
138
+ // -------------------------------------------------------------------------
139
+ // Showing twice replaces old overlay
140
+ // -------------------------------------------------------------------------
141
+
142
+ @Test
143
+ fun testShowTwiceReplacesOverlay() {
144
+ val activity = createActivity()
145
+
146
+ ErrorOverlayView.show(activity, "First error")
147
+ ErrorOverlayView.show(activity, "Second error")
148
+
149
+ val decorView = activity.window.decorView as android.view.ViewGroup
150
+
151
+ // Count overlays with the tag
152
+ var overlayCount = 0
153
+ for (i in 0 until decorView.childCount) {
154
+ if (decorView.getChildAt(i).tag == "vue_native_error_overlay") {
155
+ overlayCount++
156
+ }
157
+ }
158
+ assertEquals("Should only have one overlay", 1, overlayCount)
159
+
160
+ // Verify it shows the second error
161
+ val overlay = decorView.findViewWithTag<FrameLayout>("vue_native_error_overlay")!!
162
+ val card = overlay.getChildAt(0) as LinearLayout
163
+ val scroll = card.getChildAt(1) as ScrollView
164
+ val errorText = scroll.getChildAt(0) as TextView
165
+ assertEquals("Second error", errorText.text.toString())
166
+ }
167
+
168
+ // -------------------------------------------------------------------------
169
+ // Overlay tag is correct
170
+ // -------------------------------------------------------------------------
171
+
172
+ @Test
173
+ fun testOverlayTag() {
174
+ val activity = createActivity()
175
+
176
+ ErrorOverlayView.show(activity, "Error")
177
+
178
+ val decorView = activity.window.decorView as android.view.ViewGroup
179
+ val overlay = decorView.findViewWithTag<FrameLayout>("vue_native_error_overlay")
180
+ assertNotNull("Overlay should be findable by tag", overlay)
181
+ assertEquals("vue_native_error_overlay", overlay!!.tag)
182
+ }
183
+ }
@@ -0,0 +1,203 @@
1
+ package com.vuenative.core
2
+
3
+ import android.os.Looper
4
+ import java.time.Duration
5
+ import org.junit.Assert.assertEquals
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Before
8
+ import org.junit.Test
9
+ import org.junit.runner.RunWith
10
+ import org.robolectric.RobolectricTestRunner
11
+ import org.robolectric.Shadows
12
+ import org.robolectric.annotation.Config
13
+
14
+ @RunWith(RobolectricTestRunner::class)
15
+ @Config(sdk = [34])
16
+ class EventThrottleTest {
17
+
18
+ private val firedPayloads = mutableListOf<Any?>()
19
+
20
+ @Before
21
+ fun setUp() {
22
+ firedPayloads.clear()
23
+ }
24
+
25
+ private fun flush() {
26
+ Shadows.shadowOf(Looper.getMainLooper()).idle()
27
+ }
28
+
29
+ /**
30
+ * Resets lastFireTime to a very old value via reflection so that
31
+ * the first fire() call sees elapsed >= intervalMs.
32
+ * This is needed because Robolectric's SystemClock.uptimeMillis()
33
+ * returns 0 and lastFireTime starts at 0, so elapsed=0 < intervalMs.
34
+ */
35
+ private fun ensureFirstFireImmediate(throttle: EventThrottle) {
36
+ val field = EventThrottle::class.java.getDeclaredField("lastFireTime")
37
+ field.isAccessible = true
38
+ field.setLong(throttle, -100_000L)
39
+ }
40
+
41
+ // -------------------------------------------------------------------------
42
+ // Initialization
43
+ // -------------------------------------------------------------------------
44
+
45
+ @Test
46
+ fun testCreation() {
47
+ val throttle = EventThrottle(intervalMs = 100L) { firedPayloads.add(it) }
48
+ // Should not fire anything on creation
49
+ assertEquals(0, firedPayloads.size)
50
+ throttle.cancel()
51
+ }
52
+
53
+ // -------------------------------------------------------------------------
54
+ // First event fires immediately
55
+ // -------------------------------------------------------------------------
56
+
57
+ @Test
58
+ fun testFirstEventFiresImmediately() {
59
+ val throttle = EventThrottle(intervalMs = 100L) { firedPayloads.add(it) }
60
+ ensureFirstFireImmediate(throttle)
61
+
62
+ throttle.fire("first")
63
+
64
+ assertEquals("First event should fire immediately", 1, firedPayloads.size)
65
+ assertEquals("first", firedPayloads[0])
66
+ throttle.cancel()
67
+ }
68
+
69
+ // -------------------------------------------------------------------------
70
+ // Events within window are throttled (trailing fires after delay)
71
+ // -------------------------------------------------------------------------
72
+
73
+ @Test
74
+ fun testEventsWithinWindowAreThrottled() {
75
+ val throttle = EventThrottle(intervalMs = 5000L) { firedPayloads.add(it) }
76
+ ensureFirstFireImmediate(throttle)
77
+
78
+ // First fires immediately
79
+ throttle.fire("first")
80
+ assertEquals(1, firedPayloads.size)
81
+
82
+ // Second should be throttled (within the 5000ms window)
83
+ throttle.fire("second")
84
+ assertEquals("Second event should be throttled", 1, firedPayloads.size)
85
+
86
+ // Third should update the pending payload but not fire
87
+ throttle.fire("third")
88
+ assertEquals("Third event should still be throttled", 1, firedPayloads.size)
89
+
90
+ // Advance the looper clock past the interval to let the trailing callback fire
91
+ Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(6000))
92
+
93
+ assertEquals("Trailing event should fire with latest payload", 2, firedPayloads.size)
94
+ assertEquals("third", firedPayloads[1])
95
+ throttle.cancel()
96
+ }
97
+
98
+ // -------------------------------------------------------------------------
99
+ // Cancel stops pending trailing call
100
+ // -------------------------------------------------------------------------
101
+
102
+ @Test
103
+ fun testCancelStopsPendingTrailing() {
104
+ val throttle = EventThrottle(intervalMs = 5000L) { firedPayloads.add(it) }
105
+ ensureFirstFireImmediate(throttle)
106
+
107
+ // First fires immediately
108
+ throttle.fire("first")
109
+ assertEquals(1, firedPayloads.size)
110
+
111
+ // Second is throttled (trailing scheduled)
112
+ throttle.fire("second")
113
+ assertEquals(1, firedPayloads.size)
114
+
115
+ // Cancel before trailing fires
116
+ throttle.cancel()
117
+ flush()
118
+
119
+ assertEquals("Trailing should NOT fire after cancel", 1, firedPayloads.size)
120
+ }
121
+
122
+ // -------------------------------------------------------------------------
123
+ // Multiple fires with cancel in between
124
+ // -------------------------------------------------------------------------
125
+
126
+ @Test
127
+ fun testFireAfterCancel() {
128
+ val throttle = EventThrottle(intervalMs = 5000L) { firedPayloads.add(it) }
129
+ ensureFirstFireImmediate(throttle)
130
+
131
+ throttle.fire("first")
132
+ assertEquals(1, firedPayloads.size)
133
+
134
+ throttle.fire("second")
135
+ throttle.cancel()
136
+ flush()
137
+
138
+ assertEquals("Only the first event should have fired", 1, firedPayloads.size)
139
+ }
140
+
141
+ // -------------------------------------------------------------------------
142
+ // Payload is passed correctly
143
+ // -------------------------------------------------------------------------
144
+
145
+ @Test
146
+ fun testPayloadPassedCorrectly() {
147
+ val throttle = EventThrottle(intervalMs = 5000L) { firedPayloads.add(it) }
148
+ ensureFirstFireImmediate(throttle)
149
+
150
+ val payload = mapOf("x" to 10, "y" to 20)
151
+ throttle.fire(payload)
152
+
153
+ assertEquals(1, firedPayloads.size)
154
+ @Suppress("UNCHECKED_CAST")
155
+ val received = firedPayloads[0] as Map<String, Int>
156
+ assertEquals(10, received["x"])
157
+ assertEquals(20, received["y"])
158
+ throttle.cancel()
159
+ }
160
+
161
+ // -------------------------------------------------------------------------
162
+ // Null payload is valid
163
+ // -------------------------------------------------------------------------
164
+
165
+ @Test
166
+ fun testNullPayload() {
167
+ val throttle = EventThrottle(intervalMs = 5000L) { firedPayloads.add(it) }
168
+ ensureFirstFireImmediate(throttle)
169
+
170
+ throttle.fire(null)
171
+
172
+ assertEquals(1, firedPayloads.size)
173
+ assertEquals(null, firedPayloads[0])
174
+ throttle.cancel()
175
+ }
176
+
177
+ // -------------------------------------------------------------------------
178
+ // Default interval is 16ms
179
+ // -------------------------------------------------------------------------
180
+
181
+ @Test
182
+ fun testDefaultInterval() {
183
+ val throttle = EventThrottle { firedPayloads.add(it) }
184
+ ensureFirstFireImmediate(throttle)
185
+ throttle.fire("test")
186
+ assertEquals(1, firedPayloads.size)
187
+ throttle.cancel()
188
+ }
189
+
190
+ // -------------------------------------------------------------------------
191
+ // Multiple cancel calls don't crash
192
+ // -------------------------------------------------------------------------
193
+
194
+ @Test
195
+ fun testMultipleCancelCalls() {
196
+ val throttle = EventThrottle(intervalMs = 100L) { firedPayloads.add(it) }
197
+ throttle.cancel()
198
+ throttle.cancel()
199
+ throttle.cancel()
200
+ // Should not crash
201
+ assertTrue(true)
202
+ }
203
+ }
@@ -0,0 +1,162 @@
1
+ package com.vuenative.core
2
+
3
+ import android.content.Context
4
+ import androidx.test.core.app.ApplicationProvider
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertNotNull
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Before
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+ import org.robolectric.annotation.Config
13
+
14
+ @RunWith(RobolectricTestRunner::class)
15
+ @Config(sdk = [34])
16
+ class HotReloadManagerTest {
17
+
18
+ private lateinit var context: Context
19
+ private var lastReloadCode: String? = null
20
+ private lateinit var manager: HotReloadManager
21
+
22
+ @Before
23
+ fun setUp() {
24
+ // Reset ComponentRegistry singleton via reflection
25
+ val crField = ComponentRegistry::class.java.getDeclaredField("instance")
26
+ crField.isAccessible = true
27
+ crField.set(null, null)
28
+
29
+ // Reset NativeModuleRegistry singleton via reflection
30
+ val nmrField = NativeModuleRegistry::class.java.getDeclaredField("instance")
31
+ nmrField.isAccessible = true
32
+ nmrField.set(null, null)
33
+
34
+ context = ApplicationProvider.getApplicationContext()
35
+ lastReloadCode = null
36
+
37
+ val runtime = JSRuntime(context)
38
+ manager = HotReloadManager(runtime) { code ->
39
+ lastReloadCode = code
40
+ }
41
+ }
42
+
43
+ // -------------------------------------------------------------------------
44
+ // Initialization
45
+ // -------------------------------------------------------------------------
46
+
47
+ @Test
48
+ fun testCreation() {
49
+ assertNotNull("HotReloadManager should be created", manager)
50
+ }
51
+
52
+ // -------------------------------------------------------------------------
53
+ // Initial connection state is disconnected
54
+ // -------------------------------------------------------------------------
55
+
56
+ @Test
57
+ fun testInitialConnectionState() {
58
+ val field = HotReloadManager::class.java.getDeclaredField("isConnected")
59
+ field.isAccessible = true
60
+ val connected = field.getBoolean(manager)
61
+ assertFalse("Should not be connected initially", connected)
62
+ }
63
+
64
+ // -------------------------------------------------------------------------
65
+ // disconnect() clears state
66
+ // -------------------------------------------------------------------------
67
+
68
+ @Test
69
+ fun testDisconnect() {
70
+ manager.disconnect()
71
+
72
+ val connField = HotReloadManager::class.java.getDeclaredField("isConnected")
73
+ connField.isAccessible = true
74
+ assertFalse("isConnected should be false after disconnect", connField.getBoolean(manager))
75
+
76
+ val devField = HotReloadManager::class.java.getDeclaredField("devServerUrl")
77
+ devField.isAccessible = true
78
+ val devUrl = devField.get(manager)
79
+ assertTrue("devServerUrl should be null after disconnect", devUrl == null)
80
+
81
+ val bundleField = HotReloadManager::class.java.getDeclaredField("bundleUrl")
82
+ bundleField.isAccessible = true
83
+ val bundleUrl = bundleField.get(manager)
84
+ assertTrue("bundleUrl should be null after disconnect", bundleUrl == null)
85
+
86
+ val wsField = HotReloadManager::class.java.getDeclaredField("wsSession")
87
+ wsField.isAccessible = true
88
+ val ws = wsField.get(manager)
89
+ assertTrue("wsSession should be null after disconnect", ws == null)
90
+ }
91
+
92
+ // -------------------------------------------------------------------------
93
+ // disconnect() is idempotent
94
+ // -------------------------------------------------------------------------
95
+
96
+ @Test
97
+ fun testDisconnectIdempotent() {
98
+ manager.disconnect()
99
+ manager.disconnect()
100
+ manager.disconnect()
101
+ // Should not throw
102
+ assertTrue(true)
103
+ }
104
+
105
+ // -------------------------------------------------------------------------
106
+ // connect() stores URLs
107
+ // -------------------------------------------------------------------------
108
+
109
+ @Test
110
+ fun testConnectStoresUrls() {
111
+ manager.connect("ws://localhost:3000", "http://localhost:3000/bundle.js")
112
+
113
+ val devField = HotReloadManager::class.java.getDeclaredField("devServerUrl")
114
+ devField.isAccessible = true
115
+ val devUrl = devField.get(manager) as? String
116
+ assertTrue("devServerUrl should be set", devUrl == "ws://localhost:3000")
117
+
118
+ val bundleField = HotReloadManager::class.java.getDeclaredField("bundleUrl")
119
+ bundleField.isAccessible = true
120
+ val bundleUrl = bundleField.get(manager) as? String
121
+ assertTrue("bundleUrl should be set", bundleUrl == "http://localhost:3000/bundle.js")
122
+
123
+ manager.disconnect()
124
+ }
125
+
126
+ // -------------------------------------------------------------------------
127
+ // After disconnect, can reconnect
128
+ // -------------------------------------------------------------------------
129
+
130
+ @Test
131
+ fun testReconnectAfterDisconnect() {
132
+ manager.connect("ws://localhost:3000", "http://localhost:3000/bundle.js")
133
+ manager.disconnect()
134
+
135
+ // Scope and job should be fresh after disconnect
136
+ val scopeJobField = HotReloadManager::class.java.getDeclaredField("scopeJob")
137
+ scopeJobField.isAccessible = true
138
+ val scopeJob = scopeJobField.get(manager) as kotlinx.coroutines.Job
139
+ assertFalse("scopeJob should not be cancelled after disconnect+reinit", scopeJob.isCancelled)
140
+
141
+ manager.connect("ws://localhost:4000", "http://localhost:4000/bundle.js")
142
+
143
+ val devField = HotReloadManager::class.java.getDeclaredField("devServerUrl")
144
+ devField.isAccessible = true
145
+ val devUrl = devField.get(manager) as? String
146
+ assertTrue("devServerUrl should be updated", devUrl == "ws://localhost:4000")
147
+
148
+ manager.disconnect()
149
+ }
150
+
151
+ // -------------------------------------------------------------------------
152
+ // httpClient exists
153
+ // -------------------------------------------------------------------------
154
+
155
+ @Test
156
+ fun testHttpClientExists() {
157
+ val field = HotReloadManager::class.java.getDeclaredField("httpClient")
158
+ field.isAccessible = true
159
+ val client = field.get(manager)
160
+ assertNotNull("httpClient should not be null", client)
161
+ }
162
+ }