@marimo-team/islands 0.25.3-dev6 → 0.25.3-dev61

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 (236) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-Ca-Sa492.js → ErrorBoundary-BEfUX0zz.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C0_lCzql.js} +1139 -1140
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-NGRjhbtA.js → code-visibility-CA0UOEgk.js} +6 -6
  19. package/dist/{common-B7_ZSg0P.js → common-BgoPSdwN.js} +4960 -4835
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-B_cmrkBm.js → data-grid-overlay-editor-CRjm80Ab.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-B3eUiVJB.js} +177 -169
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-CTF2Nj9s.js → feedback-button-CZA7i0Ir.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-BQHF9puC.js → glide-data-editor-Bd52MLtf.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-Cj8iqMEp.js → globals-BvANmUu6.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-6xcoTKhC.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +3177 -2980
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-DBcnm0SY.js → number-overlay-editor-C7ffluCS.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-BJRJFobf.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-DTBSuGkC.js → reveal-component-Dfuz8zQ_.js} +19 -19
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/host-imports.test.ts +1 -0
  96. package/src/components/app-config/ai-config.tsx +6 -2
  97. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  98. package/src/components/chat/chat-panel.tsx +3 -7
  99. package/src/components/chat/chat-utils.ts +4 -8
  100. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  101. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  102. package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
  103. package/src/components/data-table/charts/charts.tsx +14 -19
  104. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  105. package/src/components/data-table/columns.tsx +2 -1
  106. package/src/components/data-table/context-menu.tsx +36 -20
  107. package/src/components/data-table/date-popover.tsx +6 -4
  108. package/src/components/data-table/export-actions.tsx +267 -163
  109. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  110. package/src/components/data-table/schemas.ts +14 -36
  111. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  112. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
  113. package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
  114. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  115. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  116. package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
  117. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  118. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  119. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  120. package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
  121. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
  122. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
  124. package/src/components/editor/connections/components.tsx +1 -1
  125. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  126. package/src/components/editor/controls/command-palette.tsx +22 -10
  127. package/src/components/editor/dynamic-favicon.tsx +21 -40
  128. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
  129. package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
  130. package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
  131. package/src/components/editor/file-tree/file-explorer.tsx +10 -5
  132. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  133. package/src/components/editor/file-tree/use-file-search.ts +35 -21
  134. package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
  135. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  136. package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
  137. package/src/components/icons/large-spinner.tsx +3 -14
  138. package/src/components/pages/home-page.tsx +4 -4
  139. package/src/components/slides/slide-cell-view.tsx +1 -1
  140. package/src/components/tracing/tracing.tsx +2 -2
  141. package/src/core/attachments/state.ts +5 -8
  142. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/pending-delete-service.ts +2 -6
  145. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  146. package/src/core/datasets/data-source-discovery.ts +4 -6
  147. package/src/core/edit-app.tsx +2 -2
  148. package/src/core/host/AGENTS.md +11 -2
  149. package/src/core/host/__tests__/attachments.test.ts +56 -0
  150. package/src/core/host/__tests__/document.test.ts +126 -0
  151. package/src/core/host/__tests__/events.test.ts +49 -0
  152. package/src/core/host/__tests__/fixtures.ts +41 -0
  153. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  154. package/src/core/host/connection.ts +81 -0
  155. package/src/core/host/events.ts +68 -0
  156. package/src/core/host/notebook-client.ts +316 -0
  157. package/src/core/host/resources/attachments.ts +19 -0
  158. package/src/core/host/resources/document.ts +101 -0
  159. package/src/core/hotkeys/actions.ts +2 -4
  160. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  161. package/src/core/islands/bootstrap.ts +0 -1
  162. package/src/core/network/connection.ts +4 -2
  163. package/src/core/network/useConnectionNotice.ts +8 -2
  164. package/src/core/run-app.tsx +2 -2
  165. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  166. package/src/core/runtime/runtime.ts +5 -5
  167. package/src/core/static/files.ts +2 -0
  168. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  169. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  170. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  171. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  172. package/src/core/websocket/connection-state.ts +70 -0
  173. package/src/core/websocket/state.ts +7 -0
  174. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  175. package/src/core/websocket/transports/create.ts +31 -0
  176. package/src/core/websocket/transports/sse.ts +14 -2
  177. package/src/core/websocket/transports/transport.ts +12 -23
  178. package/src/core/websocket/types.ts +9 -39
  179. package/src/core/websocket/useNotebookClient.ts +78 -0
  180. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  181. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  182. package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
  183. package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
  184. package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
  185. package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
  186. package/src/hooks/__tests__/useScript.test.tsx +77 -0
  187. package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
  188. package/src/hooks/useAsyncData.ts +3 -3
  189. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  190. package/src/hooks/useDebounce.ts +5 -8
  191. package/src/hooks/useDeepCompareMemoize.ts +1 -1
  192. package/src/hooks/useDelayElapsed.ts +14 -10
  193. package/src/hooks/useElapsedTime.ts +3 -3
  194. package/src/hooks/useInputHistory.ts +4 -7
  195. package/src/hooks/useInternalStateWithSync.ts +6 -6
  196. package/src/hooks/useResizeObserver.ts +4 -7
  197. package/src/hooks/useScript.ts +67 -70
  198. package/src/hooks/useStateForKey.ts +93 -0
  199. package/src/plugins/core/registerReactComponent.tsx +1 -1
  200. package/src/plugins/impl/DataTablePlugin.tsx +14 -15
  201. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  202. package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
  203. package/src/plugins/impl/FormPlugin.tsx +1 -1
  204. package/src/plugins/impl/SliderPlugin.tsx +6 -8
  205. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +201 -1
  206. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
  207. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
  208. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  209. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  210. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  211. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  212. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
  213. package/src/plugins/impl/chat/chat-ui.tsx +10 -9
  214. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  215. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  216. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  217. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  218. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  219. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  220. package/src/plugins/impl/panel/utils.ts +17 -0
  221. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  222. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  223. package/src/stories/editor.stories.tsx +24 -17
  224. package/src/utils/__tests__/arrays.test.ts +35 -0
  225. package/src/utils/__tests__/numbers.test.ts +48 -1
  226. package/src/utils/__tests__/objects.test.ts +21 -0
  227. package/src/utils/arrays.ts +10 -0
  228. package/src/utils/numbers.ts +28 -17
  229. package/src/utils/objects.ts +3 -7
  230. package/src/utils/once.ts +19 -19
  231. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  232. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  233. package/dist/precisionRound-DMIVQrt2.js +0 -97
  234. package/src/core/host/__tests__/protocol.test.ts +0 -214
  235. package/src/core/host/protocol.ts +0 -1943
  236. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -0,0 +1,148 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { act, render, renderHook, waitFor } from "@testing-library/react";
