@hanzogui/kitchen-sink 3.0.5

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 (431) hide show
  1. package/.detoxrc.js +130 -0
  2. package/.env.production +2 -0
  3. package/.maestro/config.yaml +4 -0
  4. package/.maestro/flows/shorthand-variables.yaml +23 -0
  5. package/.watchmanconfig +1 -0
  6. package/LICENSE +21 -0
  7. package/README.md +11 -0
  8. package/app.json +43 -0
  9. package/assets/adaptive-icon.png +0 -0
  10. package/assets/favicon.png +0 -0
  11. package/assets/icon.png +0 -0
  12. package/assets/splash.png +0 -0
  13. package/babel.config.js +25 -0
  14. package/e2e/CompilerExtraction.test.ts +147 -0
  15. package/e2e/GroupPressNative.test.ts +167 -0
  16. package/e2e/MediaQueryGtMd.test.ts +71 -0
  17. package/e2e/NativePortal.test.ts +113 -0
  18. package/e2e/PointerEvents.test.ts +116 -0
  19. package/e2e/PressStyleNative.noRngh.test.ts +191 -0
  20. package/e2e/PressStyleNative.test.ts +231 -0
  21. package/e2e/SafeArea.test.ts +57 -0
  22. package/e2e/SelectAndroidOnPress.test.ts +181 -0
  23. package/e2e/SelectRemount.test.ts +137 -0
  24. package/e2e/SheetDragResist.test.ts +370 -0
  25. package/e2e/SheetKeyboardDrag.test.ts +249 -0
  26. package/e2e/SheetScrollableDrag.test.ts +560 -0
  27. package/e2e/ShorthandVariables.test.ts +53 -0
  28. package/e2e/ThemeChangeBasic.test.ts +123 -0
  29. package/e2e/ThemeMutation.test.ts +80 -0
  30. package/e2e/check-rngh-status.test.ts +31 -0
  31. package/e2e/jest.config.js +19 -0
  32. package/e2e/utils/colors.ts +75 -0
  33. package/e2e/utils/navigation.ts +53 -0
  34. package/eas.json +22 -0
  35. package/flows/AlertDialog.yaml +17 -0
  36. package/flows/OpenApp.yaml +25 -0
  37. package/flows/Select.yaml +13 -0
  38. package/flows/Sheet.yaml +12 -0
  39. package/flows/Tabs.yaml +13 -0
  40. package/flows/Toast.yaml +14 -0
  41. package/flows/WarmUp.yaml +24 -0
  42. package/index.html +21 -0
  43. package/index.js +17 -0
  44. package/metro.config.js +64 -0
  45. package/next-router-shim.ts +9 -0
  46. package/package.json +118 -0
  47. package/plans/toast-2.md +471 -0
  48. package/playwright.config.ts +71 -0
  49. package/plugins/expo-modules-core-swift6.js +76 -0
  50. package/pod-install.sh +7 -0
  51. package/public/favicon.svg +70 -0
  52. package/public/fonts/inter.css +15 -0
  53. package/public/fonts/noto-cn.otf +0 -0
  54. package/public/gui-icon.svg +68 -0
  55. package/run-detox.sh +230 -0
  56. package/run-native-tests.sh +4 -0
  57. package/run-tests-parallel.ts +195 -0
  58. package/screenshots/Screenshotter.test.tsx +48 -0
  59. package/src/AnimationDemos.tsx +131 -0
  60. package/src/App.native.tsx +121 -0
  61. package/src/App.tsx +121 -0
  62. package/src/Navigation.tsx +98 -0
  63. package/src/Sandbox.tsx +87 -0
  64. package/src/TestDynamicEval.tsx +33 -0
  65. package/src/TestNativeSheet.tsx +100 -0
  66. package/src/components/TimedRender.tsx +18 -0
  67. package/src/constants/test-ids.ts +52 -0
  68. package/src/features/demos/demo-screen.tsx +72 -0
  69. package/src/features/home/ColorSchemeListItem.tsx +41 -0
  70. package/src/features/home/TestBuildAButton.tsx +102 -0
  71. package/src/features/home/TestSeparator.tsx +0 -0
  72. package/src/features/home/screen.tsx +285 -0
  73. package/src/features/testcases/screen.tsx +59 -0
  74. package/src/features/testcases/test-screen.tsx +50 -0
  75. package/src/generatedV5Theme.ts +112 -0
  76. package/src/gui.config.ts +411 -0
  77. package/src/guy.png +0 -0
  78. package/src/index.tsx +6 -0
  79. package/src/provider/index.tsx +18 -0
  80. package/src/test-gui-stack.tsx +11 -0
  81. package/src/test.tsx +3 -0
  82. package/src/useKitchenSinkTheme.tsx +15 -0
  83. package/src/usecases/ActionsSheetComparison.tsx +194 -0
  84. package/src/usecases/AnimatePresenceEnterExitCase.tsx +255 -0
  85. package/src/usecases/AnimatePresenceExitTest.tsx +69 -0
  86. package/src/usecases/AnimatedByProp.tsx +39 -0
  87. package/src/usecases/AnimationComprehensiveCase.tsx +2515 -0
  88. package/src/usecases/AnimationValueLoggingCase.tsx +526 -0
  89. package/src/usecases/AnimationsWithMediaQueriesCase.tsx +110 -0
  90. package/src/usecases/Benchmark.tsx +148 -0
  91. package/src/usecases/BenchmarkSelect.tsx +34 -0
  92. package/src/usecases/ButtonCircular.tsx +3 -0
  93. package/src/usecases/ButtonCustom.tsx +33 -0
  94. package/src/usecases/ButtonIconColor.tsx +18 -0
  95. package/src/usecases/ButtonInverse.tsx +30 -0
  96. package/src/usecases/ButtonUnstyled.tsx +31 -0
  97. package/src/usecases/CheckboxDisabledOnPress.tsx +62 -0
  98. package/src/usecases/ClickDuringEnterCase.tsx +59 -0
  99. package/src/usecases/CodeExamplesInput.tsx +9 -0
  100. package/src/usecases/ColorTokenFallback.tsx +52 -0
  101. package/src/usecases/CompilerExtraction.tsx +380 -0
  102. package/src/usecases/ComplexVariants.tsx +164 -0
  103. package/src/usecases/CrashAdaptSheet.tsx +98 -0
  104. package/src/usecases/CustomStyledAnimatedPopover.tsx +42 -0
  105. package/src/usecases/CustomStyledAnimatedTooltip.tsx +72 -0
  106. package/src/usecases/DOMNodeAPIs.tsx +154 -0
  107. package/src/usecases/DialogFocusScopeCase.tsx +277 -0
  108. package/src/usecases/DialogFocusScopeDebug.tsx +85 -0
  109. package/src/usecases/DialogNestedCase.tsx +121 -0
  110. package/src/usecases/DialogOpenControlled.tsx +49 -0
  111. package/src/usecases/DialogPointerEventsCase.tsx +58 -0
  112. package/src/usecases/DialogScopedCase.tsx +106 -0
  113. package/src/usecases/DialogSheetAdaptCase.tsx +178 -0
  114. package/src/usecases/DialogSheetAdaptResizeCase.tsx +98 -0
  115. package/src/usecases/DismissLayerStackingCase.tsx +223 -0
  116. package/src/usecases/DriverDisableAnimationPropsCase.tsx +44 -0
  117. package/src/usecases/Example.tsx +10 -0
  118. package/src/usecases/ExitCompletionCase.tsx +713 -0
  119. package/src/usecases/FocusVisibleButton.tsx +14 -0
  120. package/src/usecases/FocusVisibleButtonPointer.tsx +13 -0
  121. package/src/usecases/FocusVisibleButtonWithFocusStyle.tsx +16 -0
  122. package/src/usecases/FocusWithinCase.tsx +55 -0
  123. package/src/usecases/FontTokensInVariants.tsx +14 -0
  124. package/src/usecases/FormButtonTypeCase.tsx +34 -0
  125. package/src/usecases/GlobalScopedTriggerIsolationCase.tsx +178 -0
  126. package/src/usecases/GroupHoverMobile.tsx +39 -0
  127. package/src/usecases/GroupPressInVariant.tsx +92 -0
  128. package/src/usecases/GroupPressNative.tsx +200 -0
  129. package/src/usecases/GroupProp.tsx +96 -0
  130. package/src/usecases/GroupPseudoVariantOverride.tsx +56 -0
  131. package/src/usecases/GroupUseCases.tsx +94 -0
  132. package/src/usecases/HeightMediaQueryOverrideCase.tsx +183 -0
  133. package/src/usecases/InputAutoFocusAfterMenuCase.tsx +105 -0
  134. package/src/usecases/InputAutoFocusStyledCase.tsx +39 -0
  135. package/src/usecases/KeyboardControllerTest.tsx +146 -0
  136. package/src/usecases/ListItem.tsx +123 -0
  137. package/src/usecases/MediaQueriesV5.tsx +137 -0
  138. package/src/usecases/MediaQueryGtMd.tsx +73 -0
  139. package/src/usecases/MenuAboveDialogCase.tsx +75 -0
  140. package/src/usecases/MenuAccessibilityCase.tsx +133 -0
  141. package/src/usecases/MenuAnimatePositionCase.tsx +41 -0
  142. package/src/usecases/MenuArrowAnimatePresenceCase.tsx +98 -0
  143. package/src/usecases/MenuAsChildPositionCase.tsx +24 -0
  144. package/src/usecases/MenuAutoResizeCase.tsx +57 -0
  145. package/src/usecases/MenuBottomCase.tsx +55 -0
  146. package/src/usecases/MenuFocusLeaveCase.tsx +135 -0
  147. package/src/usecases/MenuHighlightCase.tsx +44 -0
  148. package/src/usecases/MenuItemFocusCase.tsx +79 -0
  149. package/src/usecases/MenuItemPseudoOverrideCase.tsx +270 -0
  150. package/src/usecases/MenuMultiTriggerCase.tsx +47 -0
  151. package/src/usecases/MenuOverflowCase.tsx +60 -0
  152. package/src/usecases/MenuSubCase.tsx +223 -0
  153. package/src/usecases/MenuSubLeftCase.tsx +178 -0
  154. package/src/usecases/MenuSubNestedPositionCase.tsx +171 -0
  155. package/src/usecases/MenuSubStyledCase.tsx +145 -0
  156. package/src/usecases/MenuThemeCase.tsx +50 -0
  157. package/src/usecases/MenuUnstyledCase.tsx +52 -0
  158. package/src/usecases/MultiDriverAnimation.tsx +118 -0
  159. package/src/usecases/NativePortalTest.tsx +179 -0
  160. package/src/usecases/NewInputBasic.tsx +16 -0
  161. package/src/usecases/NewInputEvents.tsx +29 -0
  162. package/src/usecases/NonGuiTextStyledType.tsx +23 -0
  163. package/src/usecases/OnLayoutCase.tsx +134 -0
  164. package/src/usecases/OnLayoutScaleCase.tsx +88 -0
  165. package/src/usecases/OnLayoutStressCase.tsx +353 -0
  166. package/src/usecases/OpacityModifierCase.tsx +113 -0
  167. package/src/usecases/OverlayStyled.tsx +66 -0
  168. package/src/usecases/ParagraphSpanFontInheritance.tsx +53 -0
  169. package/src/usecases/PlaceholderTextColor.tsx +20 -0
  170. package/src/usecases/PointerEventsCase.tsx +100 -0
  171. package/src/usecases/PopoverAndMenuMultiTriggerCase.tsx +138 -0
  172. package/src/usecases/PopoverCase.tsx +222 -0
  173. package/src/usecases/PopoverContentStyledPlusAnimations.tsx +44 -0
  174. package/src/usecases/PopoverFocusScopeCase.tsx +171 -0
  175. package/src/usecases/PopoverHoverableCase.tsx +167 -0
  176. package/src/usecases/PopoverHoverableDisableClickCase.tsx +118 -0
  177. package/src/usecases/PopoverHoverableRapidCase.tsx +103 -0
  178. package/src/usecases/PopoverHoverableScopedCase.tsx +135 -0
  179. package/src/usecases/PopoverScopedCase.tsx +76 -0
  180. package/src/usecases/PopoverTriggerIsolationCase.tsx +80 -0
  181. package/src/usecases/PressStyleNative.tsx +143 -0
  182. package/src/usecases/PseudoStyleMerge.tsx +25 -0
  183. package/src/usecases/PseudoTransitionCase.tsx +174 -0
  184. package/src/usecases/RawAnimatedValueCase.tsx +231 -0
  185. package/src/usecases/RemoveScrollCase.tsx +66 -0
  186. package/src/usecases/RenderPropCase.tsx +263 -0
  187. package/src/usecases/SafeAreaCase.tsx +236 -0
  188. package/src/usecases/ScrollViewRefCase.tsx +88 -0
  189. package/src/usecases/SecondPage.tsx +5 -0
  190. package/src/usecases/SelectAndroidOnPress.tsx +129 -0
  191. package/src/usecases/SelectFocusScopeCase.tsx +270 -0
  192. package/src/usecases/SelectRemount.tsx +136 -0
  193. package/src/usecases/Shadows.tsx +5 -0
  194. package/src/usecases/SheetAnimationCase.tsx +155 -0
  195. package/src/usecases/SheetDragCase.tsx +183 -0
  196. package/src/usecases/SheetDragResistCase.tsx +433 -0
  197. package/src/usecases/SheetDragResistCase.web.tsx +359 -0
  198. package/src/usecases/SheetKeyboardDragCase.tsx +328 -0
  199. package/src/usecases/SheetKeyboardFitContentCase.tsx +165 -0
  200. package/src/usecases/SheetOnAnimationCompleteCase.tsx +54 -0
  201. package/src/usecases/SheetScrollLockCase.tsx +166 -0
  202. package/src/usecases/SheetScrollableDrag.tsx +249 -0
  203. package/src/usecases/SheetSnapPointsFitCase.tsx +393 -0
  204. package/src/usecases/ShorthandVariables.tsx +49 -0
  205. package/src/usecases/SlowThemeReRender.tsx +48 -0
  206. package/src/usecases/SpinnerCustomColors.tsx +34 -0
  207. package/src/usecases/StackZIndex.tsx +82 -0
  208. package/src/usecases/StressPage.tsx +301 -0
  209. package/src/usecases/StylePlatform.tsx +30 -0
  210. package/src/usecases/StyleProp.tsx +29 -0
  211. package/src/usecases/StyledAnchor.tsx +27 -0
  212. package/src/usecases/StyledButtonAnimationAuto.tsx +99 -0
  213. package/src/usecases/StyledButtonTheme.tsx +63 -0
  214. package/src/usecases/StyledButtonVariantPseudo.tsx +25 -0
  215. package/src/usecases/StyledButtonVariantPseudoMerge.tsx +77 -0
  216. package/src/usecases/StyledCheckboxTheme.tsx +23 -0
  217. package/src/usecases/StyledContextColor.tsx +246 -0
  218. package/src/usecases/StyledContextTokens.tsx +147 -0
  219. package/src/usecases/StyledHOCNamed.tsx +20 -0
  220. package/src/usecases/StyledHtmlCase.tsx +144 -0
  221. package/src/usecases/StyledIconColor.tsx +19 -0
  222. package/src/usecases/StyledInputFocusStyle.tsx +21 -0
  223. package/src/usecases/StyledInputOnFocus.tsx +30 -0
  224. package/src/usecases/StyledMediaQueryMerge.tsx +95 -0
  225. package/src/usecases/StyledOverridePsuedo.tsx +26 -0
  226. package/src/usecases/StyledRNW.tsx +61 -0
  227. package/src/usecases/StyledStyleableInputOnFocus.tsx +34 -0
  228. package/src/usecases/StyledStyleableInputVariant.tsx +48 -0
  229. package/src/usecases/StyledStyledStyleableInputOnFocus.tsx +36 -0
  230. package/src/usecases/StyledVariantTextColor.tsx +25 -0
  231. package/src/usecases/StyledViewOnFocus.tsx +32 -0
  232. package/src/usecases/TabHoverAnimationCase.tsx +212 -0
  233. package/src/usecases/TextNestedInheritance.tsx +80 -0
  234. package/src/usecases/ThemeChange.tsx +100 -0
  235. package/src/usecases/ThemeChangeBasic.tsx +52 -0
  236. package/src/usecases/ThemeComponentResolution.tsx +119 -0
  237. package/src/usecases/ThemeConditionalName.tsx +31 -0
  238. package/src/usecases/ThemeMediaAnimationCase.tsx +39 -0
  239. package/src/usecases/ThemeMutation.tsx +86 -0
  240. package/src/usecases/ThemeNested.tsx +103 -0
  241. package/src/usecases/ThemeReset.tsx +62 -0
  242. package/src/usecases/ThemeShallowCase.tsx +83 -0
  243. package/src/usecases/ToastCase.tsx +46 -0
  244. package/src/usecases/ToggleGroupActiveProps.tsx +40 -0
  245. package/src/usecases/ToggleGroupXGroupCase.tsx +104 -0
  246. package/src/usecases/TooltipAnimationCase.tsx +99 -0
  247. package/src/usecases/TooltipCase.tsx +32 -0
  248. package/src/usecases/TooltipGlobalPatternCase.tsx +83 -0
  249. package/src/usecases/TooltipGroupCase.tsx +102 -0
  250. package/src/usecases/TooltipMultiTriggerCase.tsx +88 -0
  251. package/src/usecases/TooltipPositionJumpCase.tsx +91 -0
  252. package/src/usecases/TooltipTriggerInlineCase.tsx +60 -0
  253. package/src/usecases/TransformMediaQueryMerge.tsx +98 -0
  254. package/src/usecases/UseCases.tsx +409 -0
  255. package/src/usecases/UseTheme.tsx +41 -0
  256. package/src/usecases/V5ThemeBuilderOutput.tsx +231 -0
  257. package/src/usecases/VariantFontFamily.tsx +25 -0
  258. package/src/usecases/VariantsOrder.tsx +117 -0
  259. package/src/usecases/ZIndex.tsx +155 -0
  260. package/src/usecases/helpers.tsx +44 -0
  261. package/src/usecases/index.native.ts +122 -0
  262. package/src/usecases/index.ts +3 -0
  263. package/src/usecases/index.web.ts +177 -0
  264. package/tests/AnimatePresenceEnterExit.animated.test.tsx +176 -0
  265. package/tests/AnimatedByProp.animated.test.tsx +138 -0
  266. package/tests/AnimationBehavior.animated.test.tsx +543 -0
  267. package/tests/AnimationTiming.animated.test.tsx +195 -0
  268. package/tests/AnimationsWithMediaQueries.animated.test.tsx +154 -0
  269. package/tests/BuildAButton.test.tsx +87 -0
  270. package/tests/ButtonCircular.test.tsx +17 -0
  271. package/tests/ButtonCustom.test.tsx +17 -0
  272. package/tests/ButtonIconColor.test.tsx +23 -0
  273. package/tests/ButtonUnstyled.test.tsx +56 -0
  274. package/tests/ClickDuringEnter.animated.test.tsx +174 -0
  275. package/tests/ColorTokenFallback.test.tsx +45 -0
  276. package/tests/DOMNodeAPIs.test.tsx +161 -0
  277. package/tests/DialogFocusScope.animated.test.tsx +309 -0
  278. package/tests/DialogNested.test.tsx +128 -0
  279. package/tests/DialogOpenControlled.test.tsx +42 -0
  280. package/tests/DialogPointerEvents.animated.test.tsx +108 -0
  281. package/tests/DialogScoped.test.tsx +137 -0
  282. package/tests/DialogSheetAdapt.test.tsx +68 -0
  283. package/tests/DialogSheetAdaptResize.test.tsx +161 -0
  284. package/tests/DismissLayerStacking.test.tsx +292 -0
  285. package/tests/DriverDisableAnimationProps.animated.test.tsx +157 -0
  286. package/tests/ExitCompletion.animated.test.tsx +425 -0
  287. package/tests/ExitTimingCheck.animated.test.ts +34 -0
  288. package/tests/FocusVisibleButton.test.tsx +41 -0
  289. package/tests/FocusVisibleButtonPointerFocus.test.tsx +23 -0
  290. package/tests/FocusVisibleButtonPointerFocusWithFocusStyle.test.tsx +40 -0
  291. package/tests/FocusWithinStyle.animated.test.tsx +66 -0
  292. package/tests/FocusWithinStyle.test.tsx +60 -0
  293. package/tests/FormButtonType.test.tsx +42 -0
  294. package/tests/GlobalScopedTriggerIsolation.test.tsx +89 -0
  295. package/tests/GroupHoverMobile.test.tsx +52 -0
  296. package/tests/GroupPressInVariant.test.tsx +82 -0
  297. package/tests/GroupProp.test.tsx +30 -0
  298. package/tests/GroupPseudoVariantOverride.test.tsx +57 -0
  299. package/tests/GroupUseCases.test.tsx +111 -0
  300. package/tests/GuiSiteMotion.test.ts +481 -0
  301. package/tests/HeightMediaQueryOverride.test.tsx +112 -0
  302. package/tests/InputAutoFocusAfterMenu.test.tsx +55 -0
  303. package/tests/InputAutoFocusStyled.test.tsx +22 -0
  304. package/tests/ListItem.test.tsx +129 -0
  305. package/tests/MediaQueriesV5.test.tsx +113 -0
  306. package/tests/MediaQueryGtMd.test.tsx +84 -0
  307. package/tests/MenuAboveDialog.test.tsx +108 -0
  308. package/tests/MenuAccessibility.test.tsx +346 -0
  309. package/tests/MenuAnimatePosition.animated.test.tsx +57 -0
  310. package/tests/MenuArrowAnimatePresence.animated.test.tsx +71 -0
  311. package/tests/MenuAsChildPosition.test.tsx +16 -0
  312. package/tests/MenuAutoResize.test.tsx +54 -0
  313. package/tests/MenuFocusLeave.test.tsx +181 -0
  314. package/tests/MenuHighlight.test.tsx +165 -0
  315. package/tests/MenuHoverKeyboardBugs.test.tsx +252 -0
  316. package/tests/MenuItemFocus.test.tsx +59 -0
  317. package/tests/MenuItemPseudoOverride.test.tsx +231 -0
  318. package/tests/MenuMultiTrigger.test.tsx +101 -0
  319. package/tests/MenuOverflow.test.tsx +93 -0
  320. package/tests/MenuStayInFrame.test.tsx +102 -0
  321. package/tests/MenuSubKeyboardFocus.test.tsx +220 -0
  322. package/tests/MenuSubLeftSafePolygon.test.tsx +88 -0
  323. package/tests/MenuSubNestedPosition.test.tsx +48 -0
  324. package/tests/MenuSubSafePolygon.test.tsx +97 -0
  325. package/tests/MenuSubStyled.test.tsx +40 -0
  326. package/tests/MenuTheme.test.tsx +34 -0
  327. package/tests/MenuUnstyled.test.tsx +56 -0
  328. package/tests/MultiDriverAnimation.test.tsx +207 -0
  329. package/tests/NewInputBasic.test.tsx +50 -0
  330. package/tests/NewInputEvents.test.tsx +55 -0
  331. package/tests/OnLayout.test.tsx +163 -0
  332. package/tests/OnLayoutScale.test.tsx +100 -0
  333. package/tests/OnLayoutStress.test.tsx +304 -0
  334. package/tests/ParagraphSpanFontInheritance.test.tsx +73 -0
  335. package/tests/PointerEvents.test.tsx +123 -0
  336. package/tests/Popover.animated.test.tsx +234 -0
  337. package/tests/PopoverAndMenuMultiTrigger.test.tsx +184 -0
  338. package/tests/PopoverAnimatePosition.animated.test.tsx +51 -0
  339. package/tests/PopoverClickDuringEnter.animated.test.tsx +197 -0
  340. package/tests/PopoverFocusScope.test.tsx +242 -0
  341. package/tests/PopoverHoverable.test.tsx +383 -0
  342. package/tests/PopoverHoverableDisableClick.test.tsx +106 -0
  343. package/tests/PopoverHoverableRapid.test.tsx +129 -0
  344. package/tests/PopoverHoverableReposition.test.tsx +111 -0
  345. package/tests/PopoverHoverableScoped.animated.test.tsx +103 -0
  346. package/tests/PopoverHoverableStress.test.tsx +169 -0
  347. package/tests/PopoverInitialPosition.animated.test.tsx +82 -0
  348. package/tests/PopoverMiddlewareSkipRegression.animated.test.tsx +221 -0
  349. package/tests/PopoverScoped.test.tsx +128 -0
  350. package/tests/PopoverScopedPositionGlitch.animated.test.tsx +184 -0
  351. package/tests/PopoverTriggerIsolation.test.tsx +62 -0
  352. package/tests/PseudoTransition.animated.test.tsx +319 -0
  353. package/tests/RawAnimatedValue.test.tsx +147 -0
  354. package/tests/RemoveScroll.test.tsx +223 -0
  355. package/tests/RenderProp.test.tsx +293 -0
  356. package/tests/ScrollViewRef.test.tsx +39 -0
  357. package/tests/SelectClickHold.test.tsx +147 -0
  358. package/tests/SelectFocusScope.test.tsx +176 -0
  359. package/tests/SelectInnerPositioning.test.tsx +82 -0
  360. package/tests/SelectKeyboardNav.test.tsx +173 -0
  361. package/tests/SelectPositioning.test.tsx +56 -0
  362. package/tests/SelectTypeahead.test.tsx +63 -0
  363. package/tests/Shadows.test.tsx +14 -0
  364. package/tests/SheetAnimation.animated.test.tsx +413 -0
  365. package/tests/SheetDrag.animated.test.tsx +223 -0
  366. package/tests/SheetDragResist.animated.test.tsx +393 -0
  367. package/tests/SheetOnAnimationComplete.animated.test.tsx +62 -0
  368. package/tests/SheetScrollLock.animated.test.tsx +287 -0
  369. package/tests/SheetScrollableDrag.animated.test.tsx +1264 -0
  370. package/tests/SheetSnapPointsFit.animated.test.tsx +259 -0
  371. package/tests/ShorthandVariables.test.tsx +44 -0
  372. package/tests/SpinnerCustomColors.test.tsx +67 -0
  373. package/tests/StackZIndex.test.tsx +51 -0
  374. package/tests/StressPagePerf.test.tsx +76 -0
  375. package/tests/StylePlatform.test.tsx +38 -0
  376. package/tests/StyleProp.test.tsx +20 -0
  377. package/tests/StyledAnchor.test.tsx +17 -0
  378. package/tests/StyledButtonTheme.test.tsx +22 -0
  379. package/tests/StyledButtonVariantPseudo.test.tsx +20 -0
  380. package/tests/StyledButtonVariantPseudoMerge.animated.test.tsx +33 -0
  381. package/tests/StyledCheckboxTheme.test.tsx +16 -0
  382. package/tests/StyledContextColor.test.tsx +119 -0
  383. package/tests/StyledContextTokens.test.tsx +56 -0
  384. package/tests/StyledHOCNamed.test.tsx +16 -0
  385. package/tests/StyledHtml.test.tsx +161 -0
  386. package/tests/StyledIconColor.test.tsx +32 -0
  387. package/tests/StyledInputFocusStyle.test.tsx +19 -0
  388. package/tests/StyledInputOnFocus.test.tsx +27 -0
  389. package/tests/StyledMediaQueryMerge.test.tsx +66 -0
  390. package/tests/StyledRNW.test.tsx +17 -0
  391. package/tests/StyledStyleableInputOnFocus.test.tsx +27 -0
  392. package/tests/StyledStyleableInputVariant.test.tsx +22 -0
  393. package/tests/StyledStyledStyleableInputOnFocus.test.tsx +27 -0
  394. package/tests/StyledVariantTextColor.test.tsx +24 -0
  395. package/tests/StyledViewOnFocus.test.tsx +27 -0
  396. package/tests/TabHoverAnimation.animated.test.tsx +468 -0
  397. package/tests/TabHoverPositionSmooth.animated.test.tsx +129 -0
  398. package/tests/TextNestedInheritance.test.tsx +93 -0
  399. package/tests/ThemeChange.test.tsx +70 -0
  400. package/tests/ThemeComponentResolution.test.tsx +82 -0
  401. package/tests/ThemeConditionalName.test.tsx +34 -0
  402. package/tests/ThemeMediaAnimation.test.tsx +65 -0
  403. package/tests/ThemeNested.test.tsx +141 -0
  404. package/tests/ThemeReset.test.tsx +63 -0
  405. package/tests/ThemeShallow.test.tsx +95 -0
  406. package/tests/Toast.test.tsx +106 -0
  407. package/tests/ToggleGroup.test.tsx +61 -0
  408. package/tests/ToggleGroupActiveProps.test.tsx +38 -0
  409. package/tests/ToggleGroupXGroup.test.tsx +172 -0
  410. package/tests/TooltipAnimation.animated.test.tsx +260 -0
  411. package/tests/TooltipEnterInterrupt.animated.test.tsx +76 -0
  412. package/tests/TooltipGlobalPattern.animated.test.tsx +208 -0
  413. package/tests/TooltipGroup.animated.test.tsx +79 -0
  414. package/tests/TooltipMultiTrigger.test.tsx +116 -0
  415. package/tests/TooltipPositionJump.animated.test.tsx +229 -0
  416. package/tests/TooltipPositionJumpNotes.md +219 -0
  417. package/tests/TooltipRapidSwitch.animated.test.tsx +399 -0
  418. package/tests/TooltipTriggerInline.test.tsx +65 -0
  419. package/tests/TransformMediaQueryMerge.test.tsx +104 -0
  420. package/tests/TransitionEnterExit.animated.test.tsx +311 -0
  421. package/tests/UseTheme.test.tsx +16 -0
  422. package/tests/V5ThemeBuilderOutput.test.tsx +164 -0
  423. package/tests/VariantFontFamily.test.tsx +11 -0
  424. package/tests/VariantsOrder.test.tsx +53 -0
  425. package/tests/_debug_position.mjs +52 -0
  426. package/tests/test-utils.ts +106 -0
  427. package/tests/utils.tsx +54 -0
  428. package/tsconfig.json +45 -0
  429. package/vite-env.d.ts +1 -0
  430. package/vite.config.ts +14 -0
  431. package/webpack.config.js +139 -0
