basalt-core 0.0.0-rc5

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 (325) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli/basalt.d.ts +3 -0
  3. package/dist/cli/basalt.d.ts.map +1 -0
  4. package/dist/cli/basalt.js +47 -0
  5. package/dist/cli/basalt.js.map +1 -0
  6. package/dist/cli/bundle-cli.d.ts +12 -0
  7. package/dist/cli/bundle-cli.d.ts.map +1 -0
  8. package/dist/cli/bundle-cli.js +124 -0
  9. package/dist/cli/bundle-cli.js.map +1 -0
  10. package/dist/cli/bundleWithAssets.d.ts +79 -0
  11. package/dist/cli/bundleWithAssets.d.ts.map +1 -0
  12. package/dist/cli/bundleWithAssets.js +198 -0
  13. package/dist/cli/bundleWithAssets.js.map +1 -0
  14. package/dist/cli/desktop.d.ts +339 -0
  15. package/dist/cli/desktop.d.ts.map +1 -0
  16. package/dist/cli/desktop.js +1029 -0
  17. package/dist/cli/desktop.js.map +1 -0
  18. package/dist/cli/doctor.d.ts +31 -0
  19. package/dist/cli/doctor.d.ts.map +1 -0
  20. package/dist/cli/doctor.js +283 -0
  21. package/dist/cli/doctor.js.map +1 -0
  22. package/dist/cli/init.d.ts +77 -0
  23. package/dist/cli/init.d.ts.map +1 -0
  24. package/dist/cli/init.js +442 -0
  25. package/dist/cli/init.js.map +1 -0
  26. package/dist/cli/metro.d.ts +49 -0
  27. package/dist/cli/metro.d.ts.map +1 -0
  28. package/dist/cli/metro.js +148 -0
  29. package/dist/cli/metro.js.map +1 -0
  30. package/dist/cli/packageApp.d.ts +102 -0
  31. package/dist/cli/packageApp.d.ts.map +1 -0
  32. package/dist/cli/packageApp.js +319 -0
  33. package/dist/cli/packageApp.js.map +1 -0
  34. package/dist/cli/steps.d.ts +34 -0
  35. package/dist/cli/steps.d.ts.map +1 -0
  36. package/dist/cli/steps.js +53 -0
  37. package/dist/cli/steps.js.map +1 -0
  38. package/dist/metro-config.d.ts +168 -0
  39. package/dist/metro-config.d.ts.map +1 -0
  40. package/dist/metro-config.js +671 -0
  41. package/dist/metro-config.js.map +1 -0
  42. package/dist/react-native.config.d.ts +7 -0
  43. package/dist/react-native.config.d.ts.map +1 -0
  44. package/dist/react-native.config.js +80 -0
  45. package/dist/react-native.config.js.map +1 -0
  46. package/dist/src/DragSource.d.ts +52 -0
  47. package/dist/src/DragSource.d.ts.map +1 -0
  48. package/dist/src/DragSource.js +97 -0
  49. package/dist/src/DragSource.js.map +1 -0
  50. package/dist/src/DropTarget.d.ts +58 -0
  51. package/dist/src/DropTarget.d.ts.map +1 -0
  52. package/dist/src/DropTarget.js +131 -0
  53. package/dist/src/DropTarget.js.map +1 -0
  54. package/dist/src/KeyHandler.d.ts +57 -0
  55. package/dist/src/KeyHandler.d.ts.map +1 -0
  56. package/dist/src/KeyHandler.js +48 -0
  57. package/dist/src/KeyHandler.js.map +1 -0
  58. package/dist/src/Menu.d.ts +72 -0
  59. package/dist/src/Menu.d.ts.map +1 -0
  60. package/dist/src/Menu.js +179 -0
  61. package/dist/src/Menu.js.map +1 -0
  62. package/dist/src/TitleBar.d.ts +75 -0
  63. package/dist/src/TitleBar.d.ts.map +1 -0
  64. package/dist/src/TitleBar.js +236 -0
  65. package/dist/src/TitleBar.js.map +1 -0
  66. package/dist/src/Window.d.ts +87 -0
  67. package/dist/src/Window.d.ts.map +1 -0
  68. package/dist/src/Window.js +248 -0
  69. package/dist/src/Window.js.map +1 -0
  70. package/dist/src/closeRequest.d.ts +55 -0
  71. package/dist/src/closeRequest.d.ts.map +1 -0
  72. package/dist/src/closeRequest.js +134 -0
  73. package/dist/src/closeRequest.js.map +1 -0
  74. package/dist/src/index.d.ts +45 -0
  75. package/dist/src/index.d.ts.map +1 -0
  76. package/dist/src/index.js +92 -0
  77. package/dist/src/index.js.map +1 -0
  78. package/dist/src/overrides/Alert.d.ts +69 -0
  79. package/dist/src/overrides/Alert.d.ts.map +1 -0
  80. package/dist/src/overrides/Alert.js +112 -0
  81. package/dist/src/overrides/Alert.js.map +1 -0
  82. package/dist/src/overrides/BaseViewConfig.d.ts +28 -0
  83. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -0
  84. package/dist/src/overrides/BaseViewConfig.js +72 -0
  85. package/dist/src/overrides/BaseViewConfig.js.map +1 -0
  86. package/dist/src/overrides/Platform.linux.d.ts +10 -0
  87. package/dist/src/overrides/Platform.linux.d.ts.map +1 -0
  88. package/dist/src/overrides/Platform.linux.js +15 -0
  89. package/dist/src/overrides/Platform.linux.js.map +1 -0
  90. package/dist/src/overrides/Platform.macos.d.ts +10 -0
  91. package/dist/src/overrides/Platform.macos.d.ts.map +1 -0
  92. package/dist/src/overrides/Platform.macos.js +15 -0
  93. package/dist/src/overrides/Platform.macos.js.map +1 -0
  94. package/dist/src/overrides/Platform.windows.d.ts +10 -0
  95. package/dist/src/overrides/Platform.windows.d.ts.map +1 -0
  96. package/dist/src/overrides/Platform.windows.js +15 -0
  97. package/dist/src/overrides/Platform.windows.js.map +1 -0
  98. package/dist/src/overrides/ReactDevToolsSettingsManager.d.ts +24 -0
  99. package/dist/src/overrides/ReactDevToolsSettingsManager.d.ts.map +1 -0
  100. package/dist/src/overrides/ReactDevToolsSettingsManager.js +30 -0
  101. package/dist/src/overrides/ReactDevToolsSettingsManager.js.map +1 -0
  102. package/dist/src/overrides/RefreshControl.d.ts +52 -0
  103. package/dist/src/overrides/RefreshControl.d.ts.map +1 -0
  104. package/dist/src/overrides/RefreshControl.js +92 -0
  105. package/dist/src/overrides/RefreshControl.js.map +1 -0
  106. package/dist/src/overrides/ScrollView.d.ts +39 -0
  107. package/dist/src/overrides/ScrollView.d.ts.map +1 -0
  108. package/dist/src/overrides/ScrollView.js +59 -0
  109. package/dist/src/overrides/ScrollView.js.map +1 -0
  110. package/dist/src/overrides/Share.d.ts +96 -0
  111. package/dist/src/overrides/Share.d.ts.map +1 -0
  112. package/dist/src/overrides/Share.js +97 -0
  113. package/dist/src/overrides/Share.js.map +1 -0
  114. package/dist/src/overrides/TextInput.d.ts +81 -0
  115. package/dist/src/overrides/TextInput.d.ts.map +1 -0
  116. package/dist/src/overrides/TextInput.js +213 -0
  117. package/dist/src/overrides/TextInput.js.map +1 -0
  118. package/dist/src/overrides/View.d.ts +36 -0
  119. package/dist/src/overrides/View.d.ts.map +1 -0
  120. package/dist/src/overrides/View.js +99 -0
  121. package/dist/src/overrides/View.js.map +1 -0
  122. package/dist/src/overrides/createPlatform.d.ts +32 -0
  123. package/dist/src/overrides/createPlatform.d.ts.map +1 -0
  124. package/dist/src/overrides/createPlatform.js +84 -0
  125. package/dist/src/overrides/createPlatform.js.map +1 -0
  126. package/dist/src/overrides/setUpDeveloperTools.d.ts +28 -0
  127. package/dist/src/overrides/setUpDeveloperTools.d.ts.map +1 -0
  128. package/dist/src/overrides/setUpDeveloperTools.js +84 -0
  129. package/dist/src/overrides/setUpDeveloperTools.js.map +1 -0
  130. package/dist/src/overrides/setUpXHR.d.ts +46 -0
  131. package/dist/src/overrides/setUpXHR.d.ts.map +1 -0
  132. package/dist/src/overrides/setUpXHR.js +236 -0
  133. package/dist/src/overrides/setUpXHR.js.map +1 -0
  134. package/dist/src/quitRequest.d.ts +55 -0
  135. package/dist/src/quitRequest.d.ts.map +1 -0
  136. package/dist/src/quitRequest.js +121 -0
  137. package/dist/src/quitRequest.js.map +1 -0
  138. package/dist/src/resourcePath.d.ts +18 -0
  139. package/dist/src/resourcePath.d.ts.map +1 -0
  140. package/dist/src/resourcePath.js +31 -0
  141. package/dist/src/resourcePath.js.map +1 -0
  142. package/dist/src/responseBody.d.ts +76 -0
  143. package/dist/src/responseBody.d.ts.map +1 -0
  144. package/dist/src/responseBody.js +94 -0
  145. package/dist/src/responseBody.js.map +1 -0
  146. package/dist/src/responseHeaders.d.ts +40 -0
  147. package/dist/src/responseHeaders.d.ts.map +1 -0
  148. package/dist/src/responseHeaders.js +75 -0
  149. package/dist/src/responseHeaders.js.map +1 -0
  150. package/dist/src/titleBarState.d.ts +43 -0
  151. package/dist/src/titleBarState.d.ts.map +1 -0
  152. package/dist/src/titleBarState.js +77 -0
  153. package/dist/src/titleBarState.js.map +1 -0
  154. package/dist/src/useContextMenu.d.ts +113 -0
  155. package/dist/src/useContextMenu.d.ts.map +1 -0
  156. package/dist/src/useContextMenu.js +109 -0
  157. package/dist/src/useContextMenu.js.map +1 -0
  158. package/dist/src/useDialog.d.ts +98 -0
  159. package/dist/src/useDialog.d.ts.map +1 -0
  160. package/dist/src/useDialog.js +112 -0
  161. package/dist/src/useDialog.js.map +1 -0
  162. package/dist/src/useDisplays.d.ts +70 -0
  163. package/dist/src/useDisplays.d.ts.map +1 -0
  164. package/dist/src/useDisplays.js +118 -0
  165. package/dist/src/useDisplays.js.map +1 -0
  166. package/dist/src/useHandledKeys.d.ts +38 -0
  167. package/dist/src/useHandledKeys.d.ts.map +1 -0
  168. package/dist/src/useHandledKeys.js +70 -0
  169. package/dist/src/useHandledKeys.js.map +1 -0
  170. package/dist/src/useWindow.d.ts +140 -0
  171. package/dist/src/useWindow.d.ts.map +1 -0
  172. package/dist/src/useWindow.js +233 -0
  173. package/dist/src/useWindow.js.map +1 -0
  174. package/dist/tsconfig.tsbuildinfo +1 -0
  175. package/native/CMakeLists.txt +355 -0
  176. package/native/bootstrap.sh +613 -0
  177. package/native/cmake/CompilerFlags.cmake +49 -0
  178. package/native/cmake/Expo.cmake +125 -0
  179. package/native/cmake/ReactNativeCore.cmake +612 -0
  180. package/native/cmake/Reanimated.cmake +108 -0
  181. package/native/cmake/Skia.cmake +379 -0
  182. package/native/cmake/ThirdParty.cmake +290 -0
  183. package/native/cmake/Worklets.cmake +67 -0
  184. package/native/cmake/codegen/CMakeLists.txt +32 -0
  185. package/native/core/AppIdentity.cpp +85 -0
  186. package/native/core/AppIdentity.h +59 -0
  187. package/native/core/AppPaths.cpp +96 -0
  188. package/native/core/AppPaths.h +53 -0
  189. package/native/core/AppearanceModule.cpp +60 -0
  190. package/native/core/AppearanceModule.h +40 -0
  191. package/native/core/Backface.h +59 -0
  192. package/native/core/BlobModule.cpp +283 -0
  193. package/native/core/BlobModule.h +76 -0
  194. package/native/core/BlobRegistry.cpp +75 -0
  195. package/native/core/BlobRegistry.h +44 -0
  196. package/native/core/ColorScheme.cpp +105 -0
  197. package/native/core/ColorScheme.h +59 -0
  198. package/native/core/ComponentRegistry.h +30 -0
  199. package/native/core/ControlMetrics.h +37 -0
  200. package/native/core/CoreModules.cpp +592 -0
  201. package/native/core/CoreModules.h +308 -0
  202. package/native/core/DebuggingOverlay.cpp +106 -0
  203. package/native/core/DebuggingOverlay.h +65 -0
  204. package/native/core/DesktopControls.cpp +232 -0
  205. package/native/core/DesktopControls.h +170 -0
  206. package/native/core/DevBundle.cpp +108 -0
  207. package/native/core/DevBundle.h +69 -0
  208. package/native/core/DevMenu.cpp +99 -0
  209. package/native/core/DevMenu.h +75 -0
  210. package/native/core/DialogModule.cpp +150 -0
  211. package/native/core/DialogModule.h +51 -0
  212. package/native/core/DragAndDrop.cpp +45 -0
  213. package/native/core/DragAndDrop.h +225 -0
  214. package/native/core/ExpoImageComponent.cpp +79 -0
  215. package/native/core/ExpoImageComponent.h +80 -0
  216. package/native/core/ExpoModules.cpp +415 -0
  217. package/native/core/ExpoModules.h +115 -0
  218. package/native/core/ExpoRuntime.cpp +366 -0
  219. package/native/core/ExpoRuntime.h +42 -0
  220. package/native/core/FocusRing.h +36 -0
  221. package/native/core/FontRegistry.h +51 -0
  222. package/native/core/GestureHandlerModule.cpp +136 -0
  223. package/native/core/GestureHandlerModule.h +71 -0
  224. package/native/core/Gestures.cpp +533 -0
  225. package/native/core/Gestures.h +145 -0
  226. package/native/core/HoverTracker.cpp +69 -0
  227. package/native/core/HoverTracker.h +109 -0
  228. package/native/core/HttpClient.cpp +282 -0
  229. package/native/core/ImageBytes.cpp +205 -0
  230. package/native/core/ImageBytes.h +29 -0
  231. package/native/core/ImageCache.cpp +51 -0
  232. package/native/core/ImageCache.h +89 -0
  233. package/native/core/JsRuntimeAccess.cpp +70 -0
  234. package/native/core/JsRuntimeAccess.h +60 -0
  235. package/native/core/JsiPromise.h +51 -0
  236. package/native/core/KeyEvents.cpp +116 -0
  237. package/native/core/KeyEvents.h +144 -0
  238. package/native/core/LogBoxSurface.cpp +46 -0
  239. package/native/core/LogBoxSurface.h +68 -0
  240. package/native/core/MenuModel.h +117 -0
  241. package/native/core/MenuModule.cpp +209 -0
  242. package/native/core/MenuModule.h +66 -0
  243. package/native/core/MountingWalk.h +524 -0
  244. package/native/core/PackageModules.cpp.in +25 -0
  245. package/native/core/PackageModules.h +50 -0
  246. package/native/core/PlatformConstantsModule.cpp +63 -0
  247. package/native/core/PlatformConstantsModule.h +64 -0
  248. package/native/core/PlatformServices.h +213 -0
  249. package/native/core/PointerButtons.h +63 -0
  250. package/native/core/PullToRefresh.cpp +57 -0
  251. package/native/core/PullToRefresh.h +73 -0
  252. package/native/core/ReanimatedCompat.h +47 -0
  253. package/native/core/ReanimatedModule.cpp +203 -0
  254. package/native/core/ReanimatedModule.h +55 -0
  255. package/native/core/ScrollAnimation.cpp +59 -0
  256. package/native/core/ScrollAnimation.h +70 -0
  257. package/native/core/ScrollBounds.cpp +26 -0
  258. package/native/core/ScrollBounds.h +68 -0
  259. package/native/core/ScrollIndicator.cpp +50 -0
  260. package/native/core/ScrollIndicator.h +69 -0
  261. package/native/core/ScrollMomentum.cpp +71 -0
  262. package/native/core/ScrollMomentum.h +73 -0
  263. package/native/core/ScrollSnap.cpp +123 -0
  264. package/native/core/ScrollSnap.h +80 -0
  265. package/native/core/ShareFallback.cpp +101 -0
  266. package/native/core/ShareFallback.h +45 -0
  267. package/native/core/SkiaPictureViewComponent.cpp +11 -0
  268. package/native/core/SkiaPictureViewComponent.h +47 -0
  269. package/native/core/SourceCodeModule.cpp +55 -0
  270. package/native/core/SourceCodeModule.h +53 -0
  271. package/native/core/StatusBarModule.cpp +23 -0
  272. package/native/core/StatusBarModule.h +42 -0
  273. package/native/core/TestDialog.cpp +148 -0
  274. package/native/core/TestDialog.h +96 -0
  275. package/native/core/TestKeys.cpp +78 -0
  276. package/native/core/TestKeys.h +50 -0
  277. package/native/core/TestQuitFile.cpp +33 -0
  278. package/native/core/TestQuitFile.h +60 -0
  279. package/native/core/TestSettle.h +192 -0
  280. package/native/core/TitleBarRegions.h +31 -0
  281. package/native/core/TurboModuleProxy.cpp +51 -0
  282. package/native/core/TurboModuleProxy.h +46 -0
  283. package/native/core/UIManagerAccess.cpp +43 -0
  284. package/native/core/UIManagerAccess.h +43 -0
  285. package/native/core/WindowBoundsCache.cpp +319 -0
  286. package/native/core/WindowControl.h +235 -0
  287. package/native/core/WindowHost.h +158 -0
  288. package/native/core/WindowMethods.cpp +167 -0
  289. package/native/core/WindowMethods.h +84 -0
  290. package/native/core/WindowsModule.cpp +448 -0
  291. package/native/core/WindowsModule.h +133 -0
  292. package/native/core/WorkletsModule.cpp +199 -0
  293. package/native/core/WorkletsModule.h +57 -0
  294. package/native/core/portability_probe.cpp +210 -0
  295. package/native/tests/EventRecorder.h +124 -0
  296. package/native/tests/TestHarness.cpp +109 -0
  297. package/native/tests/TestHarness.h +89 -0
  298. package/native/tests/TreeDump.h +49 -0
  299. package/native/tests/test_app_identity.cpp +114 -0
  300. package/native/tests/test_app_paths.cpp +48 -0
  301. package/native/tests/test_appearance.cpp +107 -0
  302. package/native/tests/test_backface.cpp +79 -0
  303. package/native/tests/test_blobs.cpp +157 -0
  304. package/native/tests/test_controls.cpp +185 -0
  305. package/native/tests/test_devbundle.cpp +103 -0
  306. package/native/tests/test_drag_and_drop.cpp +143 -0
  307. package/native/tests/test_gestures.cpp +331 -0
  308. package/native/tests/test_hover.cpp +92 -0
  309. package/native/tests/test_image_cache.cpp +109 -0
  310. package/native/tests/test_js_runtime_access.cpp +137 -0
  311. package/native/tests/test_key_events.cpp +235 -0
  312. package/native/tests/test_momentum.cpp +138 -0
  313. package/native/tests/test_quit_file.cpp +104 -0
  314. package/native/tests/test_scroll_animation.cpp +120 -0
  315. package/native/tests/test_scroll_bounds.cpp +66 -0
  316. package/native/tests/test_scroll_indicator.cpp +149 -0
  317. package/native/tests/test_scroll_snap.cpp +112 -0
  318. package/native/tests/test_settle.cpp +115 -0
  319. package/native/tests/test_share.cpp +75 -0
  320. package/native/tests/test_sourcecode.cpp +62 -0
  321. package/native/tests/test_window_close.cpp +119 -0
  322. package/native/tests/test_window_limits.cpp +123 -0
  323. package/package.json +43 -0
  324. package/react-native.config.js +86 -0
  325. package/supported-versions.json +66 -0