3
+ import { Activity } from "react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { Deferred } from "@/utils/Deferred";
6
+ import { useAsyncData } from "../useAsyncData";
7
+
8
+ function AsyncDataStatus({ fetch }: { fetch: () => Promise<string> }) {
9
+ const result = useAsyncData(fetch, []);
10
+ return (
11
+ <output
12
+ data-status={result.status}
13
+ data-fetching={result.isFetching}
14
+ data-error={result.error?.message ?? ""}
15
+ >
16
+ {result.data}
17
+ </output>
18
+ );
19
+ }
20
+
21
+ describe("useAsyncData", () => {
22
+ it.each(["success", "error"] as const)(
23
+ "restarts fetching when Activity reopens after %s",
24
+ async (initialStatus) => {
25
+ const initial = new Deferred<string>();
26
+ const restarted = new Deferred<string>();
27
+ const fetch = vi
28
+ .fn(() => restarted.promise)
29
+ .mockImplementationOnce(() => initial.promise);
30
+ const view = render(
31
+ <Activity mode="visible">
32
+ <AsyncDataStatus fetch={fetch} />
33
+ </Activity>,
34
+ );
35
+ await act(async () => {
36
+ if (initialStatus === "success") {
37
+ initial.resolve("first");
38
+ } else {
39
+ initial.reject(new Error("offline"));
40
+ }
41
+ });
42
+ expect(view.getByRole("status").dataset.status).toBe(initialStatus);
43
+
44
+ view.rerender(
45
+ <Activity mode="hidden">
46
+ <AsyncDataStatus fetch={fetch} />
47
+ </Activity>,
48
+ );
49
+ view.rerender(
50
+ <Activity mode="visible">
51
+ <AsyncDataStatus fetch={fetch} />
52
+ </Activity>,
53
+ );
54
+
55
+ expect(fetch).toHaveBeenCalledTimes(2);
56
+ expect(view.getByRole("status").dataset).toMatchObject({
57
+ status: initialStatus === "success" ? "loading" : "pending",
58
+ error: "",
59
+ fetching: "true",
60
+ });
61
+ expect(view.getByRole("status").textContent).toBe(
62
+ initialStatus === "success" ? "first" : "",
63
+ );
64
+ await act(async () => restarted.resolve("latest"));
65
+ expect(view.getByRole("status").dataset).toMatchObject({
66
+ status: "success",
67
+ fetching: "false",
68
+ });
69
+ expect(view.getByRole("status").textContent).toBe("latest");
70
+ },
71
+ );
72
+
73
+ it("ignores a request canceled when Activity hides", async () => {
74
+ const canceled = new Deferred<string>();
75
+ const restarted = new Deferred<string>();
76
+ const fetch = vi
77
+ .fn(() => restarted.promise)
78
+ .mockImplementationOnce(() => canceled.promise);
79
+ const view = render(
80
+ <Activity mode="visible">
81
+ <AsyncDataStatus fetch={fetch} />
82
+ </Activity>,
83
+ );
84
+ view.rerender(
85
+ <Activity mode="hidden">
86
+ <AsyncDataStatus fetch={fetch} />
87
+ </Activity>,
88
+ );
89
+ view.rerender(
90
+ <Activity mode="visible">
91
+ <AsyncDataStatus fetch={fetch} />
92
+ </Activity>,
93
+ );
94
+ expect(fetch).toHaveBeenCalledTimes(2);
95
+ await act(async () => canceled.resolve("obsolete"));
96
+ expect(view.getByRole("status").dataset.status).toBe("pending");
97
+ expect(view.getByRole("status").textContent).toBe("");
98
+ await act(async () => restarted.resolve("latest"));
99
+ expect(view.getByRole("status").dataset.status).toBe("success");
100
+ expect(view.getByRole("status").textContent).toBe("latest");
101
+ });
102
+
103
+ it("preserves data while refetching and ignores obsolete requests", async () => {
104
+ const requests = [
105
+ new Deferred<string>(),
106
+ new Deferred<string>(),
107
+ new Deferred<string>(),
108
+ ];
109
+ const fetch = vi
110
+ .fn(() => requests[0].promise)
111
+ .mockImplementationOnce(() => requests[0].promise)
112
+ .mockImplementationOnce(() => requests[1].promise)
113
+ .mockImplementationOnce(() => requests[2].promise);
114
+ const { result, rerender } = renderHook(
115
+ ({ key }) => useAsyncData(fetch, [key]),
116
+ { initialProps: { key: 0 } },
117
+ );
118
+ expect(result.current.status).toBe("pending");
119
+ await act(async () => requests[0].resolve("first"));
120
+ expect(result.current.data).toBe("first");
121
+ rerender({ key: 1 });
122
+ expect(result.current).toMatchObject({ status: "loading", data: "first" });
123
+ rerender({ key: 2 });
124
+ await act(async () => requests[1].resolve("obsolete"));
125
+ expect(result.current).toMatchObject({ status: "loading", data: "first" });
126
+ await act(async () => requests[2].resolve("latest"));
127
+ expect(result.current).toMatchObject({ status: "success", data: "latest" });
128
+ });
129
+
130
+ it("retries after errors and allows manual updates during a request", async () => {
131
+ const request = new Deferred<string>();
132
+ const fetch = vi
133
+ .fn<() => Promise<string>>()
134
+ .mockRejectedValueOnce(new Error("offline"))
135
+ .mockImplementation(() => request.promise);
136
+ const { result } = renderHook(() => useAsyncData(fetch, []));
137
+ await waitFor(() => expect(result.current.status).toBe("error"));
138
+ act(() => result.current.refetch());
139
+ expect(result.current.status).toBe("pending");
140
+ act(() => result.current.setData("manual"));
141
+ expect(result.current).toMatchObject({ status: "success", data: "manual" });
142
+ await act(async () => request.resolve("response"));
143
+ expect(result.current).toMatchObject({
144
+ status: "success",
145
+ data: "response",
146
+ });
147
+ });
148
+ });
@@ -121,6 +121,57 @@ describe("useDebounceControlledState", () => {
121
121
 
122
122
  expect(onChange).not.toHaveBeenCalled();
123
123
  });
