catalyst-core 0.3.0-beta.5 → 0.3.0-canary.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 (316) hide show
  1. package/changelog.md +185 -71
  2. package/dist/errors/generateDocs.js +100 -0
  3. package/dist/errors/index.js +243 -0
  4. package/dist/errors/registry.js +483 -0
  5. package/dist/errors-index.json +506 -0
  6. package/dist/globals.d.ts +62 -0
  7. package/dist/index.d.ts +5 -0
  8. package/dist/index.jsx +5 -8
  9. package/dist/logger.js +1 -1
  10. package/dist/native/androidProject/app/build.gradle.kts +142 -7
  11. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +4 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +2 -2
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +3 -0
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +3 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +43 -8
  16. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +181 -1
  17. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +156 -236
  18. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileSizeRouterUtilsTest.kt +471 -0
  19. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +222 -226
  20. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FrameworkServerUtilsTest.kt +300 -0
  21. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/IntentUtilsTest.kt +244 -0
  22. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/MetricsMonitorTest.kt +359 -0
  23. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +108 -63
  24. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +329 -385
  25. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/OfflineCacheServiceTest.kt +302 -0
  26. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/PerfEventBufferTest.kt +133 -0
  27. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/UtilsKtTest.kt +152 -0
  28. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +145 -324
  29. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/BarcodeDetectorTest.kt +132 -0
  30. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/HoldControllerTest.kt +155 -0
  31. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/TorchControllerTest.kt +150 -0
  32. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/VideoStreamStateMachineTest.kt +120 -0
  33. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/VideoStreamStateTest.kt +62 -0
  34. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/ViewfinderMapperTest.kt +143 -0
  35. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/camera/ZoomControllerTest.kt +421 -0
  36. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/design/DesignTokensTest.kt +207 -0
  37. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +368 -0
  38. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/FridaDetectorTest.kt +193 -0
  39. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/SecurityCheckManagerTest.kt +260 -0
  40. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/security/SecurityCheckSchedulerTest.kt +166 -0
  41. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/DeviceInfoUtilsTest.kt +167 -0
  42. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/DownloadUtilsTest.kt +197 -0
  43. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/NetworkMonitorTest.kt +271 -0
  44. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/PushNotificationUtilsTest.kt +119 -0
  45. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/SafeAreaUtilsTest.kt +108 -0
  46. package/dist/native/androidProject/build.gradle.kts +1 -0
  47. package/dist/native/androidProject/gradle/libs.versions.toml +26 -0
  48. package/dist/native/androidProject/settings.gradle.kts +11 -8
  49. package/dist/native/androidSetup.js +2 -2
  50. package/dist/native/bridge/WebBridge.d.ts +98 -0
  51. package/dist/native/bridge/WebBridge.js +40 -6
  52. package/dist/native/bridge/constants/NativeInterfaces.d.ts +391 -0
  53. package/dist/native/bridge/errors.d.ts +67 -0
  54. package/dist/native/bridge/errors.js +66 -151
  55. package/dist/native/bridge/hooks/index.d.ts +13 -0
  56. package/dist/native/bridge/hooks/index.js +0 -6
  57. package/dist/native/bridge/hooks/useAI.d.ts +1 -0
  58. package/dist/native/bridge/hooks/useAI.js +3 -1
  59. package/dist/native/bridge/hooks/useCamera.d.ts +86 -0
  60. package/dist/native/bridge/hooks/useCamera.js +35 -6
  61. package/dist/native/bridge/hooks/useDataProtection.d.ts +17 -0
  62. package/dist/native/bridge/hooks/useDeviceInfo.d.ts +34 -0
  63. package/dist/native/bridge/hooks/useDeviceInfo.js +5 -0
  64. package/dist/native/bridge/hooks/useFilePicker.d.ts +53 -0
  65. package/dist/native/bridge/hooks/useFilePicker.js +13 -0
  66. package/dist/native/bridge/hooks/useGoogleSignIn.d.ts +12 -0
  67. package/dist/native/bridge/hooks/useHapticFeedback.d.ts +76 -0
  68. package/dist/native/bridge/hooks/useHapticFeedback.js +6 -1
  69. package/dist/native/bridge/hooks/useIntent.d.ts +33 -0
  70. package/dist/native/bridge/hooks/useIntent.js +2 -0
  71. package/dist/native/bridge/hooks/useNativeTransition.d.ts +53 -0
  72. package/dist/native/bridge/hooks/useNativeTransition.js +12 -5
  73. package/dist/native/bridge/hooks/useNetworkStatus.d.ts +22 -0
  74. package/dist/native/bridge/hooks/useNetworkStatus.js +7 -0
  75. package/dist/native/bridge/hooks/useNotification.d.ts +83 -0
  76. package/dist/native/bridge/hooks/useNotification.js +81 -34
  77. package/dist/native/bridge/hooks/useSafeArea.d.ts +12 -0
  78. package/dist/native/bridge/hooks/useSafeArea.js +8 -0
  79. package/dist/native/bridge/hooks/useVideoStream.d.ts +67 -0
  80. package/dist/native/bridge/hooks/useVideoStream.js +38 -52
  81. package/dist/native/bridge/hooks/utils.d.ts +4 -0
  82. package/dist/native/bridge/hooks.d.ts +1 -0
  83. package/dist/native/bridge/perf/collectors/ApiCollector.d.ts +30 -0
  84. package/dist/native/bridge/perf/collectors/ApiCollector.js +4 -0
  85. package/dist/native/bridge/perf/collectors/CacheCollector.d.ts +43 -0
  86. package/dist/native/bridge/perf/collectors/CacheCollector.js +4 -0
  87. package/dist/native/bridge/perf/collectors/HookCollector.d.ts +37 -0
  88. package/dist/native/bridge/perf/collectors/HookCollector.js +4 -0
  89. package/dist/native/bridge/perf/collectors/InsightsCollector.d.ts +103 -0
  90. package/dist/native/bridge/perf/collectors/InsightsCollector.js +4 -0
  91. package/dist/native/bridge/perf/collectors/InteractionCollector.d.ts +53 -0
  92. package/dist/native/bridge/perf/collectors/InteractionCollector.js +4 -0
  93. package/dist/native/bridge/perf/collectors/KeyboardCollector.d.ts +30 -0
  94. package/dist/native/bridge/perf/collectors/KeyboardCollector.js +4 -0
  95. package/dist/native/bridge/perf/collectors/NavigationCollector.d.ts +37 -0
  96. package/dist/native/bridge/perf/collectors/NavigationCollector.js +4 -0
  97. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.d.ts +30 -0
  98. package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +4 -0
  99. package/dist/native/bridge/perf/collectors/RenderCollector.d.ts +47 -0
  100. package/dist/native/bridge/perf/collectors/RenderCollector.js +8 -0
  101. package/dist/native/bridge/perf/collectors/ScrollCollector.d.ts +29 -0
  102. package/dist/native/bridge/perf/collectors/ScrollCollector.js +4 -0
  103. package/dist/native/bridge/perf/collectors/ThreadCollector.d.ts +31 -0
  104. package/dist/native/bridge/perf/collectors/ThreadCollector.js +4 -0
  105. package/dist/native/bridge/perf/core/constants.d.ts +76 -0
  106. package/dist/native/bridge/perf/core/measure.d.ts +39 -0
  107. package/dist/native/bridge/perf/core/measure.js +4 -0
  108. package/dist/native/bridge/perf/core/session.d.ts +49 -0
  109. package/dist/native/bridge/perf/core/session.js +4 -0
  110. package/dist/native/bridge/perf/core/store.d.ts +37 -0
  111. package/dist/native/bridge/perf/core/store.js +4 -0
  112. package/dist/native/bridge/perf/core/summary.d.ts +60 -0
  113. package/dist/native/bridge/perf/core/traceExport.d.ts +10 -0
  114. package/dist/native/bridge/perf/core/waterfalls.d.ts +2 -0
  115. package/dist/native/bridge/perf/index.d.ts +63 -0
  116. package/dist/native/bridge/perf/index.js +4 -0
  117. package/dist/native/bridge/safeArea.d.ts +11 -0
  118. package/dist/native/bridge/safeArea.js +2 -0
  119. package/dist/native/bridge/useBaseHook.d.ts +137 -0
  120. package/dist/native/bridge/useBaseHook.js +34 -4
  121. package/dist/native/bridge/utils/CameraUtils.d.ts +94 -0
  122. package/dist/native/bridge/utils/CameraUtils.js +4 -0
  123. package/dist/native/bridge/utils/FileObjectConverter.d.ts +36 -0
  124. package/dist/native/bridge/utils/NativeBridge.d.ts +270 -0
  125. package/dist/native/bridge/utils/NativeBridge.js +10 -0
  126. package/dist/native/buildAndroid/index.js +1 -1
  127. package/dist/native/buildAppAndroid.js +9 -0
  128. package/dist/native/buildAppIos.js +16 -1
  129. package/dist/native/buildErrorFormat.js +57 -0
  130. package/dist/native/buildIos/config.js +15 -1
  131. package/dist/native/buildIos/index.js +1 -1
  132. package/dist/native/buildIos/plugins.js +10 -1
  133. package/dist/native/iosnativeWebView/CoreLogic/Package.swift +36 -0
  134. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Camera/VideoStreamState.swift +1 -1
  135. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Camera/VideoStreamStateMachine.swift +6 -4
  136. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/BootTimingUtility.swift +3 -3
  137. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/FrameworkServerUtils.swift +14 -9
  138. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NetworkMonitor.swift +11 -9
  139. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NotificationHandlerProvider.swift +1 -1
  140. package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/URLWhitelistManager.swift +7 -7
  141. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/AnyCodableTests.swift +154 -0
  142. package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/BootTimingUtilityTests.swift +23 -5
  143. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/CatalystConstantsTests.swift +192 -0
  144. package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/ConfigMappingTests.swift +1 -1
  145. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/FrameworkServerUtilsErrorPathTests.swift +106 -0
  146. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/FrameworkServerUtilsLoopbackTests.swift +243 -0
  147. package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/FrameworkServerUtilsTests.swift +1 -1
  148. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/NetworkMonitorTests.swift +119 -0
  149. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/NotificationHandlerProviderTests.swift +59 -0
  150. package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/NotificationHandlerTests.swift +1 -1
  151. package/dist/native/iosnativeWebView/{iosnativeWebViewTests → CoreLogic/Tests/CatalystCoreLogicTests}/URLWhitelistManagerTests.swift +1 -1
  152. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/VideoStreamStateMachineTests.swift +171 -0
  153. package/dist/native/iosnativeWebView/CoreLogic/Tests/CatalystCoreLogicTests/VideoStreamStateTests.swift +91 -0
  154. package/dist/native/iosnativeWebView/Package.swift +10 -9
  155. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +7 -2
  156. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +1 -2
  157. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +5 -4
  158. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +1 -0
  159. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +1 -0
  160. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +6 -5
  161. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +1 -0
  162. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +1 -0
  163. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +31 -30
  164. package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +2 -1
  165. package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +1 -1
  166. package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +22 -19
  167. package/dist/native/iosnativeWebView/Sources/Core/Utils/ImageHandler.swift +40 -39
  168. package/dist/native/iosnativeWebView/Sources/Core/Utils/PerfEventBuffer.swift +1 -0
  169. package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +15 -15
  170. package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +4 -3
  171. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +6 -1
  172. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +1 -0
  173. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +2 -1
  174. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +2 -1
  175. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +3 -5
  176. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +20 -13
  177. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +6 -5
  178. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +14 -13
  179. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +3 -3
  180. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +9 -8
  181. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +7 -6
  182. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +24 -25
  183. package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +6 -1
  184. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +1 -0
  185. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +4 -0
  186. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +2 -22
  187. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +1 -0
  188. package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +1 -0
  189. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +2 -1
  190. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +15 -9
  191. package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +1 -0
  192. package/dist/native/setupEmulatorIos.js +2 -2
  193. package/dist/native/terminalProgress.js +17 -17
  194. package/dist/router/ClientRouter.d.ts +8 -0
  195. package/dist/router/ClientRouter.js +13 -11
  196. package/dist/router/ServerRouter.d.ts +13 -0
  197. package/dist/router/ServerRouter.js +19 -7
  198. package/dist/scripts/build.js +14 -2
  199. package/dist/scripts/loadEnvironmentVariables.js +3 -2
  200. package/dist/scripts/plugins.js +1 -1
  201. package/dist/scripts/preServerInit.js +49 -3
  202. package/dist/scripts/preflight.js +111 -0
  203. package/dist/scripts/scriptUtils.js +46 -1
  204. package/dist/scripts/serve.js +7 -3
  205. package/dist/scripts/start.js +8 -3
  206. package/dist/server/expressServer.d.ts +1 -0
  207. package/dist/server/expressServer.js +247 -271
  208. package/dist/server/manifestCache.d.ts +2 -0
  209. package/dist/server/manifestCache.js +25 -33
  210. package/dist/server/renderer/ChunkExtractor.d.ts +44 -0
  211. package/dist/server/renderer/ChunkExtractor.js +113 -125
  212. package/dist/server/renderer/document/Body.d.ts +21 -0
  213. package/dist/server/renderer/document/Body.jsx +45 -45
  214. package/dist/server/renderer/document/FastRefresh.d.ts +3 -0
  215. package/dist/server/renderer/document/FastRefresh.jsx +7 -13
  216. package/dist/server/renderer/document/Head.d.ts +20 -0
  217. package/dist/server/renderer/document/Head.jsx +32 -32
  218. package/dist/server/renderer/document/index.d.ts +2 -0
  219. package/dist/server/renderer/document/index.jsx +2 -2
  220. package/dist/server/renderer/extract.d.ts +67 -0
  221. package/dist/server/renderer/extract.js +92 -89
  222. package/dist/server/renderer/handler.d.ts +2 -0
  223. package/dist/server/renderer/handler.jsx +401 -404
  224. package/dist/server/renderer/index.d.ts +2 -0
  225. package/dist/server/renderer/index.js +5 -8
  226. package/dist/server/renderer/render.d.ts +35 -0
  227. package/dist/server/renderer/render.js +25 -32
  228. package/dist/server/utils/botDetectionMiddleware.d.ts +1 -0
  229. package/dist/server/utils/botDetectionMiddleware.js +33 -23
  230. package/dist/server/utils/cjsRequire.d.ts +8 -0
  231. package/dist/server/utils/cjsRequire.js +2 -2
  232. package/dist/server/utils/userAgentUtil.d.ts +7 -0
  233. package/dist/server/utils/userAgentUtil.js +68 -71
  234. package/dist/server/utils/validator.d.ts +31 -0
  235. package/dist/server/utils/validator.js +133 -158
  236. package/dist/vite/inject-cache-key-plugin.js +3 -27
  237. package/dist/vite/moduleAliases.js +36 -0
  238. package/dist/vite/node-loader.mjs +2 -29
  239. package/dist/vite/resolveDevServerConfig.js +4 -1
  240. package/dist/vite/vite.config.js +2 -29
  241. package/dist/web-router/components/MetaTag.d.ts +4 -0
  242. package/dist/web-router/components/MetaTag.jsx +26 -28
  243. package/dist/web-router/components/MetaTag.test.jsx +100 -0
  244. package/dist/web-router/components/RouterDataProvider.d.ts +96 -0
  245. package/dist/web-router/components/RouterDataProvider.jsx +260 -259
  246. package/dist/web-router/components/RouterDataProvider.server.test.jsx +98 -0
  247. package/dist/web-router/components/RouterDataProvider.test.jsx +310 -0
  248. package/dist/web-router/components/Split.d.ts +74 -0
  249. package/dist/web-router/components/Split.jsx +147 -156
  250. package/dist/web-router/components/Split.server.test.jsx +52 -0
  251. package/dist/web-router/components/Split.test.jsx +167 -0
  252. package/dist/web-router/components/SplitInview.d.ts +22 -0
  253. package/dist/web-router/components/SplitInview.jsx +78 -89
  254. package/dist/web-router/components/SplitInview.test.jsx +112 -0
  255. package/dist/web-router/components/SsrRequestContext.d.ts +20 -0
  256. package/dist/web-router/components/SsrRequestContext.jsx +17 -7
  257. package/dist/web-router/components/SsrRequestContext.test.jsx +53 -0
  258. package/dist/web-router/context.d.ts +15 -0
  259. package/dist/web-router/context.jsx +5 -5
  260. package/dist/web-router/context.test.jsx +10 -0
  261. package/dist/web-router/utils/metaDataUtils.d.ts +24 -0
  262. package/dist/web-router/utils/metaDataUtils.jsx +86 -86
  263. package/dist/web-router/utils/metaDataUtils.test.jsx +188 -0
  264. package/dist/web-router/vitest.setup.js +12 -0
  265. package/mcp_v2/knowledge-base.json +115 -17
  266. package/mcp_v2/mcp.js +141 -3
  267. package/mcp_v2/package-lock.json +2031 -0
  268. package/mcp_v2/package.json +13 -2
  269. package/mcp_v2/scripts/copyErrorsIndex.cjs +34 -0
  270. package/mcp_v2/test/errors.test.ts +123 -0
  271. package/mcp_v2/test/github.test.ts +177 -0
  272. package/mcp_v2/tools/errors.d.ts +20 -0
  273. package/mcp_v2/tools/errors.js +63 -0
  274. package/mcp_v2/tools/github.d.ts +41 -0
  275. package/mcp_v2/tools/github.js +231 -0
  276. package/mcp_v2/vitest.config.mjs +22 -0
  277. package/package.json +60 -11
  278. package/test/errors/docsDrift.test.ts +113 -0
  279. package/test/errors/index.test.ts +200 -0
  280. package/test/errors/registry.test.ts +108 -0
  281. package/test/scripts/preflight.test.ts +118 -0
  282. package/test/server/ChunkExtractor.test.ts +97 -0
  283. package/test/server/extract.test.ts +128 -0
  284. package/test/server/fixtures/template/server/document.jsx +19 -0
  285. package/test/server/fixtures/template/server/index.js +10 -0
  286. package/test/server/fixtures/template/src/js/containers/App/index.jsx +33 -0
  287. package/test/server/fixtures/template/src/js/routes/index.jsx +72 -0
  288. package/test/server/fixtures/template/src/js/routes/utils.jsx +39 -0
  289. package/test/server/fixtures/template/src/js/store/index.js +19 -0
  290. package/test/server/handler.assets.test.ts +330 -0
  291. package/test/server/handler.test.ts +208 -0
  292. package/test/server/manifestCache.test.ts +71 -0
  293. package/test/server/render.test.ts +71 -0
  294. package/test/server/scriptUtils.test.ts +103 -0
  295. package/test/server/userAgentUtil.test.ts +48 -0
  296. package/test/server/validator.test.ts +225 -0
  297. package/test/vite/resolveDevServerConfig.test.js +4 -1
  298. package/tsconfig.test.json +23 -0
  299. package/vitest.config.ts +93 -0
  300. package/dist/caching.js +0 -1
  301. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +0 -187
  302. package/dist/native/bridge/standardizedHooks.js +0 -611
  303. package/dist/router.js +0 -1
  304. package/dist/scripts/validator.js +0 -149
  305. package/dist/web-router/hooks.jsx +0 -72
  306. package/dist/web-router/types/components/MetaTag.d.ts +0 -2
  307. package/dist/web-router/types/components/RouterDataProvider.d.ts +0 -95
  308. package/dist/web-router/types/context.d.ts +0 -14
  309. package/dist/web-router/types/hooks.d.ts +0 -1
  310. package/dist/web-router/types/index.d.ts +0 -3
  311. package/dist/web-router/types/utils/metaDataUtils.d.ts +0 -40
  312. /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Constants/CatalystConstants.swift +0 -0
  313. /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Constants/ConfigConstants.swift +0 -0
  314. /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Notifications/NotificationModels.swift +0 -0
  315. /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Protocols/NotificationHandlerProtocol.swift +0 -0
  316. /package/dist/native/iosnativeWebView/{Sources/Core → CoreLogic/Sources/CatalystCoreLogic}/Utils/NullNotificationHandler.swift +0 -0
