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,148 @@
1
+ #include "TestDialog.h"
2
+
3
+ #include <cstdlib>
4
+ #include <string>
5
+ #include <utility>
6
+ #include <vector>
7
+
8
+ namespace basalt {
9
+
10
+ std::optional<int> scriptedDialogButton() {
11
+ static const std::optional<int> button = []() -> std::optional<int> {
12
+ const char *value = std::getenv("BASALT_TEST_DIALOG");
13
+ if (value == nullptr || *value == '\0') {
14
+ return std::nullopt;
15
+ }
16
+ // A button index, and `dismiss` for the way out -- which is the last
17
+ // button, the one React Native's alerts and this project's share picker
18
+ // both put the cancelling choice in.
19
+ if (std::string(value) == "dismiss") {
20
+ return -1;
21
+ }
22
+ char *end = nullptr;
23
+ const long parsed = std::strtol(value, &end, 10);
24
+ if (end == value) {
25
+ return std::nullopt;
26
+ }
27
+ return static_cast<int>(parsed);
28
+ }();
29
+ return button;
30
+ }
31
+
32
+ void presentAlert(const AlertRequest &request, AlertCallback onButton) {
33
+ const std::optional<int> scripted = scriptedDialogButton();
34
+ if (!scripted.has_value()) {
35
+ showAlert(request, std::move(onButton));
36
+ return;
37
+ }
38
+
39
+ // The last button for `dismiss`, and the named one otherwise, clamped: a
40
+ // script that names a button an app did not offer should answer the nearest
41
+ // real one rather than an index nothing will understand.
42
+ const int last = request.buttons.empty() ? 0 : static_cast<int>(request.buttons.size()) - 1;
43
+ int button = *scripted < 0 ? last : *scripted;
44
+ if (button > last) {
45
+ button = last;
46
+ }
47
+
48
+ // Straight back, on this thread. The platform implementations marshal to the
49
+ // main thread because they have to touch a window; there is no window here,
50
+ // and an answer that arrives before the caller has returned is something
51
+ // every caller already handles -- `AsyncPromise` and the alert module's
52
+ // callback both settle whenever they are told to.
53
+ onButton(button, request.defaultText);
54
+ }
55
+
56
+ std::optional<int> scriptedMenuChoice() {
57
+ static const std::optional<int> choice = []() -> std::optional<int> {
58
+ const char *value = std::getenv("BASALT_TEST_MENU");
59
+ if (value == nullptr || *value == '\0') {
60
+ return std::nullopt;
61
+ }
62
+ if (std::string(value) == "dismiss") {
63
+ return -1;
64
+ }
65
+ char *end = nullptr;
66
+ const long parsed = std::strtol(value, &end, 10);
67
+ if (end == value) {
68
+ return std::nullopt;
69
+ }
70
+ return static_cast<int>(parsed);
71
+ }();
72
+ return choice;
73
+ }
74
+
75
+ void presentMenu(const MenuRequest &request, MenuCallback onChosen) {
76
+ const std::optional<int> scripted = scriptedMenuChoice();
77
+ if (!scripted.has_value()) {
78
+ showMenu(request, std::move(onChosen));
79
+ return;
80
+ }
81
+
82
+ // Out of range answers as a dismissal rather than being clamped, which is the
83
+ // opposite of what presentAlert does with a button. A menu's entries are not
84
+ // interchangeable -- picking the nearest one would run something the script
85
+ // did not ask for -- whereas an alert's last button is always the way out.
86
+ const int last = static_cast<int>(request.entries.size()) - 1;
87
+ const int index = (*scripted < 0 || *scripted > last) ? -1 : *scripted;
88
+ onChosen(index);
89
+ }
90
+
91
+ std::optional<std::vector<std::string>> scriptedFileDialogPaths() {
92
+ static const std::optional<std::vector<std::string>> paths =
93
+ []() -> std::optional<std::vector<std::string>> {
94
+ const char *value = std::getenv("BASALT_TEST_FILE_DIALOG");
95
+ if (value == nullptr || *value == '\0') {
96
+ return std::nullopt;
97
+ }
98
+ const std::string spec(value);
99
+ if (spec == "cancel") {
100
+ return std::vector<std::string>{};
101
+ }
102
+ // `;` on Windows, where a path starts "C:\\" and splitting on a colon
103
+ // would cut every path in two.
104
+ #ifdef _WIN32
105
+ const char separator = ';';
106
+ #else
107
+ const char separator = ':';
108
+ #endif
109
+ std::vector<std::string> parsed;
110
+ size_t start = 0;
111
+ while (start <= spec.size()) {
112
+ const size_t next = spec.find(separator, start);
113
+ const std::string part =
114
+ spec.substr(start, next == std::string::npos ? std::string::npos : next - start);
115
+ if (!part.empty()) {
116
+ parsed.push_back(part);
117
+ }
118
+ if (next == std::string::npos) {
119
+ break;
120
+ }
121
+ start = next + 1;
122
+ }
123
+ return parsed;
124
+ }();
125
+ return paths;
126
+ }
127
+
128
+ void presentFileDialog(const FileDialogRequest &request, FileDialogCallback onDone) {
129
+ const std::optional<std::vector<std::string>> scripted = scriptedFileDialogPaths();
130
+ if (!scripted.has_value()) {
131
+ showFileDialog(request, std::move(onDone));
132
+ return;
133
+ }
134
+
135
+ // An empty list is `cancel`, which is the distinction the whole result shape
136
+ // exists to keep: "nothing was chosen" and "the person pressed Cancel" are
137
+ // not the same answer.
138
+ const bool canceled = scripted->empty();
139
+ std::vector<std::string> paths = *scripted;
140
+ // A save and a folder are one path however many the script named, which is
141
+ // what the platform dialogs enforce and what an app is entitled to assume.
142
+ if (!canceled && request.kind != FileDialogRequest::Kind::OpenFile && paths.size() > 1) {
143
+ paths.resize(1);
144
+ }
145
+ onDone(canceled, paths);
146
+ }
147
+
148
+ } // namespace basalt
@@ -0,0 +1,96 @@
1
+ // Answering a modal dialog without showing one.
2
+ //
3
+ // The same escape hatch `BASALT_TEST_TAP` is, for the same reason and with the
4
+ // same limits. A modal dialog is the one piece of interface an automated run
5
+ // cannot get past: there is nobody to press a button, the window system will
6
+ // not synthesise a press into a sheet, and on macOS a sheet that is up stops
7
+ // `[NSApp terminate:]` outright -- so a run that shows one hangs until it is
8
+ // killed.
9
+ //
10
+ // ## Why not drive the real dialog
11
+ //
12
+ // Because that is where the flakiness lives, and the platforms say so
13
+ // themselves. Apple's own mechanism for this in UI tests,
14
+ // `addUIInterruptionMonitor`, is documented as not firing on recent iOS
15
+ // versions, and the workaround people reach for -- pressing the button through
16
+ // SpringBoard -- is widely reported as flaky; Apple's own advice for permission
17
+ // dialogs is to pre-grant them through launch arguments rather than to press
18
+ // anything. Detox, which is the closest thing React Native has to a house
19
+ // end-to-end runner, states the principle outright: gray box rather than black
20
+ // box, monitor the app from the inside, because that is what fights flakiness
21
+ // at the core.
22
+ //
23
+ // Driving it from outside is also not available here. AT-SPI on Linux, the
24
+ // Accessibility API on macOS and UI Automation on Windows can each press a
25
+ // button in a dialog, and each needs a permission or a session an automated run
26
+ // does not have -- which is the same wall `BASALT_TEST_TAP` exists because of.
27
+ //
28
+ // ## What this skips, exactly
29
+ //
30
+ // The dialog's presentation and nothing else. The request is built by the same
31
+ // module from the same JavaScript, the answer travels back through the same
32
+ // callback on the same thread, and every line above the platform runs. What
33
+ // goes untested is that the toolkit can put a dialog on screen and get a press
34
+ // out of it, which is the same thing every other instrument in this project
35
+ // leaves to a person.
36
+
37
+ #pragma once
38
+
39
+ #include "PlatformServices.h"
40
+
41
+ #include <vector>
42
+
43
+ #include <optional>
44
+
45
+ namespace basalt {
46
+
47
+ // The button index BASALT_TEST_DIALOG names, or nothing when it is unset.
48
+ //
49
+ // Read once and cached: an environment variable cannot change under a running
50
+ // process, and this is consulted on a path that runs whenever an app asks a
51
+ // question.
52
+ std::optional<int> scriptedDialogButton();
53
+
54
+ // `showAlert`, or the scripted answer when there is one.
55
+ //
56
+ // Everything that would put a modal dialog on screen goes through here rather
57
+ // than calling the platform directly, so the instrument covers the share
58
+ // picker's fallback as well as `Alert.alert` -- and covers them by *running*
59
+ // them: a scripted "Copy" still copies, because only the presentation is
60
+ // skipped.
61
+ //
62
+ // `dismiss` answers with the last button, which is where React Native's alerts
63
+ // and this project's share picker both put the cancelling choice.
64
+ void presentAlert(const AlertRequest &request, AlertCallback onButton);
65
+
66
+ // Which entry BASALT_TEST_MENU names, or nothing when it is unset. `dismiss`
67
+ // is -1, which is what closing a menu without choosing reports.
68
+ //
69
+ // A popup menu is the second thing an automated run cannot get past, and for a
70
+ // sharper reason than a dialog: on macOS `popUpMenuPositioningItem` runs the
71
+ // menu's own tracking loop on the main thread, so a menu nobody dismisses
72
+ // stops the process where it stands.
73
+ std::optional<int> scriptedMenuChoice();
74
+
75
+ // `showMenu`, or the scripted answer when there is one. Everything that would
76
+ // put a popup menu on screen goes through here rather than calling the platform
77
+ // directly, for the same reason `presentAlert` exists.
78
+ void presentMenu(const MenuRequest &request, MenuCallback onChosen);
79
+
80
+ // What BASALT_TEST_FILE_DIALOG names, or nothing when it is unset.
81
+ //
82
+ // `cancel` is the person pressing Cancel. Anything else is a list of paths
83
+ // separated by the platform's path separator -- `:` everywhere but Windows,
84
+ // where it is `;`, because a Windows path has a colon two characters in.
85
+ //
86
+ // A file dialog is the third thing an automated run cannot get past, and the
87
+ // one with the most to prove afterwards: what an app does with a path is the
88
+ // interesting part, and it cannot be reached at all without one.
89
+ std::optional<std::vector<std::string>> scriptedFileDialogPaths();
90
+
91
+ // `showFileDialog`, or the scripted answer when there is one. Everything that
92
+ // would put a file dialog on screen goes through here, for the same reason
93
+ // `presentAlert` exists.
94
+ void presentFileDialog(const FileDialogRequest &request, FileDialogCallback onDone);
95
+
96
+ } // namespace basalt
@@ -0,0 +1,78 @@
1
+ #include "TestKeys.h"
2
+
3
+ #include <cstdlib>
4
+ #include <string>
5
+ #include <vector>
6
+
7
+ namespace basalt {
8
+ namespace {
9
+
10
+ std::vector<std::string> split(const std::string &text, char separator) {
11
+ std::vector<std::string> parts;
12
+ std::string current;
13
+ for (const char character : text) {
14
+ if (character == separator) {
15
+ parts.push_back(current);
16
+ current.clear();
17
+ continue;
18
+ }
19
+ current += character;
20
+ }
21
+ parts.push_back(current);
22
+ return parts;
23
+ }
24
+
25
+ } // namespace
26
+
27
+ KeyCombination parseKeyPress(const std::string &spec) {
28
+ KeyCombination pressed;
29
+ if (spec.empty()) {
30
+ return pressed;
31
+ }
32
+
33
+ // The key first, then modifiers. Split on `+` from the left and take the first
34
+ // piece as the key, so a literal `+` as the key is `"+"` or `"++shift"` -- the
35
+ // first piece being empty is the one case where an empty piece means something.
36
+ const std::vector<std::string> pieces = split(spec, '+');
37
+ pressed.key = pieces.front();
38
+ for (size_t i = 1; i < pieces.size(); i++) {
39
+ const std::string &modifier = pieces[i];
40
+ if (modifier == "alt") {
41
+ pressed.modifiers.alt = true;
42
+ } else if (modifier == "ctrl") {
43
+ pressed.modifiers.ctrl = true;
44
+ } else if (modifier == "meta") {
45
+ pressed.modifiers.meta = true;
46
+ } else if (modifier == "shift") {
47
+ pressed.modifiers.shift = true;
48
+ } else if (modifier.empty() && pressed.key.empty()) {
49
+ // `"++shift"`: the key is a literal plus, and this empty piece is it.
50
+ pressed.key = "+";
51
+ }
52
+ // Anything else is ignored rather than refused. A typo in a modifier name
53
+ // gives a combination that matches nothing, which the scenario reports as a
54
+ // shortcut that did not fire -- and that is the honest symptom of a typo.
55
+ }
56
+ return pressed;
57
+ }
58
+
59
+ std::vector<KeyCombination> scriptedKeyPresses() {
60
+ const char *value = std::getenv(kTestKeyVar);
61
+ if (value == nullptr || *value == '\0') {
62
+ return {};
63
+ }
64
+ std::vector<KeyCombination> presses;
65
+ for (const std::string &spec : split(std::string(value), ';')) {
66
+ if (spec.empty()) {
67
+ continue;
68
+ }
69
+ KeyCombination pressed = parseKeyPress(spec);
70
+ if (pressed.key.empty()) {
71
+ continue;
72
+ }
73
+ presses.push_back(std::move(pressed));
74
+ }
75
+ return presses;
76
+ }
77
+
78
+ } // namespace basalt
@@ -0,0 +1,50 @@
1
+ // BASALT_TEST_KEY: press a key, without a keyboard.
2
+ //
3
+ // A real key press needs a window the display server considers focused, which an
4
+ // automated run does not reliably have on any of the three desktops -- the same
5
+ // reason `BASALT_TEST_TAP` exists and enters below the toolkit. So this enters
6
+ // after the platform mapping: it builds a `KeyCombination` directly and hands it
7
+ // to the host's own delivery, which is everything a real press does except the
8
+ // `NSEvent`, keyval or `WM_KEYDOWN` translation.
9
+ //
10
+ // **What it therefore does not test** is that translation, which is the half most
11
+ // likely to be wrong -- the three tables are each other's mirror and each one is
12
+ // unit-tested in its own host instead. Worth stating plainly because a scenario
13
+ // that passes here says the registry, the focus path and the round trip to
14
+ // JavaScript work, and says nothing about whether pressing the left arrow
15
+ // produces "ArrowLeft".
16
+ //
17
+ // ## The syntax
18
+ //
19
+ // BASALT_TEST_KEY="z+meta;ArrowLeft;m;A+shift"
20
+ //
21
+ // Presses separated by `;`, a key and its modifiers separated by `+`. The key is
22
+ // a W3C name and is taken literally, so `+` as a key is spelt by putting it
23
+ // first. Modifiers are `alt`, `ctrl`, `meta` and `shift`.
24
+ //
25
+ // Parsed here, once, because three hosts parsing it is three chances to disagree
26
+ // about what `z+meta` means -- and disagreeing quietly, since the wrong answer is
27
+ // a combination that matches nothing and a scenario that reports a shortcut as
28
+ // not firing.
29
+
30
+ #pragma once
31
+
32
+ #include "KeyEvents.h"
33
+
34
+ #include <string>
35
+ #include <vector>
36
+
37
+ namespace basalt {
38
+
39
+ inline constexpr const char *kTestKeyVar = "BASALT_TEST_KEY";
40
+
41
+ // The presses `BASALT_TEST_KEY` asks for, in order, or empty when it is unset.
42
+ // An entry with no key is skipped rather than refused: one malformed press should
43
+ // not lose the rest of a scripted sequence.
44
+ std::vector<KeyCombination> scriptedKeyPresses();
45
+
46
+ // The parser, exposed for its tests. `spec` is one press -- `"z+meta"` -- and the
47
+ // result has an empty key when it could not be read.
48
+ KeyCombination parseKeyPress(const std::string &spec);
49
+
50
+ } // namespace basalt
@@ -0,0 +1,33 @@
1
+ #include "TestQuitFile.h"
2
+
3
+ #include <cstdlib>
4
+ #include <filesystem>
5
+ #include <optional>
6
+ #include <string>
7
+
8
+ namespace basalt {
9
+
10
+ std::optional<std::string> testQuitFilePath() {
11
+ static const std::optional<std::string> path = []() -> std::optional<std::string> {
12
+ const char *value = std::getenv(kTestQuitFileVar);
13
+ if (value == nullptr || *value == '\0') {
14
+ return std::nullopt;
15
+ }
16
+ return std::string(value);
17
+ }();
18
+ return path;
19
+ }
20
+
21
+ bool testQuitFileAppeared() {
22
+ const std::optional<std::string> path = testQuitFilePath();
23
+ if (!path.has_value()) {
24
+ return false;
25
+ }
26
+ // The non-throwing overload. This runs on a timer on the UI thread, and a
27
+ // permission error or a vanished parent directory is a reason to keep waiting
28
+ // rather than to terminate the process from inside a timer callback.
29
+ std::error_code ignored;
30
+ return std::filesystem::exists(*path, ignored);
31
+ }
32
+
33
+ } // namespace basalt
@@ -0,0 +1,60 @@
1
+ // Quitting, gracefully, at a moment the harness chooses.
2
+ //
3
+ // `BASALT_QUIT_AFTER_MS` is the other way out, and it is decided before the
4
+ // process starts. That is fine for a scenario whose length is known and wrong
5
+ // for one whose length is not: the Fast Refresh scenario waits for an edit to
6
+ // reach the window, which takes a second on a warm laptop and up to two and a
7
+ // half minutes on a cold CI machine, so no single budget is both quick and
8
+ // safe. It used a signal instead -- and signals are not portable. Windows has
9
+ // no SIGTERM, `TerminateProcess` cannot be handled, and every host writes its
10
+ // widget tree on the way out, so a killed host on Windows failed the assertion
11
+ // that reads that tree whether or not the thing under test had worked.
12
+ //
13
+ // So: `BASALT_TEST_QUIT_FILE=<path>`, and the host quits as soon as <path>
14
+ // exists, through exactly the shutdown `BASALT_QUIT_AFTER_MS` uses. The harness
15
+ // creates the file when it has what it came for. Nothing is read from the file
16
+ // and it may be empty; its existence is the whole message, which is why there
17
+ // is no race worth worrying about between writing it and seeing it.
18
+ //
19
+ // Not refusable, for the reason the timed quit is not: this is the harness
20
+ // ending the run rather than a person closing a window, and an app that
21
+ // intercepts its own close would otherwise refuse the harness and hang. The
22
+ // scenario for *that* feature is exactly such an app.
23
+ //
24
+ // Polled rather than watched. A file watcher is three different APIs across
25
+ // these hosts and each has its own failure mode on network and virtual
26
+ // filesystems; a quarter-second poll on a timer each host already runs costs
27
+ // nothing measurable and cannot fail to be delivered.
28
+
29
+ #pragma once
30
+
31
+ #include <optional>
32
+ #include <string>
33
+
34
+ namespace basalt {
35
+
36
+ // One spelling, in one place. Three hosts reading the variable by name is three
37
+ // chances to typo it, and a typo here does not fail: the host simply never
38
+ // quits, and the scenario reports a timeout somewhere unrelated.
39
+ inline constexpr const char *kTestQuitFileVar = "BASALT_TEST_QUIT_FILE";
40
+
41
+ // How often to look. Small enough that the wait is not noticeable next to the
42
+ // process teardown that follows it, large enough to be free.
43
+ inline constexpr unsigned kTestQuitFilePollMs = 250;
44
+
45
+ // The line all three hosts log when it fires, so that three transcripts read
46
+ // the same way. `BASALT_QUIT_AFTER_MS elapsed; quitting` had to be retrofitted
47
+ // onto Windows after a scenario asserted on a string only two hosts printed;
48
+ // this starts out shared instead.
49
+ inline constexpr const char *kTestQuitFileMessage = "BASALT_TEST_QUIT_FILE appeared; quitting";
50
+
51
+ // The path the instrument was pointed at, or nothing when it is off. Read once:
52
+ // the environment does not change under a running process, and each host calls
53
+ // this on a timer.
54
+ std::optional<std::string> testQuitFilePath();
55
+
56
+ // Whether that file exists now. False when the instrument is off, so a host can
57
+ // call it without checking first.
58
+ bool testQuitFileAppeared();
59
+
60
+ } // namespace basalt
@@ -0,0 +1,192 @@
1
+ // Quitting when there is nothing left to wait for, rather than on a guess.
2
+ //
3
+ // `BASALT_QUIT_AFTER_MS` is a budget chosen before the process starts, and it
4
+ // has to be long enough for the slowest machine that will ever run it. So every
5
+ // scenario waits out the worst case on every machine: `Image.getSize` asks for
6
+ // three seconds and takes 3.7 of them, of which about 0.7 is real work.
7
+ //
8
+ // Two problems, and the smaller one is the time. The larger is that every such
9
+ // budget is a latent flake -- "should be long enough" is a guess that a loaded
10
+ // CI machine eventually falsifies, and when it does the scenario fails for a
11
+ // reason that has nothing to do with what it tests. One of those is already in
12
+ // docs/backlog/testing.md.
13
+ //
14
+ // `BASALT_QUIT_WHEN_SETTLED=<ms>` replaces the guess with a fact. The host quits
15
+ // <ms> after it has settled, where settled means:
16
+ //
17
+ // - the first mount has been applied -- React produced a tree and the host
18
+ // put it on screen. Recorded in MountingWalk::applyMutations, which is the
19
+ // one place all three hosts mount through, so this is one code site rather
20
+ // than three.
21
+ // - any scripted input has been delivered. Each host schedules its own and
22
+ // knows the total, and says so through noteScriptedInputEndsInMs.
23
+ //
24
+ // `BASALT_QUIT_AFTER_MS` stays as the backstop, so nothing gets *slower* and a
25
+ // host that never settles still ends. The two are complementary: one is the
26
+ // ceiling, this is the actual duration.
27
+ //
28
+ // The <ms> is a settle rather than a deadline, and it is small -- a few hundred
29
+ // milliseconds for React's next commit and the frame that draws it. It is still
30
+ // a guess, but it is a guess about one round trip instead of about a whole
31
+ // scenario, and quitting too early fails loudly: every scenario that opts in
32
+ // asserts on the dumped tree, so a tree that is not there yet is a failure and
33
+ // not a false pass. That direction is the whole reason this is safe to do.
34
+
35
+ #pragma once
36
+
37
+ #include <atomic>
38
+ #include <chrono>
39
+ #include <cstddef>
40
+ #include <cstdlib>
41
+ #include <optional>
42
+ #include <string>
43
+
44
+ namespace basalt {
45
+
46
+ inline constexpr const char *kQuitWhenSettledVar = "BASALT_QUIT_WHEN_SETTLED";
47
+
48
+ // One line, shared, for the reason core/TestQuitFile.h gives: three hosts
49
+ // spelling it themselves is three chances to differ, and a scenario that asserts
50
+ // on a string only two of them print fails on the third for no reason.
51
+ inline constexpr const char *kQuitWhenSettledMessage = "BASALT_QUIT_WHEN_SETTLED: settled; quitting";
52
+
53
+ inline long long testNowMs() {
54
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
55
+ std::chrono::steady_clock::now().time_since_epoch())
56
+ .count();
57
+ }
58
+
59
+ // Zero means "not yet". A steady clock could in principle read zero at process
60
+ // start, which would make the first mount look like it had never happened, so
61
+ // the stored value is never allowed to be.
62
+ inline std::atomic<long long> &firstMountAtMsRef() {
63
+ static std::atomic<long long> value{0};
64
+ return value;
65
+ }
66
+
67
+ // When the scripted input finishes, as an absolute time on the same clock. Zero
68
+ // when there is none, which is the common case.
69
+ inline std::atomic<long long> &scriptedInputEndsAtMsRef() {
70
+ static std::atomic<long long> value{0};
71
+ return value;
72
+ }
73
+
74
+ // Called from MountingWalk::applyMutations, on every transaction; only the first
75
+ // is kept. Cheap enough to sit on that path unconditionally -- one relaxed load
76
+ // and, once, one compare-exchange.
77
+ inline void noteMountApplied() {
78
+ if (firstMountAtMsRef().load(std::memory_order_relaxed) != 0) {
79
+ return;
80
+ }
81
+ long long expected = 0;
82
+ const long long now = testNowMs();
83
+ firstMountAtMsRef().compare_exchange_strong(expected, now == 0 ? 1 : now);
84
+ }
85
+
86
+ // Called by a host once, after it has scheduled whatever BASALT_TEST_TAP and its
87
+ // siblings asked for. `delayMs` is the total schedule, which is what each host
88
+ // already computes to lay the timers out.
89
+ inline void noteScriptedInputEndsInMs(long long delayMs) {
90
+ scriptedInputEndsAtMsRef().store(testNowMs() + (delayMs > 0 ? delayMs : 0));
91
+ }
92
+
93
+ // Whether anything was asked to be delivered *into* the app. The hosts lay their
94
+ // scripted input out starting 1500ms in -- a tap needs a window to land on --
95
+ // and they report that total whether or not any of it was asked for, so without
96
+ // this a scenario that only mounts would wait out a schedule that is empty. It
97
+ // measured 2.96s against a 3s budget, which is to say no saving at all on
98
+ // exactly the scenarios this should help most.
99
+ //
100
+ // **A new instrument that a host puts on its scripted timer belongs in this
101
+ // list.** Left out, its scenario settles before the input lands -- which fails
102
+ // rather than passes, because everything that opts in asserts on the mounted
103
+ // tree, but it fails for a reason a long way from the change.
104
+ //
105
+ // Scheduled, not merely test-related. `BASALT_TEST_MENU` and
106
+ // `BASALT_TEST_DIALOG` are deliberately absent: they *answer* something when it
107
+ // appears, from core/TestDialog.cpp, and are never laid out on a host's timer --
108
+ // so they say nothing about when a schedule ends. Every scenario that uses them
109
+ // also drives the thing that shows the menu or the dialog, and that instrument
110
+ // is in this list. Including them was the first version, and the check below
111
+ // caught it: a list of "instruments to do with input" is not the same as a list
112
+ // of "instruments that delay the schedule", and only the second is what
113
+ // scriptedInputEndsAtMs needs.
114
+ //
115
+ // scripts/test_harness.py holds this against what the three host mains actually
116
+ // read, in both directions -- an instrument they schedule and this omits, and an
117
+ // entry here that no host knows about, which is what a rename leaves behind.
118
+ // native/tests/test_settle.cpp checks the contents.
119
+ inline const char *const *scriptedInputVars(std::size_t &count) {
120
+ static const char *const vars[] = {
121
+ "BASALT_TEST_TAP",
122
+ "BASALT_TEST_SECONDARY_TAP",
123
+ "BASALT_TEST_CLICK",
124
+ "BASALT_TEST_HOVER",
125
+ "BASALT_TEST_SCROLL",
126
+ "BASALT_TEST_FOCUS",
127
+ "BASALT_TEST_KEY",
128
+ "BASALT_TEST_TYPE",
129
+ "BASALT_TEST_DRAG",
130
+ "BASALT_TEST_DROP",
131
+ "BASALT_TEST_QUIT",
132
+ "BASALT_TEST_CLOSE_WINDOW",
133
+ };
134
+ count = sizeof(vars) / sizeof(vars[0]);
135
+ return vars;
136
+ }
137
+
138
+ inline bool anyScriptedInputRequested() {
139
+ static const bool any = []() {
140
+ std::size_t count = 0;
141
+ const char *const *vars = scriptedInputVars(count);
142
+ for (std::size_t i = 0; i < count; i++) {
143
+ const char *value = std::getenv(vars[i]);
144
+ if (value != nullptr && *value != '\0') {
145
+ return true;
146
+ }
147
+ }
148
+ return false;
149
+ }();
150
+ return any;
151
+ }
152
+
153
+ inline std::optional<unsigned> quitWhenSettledMs() {
154
+ static const std::optional<unsigned> ms = []() -> std::optional<unsigned> {
155
+ const char *value = std::getenv(kQuitWhenSettledVar);
156
+ if (value == nullptr || *value == '\0') {
157
+ return std::nullopt;
158
+ }
159
+ char *end = nullptr;
160
+ const unsigned long parsed = std::strtoul(value, &end, 10);
161
+ if (end == value) {
162
+ return std::nullopt;
163
+ }
164
+ return static_cast<unsigned>(parsed);
165
+ }();
166
+ return ms;
167
+ }
168
+
169
+ // Whether everything worth waiting for has happened, and the settle has passed.
170
+ inline bool hasSettled() {
171
+ const std::optional<unsigned> settle = quitWhenSettledMs();
172
+ if (!settle.has_value()) {
173
+ return false;
174
+ }
175
+ const long long mounted = firstMountAtMsRef().load();
176
+ if (mounted == 0) {
177
+ return false;
178
+ }
179
+ // The later of the two, because a scenario that taps needs the tap delivered
180
+ // and a scenario that does not still needs its tree. Taking the maximum makes
181
+ // the two cases one expression rather than a branch that can be got wrong.
182
+ //
183
+ // Only when input was actually asked for. The hosts report their schedule
184
+ // total unconditionally, and that total is never zero -- it starts at 1500ms
185
+ // so a tap has a window to land on -- so an empty schedule would otherwise be
186
+ // waited out by every scenario that just mounts.
187
+ const long long scripted = anyScriptedInputRequested() ? scriptedInputEndsAtMsRef().load() : 0;
188
+ const long long ready = mounted > scripted ? mounted : scripted;
189
+ return testNowMs() - ready >= static_cast<long long>(*settle);
190
+ }
191
+
192
+ } // namespace basalt