@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.
- package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
- package/dist/{ErrorBoundary-Ca-Sa492.js → ErrorBoundary-BEfUX0zz.js} +8 -8
- package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
- package/dist/any-language-editor-k2B8YI3Y.js +75 -0
- package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
- package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
- package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
- package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
- package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C0_lCzql.js} +1139 -1140
- package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
- package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
- package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
- package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
- package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
- package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
- package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
- package/dist/{code-visibility-NGRjhbtA.js → code-visibility-CA0UOEgk.js} +6 -6
- package/dist/{common-B7_ZSg0P.js → common-BgoPSdwN.js} +4960 -4835
- package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
- package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
- package/dist/{data-grid-overlay-editor-B_cmrkBm.js → data-grid-overlay-editor-CRjm80Ab.js} +5 -5
- package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
- package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
- package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
- package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
- package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
- package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-B3eUiVJB.js} +177 -169
- package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
- package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
- package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
- package/dist/{feedback-button-CTF2Nj9s.js → feedback-button-CZA7i0Ir.js} +29 -29
- package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
- package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
- package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
- package/dist/{glide-data-editor-BQHF9puC.js → glide-data-editor-Bd52MLtf.js} +370 -368
- package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
- package/dist/{globals-Cj8iqMEp.js → globals-BvANmUu6.js} +3 -3
- package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
- package/dist/{html-to-image-B8s5SgGm.js → html-to-image-6xcoTKhC.js} +29 -29
- package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
- package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
- package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
- package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
- package/dist/main.js +3177 -2980
- package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
- package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
- package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
- package/dist/{number-overlay-editor-DBcnm0SY.js → number-overlay-editor-C7ffluCS.js} +3 -3
- package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
- package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
- package/dist/precisionRound-DhAzeRcm.js +49 -0
- package/dist/{process-output-CKRXyszb.js → process-output-BJRJFobf.js} +4 -4
- package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
- package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
- package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
- package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
- package/dist/{reveal-component-DTBSuGkC.js → reveal-component-Dfuz8zQ_.js} +19 -19
- package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
- package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
- package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
- package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
- package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
- package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
- package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
- package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
- package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
- package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
- package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
- package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
- package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
- package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
- package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
- package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
- package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
- package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
- package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
- package/package.json +4 -4
- package/src/__tests__/host-imports.test.ts +1 -0
- package/src/components/app-config/ai-config.tsx +6 -2
- package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
- package/src/components/chat/chat-panel.tsx +3 -7
- package/src/components/chat/chat-utils.ts +4 -8
- package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
- package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
- package/src/components/data-table/__tests__/export-actions.test.tsx +434 -6
- package/src/components/data-table/charts/charts.tsx +14 -19
- package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
- package/src/components/data-table/columns.tsx +2 -1
- package/src/components/data-table/context-menu.tsx +36 -20
- package/src/components/data-table/date-popover.tsx +6 -4
- package/src/components/data-table/export-actions.tsx +267 -163
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
- package/src/components/data-table/schemas.ts +14 -36
- package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +24 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +18 -15
- package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
- package/src/components/editor/cell/CreateCellButton.tsx +8 -10
- package/src/components/editor/cell/__tests__/useShouldShowInterrupt.test.ts +39 -0
- package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
- package/src/components/editor/cell/code/cell-editor.tsx +1 -1
- package/src/components/editor/cell/code/language-toggle.tsx +8 -7
- package/src/components/editor/cell/useShouldShowInterrupt.ts +2 -12
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +30 -1
- package/src/components/editor/chrome/panels/packages-panel.tsx +4 -7
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +12 -18
- package/src/components/editor/connections/components.tsx +1 -1
- package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
- package/src/components/editor/controls/command-palette.tsx +22 -10
- package/src/components/editor/dynamic-favicon.tsx +21 -40
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +12 -23
- package/src/components/editor/file-tree/__tests__/file-explorer-interactions.test.tsx +6 -0
- package/src/components/editor/file-tree/__tests__/use-file-search.test.tsx +91 -0
- package/src/components/editor/file-tree/file-explorer.tsx +10 -5
- package/src/components/editor/file-tree/file-viewer.tsx +4 -2
- package/src/components/editor/file-tree/use-file-search.ts +35 -21
- package/src/components/editor/output/MarimoTracebackOutput.tsx +1 -1
- package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
- package/src/components/icons/__tests__/large-spinner.test.tsx +39 -0
- package/src/components/icons/large-spinner.tsx +3 -14
- package/src/components/pages/home-page.tsx +4 -4
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/tracing/tracing.tsx +2 -2
- package/src/core/attachments/state.ts +5 -8
- package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
- package/src/core/cells/cells.ts +2 -1
- package/src/core/cells/pending-delete-service.ts +2 -6
- package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
- package/src/core/datasets/data-source-discovery.ts +4 -6
- package/src/core/edit-app.tsx +2 -2
- package/src/core/host/AGENTS.md +11 -2
- package/src/core/host/__tests__/attachments.test.ts +56 -0
- package/src/core/host/__tests__/document.test.ts +126 -0
- package/src/core/host/__tests__/events.test.ts +49 -0
- package/src/core/host/__tests__/fixtures.ts +41 -0
- package/src/core/host/__tests__/notebook-client.test.ts +496 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +68 -0
- package/src/core/host/notebook-client.ts +316 -0
- package/src/core/host/resources/attachments.ts +19 -0
- package/src/core/host/resources/document.ts +101 -0
- package/src/core/hotkeys/actions.ts +2 -4
- package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/network/connection.ts +4 -2
- package/src/core/network/useConnectionNotice.ts +8 -2
- package/src/core/run-app.tsx +2 -2
- package/src/core/runtime/__tests__/runtime.test.ts +5 -12
- package/src/core/runtime/runtime.ts +5 -5
- package/src/core/static/files.ts +2 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
- package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
- package/src/core/websocket/connection-state.ts +70 -0
- package/src/core/websocket/state.ts +7 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
- package/src/core/websocket/transports/create.ts +31 -0
- package/src/core/websocket/transports/sse.ts +14 -2
- package/src/core/websocket/transports/transport.ts +12 -23
- package/src/core/websocket/types.ts +9 -39
- package/src/core/websocket/useNotebookClient.ts +78 -0
- package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
- package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
- package/src/hooks/__tests__/useDebounce.test.tsx +51 -0
- package/src/hooks/__tests__/useDeepCompareMemoize.test.tsx +39 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +28 -0
- package/src/hooks/__tests__/useInternalStateWithSync.test.tsx +20 -0
- package/src/hooks/__tests__/useScript.test.tsx +77 -0
- package/src/hooks/__tests__/useStateForKey.test.tsx +257 -0
- package/src/hooks/useAsyncData.ts +3 -3
- package/src/hooks/useDataSourceDiscovery.ts +6 -4
- package/src/hooks/useDebounce.ts +5 -8
- package/src/hooks/useDeepCompareMemoize.ts +1 -1
- package/src/hooks/useDelayElapsed.ts +14 -10
- package/src/hooks/useElapsedTime.ts +3 -3
- package/src/hooks/useInputHistory.ts +4 -7
- package/src/hooks/useInternalStateWithSync.ts +6 -6
- package/src/hooks/useResizeObserver.ts +4 -7
- package/src/hooks/useScript.ts +67 -70
- package/src/hooks/useStateForKey.ts +93 -0
- package/src/plugins/core/registerReactComponent.tsx +1 -1
- package/src/plugins/impl/DataTablePlugin.tsx +14 -15
- package/src/plugins/impl/DropdownPlugin.tsx +2 -6
- package/src/plugins/impl/FileBrowserPlugin.tsx +4 -16
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/SliderPlugin.tsx +6 -8
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +201 -1
- package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +45 -0
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +24 -3
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
- package/src/plugins/impl/anywidget/runtime.ts +21 -9
- package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
- package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +93 -0
- package/src/plugins/impl/chat/chat-ui.tsx +10 -9
- package/src/plugins/impl/code/any-language-editor.tsx +2 -2
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
- package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
- package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
- package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
- package/src/plugins/impl/panel/utils.ts +17 -0
- package/src/plugins/impl/vega/make-selectable.ts +3 -6
- package/src/stories/__tests__/editor-story.test.tsx +33 -0
- package/src/stories/editor.stories.tsx +24 -17
- package/src/utils/__tests__/arrays.test.ts +35 -0
- package/src/utils/__tests__/numbers.test.ts +48 -1
- package/src/utils/__tests__/objects.test.ts +21 -0
- package/src/utils/arrays.ts +10 -0
- package/src/utils/numbers.ts +28 -17
- package/src/utils/objects.ts +3 -7
- package/src/utils/once.ts +19 -19
- package/dist/any-language-editor-DZ_YTVus.js +0 -76
- package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
- package/dist/precisionRound-DMIVQrt2.js +0 -97
- package/src/core/host/__tests__/protocol.test.ts +0 -214
- package/src/core/host/protocol.ts +0 -1943
- package/src/core/websocket/useWebSocket.tsx +0 -88
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
waitFor,
|
|
13
13
|
} from "@testing-library/react";
|
|
14
14
|
import { Provider } from "jotai";
|
|
15
|
+
import { useState } from "react";
|
|
15
16
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
17
|
import { SetupMocks } from "@/__mocks__/common";
|
|
17
18
|
import type { DownloadAsFunction } from "@/components/data-table/schemas";
|
|
18
19
|
import type { FieldTypesWithExternalType } from "@/components/data-table/types";
|
|
19
20
|
import { store } from "@/core/state/jotai";
|
|
21
|
+
import { Deferred } from "@/utils/Deferred";
|
|
20
22
|
import {
|
|
21
23
|
type GetDataUrl,
|
|
22
24
|
type GetRowIds,
|
|
@@ -136,6 +138,90 @@ describe("LoadingDataTableComponent", () => {
|
|
|
136
138
|
expect(downloadGeoJSON).toHaveBeenCalledTimes(1);
|
|
137
139
|
});
|
|
138
140
|
|
|
141
|
+
it("refreshes export metadata when a dataframe transform changes the data", async () => {
|
|
142
|
+
const host = document.createElement("div");
|
|
143
|
+
const metadataFor = (name: string) => ({
|
|
144
|
+
geometry_columns: [{ name, encoding: "objects", crs: "EPSG:4326" }],
|
|
145
|
+
primary_geometry_column: name,
|
|
146
|
+
default_geometry_column: name,
|
|
147
|
+
formats: {
|
|
148
|
+
parquet: { available: true, reason: null, missing_packages: [] },
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const getExportMetadata = vi
|
|
152
|
+
.fn()
|
|
153
|
+
.mockResolvedValueOnce(metadataFor("before"))
|
|
154
|
+
.mockResolvedValueOnce(metadataFor("after"));
|
|
155
|
+
const view = (data: string) => (
|
|
156
|
+
<Provider store={store}>
|
|
157
|
+
<TooltipProvider>
|
|
158
|
+
<LoadingDataTableComponent
|
|
159
|
+
label={null}
|
|
160
|
+
totalRows={1}
|
|
161
|
+
pagination={false}
|
|
162
|
+
pageSize={10}
|
|
163
|
+
selection={null}
|
|
164
|
+
showDownload={true}
|
|
165
|
+
showFilters={false}
|
|
166
|
+
showColumnSummaries={false}
|
|
167
|
+
showDataTypes={false}
|
|
168
|
+
showPageSizeSelector={false}
|
|
169
|
+
showColumnExplorer={false}
|
|
170
|
+
showRowExplorer={false}
|
|
171
|
+
showChartBuilder={false}
|
|
172
|
+
rowHeaders={[]}
|
|
173
|
+
fieldTypes={undefined}
|
|
174
|
+
totalColumns={1}
|
|
175
|
+
maxColumns="all"
|
|
176
|
+
hasStableRowId={false}
|
|
177
|
+
lazy={false}
|
|
178
|
+
host={host}
|
|
179
|
+
showSearch={false}
|
|
180
|
+
value={[]}
|
|
181
|
+
setValue={vi.fn()}
|
|
182
|
+
data={data}
|
|
183
|
+
search={vi.fn().mockResolvedValue({
|
|
184
|
+
data,
|
|
185
|
+
total_rows: 1,
|
|
186
|
+
cell_styles: null,
|
|
187
|
+
cell_hover_texts: null,
|
|
188
|
+
})}
|
|
189
|
+
download_as={vi.fn()}
|
|
190
|
+
download_geojson={vi.fn()}
|
|
191
|
+
get_export_metadata={getExportMetadata}
|
|
192
|
+
get_column_summaries={vi.fn()}
|
|
193
|
+
get_data_url={vi.fn() as GetDataUrl}
|
|
194
|
+
get_row_ids={vi.fn() as GetRowIds}
|
|
195
|
+
get_size_bytes={vi.fn().mockResolvedValue({ size_bytes: null })}
|
|
196
|
+
/>
|
|
197
|
+
</TooltipProvider>
|
|
198
|
+
</Provider>
|
|
199
|
+
);
|
|
200
|
+
const { rerender } = render(
|
|
201
|
+
view(JSON.stringify([{ before: "POINT (1 2)" }])),
|
|
202
|
+
);
|
|
203
|
+
await waitFor(() =>
|
|
204
|
+
expect(
|
|
205
|
+
screen.getByRole("columnheader", { name: "before" }),
|
|
206
|
+
).toBeInTheDocument(),
|
|
207
|
+
);
|
|
208
|
+
fireEvent.click(screen.getByTestId("export-button"));
|
|
209
|
+
await waitFor(() =>
|
|
210
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
211
|
+
"before",
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
rerender(view(JSON.stringify([{ after: "POINT (3 4)" }])));
|
|
216
|
+
|
|
217
|
+
await waitFor(() =>
|
|
218
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
219
|
+
"after",
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
223
|
+
});
|
|
224
|
+
|
|
139
225
|
it("does not log duplicate keys when a pivot becomes empty", async () => {
|
|
140
226
|
const host = document.createElement("div");
|
|
141
227
|
const consoleError = vi
|
|
@@ -379,7 +465,7 @@ describe("LoadingDataTableComponent", () => {
|
|
|
379
465
|
});
|
|
380
466
|
});
|
|
381
467
|
|
|
382
|
-
describe("static notebook control suppression", () => {
|
|
468
|
+
describe("table pagination and static notebook control suppression", () => {
|
|
383
469
|
const fieldTypes: FieldTypesWithExternalType = [
|
|
384
470
|
["id", ["integer", "integer"]],
|
|
385
471
|
["name", ["string", "string"]],
|
|
@@ -454,6 +540,120 @@ describe("static notebook control suppression", () => {
|
|
|
454
540
|
mockIsStatic.mockReturnValue(false);
|
|
455
541
|
});
|
|
456
542
|
|
|
543
|
+
it("requests only the first page with the new configured page size", async () => {
|
|
544
|
+
const props = makeProps();
|
|
545
|
+
const view = (pageSize: number) => (
|
|
546
|
+
<Wrapper>
|
|
547
|
+
<LoadingDataTableComponent {...props} pageSize={pageSize} />
|
|
548
|
+
</Wrapper>
|
|
549
|
+
);
|
|
550
|
+
const { rerender } = render(view(PAGE_SIZE));
|
|
551
|
+
await waitFor(() =>
|
|
552
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
553
|
+
);
|
|
554
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
555
|
+
await waitFor(() =>
|
|
556
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
557
|
+
expect.objectContaining({ page_number: 1, page_size: PAGE_SIZE }),
|
|
558
|
+
),
|
|
559
|
+
);
|
|
560
|
+
await waitFor(() =>
|
|
561
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
562
|
+
);
|
|
563
|
+
props.search.mockClear();
|
|
564
|
+
|
|
565
|
+
rerender(view(5));
|
|
566
|
+
await waitFor(() => expect(props.search).toHaveBeenCalled());
|
|
567
|
+
expect(
|
|
568
|
+
props.search.mock.calls.every(
|
|
569
|
+
([request]) => request.page_number === 0 && request.page_size === 5,
|
|
570
|
+
),
|
|
571
|
+
).toBe(true);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("preserves a new page size when a pending response changes the row count in the same batch", async () => {
|
|
575
|
+
const props = makeProps();
|
|
576
|
+
const Harness = () => {
|
|
577
|
+
const [pageSize, setPageSize] = useState(PAGE_SIZE);
|
|
578
|
+
return (
|
|
579
|
+
<Wrapper>
|
|
580
|
+
<button type="button" onClick={() => setPageSize(5)}>
|
|
581
|
+
Change configured page size
|
|
582
|
+
</button>
|
|
583
|
+
<LoadingDataTableComponent {...props} pageSize={pageSize} />
|
|
584
|
+
</Wrapper>
|
|
585
|
+
);
|
|
586
|
+
};
|
|
587
|
+
render(<Harness />);
|
|
588
|
+
await waitFor(() =>
|
|
589
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
590
|
+
);
|
|
591
|
+
const response = {
|
|
592
|
+
data: firstPage,
|
|
593
|
+
total_rows: 25,
|
|
594
|
+
cell_styles: null,
|
|
595
|
+
cell_hover_texts: null,
|
|
596
|
+
};
|
|
597
|
+
const pendingPage = new Deferred<typeof response>();
|
|
598
|
+
props.search.mockReturnValueOnce(pendingPage.promise);
|
|
599
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
600
|
+
await waitFor(() =>
|
|
601
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
602
|
+
expect.objectContaining({ page_number: 1, page_size: PAGE_SIZE }),
|
|
603
|
+
),
|
|
604
|
+
);
|
|
605
|
+
props.search.mockClear();
|
|
606
|
+
|
|
607
|
+
await act(async () => {
|
|
608
|
+
pendingPage.resolve(response);
|
|
609
|
+
// Settle the table-loading promises without committing the queued render.
|
|
610
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
611
|
+
fireEvent.click(
|
|
612
|
+
screen.getByRole("button", { name: "Change configured page size" }),
|
|
613
|
+
);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
await waitFor(() => expect(props.search).toHaveBeenCalled());
|
|
617
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
618
|
+
expect.objectContaining({ page_number: 0, page_size: 5 }),
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("returns to the first page when a refreshed result changes the row count", async () => {
|
|
623
|
+
const props = makeProps();
|
|
624
|
+
const view = () => (
|
|
625
|
+
<Wrapper>
|
|
626
|
+
<LoadingDataTableComponent {...props} />
|
|
627
|
+
</Wrapper>
|
|
628
|
+
);
|
|
629
|
+
const { rerender } = render(view());
|
|
630
|
+
await waitFor(() =>
|
|
631
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
632
|
+
);
|
|
633
|
+
fireEvent.click(screen.getByTestId("next-page-button"));
|
|
634
|
+
await waitFor(() =>
|
|
635
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
636
|
+
expect.objectContaining({ page_number: 1 }),
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
await waitFor(() =>
|
|
640
|
+
expect(screen.getByTestId("next-page-button")).toBeEnabled(),
|
|
641
|
+
);
|
|
642
|
+
props.search = vi.fn().mockResolvedValue({
|
|
643
|
+
data: firstPage,
|
|
644
|
+
total_rows: 25,
|
|
645
|
+
cell_styles: null,
|
|
646
|
+
cell_hover_texts: null,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
rerender(view());
|
|
650
|
+
await waitFor(() =>
|
|
651
|
+
expect(props.search).toHaveBeenLastCalledWith(
|
|
652
|
+
expect.objectContaining({ page_number: 0, page_size: PAGE_SIZE }),
|
|
653
|
+
),
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
|
|
457
657
|
it("renders kernel-dependent controls in normal mode", async () => {
|
|
458
658
|
const props = makeProps();
|
|
459
659
|
render(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
act,
|
|
4
5
|
fireEvent,
|
|
5
6
|
render,
|
|
6
7
|
screen,
|
|
@@ -88,6 +89,50 @@ beforeAll(() => {
|
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
describe("FileBrowserPlugin session cache recovery", () => {
|
|
92
|
+
it("delays the initial loading overlay and hides it when listing completes", async () => {
|
|
93
|
+
vi.useFakeTimers();
|
|
94
|
+
try {
|
|
95
|
+
let resolveListing: (value: {
|
|
96
|
+
files: MockFile[];
|
|
97
|
+
total_count: number;
|
|
98
|
+
is_truncated: boolean;
|
|
99
|
+
}) => void = () => undefined;
|
|
100
|
+
const listDirectory = vi.fn(
|
|
101
|
+
() =>
|
|
102
|
+
new Promise((resolve) => {
|
|
103
|
+
resolveListing = resolve;
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
const { unmount } = renderBrowser({ list_directory: listDirectory });
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
vi.advanceTimersByTime(199);
|
|
110
|
+
});
|
|
111
|
+
expect(screen.queryByText("Listing files...")).not.toBeInTheDocument();
|
|
112
|
+
await act(async () => {
|
|
113
|
+
vi.advanceTimersByTime(1);
|
|
114
|
+
});
|
|
115
|
+
expect(screen.getByText("Listing files...")).toBeInTheDocument();
|
|
116
|
+
|
|
117
|
+
await act(async () => {
|
|
118
|
+
resolveListing({
|
|
119
|
+
files: FILES,
|
|
120
|
+
total_count: FILES.length,
|
|
121
|
+
is_truncated: false,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
expect(screen.queryByText("Listing files...")).not.toBeInTheDocument();
|
|
125
|
+
expect(screen.getByText("docs")).toBeInTheDocument();
|
|
126
|
+
await act(async () => {
|
|
127
|
+
vi.advanceTimersByTime(200);
|
|
128
|
+
});
|
|
129
|
+
expect(screen.queryByText("Listing files...")).not.toBeInTheDocument();
|
|
130
|
+
unmount();
|
|
131
|
+
} finally {
|
|
132
|
+
vi.useRealTimers();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
91
136
|
it("retries a failed cached listing when the plugin resets", async () => {
|
|
92
137
|
const host = document.createElement("marimo-file-browser");
|
|
93
138
|
const wrapper = document.createElement("marimo-ui-element");
|
|
@@ -212,6 +212,28 @@ describe("SliderPlugin", () => {
|
|
|
212
212
|
expect(setValue).toHaveBeenCalledWith(9);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
it("preserves an uncommitted drag until the controlled value changes", () => {
|
|
216
|
+
const plugin = new SliderPlugin();
|
|
217
|
+
const setValue = vi.fn();
|
|
218
|
+
const props = createProps({
|
|
219
|
+
debounce: true,
|
|
220
|
+
includeInput: false,
|
|
221
|
+
setValue,
|
|
222
|
+
});
|
|
223
|
+
const { getByRole, rerender } = render(plugin.render(props));
|
|
224
|
+
|
|
225
|
+
fireEvent.click(getByRole("button", { name: "Slider change" }));
|
|
226
|
+
rerender(
|
|
227
|
+
plugin.render({ ...props, data: { ...props.data, label: "Updated" } }),
|
|
228
|
+
);
|
|
229
|
+
fireEvent.click(getByRole("button", { name: "Slider commit" }));
|
|
230
|
+
expect(setValue).toHaveBeenLastCalledWith(6);
|
|
231
|
+
|
|
232
|
+
rerender(plugin.render({ ...props, value: 2 }));
|
|
233
|
+
fireEvent.click(getByRole("button", { name: "Slider commit" }));
|
|
234
|
+
expect(setValue).toHaveBeenLastCalledWith(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
215
237
|
describe("editable input with steps (regression for #9850)", () => {
|
|
216
238
|
it("displays the actual step value, not the index", () => {
|
|
217
239
|
const plugin = new SliderPlugin();
|
|
@@ -381,9 +403,8 @@ describe("SliderPlugin", () => {
|
|
|
381
403
|
vi.advanceTimersByTime(0);
|
|
382
404
|
});
|
|
383
405
|
|
|
384
|
-
// Shrink `steps` so the held index (4) is now out of range.
|
|
385
|
-
//
|
|
386
|
-
// the stale, out-of-range index.
|
|
406
|
+
// Shrink `steps` so the held index (4) is now out of range. Updating
|
|
407
|
+
// the controlled index must render the new valid step without throwing.
|
|
387
408
|
const shrunkProps = createStepsProps({
|
|
388
409
|
steps: [10, 20],
|
|
389
410
|
valueIndex: 1,
|
|
@@ -238,6 +238,7 @@ describe("WidgetRegistry.getWidget", () => {
|
|
|
238
238
|
jsUrl: SPEC.url,
|
|
239
239
|
jsHash: SPEC.hash,
|
|
240
240
|
kernelAuthored: true,
|
|
241
|
+
lifetime: expect.any(AbortSignal),
|
|
241
242
|
});
|
|
242
243
|
expect(widgetDef.initialize).toHaveBeenCalledTimes(1);
|
|
243
244
|
expect(widget.exports).toBe(exports);
|
|
@@ -263,6 +264,7 @@ describe("WidgetRegistry.getWidget", () => {
|
|
|
263
264
|
jsUrl: SPEC.url,
|
|
264
265
|
jsHash: SPEC.hash,
|
|
265
266
|
kernelAuthored: true,
|
|
267
|
+
lifetime: expect.any(AbortSignal),
|
|
266
268
|
});
|
|
267
269
|
});
|
|
268
270
|
|
|
@@ -7,9 +7,12 @@ import { userConfigAtom } from "@/core/config/config";
|
|
|
7
7
|
import { parseUserConfig } from "@/core/config/config-schema";
|
|
8
8
|
import { initialModeAtom } from "@/core/mode";
|
|
9
9
|
import { store } from "@/core/state/jotai";
|
|
10
|
+
import type { DataURLString } from "@/utils/json/base64";
|
|
10
11
|
import { Model } from "../model";
|
|
11
12
|
import type { Host } from "../host";
|
|
12
|
-
import type { ModelState } from "../types";
|
|
13
|
+
import type { ModelState, WidgetModelId } from "../types";
|
|
14
|
+
import { WidgetRuntime } from "../runtime";
|
|
15
|
+
import { Deferred } from "@/utils/Deferred";
|
|
13
16
|
import { visibleForTesting } from "../widget-binding";
|
|
14
17
|
|
|
15
18
|
const { WidgetDefRegistry, WidgetBinding } = visibleForTesting;
|
|
@@ -30,13 +33,14 @@ const createTestHost = (): Host => ({
|
|
|
30
33
|
function createBinding<T extends ModelState>(
|
|
31
34
|
widgetDef: AnyWidget<T>,
|
|
32
35
|
model: Model<T>,
|
|
33
|
-
options: { controller?: AbortController } = {},
|
|
36
|
+
options: { controller?: AbortController; onDestroy?: () => void } = {},
|
|
34
37
|
) {
|
|
35
38
|
return WidgetBinding.create({
|
|
36
39
|
widgetDef,
|
|
37
40
|
model,
|
|
38
41
|
createHost: createTestHost,
|
|
39
42
|
controller: options.controller,
|
|
43
|
+
onDestroy: options.onDestroy,
|
|
40
44
|
});
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -123,6 +127,25 @@ describe("WidgetDefRegistry", () => {
|
|
|
123
127
|
promise2.catch(() => undefined);
|
|
124
128
|
});
|
|
125
129
|
|
|
130
|
+
it("does not evict a replacement when an invalidated import fails", async () => {
|
|
131
|
+
const oldImport = getModule(
|
|
132
|
+
registry,
|
|
133
|
+
"http://localhost/old.js",
|
|
134
|
+
"replaced",
|
|
135
|
+
);
|
|
136
|
+
registry.invalidate("replaced");
|
|
137
|
+
const newImport = registry.getModule({
|
|
138
|
+
jsUrl: "data:text/javascript,export default {}",
|
|
139
|
+
jsHash: "replaced",
|
|
140
|
+
kernelAuthored: true,
|
|
141
|
+
});
|
|
142
|
+
await expect(oldImport).rejects.toThrow(/untrusted/i);
|
|
143
|
+
await newImport;
|
|
144
|
+
expect(getModule(registry, "http://localhost/new.js", "replaced")).toBe(
|
|
145
|
+
newImport,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
126
149
|
describe("URL validation", () => {
|
|
127
150
|
it.each([
|
|
128
151
|
// Attack vector: raw <marimo-anywidget data-js-url=...> in markdown
|
|
@@ -148,9 +171,225 @@ describe("WidgetDefRegistry", () => {
|
|
|
148
171
|
).rejects.not.toThrow(/untrusted/i);
|
|
149
172
|
});
|
|
150
173
|
});
|
|
174
|
+
|
|
175
|
+
describe("static module URLs", () => {
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
delete window.__MARIMO_STATIC__;
|
|
178
|
+
vi.restoreAllMocks();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("releases the generated URL when a static module fails to import", async () => {
|
|
182
|
+
window.__MARIMO_STATIC__ = {
|
|
183
|
+
files: {
|
|
184
|
+
"/@file/widget.js":
|
|
185
|
+
"data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const create = vi
|
|
189
|
+
.spyOn(URL, "createObjectURL")
|
|
190
|
+
.mockReturnValue("blob:widget-module");
|
|
191
|
+
const revoke = vi
|
|
192
|
+
.spyOn(URL, "revokeObjectURL")
|
|
193
|
+
.mockImplementation(() => undefined);
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
getModule(registry, "./@file/widget.js", "static-module"),
|
|
197
|
+
).rejects.toThrow(/blob:/);
|
|
198
|
+
|
|
199
|
+
expect(create).toHaveBeenCalledOnce();
|
|
200
|
+
expect(revoke).toHaveBeenCalledExactlyOnceWith("blob:widget-module");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("does not revoke active modules when their cache entry is invalidated", async () => {
|
|
204
|
+
window.__MARIMO_STATIC__ = {
|
|
205
|
+
files: {
|
|
206
|
+
"/@file/widget.js":
|
|
207
|
+
"data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const oldUrl = "data:text/javascript,export const version = 1";
|
|
211
|
+
const newUrl = "data:text/javascript,export const version = 2";
|
|
212
|
+
vi.spyOn(URL, "createObjectURL")
|
|
213
|
+
.mockReturnValueOnce(oldUrl)
|
|
214
|
+
.mockReturnValueOnce(newUrl);
|
|
215
|
+
const revoke = vi
|
|
216
|
+
.spyOn(URL, "revokeObjectURL")
|
|
217
|
+
.mockImplementation(() => undefined);
|
|
218
|
+
const oldLifetime = new AbortController();
|
|
219
|
+
const newLifetime = new AbortController();
|
|
220
|
+
const options = { jsUrl: "./@file/widget.js", jsHash: "invalidated" };
|
|
221
|
+
const oldImport = registry.getModule({
|
|
222
|
+
...options,
|
|
223
|
+
lifetime: oldLifetime.signal,
|
|
224
|
+
});
|
|
225
|
+
registry.invalidate(options.jsHash);
|
|
226
|
+
const newImport = registry.getModule({
|
|
227
|
+
...options,
|
|
228
|
+
lifetime: newLifetime.signal,
|
|
229
|
+
});
|
|
230
|
+
await Promise.all([oldImport, newImport]);
|
|
231
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
232
|
+
oldLifetime.abort();
|
|
233
|
+
await vi.waitFor(() =>
|
|
234
|
+
expect(revoke).toHaveBeenCalledExactlyOnceWith(oldUrl),
|
|
235
|
+
);
|
|
236
|
+
const siblingLifetime = new AbortController();
|
|
237
|
+
expect(
|
|
238
|
+
registry.getModule({ ...options, lifetime: siblingLifetime.signal }),
|
|
239
|
+
).toBe(newImport);
|
|
240
|
+
newLifetime.abort();
|
|
241
|
+
siblingLifetime.abort();
|
|
242
|
+
await vi.waitFor(() => expect(revoke).toHaveBeenLastCalledWith(newUrl));
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("waits for a canceled import to settle before revoking its URL", async () => {
|
|
246
|
+
window.__MARIMO_STATIC__ = {
|
|
247
|
+
files: {
|
|
248
|
+
"/@file/widget.js":
|
|
249
|
+
"data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const moduleUrl = "data:text/javascript,export const pending = true";
|
|
253
|
+
vi.spyOn(URL, "createObjectURL").mockReturnValue(moduleUrl);
|
|
254
|
+
const revoke = vi
|
|
255
|
+
.spyOn(URL, "revokeObjectURL")
|
|
256
|
+
.mockImplementation(() => undefined);
|
|
257
|
+
const lifetime = new AbortController();
|
|
258
|
+
const pending = registry.getModule({
|
|
259
|
+
jsUrl: "./@file/widget.js",
|
|
260
|
+
jsHash: "pending",
|
|
261
|
+
lifetime: lifetime.signal,
|
|
262
|
+
});
|
|
263
|
+
lifetime.abort();
|
|
264
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
265
|
+
await pending;
|
|
266
|
+
expect(revoke).toHaveBeenCalledExactlyOnceWith(moduleUrl);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("does not revoke an unchanged URL for a missing static file", async () => {
|
|
270
|
+
window.__MARIMO_STATIC__ = { files: {} };
|
|
271
|
+
const revoke = vi
|
|
272
|
+
.spyOn(URL, "revokeObjectURL")
|
|
273
|
+
.mockImplementation(() => undefined);
|
|
274
|
+
|
|
275
|
+
await expect(
|
|
276
|
+
getModule(registry, "./@file/missing.js", "missing-module"),
|
|
277
|
+
).rejects.toThrow(/http:/);
|
|
278
|
+
|
|
279
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("keeps a shared module URL until the last widget generation is destroyed", async () => {
|
|
283
|
+
window.__MARIMO_STATIC__ = {
|
|
284
|
+
files: {
|
|
285
|
+
"/@file/widget.js":
|
|
286
|
+
"data:text/javascript;base64,ZXhwb3J0IHt9" as DataURLString,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
// Node cannot import blob URLs; a data URL exercises successful loading.
|
|
290
|
+
const moduleUrl = `data:text/javascript,${encodeURIComponent(
|
|
291
|
+
"export default { initialize: () => ({ moduleUrl: import.meta.url }) }",
|
|
292
|
+
)}`;
|
|
293
|
+
vi.spyOn(URL, "createObjectURL").mockReturnValue(moduleUrl);
|
|
294
|
+
const revoke = vi
|
|
295
|
+
.spyOn(URL, "revokeObjectURL")
|
|
296
|
+
.mockImplementation(() => undefined);
|
|
297
|
+
|
|
298
|
+
const createRuntime = (id: string) => {
|
|
299
|
+
const runtime = new WidgetRuntime(id as WidgetModelId, {
|
|
300
|
+
timeout: 50,
|
|
301
|
+
isEditMode: () => true,
|
|
302
|
+
createHost: createTestHost,
|
|
303
|
+
onModelTimeout: vi.fn(),
|
|
304
|
+
});
|
|
305
|
+
runtime.setModel(new Model({}, createMockComm()));
|
|
306
|
+
runtime.setSpec({ url: "./@file/widget.js", hash: "static-success" });
|
|
307
|
+
return runtime;
|
|
308
|
+
};
|
|
309
|
+
const first = createRuntime("first");
|
|
310
|
+
const second = createRuntime("second");
|
|
311
|
+
const widgets = await Promise.all([
|
|
312
|
+
first.getWidget(),
|
|
313
|
+
second.getWidget(),
|
|
314
|
+
]);
|
|
315
|
+
expect(widgets.map((widget) => widget.exports)).toEqual([
|
|
316
|
+
{ moduleUrl },
|
|
317
|
+
{ moduleUrl },
|
|
318
|
+
]);
|
|
319
|
+
expect(URL.createObjectURL).toHaveBeenCalledOnce();
|
|
320
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
321
|
+
|
|
322
|
+
first.dispose();
|
|
323
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
324
|
+
expect(revoke).not.toHaveBeenCalled();
|
|
325
|
+
|
|
326
|
+
second.dispose();
|
|
327
|
+
await vi.waitFor(() => {
|
|
328
|
+
expect(revoke).toHaveBeenCalledExactlyOnceWith(moduleUrl);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// A later generation reuses the evaluated module rather than
|
|
332
|
+
// re-running its top-level side effects, and does not revoke again.
|
|
333
|
+
const third = createRuntime("third");
|
|
334
|
+
const widget = await third.getWidget();
|
|
335
|
+
expect(widget.exports).toEqual({ moduleUrl });
|
|
336
|
+
expect(URL.createObjectURL).toHaveBeenCalledOnce();
|
|
337
|
+
third.dispose();
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
339
|
+
expect(revoke).toHaveBeenCalledOnce();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
151
342
|
});
|
|
152
343
|
|
|
153
344
|
describe("WidgetBinding", () => {
|
|
345
|
+
it("releases module resources after asynchronous generation cleanup", async () => {
|
|
346
|
+
const cleanup = new Deferred<void>();
|
|
347
|
+
const onDestroy = vi.fn();
|
|
348
|
+
const binding = await createBinding(
|
|
349
|
+
{ initialize: () => () => cleanup.promise },
|
|
350
|
+
new Model({}, createMockComm()),
|
|
351
|
+
{ onDestroy },
|
|
352
|
+
);
|
|
353
|
+
const destroying = binding.destroy();
|
|
354
|
+
expect(onDestroy).not.toHaveBeenCalled();
|
|
355
|
+
cleanup.resolve();
|
|
356
|
+
await destroying;
|
|
357
|
+
await binding.destroy();
|
|
358
|
+
expect(onDestroy).toHaveBeenCalledOnce();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("retains module resources until canceled initialization and late cleanup settle", async () => {
|
|
362
|
+
const initialize = new Deferred<() => Promise<void>>();
|
|
363
|
+
const cleanup = new Deferred<void>();
|
|
364
|
+
const controller = new AbortController();
|
|
365
|
+
const onDestroy = vi.fn();
|
|
366
|
+
const pending = createBinding(
|
|
367
|
+
{ initialize: () => initialize.promise },
|
|
368
|
+
new Model({}, createMockComm()),
|
|
369
|
+
{ controller, onDestroy },
|
|
370
|
+
);
|
|
371
|
+
controller.abort();
|
|
372
|
+
await expect(pending).rejects.toThrow(/binding destroyed/);
|
|
373
|
+
expect(onDestroy).not.toHaveBeenCalled();
|
|
374
|
+
initialize.resolve(() => cleanup.promise);
|
|
375
|
+
await Promise.resolve();
|
|
376
|
+
expect(onDestroy).not.toHaveBeenCalled();
|
|
377
|
+
cleanup.resolve();
|
|
378
|
+
await vi.waitFor(() => expect(onDestroy).toHaveBeenCalledOnce());
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("releases module resources when initialization fails", async () => {
|
|
382
|
+
const onDestroy = vi.fn();
|
|
383
|
+
await expect(
|
|
384
|
+
createBinding(
|
|
385
|
+
{ initialize: () => Promise.reject(new Error("initialize failed")) },
|
|
386
|
+
new Model({}, createMockComm()),
|
|
387
|
+
{ onDestroy },
|
|
388
|
+
),
|
|
389
|
+
).rejects.toThrow("initialize failed");
|
|
390
|
+
await vi.waitFor(() => expect(onDestroy).toHaveBeenCalledOnce());
|
|
391
|
+
});
|
|
392
|
+
|
|
154
393
|
it("should run initialize exactly once and expose exports", async () => {
|
|
155
394
|
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
156
395
|
const exports = { setValue: vi.fn() };
|
|
@@ -206,21 +206,33 @@ export class WidgetRuntime {
|
|
|
206
206
|
controller: AbortController;
|
|
207
207
|
}): Promise<WidgetBinding<ModelState>> {
|
|
208
208
|
const { spec, model, controller } = options;
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
const moduleLifetime = new AbortController();
|
|
210
|
+
let widget: NonNullable<ReturnType<typeof resolveAnyWidget>>;
|
|
211
|
+
try {
|
|
212
|
+
const mod = await WIDGET_DEF_REGISTRY.getModule({
|
|
213
|
+
jsUrl: spec.url,
|
|
214
|
+
jsHash: spec.hash,
|
|
215
|
+
kernelAuthored: true,
|
|
216
|
+
lifetime: moduleLifetime.signal,
|
|
217
|
+
});
|
|
218
|
+
const resolved = resolveAnyWidget(mod, spec.url);
|
|
219
|
+
if (!resolved) {
|
|
220
|
+
WIDGET_DEF_REGISTRY.invalidate(spec.hash);
|
|
221
|
+
throw getInvalidAnyWidgetModuleError(mod, spec.url);
|
|
222
|
+
}
|
|
223
|
+
widget = resolved;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
moduleLifetime.abort();
|
|
226
|
+
throw error;
|
|
218
227
|
}
|
|
228
|
+
// From here WidgetBinding owns release, including late cleanup when the
|
|
229
|
+
// generation is destroyed during initialize.
|
|
219
230
|
return WidgetBinding.create({
|
|
220
231
|
widgetDef: widget,
|
|
221
232
|
model,
|
|
222
233
|
createHost: this.#createHost,
|
|
223
234
|
controller,
|
|
235
|
+
onDestroy: () => moduleLifetime.abort(),
|
|
224
236
|
});
|
|
225
237
|
}
|
|
226
238
|
|