@@ -87,6 +87,11 @@ fun getLocalIpAddress(): String {
87
87
  plugins {
88
88
  alias(libs.plugins.android.application)
89
89
  alias(libs.plugins.jetbrains.kotlin.android)
90
+ jacoco
91
+ }
92
+
93
+ jacoco {
94
+ toolVersion = libs.versions.jacoco.get()
90
95
  }
91
96
 
92
97
  android {
@@ -215,18 +220,24 @@ android {
215
220
  java.srcDirs("src/noFcm/java")
216
221
  }
217
222
  // AI has no source-set swap — CatalystAIBridge self-registers via ServiceLoader
218
- // when @catalyst/cloud-ai is on the classpath.
223
+ // when catalyst-ai is on the classpath.
219
224
  }
220
225
  }
221
226
  }
222
227
 
223
228
  configurations.all {
224
229
  resolutionStrategy.eachDependency {
230
+ // kotlin-reflect is deliberately NOT forced here (unlike the
231
+ // stdlib artifacts below) — mockito-kotlin 6.x requires
232
+ // kotlin-reflect >=2.1.20, newer than this project's 2.0.21
233
+ // compiler version. kotlin-reflect is forward-compatible with
234
+ // older stdlib/compiler versions for the reflection use cases
235
+ // actually exercised in tests, so it resolves to whatever's
236
+ // requested (libs.kotlin.reflect, currently 2.1.20) instead.
225
237
  if (requested.group == "org.jetbrains.kotlin" &&
226
238
  (requested.name == "kotlin-stdlib" ||
227
239
  requested.name == "kotlin-stdlib-jdk7" ||
228
- requested.name == "kotlin-stdlib-jdk8" ||
229
- requested.name == "kotlin-reflect")) {
240
+ requested.name == "kotlin-stdlib-jdk8")) {
230
241
  useVersion("2.0.21")
231
242
  }
232
243
  }