124
+
125
+ it("replaces an uncommitted draft when the external value changes", () => {
126
+ const onChange = vi.fn();
127
+ const { result, rerender } = renderHook(
128
+ ({ value }) =>
129
+ useDebounceControlledState({
130
+ initialValue: value,
131
+ onChange,
132
+ delay: 1000,
133
+ }),
134
+ { initialProps: { value: "initial" } },
135
+ );
136
+
137
+ act(() => result.current.onChange("draft"));
138
+ rerender({ value: "external" });
139
+ expect(result.current.value).toBe("external");
140
+ act(() => vi.runAllTimers());
141
+ expect(result.current.debouncedValue).toBe("external");
142
+ expect(onChange).not.toHaveBeenCalled();
143
+ });
144
+
145
+ it("handles NaN external values without repeatedly resetting the draft", () => {
146
+ const onChange = vi.fn();
147
+ const { result, rerender } = renderHook(
148
+ ({ value }) =>
149
+ useDebounceControlledState({ initialValue: value, onChange }),
150
+ { initialProps: { value: Number.NaN } },
151
+ );
152
+
153
+ expect(result.current.value).toBeNaN();
154
+ act(() => result.current.onChange(1));
155
+ rerender({ value: Number.NaN });
156
+ expect(result.current.value).toBe(1);
157
+ rerender({ value: 2 });
158
+ expect(result.current.value).toBe(2);
159
+ rerender({ value: Number.NaN });
160
+ expect(result.current.value).toBeNaN();
161
+ });
162
+
163
+ it("resets the draft when the external value changes from zero to negative zero", () => {
164
+ const onChange = vi.fn();
165
+ const { result, rerender } = renderHook(
166
+ ({ value }) =>
167
+ useDebounceControlledState({ initialValue: value, onChange }),
168
+ { initialProps: { value: 0 } },
169
+ );
170
+
171
+ act(() => result.current.onChange(1));
172
+ rerender({ value: -0 });
173
+ expect(result.current.value).toBe(-0);
174
+ });
124
175
  });