@@ -0,0 +1,235 @@
1
+ // Which view claims which key. The decision, not the delivery.
2
+ //
3
+ // A host asks `handledBy` in the middle of `keyDown:`, `key-pressed` or
4
+ // `WM_KEYDOWN` and consumes the key or passes it on by the answer. So the two
5
+ // failures that matter are opposite and both silent: claiming a key nobody
6
+ // declared swallows it from the menu and from text fields, and failing to claim
7
+ // one that was declared means a shortcut that quietly never fires.
8
+
9
+ #include "TestHarness.h"
10
+
11
+ #include "KeyEvents.h"
12
+ #include "TestKeys.h"
13
+
14
+ #include <sstream>
15
+ #include <string>
16
+ #include <vector>
17
+
18
+ using basalt::KeyCombination;
19
+ using basalt::KeyModifiers;
20
+
21
+ namespace {
22
+
23
+ KeyCombination press(std::string key, KeyModifiers modifiers = {}) {
24
+ return KeyCombination{std::move(key), modifiers};
25
+ }
26
+
27
+ KeyModifiers meta() { return KeyModifiers{.alt = false, .ctrl = false, .meta = true, .shift = false}; }
28
+ KeyModifiers metaShift() {
29
+ return KeyModifiers{.alt = false, .ctrl = false, .meta = true, .shift = true};
30
+ }
31
+
32
+ // Tags are React's and mean nothing here, so they are just distinct numbers.
33
+ constexpr facebook::react::Tag kPane = 11;
34
+ constexpr facebook::react::Tag kWindow = 3;
35
+ constexpr facebook::react::Tag kUnrelated = 99;
36
+
37
+ void reset() {
38
+ for (facebook::react::Tag tag : {kPane, kWindow, kUnrelated}) {
39
+ basalt::clearHandledKeys(tag);
40
+ }
41
+ }
42
+
43
+ } // namespace
44
+
45
+ TEST(keys_a_view_that_claimed_nothing_claims_nothing) {
46
+ reset();
47
+ // The common case by a wide margin: no app in this repository's suite declares
48
+ // a key, and every one of them presses some.
49
+ EXPECT(!basalt::handlesKey(kPane, press("a")));
50
+ EXPECT(!basalt::handledBy({kPane, kWindow}, press("a")).has_value());
51
+ EXPECT_EQ((int)basalt::handledKeyViewCount(), 0);
52
+ }
53
+
54
+ TEST(keys_a_claim_matches_on_key_and_modifiers) {
55
+ reset();
56
+ basalt::setHandledKeys(kPane, {press("z", meta())});
57
+
58
+ EXPECT(basalt::handlesKey(kPane, press("z", meta())));
59
+ // Same key, no modifier: a different combination, and an app that bound Cmd+Z
60
+ // did not bind Z.
61
+ EXPECT(!basalt::handlesKey(kPane, press("z")));
62
+ // Same modifiers, different key.
63
+ EXPECT(!basalt::handlesKey(kPane, press("y", meta())));
64
+ // A modifier the claim did not ask for. Shift+Cmd+Z is redo and is its own
65
+ // binding; matching it here would fire undo.
66
+ EXPECT(!basalt::handlesKey(kPane, press("z", metaShift())));
67
+ }
68
+
69
+ TEST(keys_case_follows_the_character) {
70
+ reset();
71
+ // As in a browser: Shift+A reports "A" with shift set, and an app may bind
72
+ // either. They are different combinations and neither is wrong.
73
+ basalt::setHandledKeys(kPane, {press("A", KeyModifiers{.shift = true})});
74
+ EXPECT(basalt::handlesKey(kPane, press("A", KeyModifiers{.shift = true})));
75
+ EXPECT(!basalt::handlesKey(kPane, press("a", KeyModifiers{.shift = true})));
76
+ }
77
+
78
+ TEST(keys_the_innermost_claim_wins) {
79
+ reset();
80
+ // Both claim it. The path runs from the focused view outwards, so the pane
81
+ // answers -- a shortcut on a pane should beat the same one on the window.
82
+ basalt::setHandledKeys(kPane, {press(basalt::kKeyEscape)});
83
+ basalt::setHandledKeys(kWindow, {press(basalt::kKeyEscape)});
84
+
85
+ const auto handler = basalt::handledBy({kPane, kWindow}, press(basalt::kKeyEscape));
86
+ EXPECT(handler.has_value());
87
+ EXPECT_EQ((int)*handler, (int)kPane);
88
+
89
+ // And with the pane out of the path, the window answers: an ancestor's claim
90
+ // is not shadowed by a descendant that exists, only by one in the path.
91
+ const auto outer = basalt::handledBy({kWindow}, press(basalt::kKeyEscape));
92
+ EXPECT(outer.has_value());
93
+ EXPECT_EQ((int)*outer, (int)kWindow);
94
+ }
95
+
96
+ TEST(keys_a_view_outside_the_path_is_not_asked) {
97
+ reset();
98
+ // The focus path is the whole of it. A view elsewhere in the tree claiming a
99
+ // key must not swallow it -- that is how a background pane would steal typing.
100
+ basalt::setHandledKeys(kUnrelated, {press("j")});
101
+ EXPECT(!basalt::handledBy({kPane, kWindow}, press("j")).has_value());
102
+ }
103
+
104
+ TEST(keys_a_new_declaration_replaces_the_old) {
105
+ reset();
106
+ basalt::setHandledKeys(kPane, {press("j"), press("k"), press("l")});
107
+ EXPECT(basalt::handlesKey(kPane, press("k")));
108
+
109
+ // A re-render hands over the whole list. Merging would keep "k" alive after
110
+ // the app stopped declaring it, which would look like the shortcut working --
111
+ // the failure this list being assigned rather than appended exists to prevent.
112
+ basalt::setHandledKeys(kPane, {press("j"), press("l")});
113
+ EXPECT(basalt::handlesKey(kPane, press("j")));
114
+ EXPECT(!basalt::handlesKey(kPane, press("k")));
115
+ EXPECT_EQ((int)basalt::handledKeyViewCount(), 1);
116
+ }
117
+
118
+ TEST(keys_an_empty_declaration_is_not_the_same_as_unmounting) {
119
+ reset();
120
+ basalt::setHandledKeys(kPane, {press("j")});
121
+ basalt::setHandledKeys(kPane, {});
122
+ // Still registered, claiming nothing: a view whose shortcuts are all disabled
123
+ // exists and may get them back.
124
+ EXPECT_EQ((int)basalt::handledKeyViewCount(), 1);
125
+ EXPECT(!basalt::handlesKey(kPane, press("j")));
126
+
127
+ basalt::clearHandledKeys(kPane);
128
+ EXPECT_EQ((int)basalt::handledKeyViewCount(), 0);
129
+ }
130
+
131
+ TEST(keys_a_press_reaches_the_listener) {
132
+ reset();
133
+ basalt::setHandledKeys(kPane, {press("m")});
134
+
135
+ int calls = 0;
136
+ facebook::react::Tag seenTag = 0;
137
+ std::string seenKey;
138
+ bool seenShift = false;
139
+ basalt::setKeyListener([&](facebook::react::Tag tag, const KeyCombination &pressed) {
140
+ calls++;
141
+ seenTag = tag;
142
+ seenKey = pressed.key;
143
+ seenShift = pressed.modifiers.shift;
144
+ });
145
+
146
+ basalt::reportKey(kPane, press("m", KeyModifiers{.shift = true}));
147
+ EXPECT_EQ(calls, 1);
148
+ EXPECT_EQ((int)seenTag, (int)kPane);
149
+ EXPECT_EQ(seenKey, std::string("m"));
150
+ EXPECT(seenShift);
151
+
152
+ // Reported as given, not re-matched: the host has already decided, and a
153
+ // second opinion here could disagree with the one the key was consumed on.
154
+ basalt::reportKey(kPane, press("not-a-key"));
155
+ EXPECT_EQ(calls, 2);
156
+
157
+ basalt::setKeyListener(nullptr);
158
+ basalt::reportKey(kPane, press("m"));
159
+ EXPECT_EQ(calls, 2);
160
+ }
161
+
162
+ TEST(keys_the_special_names_are_the_ones_a_browser_uses) {
163
+ // Spelled once, here, because three hosts mapping their own key codes is three
164
+ // chances to write "Arrowleft" -- and the failure is a shortcut that silently
165
+ // never fires, on one platform only. core/TestQuitFile.h has the same note for
166
+ // the same reason.
167
+ EXPECT_EQ(std::string(basalt::kKeyArrowLeft), std::string("ArrowLeft"));
168
+ EXPECT_EQ(std::string(basalt::kKeyArrowRight), std::string("ArrowRight"));
169
+ EXPECT_EQ(std::string(basalt::kKeyArrowUp), std::string("ArrowUp"));
170
+ EXPECT_EQ(std::string(basalt::kKeyArrowDown), std::string("ArrowDown"));
171
+ EXPECT_EQ(std::string(basalt::kKeyEscape), std::string("Escape"));
172
+ EXPECT_EQ(std::string(basalt::kKeyEnter), std::string("Enter"));
173
+ EXPECT_EQ(std::string(basalt::kKeyTab), std::string("Tab"));
174
+ EXPECT_EQ(std::string(basalt::kKeyPageUp), std::string("PageUp"));
175
+ EXPECT_EQ(std::string(basalt::kKeyPageDown), std::string("PageDown"));
176
+ // The one that looks like a mistake and is not: a space is a character.
177
+ EXPECT_EQ(std::string(basalt::kKeySpace), std::string(" "));
178
+ }
179
+
180
+
181
+ // --- BASALT_TEST_KEY's syntax ----------------------------------------------
182
+ //
183
+ // Parsed in core rather than in three hosts, because three parsers is three
184
+ // chances to disagree about what "z+meta" means -- and to disagree quietly: the
185
+ // wrong answer is a combination that matches nothing, which a scenario reports as
186
+ // a shortcut that did not fire rather than as a bad instrument.
187
+
188
+ TEST(keys_the_instrument_reads_a_bare_key) {
189
+ const KeyCombination pressed = basalt::parseKeyPress("m");
190
+ EXPECT_EQ(pressed.key, std::string("m"));
191
+ EXPECT(pressed.modifiers == KeyModifiers{});
192
+ }
193
+
194
+ TEST(keys_the_instrument_reads_modifiers) {
195
+ const KeyCombination undo = basalt::parseKeyPress("z+meta");
196
+ EXPECT_EQ(undo.key, std::string("z"));
197
+ EXPECT(undo.modifiers.meta);
198
+ EXPECT(!undo.modifiers.shift);
199
+
200
+ const KeyCombination redo = basalt::parseKeyPress("z+meta+shift");
201
+ EXPECT(redo.modifiers.meta);
202
+ EXPECT(redo.modifiers.shift);
203
+ // The pair that must differ, or a test for redo would pass on undo.
204
+ EXPECT(!(undo == redo));
205
+ }
206
+
207
+ TEST(keys_the_instrument_reads_a_named_key) {
208
+ EXPECT_EQ(basalt::parseKeyPress("ArrowLeft").key, std::string("ArrowLeft"));
209
+ // A space is a key named " ", and writing it in an environment variable means
210
+ // the parser must not trim.
211
+ EXPECT_EQ(basalt::parseKeyPress(" ").key, std::string(" "));
212
+ }
213
+
214
+ TEST(keys_the_instrument_reads_a_literal_plus) {
215
+ // "+" is both the separator and a key an editor binds -- zoom in. Spelt by
216
+ // being first, which is the one case where an empty piece means something.
217
+ EXPECT_EQ(basalt::parseKeyPress("+").key, std::string("+"));
218
+ const KeyCombination zoom = basalt::parseKeyPress("++meta");
219
+ EXPECT_EQ(zoom.key, std::string("+"));
220
+ EXPECT(zoom.modifiers.meta);
221
+ }
222
+
223
+ TEST(keys_the_instrument_ignores_a_modifier_it_does_not_know) {
224
+ // A typo gives a combination that matches nothing rather than an error, and the
225
+ // scenario then reports a shortcut that did not fire -- the honest symptom.
226
+ const KeyCombination pressed = basalt::parseKeyPress("z+command");
227
+ EXPECT_EQ(pressed.key, std::string("z"));
228
+ EXPECT(pressed.modifiers == KeyModifiers{});
229
+ }
230
+
231
+ TEST(keys_the_instrument_is_empty_when_unset) {
232
+ // Every scenario that does not press a key relies on this, which is all but
233
+ // one of them.
234
+ EXPECT(basalt::scriptedKeyPresses().empty());
235
+ }
@@ -0,0 +1,138 @@
1
+ // Tests for the fling model.
2
+ //
3
+ // Core's, so compiled into every platform's suite -- although only one platform
4
+ // uses it. macOS decelerates a scroll itself and the AppKit host only reports
5
+ // what the system sends; GTK emits one velocity and stops, so the coasting is
6
+ // this project's. See core/ScrollMomentum.h.
7
+ //
8
+ // Testable at all because the model is a pure function of a velocity and a
9
+ // clock: no widget, no frame clock, and no waiting a second for a fling to
10
+ // finish.
11
+
12
+ #include "TestHarness.h"
13
+
14
+ #include "ScrollMomentum.h"
15
+
16
+ #include <sstream>
17
+
18
+ namespace {
19
+
20
+ using basalt::ScrollMomentum;
21
+
22
+ // A frame at sixty a second, which is what a tick callback hands over.
23
+ constexpr double kFrame = 1.0 / 60.0;
24
+
25
+ // How far a fling travels in total, and how many frames it takes. Bounded, so
26
+ // a model that never stops fails the assertion rather than hanging the suite.
27
+ struct Flight {
28
+ double distanceY{0};
29
+ int frames{0};
30
+ };
31
+
32
+ Flight flyY(ScrollMomentum &momentum) {
33
+ Flight flight;
34
+ double dx = 0;
35
+ double dy = 0;
36
+ while (momentum.advance(kFrame, dx, dy) && flight.frames < 6000) {
37
+ flight.distanceY += dy;
38
+ flight.frames++;
39
+ }
40
+ flight.distanceY += dy;
41
+ flight.frames++;
42
+ return flight;
43
+ }
44
+
45
+ } // namespace
46
+
47
+ TEST(momentum_a_fling_travels_and_stops) {
48
+ ScrollMomentum momentum;
49
+ EXPECT(momentum.start(0, 1200, ScrollMomentum::kNormalDeceleration));
50
+ EXPECT(momentum.isRunning());
51
+
52
+ const Flight flight = flyY(momentum);
53
+ EXPECT(!momentum.isRunning());
54
+ // The numbers are the model's, not a target: 1200px/s at 0.998 per
55
+ // millisecond comes out at about 595 pixels over about 111 frames, which is
56
+ // roughly what the same flick does on a phone. Asserted as a range, so a
57
+ // change in the constants is caught and a change in the integration step is
58
+ // not.
59
+ EXPECT(flight.distanceY > 450);
60
+ EXPECT(flight.distanceY < 750);
61
+ EXPECT(flight.frames > 60);
62
+ EXPECT(flight.frames < 200);
63
+ }
64
+
65
+ TEST(momentum_a_faster_deceleration_travels_less) {
66
+ ScrollMomentum normal;
67
+ normal.start(0, 1200, ScrollMomentum::kNormalDeceleration);
68
+ const Flight slow = flyY(normal);
69
+
70
+ ScrollMomentum fast;
71
+ fast.start(0, 1200, ScrollMomentum::kFastDeceleration);
72
+ const Flight quick = flyY(fast);
73
+
74
+ // What React Native's `decelerationRate` prop is for. 'fast' is the smaller
75
+ // number and the shorter fling, which reads backwards until you remember it
76
+ // is the fraction of the velocity that survives each millisecond.
77
+ EXPECT(quick.distanceY < slow.distanceY);
78
+ EXPECT(quick.frames < slow.frames);
79
+ }
80
+
81
+ TEST(momentum_a_fling_keeps_its_direction_on_both_axes) {
82
+ ScrollMomentum momentum;
83
+ momentum.start(-800, 1600, ScrollMomentum::kNormalDeceleration);
84
+ double dx = 0;
85
+ double dy = 0;
86
+ momentum.advance(kFrame, dx, dy);
87
+ EXPECT(dx < 0);
88
+ EXPECT(dy > 0);
89
+ // And in proportion: the model decays a velocity, it does not reshape one.
90
+ EXPECT_NEAR(dy / dx, -2.0, 0.001);
91
+ }
92
+
93
+ TEST(momentum_a_slow_flick_is_not_a_fling) {
94
+ ScrollMomentum momentum;
95
+ // Below the stopping speed, so it would report a momentum scroll that began
96
+ // and ended with nothing in between -- worse than not reporting one.
97
+ EXPECT(!momentum.start(0, 5, ScrollMomentum::kNormalDeceleration));
98
+ EXPECT(!momentum.isRunning());
99
+ double dx = 0;
100
+ double dy = 0;
101
+ EXPECT(!momentum.advance(kFrame, dx, dy));
102
+ EXPECT_NEAR(dx, 0.0, 0.0001);
103
+ EXPECT_NEAR(dy, 0.0, 0.0001);
104
+ }
105
+
106
+ TEST(momentum_stopping_ends_it_at_once) {
107
+ ScrollMomentum momentum;
108
+ momentum.start(0, 2000, ScrollMomentum::kNormalDeceleration);
109
+ momentum.stop();
110
+ EXPECT(!momentum.isRunning());
111
+ double dx = 0;
112
+ double dy = 0;
113
+ EXPECT(!momentum.advance(kFrame, dx, dy));
114
+ EXPECT_NEAR(dy, 0.0, 0.0001);
115
+ }
116
+
117
+ TEST(momentum_a_stalled_frame_clock_does_not_teleport_the_list) {
118
+ ScrollMomentum momentum;
119
+ momentum.start(0, 1000, ScrollMomentum::kNormalDeceleration);
120
+ double dx = 0;
121
+ double dy = 0;
122
+ // A whole second between frames -- a window dragged between monitors, or a
123
+ // machine that swapped. Without a clamp this is a thousand pixels in one
124
+ // step, which reads as the list jumping rather than coasting.
125
+ momentum.advance(1.0, dx, dy);
126
+ EXPECT(dy <= 100.0);
127
+ }
128
+
129
+ TEST(momentum_a_deceleration_rate_of_one_would_never_stop) {
130
+ ScrollMomentum momentum;
131
+ // `decelerationRate` is a float prop an app can set to anything. A rate of 1
132
+ // keeps the whole velocity every millisecond, which is a list that scrolls
133
+ // for ever; the model clamps rather than trusting it.
134
+ momentum.start(0, 1000, 1.0);
135
+ const Flight flight = flyY(momentum);
136
+ EXPECT(flight.frames < 6000);
137
+ EXPECT(!momentum.isRunning());
138
+ }
@@ -0,0 +1,104 @@
1
+ // The instrument that lets the harness end a run at a moment of its choosing.
2
+ //
3
+ // What is testable here is the decision -- is the variable set, and is the file
4
+ // there yet -- rather than the quitting, which is three different shutdowns and
5
+ // belongs to each host. This runs on all three, which is the point: the
6
+ // instrument exists because the *portable* way to end a host was a signal, and
7
+ // Windows has no SIGTERM.
8
+
9
+ #include "TestHarness.h"
10
+
11
+ #include "TestQuitFile.h"
12
+
13
+ #include <cstdio>
14
+ #include <cstdlib>
15
+ #include <filesystem>
16
+ #include <fstream>
17
+ #include <sstream>
18
+ #include <string>
19
+
20
+ namespace {
21
+
22
+ // A path that does not exist yet, in the place the platform keeps such things.
23
+ std::string scratchPath() {
24
+ std::error_code ignored;
25
+ const std::filesystem::path path =
26
+ std::filesystem::temp_directory_path(ignored) / "basalt-test-quit-file-probe";
27
+ return path.string();
28
+ }
29
+
30
+ void setVariable(const std::string &value) {
31
+ #ifdef _WIN32
32
+ _putenv_s(basalt::kTestQuitFileVar, value.c_str());
33
+ #else
34
+ setenv(basalt::kTestQuitFileVar, value.c_str(), 1);
35
+ #endif
36
+ }
37
+
38
+ } // namespace
39
+
40
+ // Set before anything reads it, because the path is cached on first read -- the
41
+ // environment does not change under a running process, and each host asks for
42
+ // this on a timer. One test therefore establishes it for the file, which is why
43
+ // the ordering below is deliberate rather than incidental.
44
+ TEST(quit_file_reads_the_variable) {
45
+ const std::string path = scratchPath();
46
+ std::error_code ignored;
47
+ std::filesystem::remove(path, ignored);
48
+ setVariable(path);
49
+
50
+ EXPECT(basalt::testQuitFilePath().has_value());
51
+ EXPECT_EQ(*basalt::testQuitFilePath(), path);
52
+ }
53
+
54
+ TEST(quit_file_is_not_there_until_it_is) {
55
+ const std::string path = *basalt::testQuitFilePath();
56
+ std::error_code ignored;
57
+ std::filesystem::remove(path, ignored);
58
+
59
+ // The whole contract: false until the file exists, and true once it does.
60
+ // A host polls this, so the transition is the instrument.
61
+ EXPECT(!basalt::testQuitFileAppeared());
62
+
63
+ { std::ofstream created(path); }
64
+ EXPECT(basalt::testQuitFileAppeared());
65
+
66
+ // And not cached, unlike the path: a stale true would quit the next run at
67
+ // startup, before it had done anything, and the failure would look like a
68
+ // crash rather than like a leftover file.
69
+ std::filesystem::remove(path, ignored);
70
+ EXPECT(!basalt::testQuitFileAppeared());
71
+ }
72
+
73
+ TEST(quit_file_is_content_free) {
74
+ const std::string path = *basalt::testQuitFilePath();
75
+ std::error_code ignored;
76
+
77
+ // Existence is the message. An empty file is the normal case, because the
78
+ // harness has nothing to say beyond "now" -- and if content mattered there
79
+ // would be a race between creating the file and writing it.
80
+ { std::ofstream created(path); }
81
+ EXPECT(basalt::testQuitFileAppeared());
82
+
83
+ { std::ofstream created(path); created << "ignored"; }
84
+ EXPECT(basalt::testQuitFileAppeared());
85
+
86
+ // A directory at that path counts too. Nothing creates one, and reporting it
87
+ // as "not yet" would hang the run instead of ending it.
88
+ std::filesystem::remove(path, ignored);
89
+ std::filesystem::create_directory(path, ignored);
90
+ EXPECT(basalt::testQuitFileAppeared());
91
+ std::filesystem::remove(path, ignored);
92
+ }
93
+
94
+ TEST(quit_file_poll_and_message_are_shared) {
95
+ // The three hosts read these rather than spelling them out, so that a person
96
+ // reading three transcripts side by side sees the same shutdown in each.
97
+ // BASALT_QUIT_AFTER_MS had to have its line retrofitted onto Windows after a
98
+ // scenario asserted on a string only two hosts printed.
99
+ EXPECT_EQ(std::string(basalt::kTestQuitFileVar), std::string("BASALT_TEST_QUIT_FILE"));
100
+ EXPECT_EQ(std::string(basalt::kTestQuitFileMessage),
101
+ std::string("BASALT_TEST_QUIT_FILE appeared; quitting"));
102
+ // Responsive enough not to show next to process teardown, and not a busy loop.
103
+ EXPECT(basalt::kTestQuitFilePollMs > 0 && basalt::kTestQuitFilePollMs <= 500);
104
+ }
@@ -0,0 +1,120 @@
1
+ // An animated scroll, which is arithmetic and so can be tested without a
2
+ // display -- unlike almost everything else about a scroll view on these hosts.
3
+
4
+ #include "TestHarness.h"
5
+
6
+ #include "ScrollAnimation.h"
7
+
8
+ #include <cmath>
9
+ #include <sstream>
10
+
11
+ using basalt::ScrollAnimation;
12
+
13
+ namespace {
14
+
15
+ TEST(scroll_animation_a_target_equal_to_the_start_is_not_an_animation) {
16
+ // So a caller can apply the offset and be done rather than running a curve
17
+ // that goes nowhere -- and, more to the point, never reporting that it
18
+ // finished.
19
+ ScrollAnimation animation;
20
+ EXPECT(!animation.start(10, 20, 10, 20));
21
+ EXPECT(!animation.isRunning());
22
+ }
23
+
24
+ TEST(scroll_animation_arrives_exactly_at_its_target) {
25
+ // Evaluated at the end rather than approached. An app that scrolls to a
26
+ // computed offset and reads it back should get its own number, not one a
27
+ // curve came close to.
28
+ ScrollAnimation animation;
29
+ EXPECT(animation.start(0, 0, 100, 250));
30
+
31
+ double x = 0;
32
+ double y = 0;
33
+ while (animation.advance(1.0 / 60.0, x, y)) {
34
+ }
35
+ EXPECT_EQ(x, 100.0);
36
+ EXPECT_EQ(y, 250.0);
37
+ EXPECT(!animation.isRunning());
38
+ }
39
+
40
+ TEST(scroll_animation_moves_monotonically_towards_the_target) {
41
+ ScrollAnimation animation;
42
+ animation.start(0, 0, 0, 300);
43
+
44
+ double x = 0;
45
+ double y = 0;
46
+ double previous = -1;
47
+ int steps = 0;
48
+ while (animation.advance(1.0 / 60.0, x, y)) {
49
+ EXPECT(y >= previous);
50
+ previous = y;
51
+ steps++;
52
+ }
53
+ // Something between "instant" and "a second". At 60Hz a 0.3s curve is about
54
+ // eighteen frames, and asserting the shape rather than the number keeps this
55
+ // from failing if the duration is tuned.
56
+ EXPECT(steps > 5);
57
+ EXPECT(steps < 60);
58
+ }
59
+
60
+ TEST(scroll_animation_starts_and_ends_slowly) {
61
+ // The property the curve was chosen for: zero derivative at both ends, so two
62
+ // scrolls in a row have no visible seam. Asserted as "the first step is
63
+ // smaller than a middle step", which is what that means in practice.
64
+ ScrollAnimation animation;
65
+ animation.start(0, 0, 0, 1000);
66
+
67
+ double x = 0;
68
+ double y = 0;
69
+ const double step = ScrollAnimation::kDurationSeconds / 10.0;
70
+
71
+ animation.advance(step, x, y);
72
+ const double first = y;
73
+
74
+ double previous = y;
75
+ for (int i = 0; i < 4; i++) {
76
+ animation.advance(step, x, y);
77
+ }
78
+ const double middle = y - previous;
79
+
80
+ EXPECT(first < middle);
81
+ }
82
+
83
+ TEST(scroll_animation_a_stopped_animation_reports_nothing_further) {
84
+ ScrollAnimation animation;
85
+ animation.start(0, 0, 0, 500);
86
+ double x = 0;
87
+ double y = 0;
88
+ animation.advance(0.05, x, y);
89
+ animation.stop();
90
+ EXPECT(!animation.isRunning());
91
+ EXPECT(!animation.advance(0.05, x, y));
92
+ }
93
+
94
+ TEST(scroll_animation_a_long_frame_does_not_overshoot) {
95
+ // A stalled frame -- a garbage collection, a slow mount -- hands this more
96
+ // time than the whole animation. It has to land on the target rather than
97
+ // past it, because an offset beyond the content is a scroll view showing
98
+ // nothing.
99
+ ScrollAnimation animation;
100
+ animation.start(0, 0, 0, 400);
101
+ double x = 0;
102
+ double y = 0;
103
+ EXPECT(!animation.advance(5.0, x, y));
104
+ EXPECT_EQ(y, 400.0);
105
+ }
106
+
107
+ TEST(scroll_animation_runs_backwards_as_well) {
108
+ ScrollAnimation animation;
109
+ animation.start(0, 900, 0, 0);
110
+ double x = 0;
111
+ double y = 900;
112
+ double previous = 901;
113
+ while (animation.advance(1.0 / 60.0, x, y)) {
114
+ EXPECT(y <= previous);
115
+ previous = y;
116
+ }
117
+ EXPECT_EQ(y, 0.0);
118
+ }
119
+
120
+ } // namespace
@@ -0,0 +1,66 @@
1
+ // The scrollable range, and what `contentInset` does to it.
2
+ //
3
+ // The arithmetic is four lines and was duplicated in three hosts, none of which
4
+ // read the insets. These are the cases that tell the old behaviour from the new
5
+ // one -- and the first three are the old behaviour, which must not move.
6
+
7
+ #include "TestHarness.h"
8
+
9
+ #include "ScrollBounds.h"
10
+
11
+ // EXPECT_NEAR builds its message with a stream; the harness's header does not
12
+ // pull one in, so each suite brings its own.
13
+ #include <sstream>
14
+
15
+ using basalt::clampScrollOffset;
16
+ using basalt::ScrollAxisInsets;
17
+ using basalt::scrollRangeFor;
18
+
19
+ TEST(bounds_without_insets_are_what_they_always_were) {
20
+ const auto range = scrollRangeFor(100.0, 400.0);
21
+ EXPECT_NEAR(range.minimum, 0.0, 0.001);
22
+ EXPECT_NEAR(range.maximum, 300.0, 0.001);
23
+ }
24
+
25
+ TEST(bounds_content_that_fits_cannot_scroll) {
26
+ const auto range = scrollRangeFor(400.0, 100.0);
27
+ EXPECT_NEAR(range.minimum, 0.0, 0.001);
28
+ EXPECT_NEAR(range.maximum, 0.0, 0.001);
29
+ }
30
+
31
+ TEST(bounds_clamp_holds_an_offset_inside) {
32
+ EXPECT_NEAR(clampScrollOffset(-50.0, 100.0, 400.0), 0.0, 0.001);
33
+ EXPECT_NEAR(clampScrollOffset(999.0, 100.0, 400.0), 300.0, 0.001);
34
+ EXPECT_NEAR(clampScrollOffset(120.0, 100.0, 400.0), 120.0, 0.001);
35
+ }
36
+
37
+ TEST(bounds_a_leading_inset_allows_pulling_before_the_content) {
38
+ const auto range = scrollRangeFor(100.0, 400.0, ScrollAxisInsets{50.0, 0.0});
39
+ EXPECT_NEAR(range.minimum, -50.0, 0.001);
40
+ // The far end is unchanged: a top inset does not add room at the bottom.
41
+ EXPECT_NEAR(range.maximum, 300.0, 0.001);
42
+ EXPECT_NEAR(clampScrollOffset(-50.0, 100.0, 400.0, ScrollAxisInsets{50.0, 0.0}), -50.0, 0.001);
43
+ EXPECT_NEAR(clampScrollOffset(-80.0, 100.0, 400.0, ScrollAxisInsets{50.0, 0.0}), -50.0, 0.001);
44
+ }
45
+
46
+ TEST(bounds_a_trailing_inset_adds_room_at_the_far_end) {
47
+ const auto range = scrollRangeFor(100.0, 400.0, ScrollAxisInsets{0.0, 40.0});
48
+ EXPECT_NEAR(range.minimum, 0.0, 0.001);
49
+ EXPECT_NEAR(range.maximum, 340.0, 0.001);
50
+ }
51
+
52
+ // Content shorter than its container, with a leading inset: there is somewhere
53
+ // to pull to and nowhere to scroll to, and the two ends must not cross.
54
+ TEST(bounds_an_inset_on_content_that_fits_does_not_invert_the_range) {
55
+ const auto range = scrollRangeFor(400.0, 100.0, ScrollAxisInsets{50.0, 0.0});
56
+ EXPECT_NEAR(range.minimum, -50.0, 0.001);
57
+ EXPECT_NEAR(range.maximum, -50.0, 0.001);
58
+ EXPECT_NEAR(clampScrollOffset(10.0, 400.0, 100.0, ScrollAxisInsets{50.0, 0.0}), -50.0, 0.001);
59
+ }
60
+
61
+ TEST(bounds_both_insets_extend_both_ends) {
62
+ const auto range = scrollRangeFor(100.0, 400.0, ScrollAxisInsets{20.0, 30.0});
63
+ EXPECT_NEAR(range.minimum, -20.0, 0.001);
64
+ EXPECT_NEAR(range.maximum, 330.0, 0.001);
65
+ EXPECT_NEAR(range.length(), 350.0, 0.001);
66
+ }