@@ -238,6 +249,16 @@ dependencies {
238
249
  implementation(libs.material)
239
250
  implementation(libs.androidx.constraintlayout)
240
251
  testImplementation(libs.junit)
252
+ // #413: Mockito for JVM unit tests (app/src/test). mockito-core 5.x
253
+ // defaults to the inline mock-maker, so Kotlin's final classes don't
254
+ // need extra `open` keywords. mockito-kotlin adds the idiomatic
255
+ // whenever()/mock<T>() DSL on top. kotlinx-coroutines-test matches
256
+ // the existing kotlinx-coroutines-android dependency's release line
257
+ // for testing suspend funs in NativeBridge/OfflineCacheService.
258
+ testImplementation(libs.mockito.core)
259
+ testImplementation(libs.mockito.kotlin)
260
+ testImplementation(libs.kotlin.reflect)
261
+ testImplementation(libs.kotlinx.coroutines.test)
241
262
  androidTestImplementation(libs.androidx.junit)
242
263
  androidTestImplementation(libs.androidx.espresso.core)
243
264
  implementation(libs.androidx.webkit)
@@ -252,11 +273,18 @@ dependencies {
252
273
  implementation("io.ktor:ktor-server-sse:3.0.3")
253
274
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
254
275
 
255
- // Native AI — compileOnly always (for types), implementation only when ai.enabled=true (for runtime bundling)
256
- if (project.findProject(":catalyst-cloud-ai") != null) {
257
- compileOnly(project(":catalyst-cloud-ai"))
276
+ // Native AI (catalyst-ai). The module is only ever on the classpath when
277
+ // ai.enabled=true -- that's the sole sync trigger (settings.gradle.kts
278
+ // guards on node_modules/catalyst-ai/android existing, and the auto-sync
279
+ // in buildAndroid/index.js only runs for ai.enabled). The app has no
280
+ // compile-time dependency on it: NativeBridge.kt discovers CatalystAIBridge
281
+ // at runtime via Class.forName + ServiceLoader. compileOnly is kept so the
282
+ // module still builds/type-checks when present; implementation adds it to
283
+ // the APK for runtime bundling.
284
+ if (project.findProject(":catalyst-ai") != null) {
285
+ compileOnly(project(":catalyst-ai"))
258
286
  if (isAIEnabled()) {
259
- implementation(project(":catalyst-cloud-ai"))
287
+ implementation(project(":catalyst-ai"))
260
288
  }
261
289
  }
262
290
 
@@ -284,6 +312,113 @@ dependencies {
284
312
  }
285
313
  }
286
314
 
315
+ // JVM unit test coverage (Tier 1). Mirrors the CatalystCoreLogic
316
+ // llvm-cov setup on iOS (#432): real, measured line coverage for
317
+ // app/src/test, not just a pass/fail count. Scoped to testDebugUnitTest —
318
+ // enableUnitTestCoverage=true on the debug build type (above) already
319
+ // makes Gradle emit the raw .exec coverage data; this task turns that
320
+ // into the human/CI-readable XML + HTML report.
321
+ tasks.register<JacocoReport>("jacocoTestReport") {
322
+ dependsOn("testDebugUnitTest")
323
+ group = "verification"
324
+ description = "Generates JVM unit test coverage report for app/src/test (Tier 1)."
325
+
326
+ reports {
327
+ xml.required.set(true)
328
+ html.required.set(true)
329
+ }
330
+
331
+ // Excludes are split into two groups. The first is generated/non-authored
332
+ // code (R.class, BuildConfig, view/data binding scaffolding) — no
333
+ // coverage story either way, always excluded.
334
+ //
335
+ // The second is the Tier 2 classification below: files that build real
336
+ // Android Views, extend Activity/Fragment, or otherwise structurally
337
+ // require Robolectric or a real device/emulator to exercise (confirmed
338
+ // via a full-repo classification pass, not a guess from filenames —
339
+ // see PR description / issue for the per-file reasoning). These ARE
340
+ // real, authored logic — excluding them isn't "inflating" the number,
341
+ // it's making the gate honest: a Tier 1/Tier 2 coverage gate that
342
+ // includes Tier 2 in its denominator would fail any PR that touches
343
+ // WebView/Activity/View code regardless of how well-tested that PR's
344
+ // actual testable logic is. Mirrors the CoreLogic/UI split already
345
+ // accepted on iOS (#432).
346
+ //
347
+ // Each Tier 2 file's top-level class, Kt-file facade, and compiled
348
+ // lambda/inner classes are all excluded, so nothing structurally
349
+ // untestable is left counting against the gate. The one exception is
350
+ // NativeBridge, which has an already-tested companion object
351
+ // (parseAndValidateMessage, compiled separately as
352
+ // NativeBridge$Companion.class) — Gradle's Ant-style exclude() does
353
+ // not reliably support "!" negation, so instead of pattern-negating
354
+ // the companion out, its lambda exclude uses "$*$*" (two "$"
355
+ // segments), which Kotlin lambda class names always have and the
356
+ // single-"$" companion class name never does. See the comment next
357
+ // to that pattern below for the concrete example.
358
+ val generatedCodeFilter = listOf(
359
+ "**/R.class", "**/R$*.class",
360
+ "**/BuildConfig.*",
361
+ "**/Manifest*.*",
362
+ "**/*Test*.*",
363
+ "android/**/*.*",
364
+ "**/databinding/**",
365
+ "**/android/databinding/**",
366
+ "**/androidx/databinding/**",
367
+ "**/*_ViewBinding*.*",
368
+ "**/*\$ViewInjector*.*",
369
+ "**/*\$ViewBinder*.*",
370
+ "**/*_MembersInjector.class"
371
+ )
372
+
373
+ val tier2FrameworkBoundFilter = listOf(
374
+ // Real WebView/Activity construction, ViewBinding, real lifecycle.
375
+ // Each file's top-level class, Kt-file facade, and compiled lambda
376
+ // classes (Kotlin: OuterClass$methodName$N.class) are all excluded.
377
+ // None of these files has a companion object worth protecting
378
+ // (that only applies to NativeBridge, handled separately below),
379
+ // so a plain "$*" is safe here.
380
+ "**/CustomWebView.class", "**/CustomWebView\$*.class", "**/CustomWebViewKt.class",
381
+ "**/MainActivity.class", "**/MainActivity\$*.class",
382
+ "**/SplashActivity.class", "**/SplashActivity\$*.class",
383
+ "**/NativeCameraManager.class", "**/NativeCameraManager\$*.class",
384
+ "**/camera/CameraSessionManager.class", "**/camera/CameraSessionManager\$*.class",
385
+ "**/TransitionManager.class", "**/TransitionManager\$*.class",
386
+ "**/utils/KeyboardUtil.class", "**/utils/KeyboardUtil\$*.class",
387
+ "**/security/SecurityAlertUI.class", "**/security/SecurityAlertUI\$*.class",
388
+ "**/security/SecurityAlertHandler.class", "**/security/SecurityAlertHandler\$*.class",
389
+ "**/security/SecurityBottomSheet.class", "**/security/SecurityBottomSheet\$*.class",
390
+ // NativeBridge has an already-tested companion (parseAndValidateMessage,
391
+ // compiled as NativeBridge$Companion.class — one "$"). Kotlin lambda
392
+ // classes always carry two "$" segments (e.g.
393
+ // NativeBridge$downloadAndOpenFile$1.class), so "$*$*" excludes every
394
+ // lambda while a single-"$" glob would be needed to also exclude the
395
+ // companion — which this pattern does NOT match, by construction.
396
+ "**/NativeBridge.class", "**/NativeBridgeKt.class", "**/NativeBridge\$*\$*.class"
397
+ )
398
+
399
+ val debugTree = fileTree(layout.buildDirectory.dir("intermediates/javac/debug/compileDebugJavaWithJavac/classes")) {
400
+ exclude(generatedCodeFilter + tier2FrameworkBoundFilter)
401
+ }
402
+ val kotlinDebugTree = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) {
403
+ exclude(generatedCodeFilter + tier2FrameworkBoundFilter)
404
+ }
405
+ // Both noFcm/withFcm are listed (not just whichever isNotificationsEnabled()
406
+ // picked for this build) purely so the HTML report can resolve source
407
+ // lines for whichever one actually got compiled — listing the inactive
408
+ // one is harmless, Jacoco just won't find matching class files for it.
409
+ val sourceDirs = listOf(
410
+ "${project.projectDir}/src/main/java",
411
+ "${project.projectDir}/src/noFcm/java",
412
+ "${project.projectDir}/src/withFcm/java"
413
+ )
414
+
415
+ sourceDirectories.setFrom(files(sourceDirs))
416
+ classDirectories.setFrom(files(debugTree, kotlinDebugTree))
417
+ executionData.setFrom(fileTree(layout.buildDirectory) {
418
+ include("outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
419
+ })
420
+ }
421
+
287
422
  // Task to verify local IP
288
423
  tasks.register("printLocalIp") {
289
424
  doLast {
@@ -5,6 +5,10 @@
5
5
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6
6
  <uses-permission android:name="android.permission.CAMERA" />
7
7
  <uses-permission android:name="android.permission.VIBRATE" />
8
+ <!-- Runtime-guarded in NotificationUtils.showNotification (checkSelfPermission
9
+ || SDK_INT < TIRAMISU); only requested/used when the notifications
10
+ feature is enabled at build time. -->
11
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8
12
  <uses-feature android:name="android.hardware.camera" android:required="false" />
9
13
  <queries>
10
14
  <intent>
@@ -27,7 +27,7 @@ import java.util.Properties
27
27
  import java.util.ServiceLoader
28
28
  import java.util.concurrent.atomic.AtomicBoolean
29
29
 
30
- private data class FilePickerOptions(
30
+ internal data class FilePickerOptions(
31
31
  val mimeType: String = "*/*",
32
32
  val multiple: Boolean = false,
33
33
  val minFiles: Int? = null,
@@ -96,7 +96,7 @@ private fun JSONObject.optIntNullable(key: String): Int? =
96
96
  private fun JSONObject.optLongNullable(key: String): Long? =
97
97
  if (has(key) && !isNull(key)) getLong(key) else null
98
98
 
99
- private data class GoogleSignInOptions(
99
+ internal data class GoogleSignInOptions(
100
100
  val clientId: String,
101
101
  val nonce: String? = null,
102
102
  val autoSelect: Boolean = false,
@@ -1,6 +1,8 @@
1
1
  package io.yourname.androidproject.camera
2
2
 
3
3
  import android.util.Log
4
+ import androidx.annotation.OptIn
5
+ import androidx.camera.core.ExperimentalGetImage
4
6
  import androidx.camera.core.ImageAnalysis
5
7
  import androidx.camera.core.ImageProxy
6
8
  import com.google.mlkit.vision.barcode.BarcodeScanner
@@ -72,6 +74,7 @@ class BarcodeDetector(
72
74
  scanner = null
73
75
  }
74
76
 
77
+ @OptIn(ExperimentalGetImage::class)
75
78
  private fun processFrame(imageProxy: ImageProxy, activeScanner: BarcodeScanner) {
76
79
  val mediaImage = imageProxy.image
77
80
  if (mediaImage == null) {
@@ -7,7 +7,9 @@ import android.util.Size
7
7
  import android.view.View
8
8
  import android.hardware.camera2.CaptureRequest
9
9
  import android.util.Range
10
+ import androidx.annotation.OptIn
10
11
  import androidx.camera.camera2.interop.Camera2Interop
12
+ import androidx.camera.camera2.interop.ExperimentalCamera2Interop
11
13
  import androidx.camera.core.Camera
12
14
  import androidx.camera.core.CameraSelector
13
15
  import androidx.camera.core.ImageAnalysis
@@ -152,6 +154,7 @@ class CameraSessionManager(
152
154
 
153
155
  // ---- Internal ----
154
156
 
157
+ @OptIn(ExperimentalCamera2Interop::class)
155
158
  private fun bindCamera() {
156
159
  val future = ProcessCameraProvider.getInstance(activity)
157
160
  future.addListener({
@@ -64,8 +64,23 @@ object FrameworkServerUtils {
64
64
  // System prompt injected into every native AI request (built from attachmentComponents at initAI time)
65
65
  private var nativeSystemPrompt: String = ""
66
66
 
67
- // Coroutine scope for server operations
68
- private val serverScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
67
+ // Coroutine scope for server operations. A `var`, re-created at the
68
+ // top of startServer() rather than a `val` created once — stopServer()
69
+ // calls serverScope.cancel(), and a CoroutineScope is single-use: once
70
+ // cancelled it never accepts new coroutines again. With this as a
71
+ // `val`, any startServer() after a stop() launched startCleanupTask()
72
+ // into an already-cancelled scope, silently disabling periodic served-
73
+ // file cleanup for the rest of that server's lifetime. Found via a
74
+ // real start/stop/restart sequence in FrameworkServerUtilsTest.
75
+ // @Volatile: startServer()/stopServer() can run on different threads
76
+ // (this object is a singleton reached from NativeBridge's coroutine
77
+ // scope and from test code alike) — without it, a reassignment in
78
+ // startServer() has no guaranteed visibility to a reader on another
79
+ // thread (e.g. a coroutine already launched via serverScope.launch{}
80
+ // from a prior start), same class of bug as a non-volatile double-
81
+ // checked-locking field.
82
+ @Volatile
83
+ private var serverScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
69
84
 
70
85
  data class ServedFile(
71
86
  val file: File,
@@ -90,6 +105,14 @@ object FrameworkServerUtils {
90
105
  }
91
106
 
92
107
  return try {
108
+ // A cancelled CoroutineScope never accepts new coroutines —
109
+ // re-create it here so startCleanupTask() below (and any
110
+ // other serverScope.launch call during this server's
111
+ // lifetime) works correctly even after a prior stopServer().
112
+ if (!serverScope.isActive) {
113
+ serverScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
114
+ }
115
+
93
116
  // Initialize cache directory
94
117
  initializeCacheDirectory(context)
95
118
 
@@ -504,12 +527,24 @@ object FrameworkServerUtils {
504
527
  call.response.header(HttpHeaders.AccessControlAllowMethods, "GET, OPTIONS")
505
528
  call.response.header(HttpHeaders.AccessControlAllowHeaders, "*")
506
529
 
507
- call.respond(HttpStatusCode.OK, mapOf(
508
- "status" to "running",
509
- "sessionId" to sessionId,
510
- "port" to serverPort,
511
- "servedFiles" to servedFiles.size
512
- ))
530
+ // respondText with a manually-built JSONObject, not
531
+ // call.respond(status, mapOf(...)) — that call needs a
532
+ // registered ContentNegotiation converter to serialize
533
+ // a raw Map, which this server never installs, so it
534
+ // was returning 406 Not Acceptable to every real HTTP
535
+ // client (confirmed via a real loopback request in
536
+ // FrameworkServerUtilsTest — no test caught this
537
+ // before because no test made a real request against
538
+ // this route). Matches the /ai/generate route's own
539
+ // JSONObject + respondText pattern elsewhere in this
540
+ // file, avoiding a new ContentNegotiation dependency
541
+ // for one endpoint.
542
+ val statusJson = JSONObject()
543
+ .put("status", "running")
544
+ .put("sessionId", sessionId)
545
+ .put("port", serverPort)
546
+ .put("servedFiles", servedFiles.size)
547
+ call.respondText(statusJson.toString(), ContentType.Application.Json, HttpStatusCode.OK)
513
548
  }
514
549
 
515
550
  // Handle OPTIONS preflight requests for CORS
@@ -1,9 +1,15 @@
1
1
  package io.yourname.androidproject
2
2
 
3
+ import android.webkit.WebView
3
4
  import io.yourname.androidproject.utils.BridgeUtils
4
5
  import org.json.JSONObject
5
6
  import org.junit.Assert.*
6
7
  import org.junit.Test
8
+ import org.mockito.kotlin.any
9
+ import org.mockito.kotlin.eq
10
+ import org.mockito.kotlin.isNull
11
+ import org.mockito.kotlin.mock
12
+ import org.mockito.kotlin.verify
7
13
 
8
14
  /**
9
15
  * Unit tests for BridgeUtils
@@ -12,8 +18,15 @@ import org.junit.Test
12
18
  * - Bridge message formatting (3 tests)
13
19
  * - Response serialization (2 tests)
14
20
  * - Error message formatting (3 tests)
21
+ * - notifyWeb-family, emitPerfEvent, and log-family calls against a
22
+ * mocked WebView (extension — these were previously untested; only the
23
+ * pure string/JSON helpers above had coverage). Looper.myLooper() and
24
+ * Looper.getMainLooper() both return the SDK stub's default null in a
25
+ * JVM unit test, so `null != null` is false and every notifyWeb/
26
+ * emitPerfEvent call below takes the synchronous "already on main
27
+ * thread" branch directly — no WebView.post dispatch to account for.
15
28
  *
16
- * Total: 8 tests
29
+ * Total: 8 original + 12 extension tests
17
30
  *
18
31
  * Note: Tests focus on testable logic and string formatting without WebView
19
32
  * following the same pattern as FileUtilsTest.kt
@@ -283,4 +296,171 @@ class BridgeUtilsTest {
283
296
  assertTrue(kbFormatted.contains("."))
284
297
  assertTrue(kbFormatted.endsWith("KB"))
285
298
  }
299
+
300
+ // ============================================================
301
+ // CATEGORY 4: notifyWeb* against a mocked WebView (extension)
302
+ // ============================================================
303
+
304
+ @Test
305
+ fun `notifyWeb evaluates a null-data callback when data is null`() {
306
+ val webView: WebView = mock()
307
+
308
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_CAMERA_CAPTURE, null)
309
+
310
+ verify(webView).evaluateJavascript(
311
+ eq("window.WebBridge.callback('ON_CAMERA_CAPTURE', null)"),
312
+ isNull()
313
+ )
314
+ }
315
+
316
+ @Test
317
+ fun `notifyWeb quotes plain-string data as a JS string literal`() {
318
+ val webView: WebView = mock()
319
+
320
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_INTENT_SUCCESS, "GRANTED")
321
+
322
+ verify(webView).evaluateJavascript(
323
+ eq("window.WebBridge.callback('ON_INTENT_SUCCESS', ${JSONObject.quote("GRANTED")})"),
324
+ isNull()
325
+ )
326
+ }
327
+
328
+ @Test
329
+ fun `notifyWeb injects JSON-object-shaped data raw, not double-quoted`() {
330
+ val webView: WebView = mock()
331
+
332
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_DEVICE_INFO_SUCCESS, """{"a":1}""")
333
+
334
+ verify(webView).evaluateJavascript(
335
+ eq("""window.WebBridge.callback('ON_DEVICE_INFO_SUCCESS', {"a":1})"""),
336
+ isNull()
337
+ )
338
+ }
339
+
340
+ @Test
341
+ fun `notifyWeb swallows an exception from evaluateJavascript`() {
342
+ val webView: WebView = mock()
343
+ org.mockito.kotlin.whenever(webView.evaluateJavascript(any(), isNull()))
344
+ .thenThrow(RuntimeException("boom"))
345
+
346
+ // Does not propagate — notifyWeb catches and logs internally.
347
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "x")
348
+ }
349
+
350
+ @Test
351
+ fun `notifyWebError defaults to a fallback message when errorMessage is null`() {
352
+ val webView: WebView = mock()
353
+
354
+ BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, null)
355
+
356
+ verify(webView).evaluateJavascript(
357
+ eq("window.WebBridge.callback('ON_CAMERA_ERROR', ${JSONObject.quote("Unknown error occurred")})"),
358
+ isNull()
359
+ )
360
+ }
361
+
362
+ @Test
363
+ fun `notifyWebSuccess defaults to SUCCESS when successMessage is not provided`() {
364
+ val webView: WebView = mock()
365
+
366
+ BridgeUtils.notifyWebSuccess(webView, BridgeUtils.WebEvents.ON_INTENT_SUCCESS)
367
+
368
+ verify(webView).evaluateJavascript(
369
+ eq("window.WebBridge.callback('ON_INTENT_SUCCESS', ${JSONObject.quote("SUCCESS")})"),
370
+ isNull()
371
+ )
372
+ }
373
+
374
+ @Test
375
+ fun `notifyWebJson injects the JSON payload raw`() {
376
+ val webView: WebView = mock()
377
+ val payload = JSONObject().apply { put("ok", true) }
378
+
379
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_DEVICE_INFO_SUCCESS, payload)
380
+
381
+ verify(webView).evaluateJavascript(
382
+ eq("window.WebBridge.callback('ON_DEVICE_INFO_SUCCESS', ${payload})"),
383
+ isNull()
384
+ )
385
+ }
386
+
387
+ // ============================================================
388
+ // CATEGORY 5: emitPerfEvent / bridgeCallReceived / bridgeCallDispatched
389
+ // ============================================================
390
+
391
+ @Test
392
+ fun `emitPerfEvent is a no-op when PerfEventBuffer is disabled`() {
393
+ val webView: WebView = mock()
394
+ io.yourname.androidproject.utils.PerfEventBuffer.configure(enabled = false)
395
+
396
+ BridgeUtils.emitPerfEvent(webView, JSONObject().apply { put("type", "test") })
397
+
398
+ verify(webView, org.mockito.kotlin.never()).evaluateJavascript(any(), any())
399
+ }
400
+
401
+ @Test
402
+ fun `emitPerfEvent calls evaluateJavascript when enabled`() {
403
+ val webView: WebView = mock()
404
+ io.yourname.androidproject.utils.PerfEventBuffer.configure(enabled = true)
405
+
406
+ try {
407
+ BridgeUtils.emitPerfEvent(webView, JSONObject().apply { put("type", "test") })
408
+ verify(webView).evaluateJavascript(any(), isNull())
409
+ } finally {
410
+ io.yourname.androidproject.utils.PerfEventBuffer.configure(enabled = false)
411
+ }
412
+ }
413
+
414
+ @Test
415
+ fun `bridgeCallReceived and bridgeCallDispatched do not throw`() {
416
+ // BuildConfig.DEBUG's value in a JVM unit test depends on the build
417
+ // variant under test; either branch (no-op or delegate to
418
+ // PerfEventBuffer) should complete without throwing.
419
+ BridgeUtils.bridgeCallReceived("call-1", "getDeviceInfo")
420
+ BridgeUtils.bridgeCallDispatched("call-1")
421
+ }
422
+
423
+ // ============================================================
424
+ // CATEGORY 6: log helpers
425
+ // ============================================================
426
+
427
+ @Test
428
+ fun `logDebug, logError, logInfo, and logWarning do not throw`() {
429
+ BridgeUtils.logDebug("TAG", "debug message")
430
+ BridgeUtils.logError("TAG", "error message")
431
+ BridgeUtils.logError("TAG", "error with throwable", RuntimeException("boom"))
432
+ BridgeUtils.logInfo("TAG", "info message")
433
+ BridgeUtils.logWarning("TAG", "warning message")
434
+ }
435
+
436
+ // ============================================================
437
+ // CATEGORY 7: safeExecute
438
+ // ============================================================
439
+
440
+ @Test
441
+ fun `safeExecute runs the block and does not notify web on success`() {
442
+ val webView: WebView = mock()
443
+ var ran = false
444
+
445
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "test op") {
446
+ ran = true
447
+ }
448
+
449
+ assertTrue(ran)
450
+ verify(webView, org.mockito.kotlin.never()).evaluateJavascript(any(), any())
451
+ }
452
+
453
+ @Test
454
+ fun `safeExecute catches an exception and notifies web with a formatted error`() {
455
+ val webView: WebView = mock()
456
+
457
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "test op") {
458
+ throw RuntimeException("boom")
459
+ }
460
+
461
+ verify(webView).evaluateJavascript(
462
+ eq("window.WebBridge.callback('ON_CAMERA_ERROR', ${JSONObject.quote("Failed to test op: boom")})"),
463
+ isNull()
464
+ )
465
+ }
286
466
  }