125
176
 
126
177
  describe("useDebouncedCallback", () => {
@@ -0,0 +1,39 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { renderHook } from "@testing-library/react";
3
+ import { describe, expect, it } from "vitest";
4
+ import { useDeepCompareMemoize } from "../useDeepCompareMemoize";
5
+
6
+ describe("useDeepCompareMemoize", () => {
7
+ it("keeps equal values stable and updates changed values immediately", () => {
8
+ const initial = { nested: [1, 2] };
9
+ const { result, rerender } = renderHook(
10
+ ({ value }) => useDeepCompareMemoize(value),
11
+ {
12
+ initialProps: { value: initial },
13
+ },
14
+ );
15
+ rerender({ value: { nested: [1, 2] } });
16
+ expect(result.current).toBe(initial);
17
+ const changed = { nested: [1, 3] };
18
+ rerender({ value: changed });
19
+ expect(result.current).toBe(changed);
20
+ rerender({ value: { nested: [1, 3] } });
21
+ expect(result.current).toBe(changed);
22
+ });
23
+
24
+ it("handles inline callbacks without causing additional renders", () => {
25
+ let renders = 0;
26
+ const { result, rerender } = renderHook(
27
+ ({ value }) => {
28
+ renders++;
29
+ return useDeepCompareMemoize({ callback: () => value });
30
+ },
31
+ { initialProps: { value: "initial" } },
32
+ );
33
+
34
+ const initialRenders = renders;
35
+ rerender({ value: "changed" });
36
+ expect(result.current.callback()).toBe("changed");
37
+ expect(renders).toBe(initialRenders + 1);
38
+ });
39
+ });
@@ -52,4 +52,32 @@ describe("useDelayElapsed", () => {
52
52
  // The pending timeout must be cleared, not left dangling.
53
53
  expect(vi.getTimerCount()).toBe(0);
54
54
  });
55
+
56
+ it("handles a NaN delay without resetting on every render", () => {
57
+ const { result, rerender } = renderHook(() => useDelayElapsed(Number.NaN));
58
+ expect(result.current).toBe(false);
59
+
60
+ act(() => vi.runAllTimers());
61
+ expect(result.current).toBe(true);
62
+ rerender();
63
+ expect(result.current).toBe(true);
64
+ expect(vi.getTimerCount()).toBe(0);
65
+ });
66
+
67
+ it("restarts a completed delay after the gate is temporarily disabled", () => {
68
+ const { result, rerender } = renderHook(
69
+ ({ delay }) => useDelayElapsed(delay),
70
+ { initialProps: { delay: 1000 } },
71
+ );
72
+ act(() => vi.advanceTimersByTime(1000));
73
+ expect(result.current).toBe(true);
74
+ rerender({ delay: 0 });
75
+ expect(result.current).toBe(true);
76
+ rerender({ delay: 1000 });
77
+ expect(result.current).toBe(false);
78
+ act(() => vi.advanceTimersByTime(999));
79
+ expect(result.current).toBe(false);
80
+ act(() => vi.advanceTimersByTime(1));
81
+ expect(result.current).toBe(true);
82
+ });
55
83
  });
