@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,82 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ // regression: popover content should appear near its trigger, not fly in
5
+ // from the top-left corner. when floating-ui resets isPositioned on close
6
+ // but x/y retain stale values, the hide logic must still prevent the
7
+ // content from being visible at the wrong position.
8
+
9
+ // native animation driver has a pre-existing initial position bug
10
+ const driverName = process.env.HANZO_GUI_TEST_ANIMATION_DRIVER || ''
11
+ test.skip(
12
+ driverName === 'native',
13
+ 'native driver has pre-existing initial position issue'
14
+ )
15
+
16
+ test.describe('Popover initial position', () => {
17
+ test('hoverable popover appears near trigger, not at top-left', async ({ page }) => {
18
+ await setupPage(page, {
19
+ name: 'PopoverHoverableScopedCase',
20
+ type: 'useCase',
21
+ })
22
+
23
+ const content = page.locator('#nav-content')
24
+ const trigger = page.locator('#nav-trigger-about')
25
+ const triggerBox = await trigger.boundingBox()
26
+ expect(triggerBox).toBeTruthy()
27
+
28
+ // hover trigger to open
29
+ await trigger.hover()
30
+ await page.waitForTimeout(500)
31
+ await expect(content).toBeVisible()
32
+
33
+ const contentBox = await content.boundingBox()
34
+ expect(contentBox).toBeTruthy()
35
+
36
+ // content should be near the trigger, not at the page origin
37
+ // popover is placed below trigger, so content.y should be close to trigger bottom
38
+ const triggerBottom = triggerBox!.y + triggerBox!.height
39
+ expect(contentBox!.y).toBeGreaterThan(triggerBottom - 20)
40
+ expect(contentBox!.y).toBeLessThan(triggerBottom + 100)
41
+
42
+ // content x should be in the general area of the trigger, not at 0
43
+ expect(contentBox!.x).toBeGreaterThan(triggerBox!.x - contentBox!.width)
44
+ })
45
+
46
+ test('popover does not briefly appear at origin on reopen', async ({ page }) => {
47
+ await setupPage(page, {
48
+ name: 'PopoverHoverableScopedCase',
49
+ type: 'useCase',
50
+ })
51
+
52
+ const content = page.locator('#nav-content')
53
+ const trigger = page.locator('#nav-trigger-contact')
54
+
55
+ // open
56
+ await trigger.hover()
57
+ await page.waitForTimeout(500)
58
+ await expect(content).toBeVisible()
59
+
60
+ // close fully
61
+ await page.mouse.move(10, 600, { steps: 2 })
62
+ await page.waitForTimeout(800)
63
+ await expect(content).not.toBeVisible()
64
+
65
+ // reopen on same trigger and check position EARLY
66
+ await trigger.hover()
67
+ await page.waitForTimeout(100)
68
+
69
+ const earlyBox = await content.boundingBox()
70
+ if (earlyBox) {
71
+ const triggerBox = await trigger.boundingBox()
72
+ // if content is visible, it must NOT be at the top-left corner
73
+ // (the fly-from-origin bug shows content starting near 0,0)
74
+ const distFromOrigin = Math.sqrt(earlyBox.x ** 2 + earlyBox.y ** 2)
75
+ const triggerDist = Math.sqrt(
76
+ (earlyBox.x - triggerBox!.x) ** 2 + (earlyBox.y - triggerBox!.y) ** 2
77
+ )
78
+ // content should be closer to trigger than to the page origin
79
+ expect(triggerDist).toBeLessThan(distFromOrigin + 50)
80
+ }
81
+ })
82
+ })
@@ -0,0 +1,221 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ /**
5
+ * Regression test for: skipping middleware when closed causes position/arrow
6
+ * glitches on reopen. When middleware is set to [] while closed, computePosition
7
+ * runs without offset/arrow/transformOrigin, producing wrong cached position.
8
+ * On reopen, the animation starts from that wrong position.
9
+ */
10
+ test.describe('Popover middleware skip regression', () => {
11
+ test.beforeEach(async ({ page }, testInfo) => {
12
+ const driver = (testInfo.project?.metadata as any)?.animationDriver
13
+ if (driver !== 'css' && driver !== 'motion') {
14
+ test.skip()
15
+ return
16
+ }
17
+
18
+ await setupPage(page, {
19
+ name: 'PopoverHoverableScopedCase',
20
+ type: 'useCase',
21
+ searchParams: { animationDriver: driver },
22
+ })
23
+ await page.waitForLoadState('networkidle')
24
+ })
25
+
26
+ test('position is stable across close/reopen cycles at same trigger', async ({
27
+ page,
28
+ }) => {
29
+ const about = page.locator('#nav-trigger-about')
30
+ const content = page.locator('#nav-content')
31
+
32
+ // open at "about"
33
+ await about.hover()
34
+ await page.waitForTimeout(500)
35
+ await expect(content).toBeVisible({ timeout: 3000 })
36
+ await page.waitForTimeout(700)
37
+
38
+ // record settled position
39
+ const pos1 = await content.evaluate((el) => {
40
+ const r = el.getBoundingClientRect()
41
+ return { x: r.left, y: r.top }
42
+ })
43
+
44
+ // close
45
+ await page.mouse.move(0, 0)
46
+ await page.waitForTimeout(600)
47
+
48
+ // reopen at same trigger
49
+ await about.hover()
50
+ await page.waitForTimeout(500)
51
+ await expect(content).toBeVisible({ timeout: 3000 })
52
+ await page.waitForTimeout(700)
53
+
54
+ // record settled position again
55
+ const pos2 = await content.evaluate((el) => {
56
+ const r = el.getBoundingClientRect()
57
+ return { x: r.left, y: r.top }
58
+ })
59
+
60
+ // positions should match (same trigger, same content)
61
+ expect(
62
+ Math.abs(pos2.x - pos1.x),
63
+ `X position shifted by ${(pos2.x - pos1.x).toFixed(1)}px after close/reopen`
64
+ ).toBeLessThan(2)
65
+ expect(
66
+ Math.abs(pos2.y - pos1.y),
67
+ `Y position shifted by ${(pos2.y - pos1.y).toFixed(1)}px after close/reopen`
68
+ ).toBeLessThan(2)
69
+ })
70
+
71
+ test('reopen animation starts near final position, not from origin', async ({
72
+ page,
73
+ }) => {
74
+ const about = page.locator('#nav-trigger-about')
75
+ const content = page.locator('#nav-content')
76
+
77
+ // first open to establish position
78
+ await about.hover()
79
+ await page.waitForTimeout(500)
80
+ await expect(content).toBeVisible({ timeout: 3000 })
81
+ await page.waitForTimeout(700)
82
+
83
+ const settledPos = await content.evaluate((el) => {
84
+ const r = el.getBoundingClientRect()
85
+ return { x: r.left, y: r.top }
86
+ })
87
+
88
+ // close
89
+ await page.mouse.move(0, 0)
90
+ await page.waitForTimeout(600)
91
+
92
+ // start tracking positions immediately, then reopen
93
+ await page.evaluate(() => {
94
+ ;(window as any).__reopenPositions = []
95
+ ;(window as any).__trackReopen = true
96
+ const track = () => {
97
+ if (!(window as any).__trackReopen) return
98
+ const el = document.getElementById('nav-content')
99
+ if (el) {
100
+ const style = window.getComputedStyle(el)
101
+ if (parseFloat(style.opacity) > 0.05) {
102
+ const r = el.getBoundingClientRect()
103
+ ;(window as any).__reopenPositions.push({
104
+ x: r.left,
105
+ y: r.top,
106
+ opacity: style.opacity,
107
+ time: Date.now(),
108
+ })
109
+ }
110
+ }
111
+ requestAnimationFrame(track)
112
+ }
113
+ requestAnimationFrame(track)
114
+ })
115
+
116
+ // reopen
117
+ await about.hover()
118
+ await page.waitForTimeout(500)
119
+ await expect(content).toBeVisible({ timeout: 3000 })
120
+ await page.waitForTimeout(700)
121
+
122
+ await page.evaluate(() => {
123
+ ;(window as any).__trackReopen = false
124
+ })
125
+
126
+ const positions: { x: number; y: number; opacity: string; time: number }[] =
127
+ await page.evaluate(() => (window as any).__reopenPositions)
128
+
129
+ if (positions.length === 0) return
130
+
131
+ // the FIRST visible frame should be near the settled position, not at (0,0)
132
+ // or some other wrong location caused by missing middleware
133
+ const firstFrame = positions[0]
134
+ const distFromSettled = Math.sqrt(
135
+ (firstFrame.x - settledPos.x) ** 2 + (firstFrame.y - settledPos.y) ** 2
136
+ )
137
+
138
+ // allow up to 50px for enter animation movement (enterStyle y: -6)
139
+ // but reject positions that are hundreds of pixels away (middleware bug)
140
+ expect(
141
+ distFromSettled,
142
+ `First visible frame at (${firstFrame.x.toFixed(0)}, ${firstFrame.y.toFixed(0)}) ` +
143
+ `is ${distFromSettled.toFixed(0)}px from settled position ` +
144
+ `(${settledPos.x.toFixed(0)}, ${settledPos.y.toFixed(0)})`
145
+ ).toBeLessThan(50)
146
+ })
147
+
148
+ test('switching triggers: no position jump on entry', async ({ page }) => {
149
+ const about = page.locator('#nav-trigger-about')
150
+ const contact = page.locator('#nav-trigger-contact')
151
+ const content = page.locator('#nav-content')
152
+
153
+ // open on about first
154
+ await about.hover()
155
+ await page.waitForTimeout(500)
156
+ await expect(content).toBeVisible({ timeout: 3000 })
157
+ await page.waitForTimeout(700)
158
+
159
+ // close
160
+ await page.mouse.move(0, 0)
161
+ await page.waitForTimeout(600)
162
+
163
+ // track positions during reopen at contact
164
+ await page.evaluate(() => {
165
+ ;(window as any).__switchPositions = []
166
+ ;(window as any).__trackSwitch = true
167
+ const track = () => {
168
+ if (!(window as any).__trackSwitch) return
169
+ const el = document.getElementById('nav-content')
170
+ if (el) {
171
+ const style = window.getComputedStyle(el)
172
+ if (parseFloat(style.opacity) > 0.05) {
173
+ const r = el.getBoundingClientRect()
174
+ ;(window as any).__switchPositions.push({
175
+ x: r.left,
176
+ y: r.top,
177
+ time: Date.now(),
178
+ })
179
+ }
180
+ }
181
+ requestAnimationFrame(track)
182
+ }
183
+ requestAnimationFrame(track)
184
+ })
185
+
186
+ // open on contact
187
+ await contact.hover()
188
+ await page.waitForTimeout(500)
189
+ await expect(content).toBeVisible({ timeout: 3000 })
190
+ await page.waitForTimeout(700)
191
+
192
+ await page.evaluate(() => {
193
+ ;(window as any).__trackSwitch = false
194
+ })
195
+
196
+ const contactPos = await content.evaluate((el) => {
197
+ const r = el.getBoundingClientRect()
198
+ return { x: r.left, y: r.top }
199
+ })
200
+
201
+ const positions: { x: number; y: number; time: number }[] = await page.evaluate(
202
+ () => (window as any).__switchPositions
203
+ )
204
+
205
+ if (positions.length < 3) return
206
+
207
+ // check that no frame during entry is wildly far from final position
208
+ // (would indicate middleware was missing during position computation)
209
+ const wildFrames = positions.filter((p) => {
210
+ const dist = Math.sqrt((p.x - contactPos.x) ** 2 + (p.y - contactPos.y) ** 2)
211
+ return dist > 100 // more than 100px from where it should end up
212
+ })
213
+
214
+ expect(
215
+ wildFrames.length,
216
+ `${wildFrames.length} frames were >100px from final position. ` +
217
+ `First wild frame: (${wildFrames[0]?.x.toFixed(0)}, ${wildFrames[0]?.y.toFixed(0)}), ` +
218
+ `final: (${contactPos.x.toFixed(0)}, ${contactPos.y.toFixed(0)})`
219
+ ).toBe(0)
220
+ })
221
+ })
@@ -0,0 +1,128 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ test('scoped popovers work', async ({ page }) => {
5
+ await setupPage(page, { name: 'PopoverScopedCase', type: 'useCase' })
6
+
7
+ // Wait for page to load
8
+ await page.waitForLoadState('networkidle')
9
+
10
+ async function testPopoverScoped(name: string) {
11
+ const trigger = page.getByTestId(name + '-trigger')
12
+ const content = page.getByTestId(name + '-popover-content')
13
+ const closeButton = page.getByTestId('popover-close')
14
+
15
+ // Check initial state
16
+ await expect(trigger).toBeVisible()
17
+ await expect(content).not.toBeVisible()
18
+
19
+ // Click trigger to open popover
20
+ await trigger.click()
21
+
22
+ // Wait for content to be visible
23
+ await expect(content).toBeVisible({ timeout: 5000 })
24
+
25
+ // Click close button
26
+ await closeButton.click()
27
+
28
+ // Verify popover is closed
29
+ await expect(content).not.toBeVisible()
30
+ }
31
+
32
+ await testPopoverScoped('plain')
33
+ await testPopoverScoped('a')
34
+ await testPopoverScoped('b')
35
+ })
36
+
37
+ test('popover scopes are isolated', async ({ page }) => {
38
+ await setupPage(page, { name: 'PopoverScopedCase', type: 'useCase' })
39
+
40
+ // Wait for page to load
41
+ await page.waitForLoadState('networkidle')
42
+
43
+ const plainTrigger = page.getByTestId('plain-trigger')
44
+ const aTrigger = page.getByTestId('a-trigger')
45
+ const bTrigger = page.getByTestId('b-trigger')
46
+
47
+ const plainContent = page.getByTestId('plain-popover-content')
48
+ const aContent = page.getByTestId('a-popover-content')
49
+ const bContent = page.getByTestId('b-popover-content')
50
+
51
+ // Open popover A
52
+ await aTrigger.click()
53
+ await expect(aContent).toBeVisible({ timeout: 5000 })
54
+
55
+ // Verify other popovers are not visible
56
+ await expect(plainContent).not.toBeVisible()
57
+ await expect(bContent).not.toBeVisible()
58
+
59
+ // Close popover A
60
+ await aContent.getByTestId('popover-close').click()
61
+ await expect(aContent).not.toBeVisible()
62
+
63
+ // Open popover B
64
+ await bTrigger.click()
65
+ await expect(bContent).toBeVisible({ timeout: 5000 })
66
+
67
+ // Verify other popovers are not visible
68
+ await expect(plainContent).not.toBeVisible()
69
+ await expect(aContent).not.toBeVisible()
70
+
71
+ // Close popover B
72
+ await bContent.getByTestId('popover-close').click()
73
+ await expect(bContent).not.toBeVisible()
74
+
75
+ // Open plain popover
76
+ await plainTrigger.click()
77
+ await expect(plainContent).toBeVisible({ timeout: 5000 })
78
+
79
+ // Verify other popovers are not visible
80
+ await expect(aContent).not.toBeVisible()
81
+ await expect(bContent).not.toBeVisible()
82
+
83
+ // Close plain popover
84
+ await plainContent.getByTestId('popover-close').click()
85
+ await expect(plainContent).not.toBeVisible()
86
+ })
87
+
88
+ test('scoped popovers adapt to sheets', async ({ page }) => {
89
+ await setupPage(page, {
90
+ name: 'PopoverScopedCase',
91
+ type: 'useCase',
92
+ adapt: true,
93
+ })
94
+
95
+ // Wait for page to load
96
+ await page.waitForLoadState('networkidle')
97
+
98
+ async function testPopoverAdapted(name: string) {
99
+ const trigger = page.getByTestId(`${name}-trigger`)
100
+ const popoverContent = page.getByTestId(`${name}-popover-content`)
101
+
102
+ // Click trigger to open sheet
103
+ await trigger.click()
104
+
105
+ const sheetContents = page.getByTestId(`${name}-sheet-contents`).first()
106
+
107
+ // Wait for sheet to be visible and open
108
+ await expect(sheetContents).toBeVisible({ timeout: 5000 })
109
+ await expect(sheetContents).toHaveAttribute('data-state', 'open', { timeout: 5000 })
110
+
111
+ // Check that popover content is inside sheet
112
+ await expect(sheetContents.locator(popoverContent)).toBeVisible()
113
+
114
+ const closeButton = sheetContents.getByTestId('popover-close')
115
+ // Click close button
116
+ await closeButton.click()
117
+
118
+ // Wait for sheet to close by checking data-state attribute
119
+ await expect(sheetContents).toHaveAttribute('data-state', 'closed', { timeout: 5000 })
120
+
121
+ // Verify sheet is visually off-screen
122
+ await expect(sheetContents).not.toBeInViewport({ ratio: 0.5 })
123
+ }
124
+
125
+ await testPopoverAdapted('plain')
126
+ await testPopoverAdapted('a')
127
+ await testPopoverAdapted('b')
128
+ })
@@ -0,0 +1,184 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ /**
5
+ * Tests for position glitches when opening/closing scoped popovers
6
+ * across multiple targets with animatePosition enabled.
7
+ *
8
+ * Tracks frame-by-frame positions to detect:
9
+ * - Jumps to (0,0) or far-off positions during enter/exit
10
+ * - Arrow misalignment
11
+ * - Position instability after multiple open/close cycles
12
+ */
13
+ test.describe('Popover scoped position glitch detection', () => {
14
+ test.beforeEach(async ({ page }, testInfo) => {
15
+ const driver = (testInfo.project?.metadata as any)?.animationDriver
16
+ if (driver !== 'css' && driver !== 'motion') {
17
+ test.skip()
18
+ return
19
+ }
20
+
21
+ await setupPage(page, {
22
+ name: 'PopoverHoverableScopedCase',
23
+ type: 'useCase',
24
+ searchParams: { animationDriver: driver },
25
+ })
26
+ await page.waitForLoadState('networkidle')
27
+ })
28
+
29
+ test('no far-off positions during open/close across targets', async ({ page }) => {
30
+ const about = page.locator('#nav-trigger-about')
31
+ const blog = page.locator('#nav-trigger-blog')
32
+ const contact = page.locator('#nav-trigger-contact')
33
+ const content = page.locator('#nav-content')
34
+
35
+ // get trigger positions for reference
36
+ const triggerBounds = await page.evaluate(() => {
37
+ const triggers = ['about', 'blog', 'contact'].map((id) => {
38
+ const el = document.getElementById(`nav-trigger-${id}`)!
39
+ const r = el.getBoundingClientRect()
40
+ return { id, x: r.left, y: r.top, right: r.right, bottom: r.bottom }
41
+ })
42
+ return triggers
43
+ })
44
+
45
+ // reasonable bounds: content should be within viewport, near the triggers
46
+ const viewportWidth = await page.evaluate(() => window.innerWidth)
47
+ const viewportHeight = await page.evaluate(() => window.innerHeight)
48
+ const triggerMinX = Math.min(...triggerBounds.map((t) => t.x))
49
+ const triggerMaxRight = Math.max(...triggerBounds.map((t) => t.right))
50
+ const triggerBottom = Math.max(...triggerBounds.map((t) => t.bottom))
51
+
52
+ // start position tracker
53
+ await page.evaluate(() => {
54
+ ;(window as any).__posLog = []
55
+ ;(window as any).__trackPositions = true
56
+ const track = () => {
57
+ if (!(window as any).__trackPositions) return
58
+ const el = document.getElementById('nav-content')
59
+ if (el) {
60
+ const r = el.getBoundingClientRect()
61
+ const style = window.getComputedStyle(el)
62
+ ;(window as any).__posLog.push({
63
+ x: r.left,
64
+ y: r.top,
65
+ w: r.width,
66
+ h: r.height,
67
+ opacity: style.opacity,
68
+ transform: style.transform,
69
+ time: Date.now(),
70
+ })
71
+ }
72
+ requestAnimationFrame(track)
73
+ }
74
+ requestAnimationFrame(track)
75
+ })
76
+
77
+ // cycle 1: open on about, wait, close
78
+ await about.hover()
79
+ await page.waitForTimeout(500)
80
+ await expect(content).toBeVisible({ timeout: 3000 })
81
+ await page.waitForTimeout(600)
82
+ await page.mouse.move(0, 0)
83
+ await page.waitForTimeout(500)
84
+
85
+ // cycle 2: open on contact
86
+ await contact.hover()
87
+ await page.waitForTimeout(500)
88
+ await expect(content).toBeVisible({ timeout: 3000 })
89
+ await page.waitForTimeout(600)
90
+ await page.mouse.move(0, 0)
91
+ await page.waitForTimeout(500)
92
+
93
+ // cycle 3: open on blog
94
+ await blog.hover()
95
+ await page.waitForTimeout(500)
96
+ await expect(content).toBeVisible({ timeout: 3000 })
97
+ await page.waitForTimeout(600)
98
+
99
+ // cycle 4: switch directly to about (no close in between)
100
+ await about.hover()
101
+ await page.waitForTimeout(700)
102
+
103
+ // cycle 5: switch directly to contact
104
+ await contact.hover()
105
+ await page.waitForTimeout(700)
106
+
107
+ // stop tracking
108
+ await page.evaluate(() => {
109
+ ;(window as any).__trackPositions = false
110
+ })
111
+
112
+ const positions: {
113
+ x: number
114
+ y: number
115
+ w: number
116
+ h: number
117
+ opacity: string
118
+ transform: string
119
+ time: number
120
+ }[] = await page.evaluate(() => (window as any).__posLog)
121
+
122
+ // filter to frames where content is visible (opacity > 0)
123
+ const visibleFrames = positions.filter((p) => parseFloat(p.opacity) > 0.1)
124
+
125
+ // check: no frame should have content at a far-off position
126
+ // "far off" = more than 200px outside the trigger area
127
+ const badFrames = visibleFrames.filter((p) => {
128
+ const tooFarLeft = p.x < triggerMinX - 200
129
+ const tooFarRight = p.x > triggerMaxRight + 200
130
+ const tooFarUp = p.y < 0
131
+ const tooFarDown = p.y > viewportHeight + 100
132
+ const atOrigin = p.x === 0 && p.y === 0
133
+ return tooFarLeft || tooFarRight || tooFarUp || tooFarDown || atOrigin
134
+ })
135
+
136
+ if (badFrames.length > 0) {
137
+ console.log('bad frames:', JSON.stringify(badFrames.slice(0, 5), null, 2))
138
+ console.log(
139
+ 'trigger area:',
140
+ JSON.stringify({ triggerMinX, triggerMaxRight, triggerBottom })
141
+ )
142
+ }
143
+
144
+ expect(
145
+ badFrames.length,
146
+ `Found ${badFrames.length} frames with far-off positions. ` +
147
+ `First bad: x=${badFrames[0]?.x}, y=${badFrames[0]?.y}`
148
+ ).toBe(0)
149
+
150
+ // check: no sudden large jumps between consecutive visible frames
151
+ const jumpThreshold = 150 // px
152
+ const jumps: { from: (typeof visibleFrames)[0]; to: (typeof visibleFrames)[0] }[] = []
153
+ for (let i = 1; i < visibleFrames.length; i++) {
154
+ const dx = Math.abs(visibleFrames[i].x - visibleFrames[i - 1].x)
155
+ const dy = Math.abs(visibleFrames[i].y - visibleFrames[i - 1].y)
156
+ if (dx > jumpThreshold || dy > jumpThreshold) {
157
+ jumps.push({ from: visibleFrames[i - 1], to: visibleFrames[i] })
158
+ }
159
+ }
160
+
161
+ if (jumps.length > 0) {
162
+ console.log(
163
+ 'position jumps:',
164
+ JSON.stringify(
165
+ jumps.slice(0, 3).map((j) => ({
166
+ from: { x: j.from.x.toFixed(1), y: j.from.y.toFixed(1) },
167
+ to: { x: j.to.x.toFixed(1), y: j.to.y.toFixed(1) },
168
+ dx: Math.abs(j.to.x - j.from.x).toFixed(1),
169
+ dy: Math.abs(j.to.y - j.from.y).toFixed(1),
170
+ })),
171
+ null,
172
+ 2
173
+ )
174
+ )
175
+ }
176
+
177
+ // allow some jumps (close→reopen at different target is expected)
178
+ // but there shouldn't be jumps DURING an animation
179
+ expect(
180
+ jumps.length,
181
+ `Found ${jumps.length} large position jumps (>${jumpThreshold}px)`
182
+ ).toBeLessThanOrEqual(4) // at most one jump per close→reopen transition
183
+ })
184
+ })
@@ -0,0 +1,62 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { setupPage } from './test-utils'
3
+
4
+ // helper to extract render count from text like "renders: 1"
5
+ function parseRenderCount(text: string | null): number {
6
+ if (!text) return 0
7
+ const match = text.match(/renders:\s*(\d+)/)
8
+ return match ? Number(match[1]) : 0
9
+ }
10
+
11
+ test.describe('Popover Trigger Render Isolation', () => {
12
+ test.beforeEach(async ({ page }) => {
13
+ await setupPage(page, { name: 'PopoverTriggerIsolationCase', type: 'useCase' })
14
+ })
15
+
16
+ test('only the active trigger re-renders when popover opens', async ({ page }) => {
17
+ await page.waitForLoadState('networkidle')
18
+
19
+ // get initial render counts
20
+ const getRenderCounts = async () => {
21
+ return {
22
+ trigger1: parseRenderCount(
23
+ await page.getByTestId('isolated-trigger-1-render-count').textContent()
24
+ ),
25
+ trigger2: parseRenderCount(
26
+ await page.getByTestId('isolated-trigger-2-render-count').textContent()
27
+ ),
28
+ trigger3: parseRenderCount(
29
+ await page.getByTestId('isolated-trigger-3-render-count').textContent()
30
+ ),
31
+ }
32
+ }
33
+
34
+ const initialCounts = await getRenderCounts()
35
+
36
+ // click trigger1 to open popover
37
+ await page.getByTestId('isolated-trigger-1').click()
38
+
39
+ // wait for popover to open
40
+ const content = page.getByTestId('isolated-popover-content')
41
+ await expect(content).toBeVisible({ timeout: 5000 })
42
+
43
+ // check render counts after opening
44
+ const afterOpenCounts = await getRenderCounts()
45
+
46
+ // trigger1 (active) may re-render to update aria-expanded
47
+ // but trigger2 and trigger3 (inactive) should NOT re-render
48
+ expect(afterOpenCounts.trigger2).toBe(initialCounts.trigger2)
49
+ expect(afterOpenCounts.trigger3).toBe(initialCounts.trigger3)
50
+
51
+ // close the popover
52
+ await page.getByTestId('isolated-close').click()
53
+ await expect(content).not.toBeVisible({ timeout: 5000 })
54
+
55
+ // check render counts after closing
56
+ const afterCloseCounts = await getRenderCounts()
57
+
58
+ // inactive triggers should still not have re-rendered
59
+ expect(afterCloseCounts.trigger2).toBe(initialCounts.trigger2)
60
+ expect(afterCloseCounts.trigger3).toBe(initialCounts.trigger3)
61
+ })
62
+ })