@@ -0,0 +1,1264 @@
1
+ /**
2
+ * Playwright E2E Test for Sheet + ScrollView gesture coordination
3
+ *
4
+ * Ported from Detox tests to verify web implementation matches native quality.
5
+ *
6
+ * Tests the smooth handoff behavior between sheet dragging and scrollview scrolling:
7
+ * 1. At top snap point + swipe up → scroll content naturally
8
+ * 2. At top snap point + swipe down → drag sheet down (NOT scroll)
9
+ * 3. Scrolled down + drag down → scroll to top THEN hand off to sheet drag
10
+ * 4. Dragging up + hit sheet top → continue into scrolling
11
+ */
12
+
13
+ import { expect, test, type Page } from '@playwright/test'
14
+ import { setupPage } from './test-utils'
15
+
16
+ // mobile viewport with touch support for realistic sheet testing
17
+ test.use({
18
+ viewport: { width: 390, height: 844 },
19
+ hasTouch: true,
20
+ isMobile: true,
21
+ })
22
+
23
+ test.beforeEach(async ({ page }) => {
24
+ // capture console logs for debugging
25
+ page.on('console', (msg) => {
26
+ if (
27
+ msg.text().includes('[Sheet') ||
28
+ msg.text().includes('[ScrollView') ||
29
+ msg.text().includes('[gesture')
30
+ ) {
31
+ console.log(`BROWSER: ${msg.text()}`)
32
+ }
33
+ })
34
+ await setupPage(page, { name: 'SheetScrollableDrag', type: 'useCase' })
35
+ })
36
+
37
+ /**
38
+ * perform a drag gesture using touch events via touchscreen API
39
+ * this properly triggers touch events which our gesture handlers listen for
40
+ *
41
+ * use mode: 'touch' for scrollview/content areas (synthetic touch events)
42
+ * use mode: 'mouse' for handle/frame areas (PanResponder uses mouse events on web)
43
+ */
44
+ async function dragSheet(
45
+ page: Page,
46
+ startX: number,
47
+ startY: number,
48
+ deltaY: number,
49
+ options: { steps?: number; stepDelay?: number; mode?: 'touch' | 'mouse' } = {}
50
+ ) {
51
+ const { steps = 20, stepDelay = 16, mode = 'touch' } = options
52
+
53
+ if (mode === 'mouse') {
54
+ // use mouse events for PanResponder-based elements (handle, frame)
55
+ await page.mouse.move(startX, startY)
56
+ await page.mouse.down()
57
+
58
+ for (let i = 1; i <= steps; i++) {
59
+ await page.mouse.move(startX, startY + (deltaY * i) / steps)
60
+ await page.waitForTimeout(stepDelay)
61
+ }
62
+
63
+ await page.mouse.up()
64
+ return
65
+ }
66
+
67
+ // touch mode for ScrollView areas
68
+ const endY = startY + deltaY
69
+
70
+ // touchscreen doesn't have a direct swipe, so we use evaluate to dispatch touch events
71
+ await page.evaluate(
72
+ ({ startX, startY, endY, steps, stepDelay }) => {
73
+ return new Promise<void>((resolve) => {
74
+ const target = document.elementFromPoint(startX, startY)
75
+ if (!target) {
76
+ resolve()
77
+ return
78
+ }
79
+
80
+ const dispatchTouch = (type: string, x: number, y: number) => {
81
+ const touch = new Touch({
82
+ identifier: 0,
83
+ target,
84
+ clientX: x,
85
+ clientY: y,
86
+ pageX: x,
87
+ pageY: y,
88
+ })
89
+ const touchEvent = new TouchEvent(type, {
90
+ bubbles: true,
91
+ cancelable: true,
92
+ touches: type === 'touchend' ? [] : [touch],
93
+ targetTouches: type === 'touchend' ? [] : [touch],
94
+ changedTouches: [touch],
95
+ })
96
+ target.dispatchEvent(touchEvent)
97
+ }
98
+
99
+ // touchstart
100
+ dispatchTouch('touchstart', startX, startY)
101
+
102
+ let step = 0
103
+ const interval = setInterval(() => {
104
+ step++
105
+ const progress = step / steps
106
+ const currentY = startY + (endY - startY) * progress
107
+ dispatchTouch('touchmove', startX, currentY)
108
+
109
+ if (step >= steps) {
110
+ clearInterval(interval)
111
+ dispatchTouch('touchend', startX, endY)
112
+ resolve()
113
+ }
114
+ }, stepDelay)
115
+ })
116
+ },
117
+ { startX, startY, endY, steps, stepDelay }
118
+ )
119
+ }
120
+
121
+ test.describe('SheetScrollableDrag - RNGH Web Equivalent', () => {
122
+ test('should open sheet at position 0', async ({ page }) => {
123
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
124
+ await page.waitForTimeout(600)
125
+
126
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
127
+ await expect(frame).toBeVisible({ timeout: 5000 })
128
+
129
+ // should be at position 0 (top snap)
130
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
131
+ 'Sheet position: 0'
132
+ )
133
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
134
+ 'ScrollView Y: 0'
135
+ )
136
+ })
137
+
138
+ test('Case 1: drag DOWN at scrollY=0 should drag sheet, NOT scroll', async ({
139
+ page,
140
+ }) => {
141
+ // open sheet
142
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
143
+ await page.waitForTimeout(600)
144
+
145
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
146
+ await expect(frame).toBeVisible({ timeout: 5000 })
147
+
148
+ // verify starting state
149
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
150
+ 'Sheet position: 0'
151
+ )
152
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
153
+ 'ScrollView Y: 0'
154
+ )
155
+
156
+ // get the scrollview for dragging
157
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
158
+ const scrollviewBox = await scrollview.boundingBox()
159
+ expect(scrollviewBox).toBeTruthy()
160
+
161
+ // drag DOWN on scrollview - should drag sheet, NOT scroll
162
+ await dragSheet(
163
+ page,
164
+ scrollviewBox!.x + scrollviewBox!.width / 2,
165
+ scrollviewBox!.y + scrollviewBox!.height / 3,
166
+ 200,
167
+ { steps: 25, stepDelay: 16 }
168
+ )
169
+ await page.waitForTimeout(600)
170
+
171
+ // EXPECTED: sheet moved to position 1, scroll stayed at 0
172
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
173
+ 'Sheet position: 1'
174
+ )
175
+
176
+ // scroll should still be at 0 (or very close to it)
177
+ const scrollY = await page.getByTestId('sheet-scrollable-drag-scroll-y').textContent()
178
+ const scrollVal = parseInt(scrollY?.replace('ScrollView Y: ', '') || '0', 10)
179
+ expect(scrollVal).toBeLessThanOrEqual(5) // allow small tolerance
180
+ })
181
+
182
+ test('STRESS: multiple handoffs starting from scroll', async ({ page }) => {
183
+ // stress test: scroll → drag → scroll → drag (4x)
184
+ // each handoff should work cleanly without jitter
185
+ if (process.env.CI) test.slow() // triple timeout for CI runners
186
+
187
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
188
+ await page.waitForTimeout(800)
189
+
190
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
191
+ await expect(frame).toBeVisible({ timeout: 5000 })
192
+
193
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
194
+ const scrollviewBox = await scrollview.boundingBox()
195
+ expect(scrollviewBox).toBeTruthy()
196
+
197
+ const cx = scrollviewBox!.x + scrollviewBox!.width / 2
198
+ const cy = scrollviewBox!.y + scrollviewBox!.height / 2
199
+
200
+ for (let i = 0; i < 4; i++) {
201
+ // 1. scroll down (drag up gesture)
202
+ await dragSheet(page, cx, cy, -150, { steps: 20, stepDelay: 12 })
203
+ await page.waitForTimeout(400)
204
+
205
+ // verify scroll happened, sheet still at 0
206
+ const scrollY1 = await page
207
+ .getByTestId('sheet-scrollable-drag-scroll-y')
208
+ .textContent()
209
+ const scrollVal1 = parseInt(scrollY1?.replace('ScrollView Y: ', '') || '0', 10)
210
+ expect(scrollVal1).toBeGreaterThan(20)
211
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
212
+ 'Sheet position: 0'
213
+ )
214
+
215
+ // 2. drag down past scroll=0 to pull sheet
216
+ await dragSheet(page, cx, cy, 300, { steps: 30, stepDelay: 12 })
217
+ await page.waitForTimeout(400)
218
+
219
+ // verify sheet moved, scroll at 0
220
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
221
+ 'Sheet position: 1'
222
+ )
223
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
224
+ 'ScrollView Y: 0'
225
+ )
226
+
227
+ // 3. drag up to bring sheet back (no scroll should happen here)
228
+ await dragSheet(page, cx, cy, -200, { steps: 25, stepDelay: 12 })
229
+ await page.waitForTimeout(400)
230
+
231
+ // verify sheet back at 0, scroll still 0
232
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
233
+ 'Sheet position: 0'
234
+ )
235
+ const scrollY2 = await page
236
+ .getByTestId('sheet-scrollable-drag-scroll-y')
237
+ .textContent()
238
+ const scrollVal2 = parseInt(scrollY2?.replace('ScrollView Y: ', '') || '0', 10)
239
+ expect(scrollVal2).toBeLessThanOrEqual(10)
240
+ }
241
+ })
242
+
243
+ test('STRESS: multiple handoffs starting from drag', async ({ page }) => {
244
+ // stress test: drag → scroll → drag → scroll (4x)
245
+ // start by dragging sheet down first
246
+
247
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
248
+ await page.waitForTimeout(800)
249
+
250
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
251
+ await expect(frame).toBeVisible({ timeout: 5000 })
252
+
253
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
254
+ const scrollviewBox = await scrollview.boundingBox()
255
+ expect(scrollviewBox).toBeTruthy()
256
+
257
+ const cx = scrollviewBox!.x + scrollviewBox!.width / 2
258
+ const cy = scrollviewBox!.y + scrollviewBox!.height / 2
259
+
260
+ for (let i = 0; i < 4; i++) {
261
+ // 1. drag down to pull sheet (scrollY=0, so pan takes over)
262
+ // use touch mode on scrollview - this should work via our touch handler
263
+ await dragSheet(page, cx, cy, 200, { steps: 25, stepDelay: 12 })
264
+ await page.waitForTimeout(400)
265
+
266
+ // verify sheet moved, scroll still 0
267
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
268
+ 'Sheet position: 1'
269
+ )
270
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
271
+ 'ScrollView Y: 0'
272
+ )
273
+
274
+ // wait for animation to settle
275
+ await page.waitForTimeout(400)
276
+
277
+ // 2. drag up to bring sheet back - this should work via touch handler
278
+ // sheet not at top + dragging up = pan owns
279
+ await dragSheet(page, cx, cy, -250, { steps: 30, stepDelay: 12 })
280
+ await page.waitForTimeout(400)
281
+
282
+ // verify sheet at 0
283
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
284
+ 'Sheet position: 0'
285
+ )
286
+ // scroll should still be 0 (no scroll during drag up when sheet not at top)
287
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
288
+ 'ScrollView Y: 0'
289
+ )
290
+ }
291
+ })
292
+
293
+ test('CRITICAL: single gesture - scroll, pull sheet, reverse to push sheet up - no scroll during push', async ({
294
+ page,
295
+ }) => {
296
+ // ONE CONTINUOUS GESTURE with direction changes:
297
+ // 1. drag up to scroll
298
+ // 2. reverse to drag down (scroll back to 0, then pull sheet)
299
+ // 3. reverse again to drag up (push sheet back up)
300
+ // BUG: step 3 was scrolling when it should only move sheet
301
+
302
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
303
+ await page.waitForTimeout(600)
304
+
305
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
306
+ await expect(frame).toBeVisible({ timeout: 5000 })
307
+
308
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
309
+ const scrollviewBox = await scrollview.boundingBox()
310
+ expect(scrollviewBox).toBeTruthy()
311
+
312
+ const cx = scrollviewBox!.x + scrollviewBox!.width / 2
313
+ const startY = scrollviewBox!.y + scrollviewBox!.height / 2
314
+
315
+ // single continuous gesture with direction changes
316
+ await page.evaluate(
317
+ ({ cx, startY }) => {
318
+ return new Promise<void>((resolve) => {
319
+ const target = document.elementFromPoint(cx, startY)
320
+ if (!target) {
321
+ resolve()
322
+ return
323
+ }
324
+
325
+ const dispatchTouch = (type: string, x: number, y: number) => {
326
+ const touch = new Touch({
327
+ identifier: 0,
328
+ target,
329
+ clientX: x,
330
+ clientY: y,
331
+ pageX: x,
332
+ pageY: y,
333
+ })
334
+ const touchEvent = new TouchEvent(type, {
335
+ bubbles: true,
336
+ cancelable: true,
337
+ touches: type === 'touchend' ? [] : [touch],
338
+ targetTouches: type === 'touchend' ? [] : [touch],
339
+ changedTouches: [touch],
340
+ })
341
+ target.dispatchEvent(touchEvent)
342
+ }
343
+
344
+ // touchstart
345
+ dispatchTouch('touchstart', cx, startY)
346
+
347
+ let step = 0
348
+ const moves = [
349
+ // cycle 1
350
+ // phase 1: drag up to scroll (20 steps, -5px each = -100px)
351
+ ...Array(20).fill(-5),
352
+ // phase 2: drag down to scroll back and pull sheet (40 steps, +8px each = +320px)
353
+ ...Array(40).fill(8),
354
+ // phase 3: drag up to push sheet back (30 steps, -6px each = -180px)
355
+ // THIS SHOULD NOT SCROLL
356
+ ...Array(30).fill(-6),
357
+ // cycle 2 - repeat to catch bugs that only show on 2nd+ cycle
358
+ ...Array(20).fill(-5),
359
+ ...Array(40).fill(8),
360
+ ...Array(30).fill(-6),
361
+ // cycle 3
362
+ ...Array(20).fill(-5),
363
+ ...Array(40).fill(8),
364
+ ...Array(30).fill(-6),
365
+ // cycle 4
366
+ ...Array(20).fill(-5),
367
+ ...Array(40).fill(8),
368
+ ...Array(30).fill(-6),
369
+ ]
370
+
371
+ let currentY = startY
372
+ const interval = setInterval(() => {
373
+ if (step >= moves.length) {
374
+ clearInterval(interval)
375
+ dispatchTouch('touchend', cx, currentY)
376
+ resolve()
377
+ return
378
+ }
379
+ currentY += moves[step]
380
+ dispatchTouch('touchmove', cx, currentY)
381
+ step++
382
+ }, 16)
383
+ })
384
+ },
385
+ { cx, startY }
386
+ )
387
+
388
+ await page.waitForTimeout(400)
389
+
390
+ // check maxScrollY - this captures if scroll happened at ANY point during "push up" phases
391
+ // if scroll happened during push up, maxScrollY will be > what it should be
392
+ const maxScrollY = await page
393
+ .getByTestId('sheet-scrollable-drag-max-scroll-y')
394
+ .textContent()
395
+ const maxScrollVal = parseInt(maxScrollY?.replace('Max scroll Y: ', '') || '0', 10)
396
+
397
+ console.log('maxScrollVal:', maxScrollVal)
398
+ // the key assertion: maxScrollY should be around 100 (from the initial scroll phase)
399
+ // if it's significantly higher, scroll happened during "push sheet up" phases
400
+ // each scroll phase does ~100px, so 4 cycles should still be ~100 max if working correctly
401
+ // if broken, we'd see 200+ because scroll happens during push-up phases too
402
+ expect(maxScrollVal).toBeLessThanOrEqual(150)
403
+ })
404
+
405
+ test('CRITICAL: from pos 1, single gesture drag up past top then reverse down - no mid-gesture snap', async ({
406
+ page,
407
+ }) => {
408
+ // BUG: sheet at position 1, one continuous gesture:
409
+ // 1. drag up to move sheet to top (pan owns, sheet moves up)
410
+ // 2. continue dragging up past top (pan→scroll handoff)
411
+ // 3. reverse to drag down (scroll→pan handoff)
412
+ //
413
+ // the bug: during the pan→scroll handoff in step 2, snapToPosition(0) is
414
+ // called MID-GESTURE. this triggers setPosition(0) → useEffect →
415
+ // setScrollEnabled → animateTo which fights with the active gesture,
416
+ // causing snap, broken state, and "Maximum update depth exceeded".
417
+ //
418
+ // snapToPosition should NEVER be called during a continuous gesture.
419
+ // position changes should only happen on release (touchend).
420
+
421
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
422
+ await page.waitForTimeout(600)
423
+
424
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
425
+ await expect(frame).toBeVisible({ timeout: 5000 })
426
+
427
+ // get sheet to position 1 via handle drag
428
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
429
+ let handleBox = await handle.boundingBox()
430
+ expect(handleBox).toBeTruthy()
431
+
432
+ await dragSheet(
433
+ page,
434
+ handleBox!.x + handleBox!.width / 2,
435
+ handleBox!.y + handleBox!.height / 2,
436
+ 200,
437
+ { steps: 25, stepDelay: 16, mode: 'mouse' }
438
+ )
439
+ await page.waitForTimeout(600)
440
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
441
+ 'Sheet position: 1'
442
+ )
443
+ await page.waitForTimeout(400)
444
+
445
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
446
+ const scrollviewBox = await scrollview.boundingBox()
447
+ expect(scrollviewBox).toBeTruthy()
448
+
449
+ const cx = scrollviewBox!.x + scrollviewBox!.width / 2
450
+ const startY = scrollviewBox!.y + scrollviewBox!.height / 2
451
+
452
+ // listen for errors
453
+ const errors: string[] = []
454
+ page.on('pageerror', (err) => errors.push(err.message))
455
+
456
+ // set up a MutationObserver to track position changes DURING the gesture.
457
+ // the position text element updates when setPosition is called.
458
+ // any position change between touchstart and touchend is a mid-gesture snap (the bug).
459
+ await page.evaluate(() => {
460
+ const posEl = document.querySelector(
461
+ '[data-testid="sheet-scrollable-drag-position"]'
462
+ )
463
+ if (!posEl) return
464
+ ;(window as any).__midGestureSnaps = []
465
+ ;(window as any).__gestureActive = false
466
+
467
+ // track touch state
468
+ document.addEventListener(
469
+ 'touchstart',
470
+ () => {
471
+ ;(window as any).__gestureActive = true
472
+ },
473
+ { capture: true }
474
+ )
475
+ document.addEventListener(
476
+ 'touchend',
477
+ () => {
478
+ ;(window as any).__gestureActive = false
479
+ },
480
+ { capture: true }
481
+ )
482
+
483
+ // observe position text changes
484
+ const observer = new MutationObserver(() => {
485
+ if ((window as any).__gestureActive) {
486
+ const text = posEl.textContent || ''
487
+ ;(window as any).__midGestureSnaps.push(text)
488
+ }
489
+ })
490
+ observer.observe(posEl, { childList: true, characterData: true, subtree: true })
491
+ })
492
+
493
+ // single continuous gesture: drag up past top, then reverse down
494
+ await page.evaluate(
495
+ ({ cx, startY }) => {
496
+ return new Promise<void>((resolve) => {
497
+ const target = document.elementFromPoint(cx, startY)
498
+ if (!target) {
499
+ resolve()
500
+ return
501
+ }
502
+
503
+ const dispatchTouch = (type: string, x: number, y: number) => {
504
+ const touch = new Touch({
505
+ identifier: 0,
506
+ target,
507
+ clientX: x,
508
+ clientY: y,
509
+ pageX: x,
510
+ pageY: y,
511
+ })
512
+ target.dispatchEvent(
513
+ new TouchEvent(type, {
514
+ bubbles: true,
515
+ cancelable: true,
516
+ touches: type === 'touchend' ? [] : [touch],
517
+ targetTouches: type === 'touchend' ? [] : [touch],
518
+ changedTouches: [touch],
519
+ })
520
+ )
521
+ }
522
+
523
+ dispatchTouch('touchstart', cx, startY)
524
+
525
+ let step = 0
526
+ const moves = [
527
+ // phase 1: drag up - moves sheet from pos 1 to top (30 steps, -8px = -240px)
528
+ ...Array(30).fill(-8),
529
+ // phase 2: continue up past top - should start scrolling (20 steps, -5px = -100px)
530
+ // THIS is where the pan→scroll handoff happens and snapToPosition(0) is called
531
+ ...Array(20).fill(-5),
532
+ // phase 3: reverse down - scroll back then pull sheet (40 steps, +8px = +320px)
533
+ ...Array(40).fill(8),
534
+ ]
535
+
536
+ let currentY = startY
537
+ const interval = setInterval(() => {
538
+ if (step >= moves.length) {
539
+ clearInterval(interval)
540
+ dispatchTouch('touchend', cx, currentY)
541
+ resolve()
542
+ return
543
+ }
544
+ currentY += moves[step]
545
+ dispatchTouch('touchmove', cx, currentY)
546
+ step++
547
+ }, 16)
548
+ })
549
+ },
550
+ { cx, startY }
551
+ )
552
+
553
+ await page.waitForTimeout(600)
554
+
555
+ // no Maximum update depth exceeded
556
+ expect(errors.filter((e) => e.includes('Maximum update depth'))).toHaveLength(0)
557
+
558
+ // CRITICAL: no position changes should happen during the gesture
559
+ // snapToPosition should only be called on release, not mid-gesture
560
+ const midGestureSnaps = await page.evaluate(
561
+ () => (window as any).__midGestureSnaps || []
562
+ )
563
+ console.log('mid-gesture snaps:', midGestureSnaps)
564
+ expect(midGestureSnaps).toHaveLength(0)
565
+ })
566
+
567
+ test('CRITICAL: from pos 1, drag up past top then back down - should snap to pos 1 not dismiss', async ({
568
+ page,
569
+ }) => {
570
+ // BUG: from pos 1, single continuous gesture:
571
+ // 1. drag up (pan moves sheet to top)
572
+ // 2. continue up past top (pan→scroll handoff, scroll owns)
573
+ // 3. reverse down past scroll=0 (scroll→pan handoff, pan owns)
574
+ // 4. release just below where pos 1 would be
575
+ //
576
+ // expected: sheet snaps to position 1
577
+ // actual bug: if touchend fires while owner='scroll', release() is never
578
+ // called. the sheet's animated position is stranded and it dismisses.
579
+
580
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
581
+ await page.waitForTimeout(600)
582
+
583
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
584
+ await expect(frame).toBeVisible({ timeout: 5000 })
585
+
586
+ // get sheet to position 1
587
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
588
+ let handleBox = await handle.boundingBox()
589
+ expect(handleBox).toBeTruthy()
590
+
591
+ await dragSheet(
592
+ page,
593
+ handleBox!.x + handleBox!.width / 2,
594
+ handleBox!.y + handleBox!.height / 2,
595
+ 200,
596
+ { steps: 25, stepDelay: 16, mode: 'mouse' }
597
+ )
598
+ await page.waitForTimeout(600)
599
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
600
+ 'Sheet position: 1'
601
+ )
602
+ await page.waitForTimeout(400)
603
+
604
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
605
+ const scrollviewBox = await scrollview.boundingBox()
606
+ expect(scrollviewBox).toBeTruthy()
607
+
608
+ const cx = scrollviewBox!.x + scrollviewBox!.width / 2
609
+ const startY = scrollviewBox!.y + scrollviewBox!.height / 2
610
+
611
+ // single continuous gesture: drag up past top, scroll a bit, reverse down to ~pos 1
612
+ await page.evaluate(
613
+ ({ cx, startY }) => {
614
+ return new Promise<void>((resolve) => {
615
+ const target = document.elementFromPoint(cx, startY)
616
+ if (!target) {
617
+ resolve()
618
+ return
619
+ }
620
+
621
+ const dispatchTouch = (type: string, x: number, y: number) => {
622
+ const touch = new Touch({
623
+ identifier: 0,
624
+ target,
625
+ clientX: x,
626
+ clientY: y,
627
+ pageX: x,
628
+ pageY: y,
629
+ })
630
+ target.dispatchEvent(
631
+ new TouchEvent(type, {
632
+ bubbles: true,
633
+ cancelable: true,
634
+ touches: type === 'touchend' ? [] : [touch],
635
+ targetTouches: type === 'touchend' ? [] : [touch],
636
+ changedTouches: [touch],
637
+ })
638
+ )
639
+ }
640
+
641
+ dispatchTouch('touchstart', cx, startY)
642
+
643
+ let step = 0
644
+ const moves = [
645
+ // phase 1: drag up - moves sheet from pos 1 to top
646
+ // pos 1 → pos 0 is ~300px, so need ~40 steps × -8px = -320px
647
+ ...Array(40).fill(-8),
648
+ // phase 2: continue up past top - should start scrolling (20 steps, -5px = -100px)
649
+ ...Array(20).fill(-5),
650
+ // phase 3: reverse down - scroll back to 0, then pull sheet down (50 steps, +8px = +400px)
651
+ // this should end near where pos 1 is
652
+ ...Array(50).fill(8),
653
+ ]
654
+
655
+ let currentY = startY
656
+ const interval = setInterval(() => {
657
+ if (step >= moves.length) {
658
+ clearInterval(interval)
659
+ dispatchTouch('touchend', cx, currentY)
660
+ resolve()
661
+ return
662
+ }
663
+ currentY += moves[step]
664
+ dispatchTouch('touchmove', cx, currentY)
665
+ step++
666
+ }, 16)
667
+ })
668
+ },
669
+ { cx, startY }
670
+ )
671
+
672
+ await page.waitForTimeout(800)
673
+
674
+ // the bug: stale startY in release() causes it to compute a position way
675
+ // below the screen. with dismissOnSnapToBottom, this triggers setOpen(false)
676
+ // which dismisses the sheet unexpectedly.
677
+ const unexpectedClose = await page
678
+ .getByTestId('sheet-scrollable-drag-unexpected-close')
679
+ .textContent()
680
+ console.log('unexpected close:', unexpectedClose)
681
+
682
+ // the sheet should NOT have been dismissed during this gesture
683
+ expect(unexpectedClose).toContain('no')
684
+ })
685
+
686
+ test('Case 1b: scroll, drag down, drag UP - scroll should NOT happen during drag up', async ({
687
+ page,
688
+ }) => {
689
+ // This tests: scroll down, drag sheet down, then drag UP
690
+ // During drag UP (bringing sheet back), scroll should NOT change
691
+
692
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
693
+ await page.waitForTimeout(600)
694
+
695
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
696
+ await expect(frame).toBeVisible({ timeout: 5000 })
697
+
698
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
699
+ const scrollviewBox = await scrollview.boundingBox()
700
+ expect(scrollviewBox).toBeTruthy()
701
+
702
+ // 1. scroll down
703
+ await page.mouse.move(
704
+ scrollviewBox!.x + scrollviewBox!.width / 2,
705
+ scrollviewBox!.y + scrollviewBox!.height / 2
706
+ )
707
+ await page.mouse.wheel(0, 150)
708
+ await page.waitForTimeout(300)
709
+
710
+ // 2. drag DOWN to pull sheet (this scrolls back to 0, then drags sheet)
711
+ await dragSheet(
712
+ page,
713
+ scrollviewBox!.x + scrollviewBox!.width / 2,
714
+ scrollviewBox!.y + scrollviewBox!.height / 3,
715
+ 300,
716
+ { steps: 30, stepDelay: 16 }
717
+ )
718
+ await page.waitForTimeout(400)
719
+
720
+ // sheet should be at position 1
721
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
722
+ 'Sheet position: 1'
723
+ )
724
+ // scroll should be 0
725
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
726
+ 'ScrollView Y: 0'
727
+ )
728
+
729
+ // 3. NOW drag UP - this should bring sheet back to position 0
730
+ // scroll should NOT change during this drag
731
+ await dragSheet(
732
+ page,
733
+ scrollviewBox!.x + scrollviewBox!.width / 2,
734
+ scrollviewBox!.y + scrollviewBox!.height / 2,
735
+ -250,
736
+ { steps: 30, stepDelay: 16 }
737
+ )
738
+ await page.waitForTimeout(300)
739
+
740
+ // sheet should be at position 0
741
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
742
+ 'Sheet position: 0'
743
+ )
744
+
745
+ // CRITICAL: scroll should still be 0 - no scrolling during drag up
746
+ const scrollAfter = await page
747
+ .getByTestId('sheet-scrollable-drag-scroll-y')
748
+ .textContent()
749
+ const scrollVal = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
750
+ expect(scrollVal).toBeLessThanOrEqual(5) // allow tiny tolerance
751
+ })
752
+
753
+ test('Case 1c: scroll then drag DOWN - scroll should NOT change during drag', async ({
754
+ page,
755
+ }) => {
756
+ // This tests the basic jitter scenario:
757
+ // 1. Scroll content down (scrollY > 0)
758
+ // 2. Drag down to pull sheet
759
+ // 3. During the drag, scrollY should NOT change (no jitter)
760
+
761
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
762
+ await page.waitForTimeout(600)
763
+
764
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
765
+ await expect(frame).toBeVisible({ timeout: 5000 })
766
+
767
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
768
+ const scrollviewBox = await scrollview.boundingBox()
769
+ expect(scrollviewBox).toBeTruthy()
770
+
771
+ // first scroll down with wheel
772
+ await page.mouse.move(
773
+ scrollviewBox!.x + scrollviewBox!.width / 2,
774
+ scrollviewBox!.y + scrollviewBox!.height / 2
775
+ )
776
+ await page.mouse.wheel(0, 150)
777
+ await page.waitForTimeout(300)
778
+
779
+ // verify we scrolled
780
+ const scrollBefore = await page
781
+ .getByTestId('sheet-scrollable-drag-scroll-y')
782
+ .textContent()
783
+ const scrollValBefore = parseInt(
784
+ scrollBefore?.replace('ScrollView Y: ', '') || '0',
785
+ 10
786
+ )
787
+ expect(scrollValBefore).toBeGreaterThan(30)
788
+
789
+ // now do a long drag DOWN - should first scroll back to 0, then drag sheet
790
+ // but DURING the sheet drag portion, scroll should stay at 0
791
+ await dragSheet(
792
+ page,
793
+ scrollviewBox!.x + scrollviewBox!.width / 2,
794
+ scrollviewBox!.y + scrollviewBox!.height / 3,
795
+ 400, // long drag to ensure we hit both scroll-back and sheet-drag phases
796
+ { steps: 40, stepDelay: 16 }
797
+ )
798
+ await page.waitForTimeout(100)
799
+
800
+ // check final scroll position - should be 0 (scrolled back) not negative or jumping around
801
+ const scrollAfter = await page
802
+ .getByTestId('sheet-scrollable-drag-scroll-y')
803
+ .textContent()
804
+ const scrollValAfter = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
805
+
806
+ // scroll should be at 0 (not negative, not jumping around)
807
+ expect(scrollValAfter).toBe(0)
808
+
809
+ // sheet should have moved to position 1
810
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
811
+ 'Sheet position: 1'
812
+ )
813
+ })
814
+
815
+ test('Case 2: at top snap, drag UP should scroll content', async ({ page }) => {
816
+ // open sheet
817
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
818
+ await page.waitForTimeout(600)
819
+
820
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
821
+ await expect(frame).toBeVisible({ timeout: 5000 })
822
+
823
+ // verify starting state - at top, scroll at 0
824
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
825
+ 'Sheet position: 0'
826
+ )
827
+
828
+ // get the scrollview
829
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
830
+ const scrollviewBox = await scrollview.boundingBox()
831
+ expect(scrollviewBox).toBeTruthy()
832
+
833
+ // use wheel event to scroll content (more reliable than drag for scroll)
834
+ await page.mouse.move(
835
+ scrollviewBox!.x + scrollviewBox!.width / 2,
836
+ scrollviewBox!.y + scrollviewBox!.height / 2
837
+ )
838
+ await page.mouse.wheel(0, 200)
839
+ await page.waitForTimeout(400)
840
+
841
+ // EXPECTED: sheet stays at position 0, content scrolled
842
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
843
+ 'Sheet position: 0'
844
+ )
845
+
846
+ // scroll should have increased
847
+ const scrollY = await page.getByTestId('sheet-scrollable-drag-scroll-y').textContent()
848
+ const scrollVal = parseInt(scrollY?.replace('ScrollView Y: ', '') || '0', 10)
849
+ expect(scrollVal).toBeGreaterThan(50)
850
+ })
851
+
852
+ test('Case 3: drag sheet up from position 1 should NOT scroll simultaneously', async ({
853
+ page,
854
+ }) => {
855
+ // open sheet
856
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
857
+ await page.waitForTimeout(600)
858
+
859
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
860
+ await expect(frame).toBeVisible({ timeout: 5000 })
861
+
862
+ // get handle for dragging
863
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
864
+ let handleBox = await handle.boundingBox()
865
+ expect(handleBox).toBeTruthy()
866
+
867
+ // first drag down to position 1 (use mouse for handle)
868
+ await dragSheet(
869
+ page,
870
+ handleBox!.x + handleBox!.width / 2,
871
+ handleBox!.y + handleBox!.height / 2,
872
+ 200,
873
+ { steps: 25, stepDelay: 16, mode: 'mouse' }
874
+ )
875
+ await page.waitForTimeout(600)
876
+
877
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
878
+ 'Sheet position: 1'
879
+ )
880
+
881
+ // verify scroll is still at 0 after first drag
882
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
883
+ 'ScrollView Y: 0'
884
+ )
885
+
886
+ // wait for animation to settle
887
+ await page.waitForTimeout(400)
888
+
889
+ // get new handle position
890
+ handleBox = await handle.boundingBox()
891
+ expect(handleBox).toBeTruthy()
892
+
893
+ // now drag UP back to position 0
894
+ await dragSheet(
895
+ page,
896
+ handleBox!.x + handleBox!.width / 2,
897
+ handleBox!.y + handleBox!.height / 2,
898
+ -250,
899
+ { steps: 30, stepDelay: 16 }
900
+ )
901
+ await page.waitForTimeout(600)
902
+
903
+ // EXPECTED: sheet back at position 0, scroll should still be 0 (no simultaneous scroll)
904
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
905
+ 'Sheet position: 0'
906
+ )
907
+ })
908
+
909
+ test('Case 4: scroll down, then drag down should scroll back first', async ({
910
+ page,
911
+ }) => {
912
+ // open sheet
913
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
914
+ await page.waitForTimeout(600)
915
+
916
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
917
+ await expect(frame).toBeVisible({ timeout: 5000 })
918
+
919
+ // get the scrollview
920
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
921
+ const scrollviewBox = await scrollview.boundingBox()
922
+ expect(scrollviewBox).toBeTruthy()
923
+
924
+ // first scroll down (wheel up to scroll content down)
925
+ await page.mouse.move(
926
+ scrollviewBox!.x + scrollviewBox!.width / 2,
927
+ scrollviewBox!.y + scrollviewBox!.height / 2
928
+ )
929
+ await page.mouse.wheel(0, 200)
930
+ await page.waitForTimeout(400)
931
+
932
+ // verify we scrolled
933
+ const scrollBefore = await page
934
+ .getByTestId('sheet-scrollable-drag-scroll-y')
935
+ .textContent()
936
+ const scrollValBefore = parseInt(
937
+ scrollBefore?.replace('ScrollView Y: ', '') || '0',
938
+ 10
939
+ )
940
+ expect(scrollValBefore).toBeGreaterThan(50)
941
+
942
+ // now drag DOWN - should scroll back first before dragging sheet
943
+ await dragSheet(
944
+ page,
945
+ scrollviewBox!.x + scrollviewBox!.width / 2,
946
+ scrollviewBox!.y + scrollviewBox!.height / 3,
947
+ 300,
948
+ { steps: 30, stepDelay: 16 }
949
+ )
950
+ await page.waitForTimeout(600)
951
+
952
+ // either scroll absorbed the swipe (scroll at 0 or close) OR sheet moved
953
+ const scrollAfter = await page
954
+ .getByTestId('sheet-scrollable-drag-scroll-y')
955
+ .textContent()
956
+ const scrollValAfter = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
957
+
958
+ const posAfter = await page
959
+ .getByTestId('sheet-scrollable-drag-position')
960
+ .textContent()
961
+ const posValAfter = parseInt(posAfter?.replace('Sheet position: ', '') || '0', 10)
962
+
963
+ // success if either: scroll went back to 0, or sheet moved to position 1
964
+ expect(scrollValAfter < scrollValBefore || posValAfter === 1).toBeTruthy()
965
+ })
966
+
967
+ test('Case 5: HANDOFF - scroll to 0 then drag sheet in one gesture', async ({
968
+ page,
969
+ }) => {
970
+ // open sheet
971
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
972
+ await page.waitForTimeout(600)
973
+
974
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
975
+ await expect(frame).toBeVisible({ timeout: 5000 })
976
+
977
+ // get the scrollview
978
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
979
+ const scrollviewBox = await scrollview.boundingBox()
980
+ expect(scrollviewBox).toBeTruthy()
981
+
982
+ // first scroll down a bit
983
+ await page.mouse.move(
984
+ scrollviewBox!.x + scrollviewBox!.width / 2,
985
+ scrollviewBox!.y + scrollviewBox!.height / 2
986
+ )
987
+ await page.mouse.wheel(0, 100)
988
+ await page.waitForTimeout(300)
989
+
990
+ // verify we scrolled
991
+ const scrollBefore = await page
992
+ .getByTestId('sheet-scrollable-drag-scroll-y')
993
+ .textContent()
994
+ const scrollValBefore = parseInt(
995
+ scrollBefore?.replace('ScrollView Y: ', '') || '0',
996
+ 10
997
+ )
998
+ expect(scrollValBefore).toBeGreaterThan(0)
999
+
1000
+ // long drag down - should scroll to 0 then drag sheet
1001
+ await dragSheet(
1002
+ page,
1003
+ scrollviewBox!.x + scrollviewBox!.width / 2,
1004
+ scrollviewBox!.y + scrollviewBox!.height / 4,
1005
+ 350,
1006
+ { steps: 40, stepDelay: 16 }
1007
+ )
1008
+ await page.waitForTimeout(600)
1009
+
1010
+ // EXPECTED: scroll should be at 0, sheet should have moved to position 1
1011
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
1012
+ 'ScrollView Y: 0'
1013
+ )
1014
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1015
+ 'Sheet position: 1'
1016
+ )
1017
+ })
1018
+
1019
+ test('Case 6: multiple direction changes without getting stuck', async ({ page }) => {
1020
+ // open sheet
1021
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
1022
+ await page.waitForTimeout(600)
1023
+
1024
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
1025
+ await expect(frame).toBeVisible({ timeout: 5000 })
1026
+
1027
+ // verify starting state
1028
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1029
+ 'Sheet position: 0'
1030
+ )
1031
+
1032
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
1033
+ let handleBox = await handle.boundingBox()
1034
+ expect(handleBox).toBeTruthy()
1035
+
1036
+ // 1. drag down to position 1
1037
+ await dragSheet(
1038
+ page,
1039
+ handleBox!.x + handleBox!.width / 2,
1040
+ handleBox!.y + handleBox!.height / 2,
1041
+ 200,
1042
+ { steps: 25, stepDelay: 16 }
1043
+ )
1044
+ await page.waitForTimeout(600)
1045
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1046
+ 'Sheet position: 1'
1047
+ )
1048
+
1049
+ // 2. drag up back to position 0
1050
+ handleBox = await handle.boundingBox()
1051
+ expect(handleBox).toBeTruthy()
1052
+ await dragSheet(
1053
+ page,
1054
+ handleBox!.x + handleBox!.width / 2,
1055
+ handleBox!.y + handleBox!.height / 2,
1056
+ -250,
1057
+ { steps: 30, stepDelay: 16 }
1058
+ )
1059
+ await page.waitForTimeout(600)
1060
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1061
+ 'Sheet position: 0'
1062
+ )
1063
+
1064
+ // 3. now scroll up (content should scroll since at top snap)
1065
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
1066
+ const scrollviewBox = await scrollview.boundingBox()
1067
+ expect(scrollviewBox).toBeTruthy()
1068
+
1069
+ await page.mouse.move(
1070
+ scrollviewBox!.x + scrollviewBox!.width / 2,
1071
+ scrollviewBox!.y + scrollviewBox!.height / 2
1072
+ )
1073
+ await page.mouse.wheel(0, 150)
1074
+ await page.waitForTimeout(400)
1075
+
1076
+ const scrollAfter = await page
1077
+ .getByTestId('sheet-scrollable-drag-scroll-y')
1078
+ .textContent()
1079
+ const scrollVal = parseInt(scrollAfter?.replace('ScrollView Y: ', '') || '0', 10)
1080
+ expect(scrollVal).toBeGreaterThan(30)
1081
+
1082
+ // 4. drag down - should scroll back first, then drag sheet
1083
+ await dragSheet(
1084
+ page,
1085
+ scrollviewBox!.x + scrollviewBox!.width / 2,
1086
+ scrollviewBox!.y + scrollviewBox!.height / 3,
1087
+ 350,
1088
+ { steps: 40, stepDelay: 16 }
1089
+ )
1090
+ await page.waitForTimeout(600)
1091
+
1092
+ // should have scrolled to 0
1093
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
1094
+ 'ScrollView Y: 0'
1095
+ )
1096
+ })
1097
+
1098
+ test('Case 7: drag UP from position 1 - scroll locked during drag, final position 0', async ({
1099
+ page,
1100
+ }) => {
1101
+ // open sheet
1102
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
1103
+ await page.waitForTimeout(600)
1104
+
1105
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
1106
+ await expect(frame).toBeVisible({ timeout: 5000 })
1107
+
1108
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
1109
+ let handleBox = await handle.boundingBox()
1110
+ expect(handleBox).toBeTruthy()
1111
+
1112
+ // first drag down to position 1 (use mouse for handle)
1113
+ await dragSheet(
1114
+ page,
1115
+ handleBox!.x + handleBox!.width / 2,
1116
+ handleBox!.y + handleBox!.height / 2,
1117
+ 200,
1118
+ { steps: 25, stepDelay: 16, mode: 'mouse' }
1119
+ )
1120
+ await page.waitForTimeout(600)
1121
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1122
+ 'Sheet position: 1'
1123
+ )
1124
+
1125
+ // verify scroll is at 0
1126
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
1127
+ 'ScrollView Y: 0'
1128
+ )
1129
+
1130
+ await page.waitForTimeout(400)
1131
+
1132
+ // drag UP via scrollview area - sheet moves to position 0
1133
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
1134
+ const scrollviewBox = await scrollview.boundingBox()
1135
+ expect(scrollviewBox).toBeTruthy()
1136
+
1137
+ await dragSheet(
1138
+ page,
1139
+ scrollviewBox!.x + scrollviewBox!.width / 2,
1140
+ scrollviewBox!.y + scrollviewBox!.height / 2,
1141
+ -250,
1142
+ { steps: 30, stepDelay: 16 }
1143
+ )
1144
+ await page.waitForTimeout(600)
1145
+
1146
+ // sheet should be at position 0
1147
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1148
+ 'Sheet position: 0'
1149
+ )
1150
+ // scroll should still be 0 (locked during drag)
1151
+ const scrollY = await page.getByTestId('sheet-scrollable-drag-scroll-y').textContent()
1152
+ const scrollVal = parseInt(scrollY?.replace('ScrollView Y: ', '') || '0', 10)
1153
+ expect(scrollVal).toBeLessThanOrEqual(5)
1154
+ })
1155
+
1156
+ test('Case 8: rubber band at top - dragging up keeps sheet at top', async ({
1157
+ page,
1158
+ }) => {
1159
+ // open sheet
1160
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
1161
+ await page.waitForTimeout(600)
1162
+
1163
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
1164
+ await expect(frame).toBeVisible({ timeout: 5000 })
1165
+
1166
+ // verify at position 0 (top snap)
1167
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1168
+ 'Sheet position: 0'
1169
+ )
1170
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
1171
+ 'ScrollView Y: 0'
1172
+ )
1173
+
1174
+ // drag UP on handle - should NOT scroll content, should show resistance
1175
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
1176
+ const handleBox = await handle.boundingBox()
1177
+ expect(handleBox).toBeTruthy()
1178
+
1179
+ // use mouse mode for handle (PanResponder-based element)
1180
+ await dragSheet(
1181
+ page,
1182
+ handleBox!.x + handleBox!.width / 2,
1183
+ handleBox!.y + handleBox!.height / 2,
1184
+ -150,
1185
+ { steps: 20, stepDelay: 16, mode: 'mouse' }
1186
+ )
1187
+ await page.waitForTimeout(600)
1188
+
1189
+ // sheet should still be at position 0 after rubber band release
1190
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1191
+ 'Sheet position: 0'
1192
+ )
1193
+ // frame should still be visible (sheet didn't disappear)
1194
+ await expect(frame).toBeVisible()
1195
+ // scroll should NOT have happened during drag
1196
+ const maxScrollY = await page
1197
+ .getByTestId('sheet-scrollable-drag-max-scroll-y')
1198
+ .textContent()
1199
+ const maxScrollVal = parseInt(maxScrollY?.replace('Max scroll Y: ', '') || '0', 10)
1200
+ expect(maxScrollVal).toBeLessThanOrEqual(5)
1201
+ })
1202
+
1203
+ test('Case 9: HANDOFF UP - drag UP from position 1, continue into scroll', async ({
1204
+ page,
1205
+ }) => {
1206
+ // open sheet
1207
+ await page.getByTestId('sheet-scrollable-drag-trigger').click()
1208
+ await page.waitForTimeout(600)
1209
+
1210
+ const frame = page.getByTestId('sheet-scrollable-drag-frame')
1211
+ await expect(frame).toBeVisible({ timeout: 5000 })
1212
+
1213
+ const handle = page.getByTestId('sheet-scrollable-drag-handle')
1214
+ let handleBox = await handle.boundingBox()
1215
+ expect(handleBox).toBeTruthy()
1216
+
1217
+ // first drag down to position 1 (use mouse for handle)
1218
+ await dragSheet(
1219
+ page,
1220
+ handleBox!.x + handleBox!.width / 2,
1221
+ handleBox!.y + handleBox!.height / 2,
1222
+ 200,
1223
+ { steps: 25, stepDelay: 16, mode: 'mouse' }
1224
+ )
1225
+ await page.waitForTimeout(600)
1226
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1227
+ 'Sheet position: 1'
1228
+ )
1229
+ await expect(page.getByTestId('sheet-scrollable-drag-scroll-y')).toContainText(
1230
+ 'ScrollView Y: 0'
1231
+ )
1232
+
1233
+ await page.waitForTimeout(400)
1234
+
1235
+ // CRITICAL: one LONG drag UP - should:
1236
+ // 1. Move sheet from position 1 to position 0
1237
+ // 2. Continue into scrolling without lifting finger
1238
+ const scrollview = page.getByTestId('sheet-scrollable-drag-scrollview')
1239
+ const scrollviewBox = await scrollview.boundingBox()
1240
+ expect(scrollviewBox).toBeTruthy()
1241
+
1242
+ await dragSheet(
1243
+ page,
1244
+ scrollviewBox!.x + scrollviewBox!.width / 2,
1245
+ scrollviewBox!.y + scrollviewBox!.height / 2,
1246
+ -400,
1247
+ { steps: 50, stepDelay: 12 } // longer, faster drag for handoff
1248
+ )
1249
+ await page.waitForTimeout(300)
1250
+
1251
+ // sheet should be at position 0
1252
+ await expect(page.getByTestId('sheet-scrollable-drag-position')).toContainText(
1253
+ 'Sheet position: 0'
1254
+ )
1255
+
1256
+ // AND scroll should have happened (handoff successful)
1257
+ // check maxScrollY instead of current scrollY because scroll may bounce back
1258
+ const maxScrollY = await page
1259
+ .getByTestId('sheet-scrollable-drag-max-scroll-y')
1260
+ .textContent()
1261
+ const maxScrollVal = parseInt(maxScrollY?.replace('Max scroll Y: ', '') || '0', 10)
1262
+ expect(maxScrollVal).toBeGreaterThan(30) // handoff occurred
1263
+ })
1264
+ })