@@ -32,6 +32,26 @@ describe("useInternalStateWithSync", () => {
32
32
  expect(result.current[0]).toBe("new value");
33
33
  });
34
34
 
35
+ it("preserves function values without calling them and resets when they change", () => {
36
+ const initial = vi.fn(() => "initial");
37
+ const draft = vi.fn(() => "draft");
38
+ const changed = vi.fn(() => "changed");
39
+ const { result, rerender } = renderHook(
40
+ ({ value }) => useInternalStateWithSync(value),
41
+ { initialProps: { value: initial } },
42
+ );
43
+
44
+ expect(result.current[0]).toBe(initial);
45
+ act(() => result.current[1](() => draft));
46
+ rerender({ value: initial });
47
+ expect(result.current[0]).toBe(draft);
48
+ rerender({ value: changed });
49
+ expect(result.current[0]).toBe(changed);
50
+ expect(initial).not.toHaveBeenCalled();
51
+ expect(draft).not.toHaveBeenCalled();
52
+ expect(changed).not.toHaveBeenCalled();
53
+ });
54
+
35
55
  it("should not update internal state if initial value remains the same", () => {
36
56
  const { result, rerender } = renderHook(
37
57
  ({ value }) => useInternalStateWithSync(value),
@@ -0,0 +1,77 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { act, renderHook } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { useScript } from "../useScript";
5
+
6
+ afterEach(() => {
7
+ document.querySelectorAll("script").forEach((script) => script.remove());
8
+ });
9
+
10
+ describe("useScript", () => {
11
+ it("observes loading and completion of a new script", async () => {
12
+ const { result } = renderHook(() => useScript("/first.js"));
13
+ expect(result.current).toBe("loading");
14
+ await act(async () => {
15
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
16
+ });
17
+ expect(result.current).toBe("ready");
18
+ });
19
+
20
+ it("uses the new source's status when the source changes", async () => {
21
+ const { result, rerender } = renderHook(({ src }) => useScript(src), {
22
+ initialProps: { src: "/first.js" },
23
+ });
24
+ await act(async () => {
25
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
26
+ });
27
+ expect(result.current).toBe("ready");
28
+ rerender({ src: "/second.js" });
29
+ expect(result.current).toBe("loading");
30
+ await act(async () => {
31
+ document
32
+ .querySelector('script[src="/second.js"]')
33
+ ?.dispatchEvent(new Event("error"));
34
+ });
35
+ expect(result.current).toBe("error");
36
+ });
37
+
38
+ it("shares an existing script and only removes scripts it owns", async () => {
39
+ const owner = renderHook(() =>
40
+ useScript("/first.js", { removeOnUnmount: true }),
41
+ );
42
+ const subscriber = renderHook(() =>
43
+ useScript("/first.js", { removeOnUnmount: true }),
44
+ );
45
+ expect(document.querySelectorAll("script")).toHaveLength(1);
46
+ await act(async () => {
47
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
48
+ });
49
+ expect(subscriber.result.current).toBe("ready");
50
+ subscriber.unmount();
51
+ expect(document.querySelectorAll("script")).toHaveLength(1);
52
+ owner.unmount();
53
+ expect(document.querySelectorAll("script")).toHaveLength(0);
54
+ });
55
+
56
+ it("settles for remaining subscribers when the first unmounts mid-load", async () => {
57
+ const first = renderHook(() => useScript("/shared.js"));
58
+ const second = renderHook(() => useScript("/shared.js"));
59
+ first.unmount();
60
+ expect(second.result.current).toBe("loading");
61
+ await act(async () => {
62
+ document.querySelector("script")?.dispatchEvent(new Event("load"));
63
+ });
64
+ expect(second.result.current).toBe("ready");
65
+ // A later subscriber sees the settled status
66
+ expect(renderHook(() => useScript("/shared.js")).result.current).toBe(
67
+ "ready",
68
+ );
69
+ });
70
+
71
+ it("handles sources that are not valid inside a CSS selector", () => {
72
+ const src = '/odd"name].js';
73
+ const { result } = renderHook(() => useScript(src));
74
+ expect(result.current).toBe("loading");
75
+ expect(document.querySelector("script")?.getAttribute("src")).toBe(src);
76
+ });
77
+ });
@@ -0,0 +1,257 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import {
3
+ act,
4
+ fireEvent,
5
+ render,
6
+ renderHook,
7
+ screen,
8
+ } from "@testing-library/react";
9
+ import {
10
+ Profiler,
11
+ startTransition,
12
+ StrictMode,
13
+ Suspense,
14
+ useLayoutEffect,
15
+ useState,
16
+ } from "react";
17
+ import { describe, expect, it, vi } from "vitest";
18
+ import { Deferred } from "@/utils/Deferred";
19
+ import { useStateForKey } from "../useStateForKey";
20
+
21
+ describe("useStateForKey", () => {
22
+ it("preserves edits for the same key and creates fresh state on key changes", () => {
23
+ const initialize = vi.fn(() => new Set<string>());
24
+ const { result, rerender } = renderHook(
25
+ ({ key }) => useStateForKey(key, initialize),
26
+ { initialProps: { key: "first" } },
27
+ );
28
+ const initial = result.current[0];
29
+ act(() => result.current[1](new Set(["expanded"])));
30
+ rerender({ key: "first" });
31
+ expect(result.current[0]).toEqual(new Set(["expanded"]));
32
+ expect(initialize).toHaveBeenCalledOnce();
33
+
34
+ rerender({ key: "second" });
35
+ expect(result.current[0]).toEqual(new Set());
36
+ expect(result.current[0]).not.toBe(initial);
37
+ expect(initialize).toHaveBeenCalledTimes(2);
38
+
39
+ rerender({ key: "first" });
40
+ expect(result.current[0]).toEqual(new Set());
41
+ expect(initialize).toHaveBeenCalledTimes(3);
42
+ });
43
+
44
+ it("preserves queued functional updates and ignores setters from another key", () => {
45
+ const { result, rerender } = renderHook(
46
+ ({ key }) => useStateForKey(key, () => 0),
47
+ { initialProps: { key: "first" } },
48
+ );
49
+ const oldSetter = result.current[1];
50
+ act(() => {
51
+ result.current[1]((value) => value + 1);
52
+ result.current[1]((value) => value + 1);
53
+ });
54
+ expect(result.current[0]).toBe(2);
55
+
56
+ rerender({ key: "second" });
57
+ const staleUpdate = vi.fn(() => 99);
58
+ act(() => oldSetter(staleUpdate));
59
+ act(() => oldSetter(99));
60
+ expect(result.current[0]).toBe(0);
61
+ expect(staleUpdate).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it("uses Object.is for NaN and signed zero keys", () => {
65
+ const { result, rerender } = renderHook(
66
+ ({ key }) => useStateForKey(key, () => "initial"),
67
+ { initialProps: { key: Number.NaN } },
68
+ );
69
+ act(() => result.current[1]("draft"));
70
+ rerender({ key: Number.NaN });
71
+ expect(result.current[0]).toBe("draft");
72
+
73
+ rerender({ key: 0 });
74
+ expect(result.current[0]).toBe("initial");
75
+ act(() => result.current[1]("draft"));
76
+ rerender({ key: -0 });
77
+ expect(result.current[0]).toBe("initial");
78
+ });
79
+
80
+ it("stores function values without invoking them", () => {
81
+ const initial = vi.fn();
82
+ const changed = vi.fn();
83
+ const { result, rerender } = renderHook(
84
+ ({ key, value }) => useStateForKey(key, () => value),
85
+ { initialProps: { key: "first", value: initial } },
86
+ );
87
+ expect(result.current[0]).toBe(initial);
88
+ act(() => result.current[1](() => changed));
89
+ expect(result.current[0]).toBe(changed);
90
+ rerender({ key: "second", value: initial });
91
+ expect(result.current[0]).toBe(initial);
92
+ expect(initial).not.toHaveBeenCalled();
93
+ expect(changed).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it("uses a changed initializer only after the key changes", () => {
97
+ const first = vi.fn(() => "first default");
98
+ const second = vi.fn(() => "second default");
99
+ const { result, rerender } = renderHook(
100
+ ({ key, initialize }) => useStateForKey(key, initialize),
101
+ { initialProps: { key: "A", initialize: first } },
102
+ );
103
+ act(() => result.current[1]("draft"));
104
+ rerender({ key: "A", initialize: second });
105
+ expect(result.current[0]).toBe("draft");
106
+ expect(second).not.toHaveBeenCalled();
107
+ rerender({ key: "B", initialize: second });
108
+ expect(result.current[0]).toBe("second default");
109
+ expect(second).toHaveBeenCalledOnce();
110
+ });
111
+
112
+ it("compares object keys by reference rather than their contents", () => {
113
+ const key = { id: "A" };
114
+ const { result, rerender } = renderHook(
115
+ ({ key }) => useStateForKey(key, () => 0),
116
+ { initialProps: { key } },
117
+ );
118
+ act(() => result.current[1](1));
119
+ rerender({ key });
120
+ expect(result.current[0]).toBe(1);
121
+ rerender({ key: { id: "A" } });
122
+ expect(result.current[0]).toBe(0);
123
+ });
124
+
125
+ it("keeps setters stable for the same key and accepts old setters when that key returns", () => {
126
+ const { result, rerender } = renderHook(
127
+ ({ key }) => useStateForKey(key, () => 0),
128
+ { initialProps: { key: "A" } },
129
+ );
130
+ const setA = result.current[1];
131
+ act(() => setA(1));
132
+ rerender({ key: "A" });
133
+ expect(result.current[1]).toBe(setA);
134
+ rerender({ key: "B" });
135
+ expect(result.current[1]).not.toBe(setA);
136
+ act(() => setA(2));
137
+ expect(result.current[0]).toBe(0);
138
+ rerender({ key: "A" });
139
+ act(() => setA(3));
140
+ expect(result.current[0]).toBe(3);
141
+ });
142
+
143
+ it("retries the owner without rendering or committing stale state to children", () => {
144
+ const onOwnerRender = vi.fn();
145
+ const onChildRender = vi.fn();
146
+ const onCommit = vi.fn();
147
+ const onLayout = vi.fn();
148
+ function Child({ owner, value }: { owner: string; value: string }) {
149
+ onChildRender(owner, value);
150
+ useLayoutEffect(() => onLayout(owner, value), [owner, value]);
151
+ return <span>{value}</span>;
152
+ }
153
+ function Owner({ owner }: { owner: string }) {
154
+ onOwnerRender();
155
+ const [value, setValue] = useStateForKey(owner, () => `${owner} default`);
156
+ return (
157
+ <>
158
+ <button onClick={() => setValue("draft")}>Edit</button>
159
+ <Child owner={owner} value={value} />
160
+ </>
161
+ );
162
+ }
163
+ const content = (owner: string) => (
164
+ <Profiler id="owner" onRender={onCommit}>
165
+ <Owner owner={owner} />
166
+ </Profiler>
167
+ );
168
+ const { rerender } = render(content("A"));
169
+ const button = screen.getByRole("button", { name: "Edit" });
170
+ fireEvent.click(button);
171
+ expect(screen.getByText("draft")).toBeInTheDocument();
172
+ onOwnerRender.mockClear();
173
+ onChildRender.mockClear();
174
+ onCommit.mockClear();
175
+ onLayout.mockClear();
176
+
177
+ rerender(content("B"));
178
+ expect(onOwnerRender.mock.calls.length).toBeGreaterThan(1);
179
+ expect(onChildRender.mock.calls).toEqual([["B", "B default"]]);
180
+ expect(onLayout.mock.calls).toEqual([["B", "B default"]]);
181
+ expect(onCommit).toHaveBeenCalledOnce();
182
+ expect(screen.getByRole("button", { name: "Edit" })).toBe(button);
183
+ });
184
+
185
+ it("keeps state correct when Strict Mode repeats initialization and updates", () => {
186
+ const initialize = vi.fn(() => new Set<string>());
187
+ const { result, rerender } = renderHook(
188
+ ({ key }) => useStateForKey(key, initialize),
189
+ { initialProps: { key: "A" }, wrapper: StrictMode },
190
+ );
191
+ expect(initialize.mock.calls.length).toBeGreaterThan(1);
192
+ act(() => result.current[1]((previous) => new Set([...previous, "draft"])));
193
+ expect(result.current[0]).toEqual(new Set(["draft"]));
194
+ rerender({ key: "B" });
195
+ expect(result.current[0]).toEqual(new Set());
196
+ });
197
+
198
+ it.each(["completes", "is canceled"])(
199
+ "preserves visible edits when a suspended transition %s",
200
+ async (outcome) => {
201
+ const pending = new Deferred<void>();
202
+ let blocked = true;
203
+ const suspended = vi.fn();
204
+ function Pending({ owner }: { owner: string }) {
205
+ if (owner === "B" && blocked) {
206
+ suspended();
207
+ throw pending.promise;
208
+ }
209
+ return null;
210
+ }
211
+ function Owner({ owner }: { owner: string }) {
212
+ const [value, setValue] = useStateForKey(owner, () => 0);
213
+ return (
214
+ <>
215
+ <button onClick={() => setValue((previous) => previous + 1)}>
216
+ {owner}:{value}
217
+ </button>
218
+ <Pending owner={owner} />
219
+ </>
220
+ );
221
+ }
222
+ function Harness() {
223
+ const [owner, setOwner] = useState("A");
224
+ return (
225
+ <>
226
+ <button onClick={() => startTransition(() => setOwner("B"))}>
227
+ Switch owner
228
+ </button>
229
+ <button onClick={() => setOwner("A")}>Keep owner</button>
230
+ <Suspense fallback="Loading">
231
+ <Owner owner={owner} />
232
+ </Suspense>
233
+ </>
234
+ );
235
+ }
236
+ render(<Harness />);
237
+ fireEvent.click(screen.getByRole("button", { name: "A:0" }));
238
+ fireEvent.click(screen.getByRole("button", { name: "Switch owner" }));
239
+ expect(suspended).toHaveBeenCalled();
240
+ fireEvent.click(screen.getByRole("button", { name: "A:1" }));
241
+ expect(screen.getByRole("button", { name: "A:2" })).toBeInTheDocument();
242
+ if (outcome === "is canceled") {
243
+ fireEvent.click(screen.getByRole("button", { name: "Keep owner" }));
244
+ }
245
+ await act(async () => {
246
+ blocked = false;
247
+ pending.resolve();
248
+ await pending.promise;
249
+ });
250
+ expect(
251
+ screen.getByRole("button", {
252
+ name: outcome === "completes" ? "B:0" : "A:2",
253
+ }),
254
+ ).toBeInTheDocument();
255
+ },
256
+ );
257
+ });
@@ -322,7 +322,7 @@ export function useAsyncData<T>(
322
322
  const [nonce, setNonce] = useState(0);
323
323
  const [result, setResult] = useState<
324
324
  PendingResult<T> | LoadingResult<T> | ErrorResult<T> | SuccessResult<T>
325
- >(Result.pending());
325
+ >(() => Result.pending());
326
326
 
327
327
  const asProps =
328
328
  typeof loaderOrProps === "function"
@@ -339,12 +339,12 @@ export function useAsyncData<T>(
339
339
  keepPrevious = true;
340
340
  },
341
341
  };
342
+ // Activity can restart this effect without changing dependencies, so
343
+ // loading state must follow the request lifecycle.
342
344
  setResult((prevResult) => {
343
- // If we have previous data, show reloading state
344
345
  if (prevResult.status === "success" || prevResult.status === "loading") {
345
346
  return Result.loading(prevResult.data);
346
347
  }
347
- // Otherwise, show initial loading state
348
348
  return Result.pending();
349
349
  });
350
350
  fetchStable(context)
@@ -42,11 +42,13 @@ export function useDetectedDataSources(
42
42
  dialects: [...connections.values()].map((connection) =>
43
43
  connection.dialect.toLowerCase(),
44
44
  ),
45
- storageProtocols: storageNamespaces.map((namespace) =>
46
- namespace.protocol.toLowerCase(),
45
+ storageProtocols: new Set(
46
+ storageNamespaces.map((namespace) => namespace.protocol.toLowerCase()),
47
47
  ),
48
- storageBackendTypes: storageNamespaces.map((namespace) =>
49
- namespace.backendType.toLowerCase(),
48
+ storageBackendTypes: new Set(
49
+ storageNamespaces.map((namespace) =>
50
+ namespace.backendType.toLowerCase(),
51
+ ),
50
52
  ),
51
53
  };
52
54