@marimo-team/islands 0.25.3-dev92 → 0.25.3-dev93
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-Da5OoEUL.js → ConnectedDataExplorerComponent-Dhf5EGs5.js} +4 -4
- package/dist/{ErrorBoundary-Bp6Cm6jU.js → ErrorBoundary-jiRCrm1p.js} +2 -2
- package/dist/{abnfDiagram-VCTEODGH-Bz3mrprX.js → abnfDiagram-VCTEODGH-Di400kYu.js} +2 -2
- package/dist/{architectureDiagram-5GKGNRK7-Cs5ujb9K.js → architectureDiagram-5GKGNRK7-BdTJcsNJ.js} +1 -1
- package/dist/{chat-ui-DvyHgEfl.js → chat-ui-D0jT6WV3.js} +7 -7
- package/dist/{chunk-IMKFNOWR-CcTSaxZt.js → chunk-IMKFNOWR-mtD7aJBw.js} +1 -1
- package/dist/{chunk-SHT3W25Y-DcJcjOaO.js → chunk-SHT3W25Y-DybB1ShH.js} +1 -1
- package/dist/{chunk-SVP7TREG-BVtDTjaF.js → chunk-SVP7TREG-DNhhdCex.js} +1 -1
- package/dist/{chunk-TICWLB2K-BkDFkh8q.js → chunk-TICWLB2K-NhAQhlDf.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-CfAt7HT5.js → classDiagram-ZZMXUADV-GTL0l5xS.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-CGEjNYa2.js → classDiagram-v2-VYDZK3BY-BWrM2ruI.js} +2 -2
- package/dist/{code-visibility-BS6srmOi.js → code-visibility-B_y_dByX.js} +4 -4
- package/dist/{common-Ci4xUkZe.js → common-CmpSkUjV.js} +9 -9
- package/dist/{cynefinDiagram-5FMLGOSQ-BW6NmfJi.js → cynefinDiagram-5FMLGOSQ-CX22wa7c.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CWg3CTuF.js → dagre-GXQ25YYZ-Df6Pp-cl.js} +1 -1
- package/dist/{data-grid-overlay-editor-3Xwrzd6B.js → data-grid-overlay-editor-CJTJtfj-.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BPxtoS-z.js → diagram-S7CK7UJ4-CctU9kVx.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DA-vJdFL.js → diagram-UQ7AKVKN-FVHjCF9j.js} +1 -1
- package/dist/{diagram-VX7I27RA-D6bkR84S.js → diagram-VX7I27RA-BSDbwFFv.js} +1 -1
- package/dist/{diagram-Z3DM3KII-m60g4cAT.js → diagram-Z3DM3KII-BX8kW7Nc.js} +1 -1
- package/dist/{dropdown-menu-B3eUiVJB.js → dropdown-menu-B_7xD2X4.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-CK99sho2.js → ebnfDiagram-PWID7BFC-DtapdLet.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-Bryq0tdE.js → erDiagram-RLTQ6QDP-BAru9jHt.js} +1 -1
- package/dist/{error-entries-5MYC7UoK.js → error-entries-BZiemZfD.js} +4 -4
- package/dist/{feedback-button-Btjd18-4.js → feedback-button-BpHnBE3A.js} +5 -5
- package/dist/{flowDiagram-HODETNUW-D-3aRpfS.js → flowDiagram-HODETNUW-CjaeFeea.js} +2 -2
- package/dist/{formats-ZmooA-MD.js → formats-V8ifA0Bk.js} +1 -1
- package/dist/{glide-data-editor-CfaicMD0.js → glide-data-editor-B3xUjh0M.js} +5 -5
- package/dist/{globals-C2faijKH.js → globals-DI7gqOyy.js} +2 -2
- package/dist/{html-to-image-DvvwId4p.js → html-to-image-DBOTlHF9.js} +5 -5
- package/dist/{infoDiagram-27XIBGKW-s1C1kgfU.js → infoDiagram-27XIBGKW-B1hmUggC.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CCsRgolY.js → ishikawaDiagram-5VMMS53U-DUeT3t-d.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BrmxDlcc.js → kanban-definition-UXKFOSKX-DEUnNNgc.js} +1 -1
- package/dist/main.js +21 -21
- package/dist/{mermaid-DKI0869I.js → mermaid-B0fXpsNP.js} +34 -34
- package/dist/{mindmap-definition-YA3MSWOX-641xJysF.js → mindmap-definition-YA3MSWOX-CPexlhDQ.js} +1 -1
- package/dist/{number-overlay-editor-wlCw5SbP.js → number-overlay-editor-DQ6KAcGy.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-keVUAUeS.js → pegDiagram-XKGWAZYB-CMpp1OOQ.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-DHktXPO_.js → pieDiagram-E7YTZNPT-DlMWg5t1.js} +1 -1
- package/dist/{process-output-BfUEswoe.js → process-output-BKLVbFnU.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-BVEF4OV_.js → railroadDiagram-O6MQD6OU-DBJfseeO.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CWo-UAKV.js → requirementDiagram-BXWQKSXE-BC7V639r.js} +1 -1
- package/dist/{reveal-component-COKTwTRj.js → reveal-component-DsWQWmYM.js} +6 -6
- package/dist/{sequenceDiagram-WJ2MYXX4-CHQvLnq4.js → sequenceDiagram-WJ2MYXX4-CgesU6F-.js} +1 -1
- package/dist/{spec-C-LRKGtT.js → spec-CcT0slVc.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-BOJZgbUJ.js → stateDiagram-D77RDMKH-lH_BUhs3.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-C53XXdYH.js → stateDiagram-v2-MP3YSRHH-8LL6FrBe.js} +2 -2
- package/dist/{swimlanes-42K2YHIH-D2o1gOtw.js → swimlanes-42K2YHIH-Cuf9B9kE.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js → swimlanesDiagram-VR7AAH4N-BoMMtOPh.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-rv7QUa46.js → timeline-definition-24CTP7MA-yzzw4TF4.js} +1 -1
- package/dist/{toDate-C6BRw8i5.js → toDate-DKH0_kMo.js} +1 -1
- package/dist/{useAsyncData-ChgNtdmE.js → useAsyncData-D0mLJstx.js} +1 -1
- package/dist/{useDeepCompareMemoize-Bet7kSsI.js → useDeepCompareMemoize-BpPWi3G3.js} +1 -1
- package/dist/{useLifecycle-VVmz_24o.js → useLifecycle-Dum8WSXq.js} +1 -1
- package/dist/{useTheme-DympCsQg.js → useTheme-CfeaVw5D.js} +1 -1
- package/dist/{vega-component-B8IjMdad.js → vega-component-DgytCKrL.js} +5 -5
- package/dist/{vennDiagram-4TSXK5OY-D36Zk7t-.js → vennDiagram-4TSXK5OY-eJOrTqqC.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-BLBolvyQ.js → wardleyDiagram-VM6X3IG4-TILPvee6.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-CfH57l0v.js → xychartDiagram-S5SC5T6Z-DUPvRwwJ.js} +1 -1
- package/package.json +1 -1
- package/src/components/datasources/__tests__/table-list.test.tsx +118 -0
- package/src/components/datasources/datasources.tsx +35 -21
- package/src/core/state/__tests__/jotai.test.tsx +108 -0
- package/src/core/state/jotai.ts +1 -1
- package/src/core/state/__tests__/jotai.test.ts +0 -25
|
@@ -4,7 +4,7 @@ import { t as i } from "./react-sHtZdjeP.js";
|
|
|
4
4
|
import { t as a } from "./compiler-runtime-CldRdSsv.js";
|
|
5
5
|
import { h as o } from "./tooltip-CKO1pHJo.js";
|
|
6
6
|
import { t as s } from "./jsx-runtime-3BD1kT0s.js";
|
|
7
|
-
import { E as c, w as l } from "./useTheme-
|
|
7
|
+
import { E as c, w as l } from "./useTheme-CfeaVw5D.js";
|
|
8
8
|
var u = o("calendar", [
|
|
9
9
|
["path", {
|
|
10
10
|
d: "M8 2v4",
|
|
@@ -3,19 +3,19 @@ import { g as t, h as n, m as r, o as i } from "./button-BxB3RDjN.js";
|
|
|
3
3
|
import { t as a } from "./react-sHtZdjeP.js";
|
|
4
4
|
import { t as o } from "./compiler-runtime-CldRdSsv.js";
|
|
5
5
|
import { t as s } from "./tooltip-CKO1pHJo.js";
|
|
6
|
-
import { C as c, a as l } from "./toDate-
|
|
6
|
+
import { C as c, a as l } from "./toDate-DKH0_kMo.js";
|
|
7
7
|
import { t as u } from "./jsx-runtime-3BD1kT0s.js";
|
|
8
8
|
import { n as d } from "./error-banner-Sl2FlSJ4.js";
|
|
9
9
|
import { t as f } from "./debounce-C3jyATpc.js";
|
|
10
|
-
import { A as p, n as ee } from "./useTheme-
|
|
10
|
+
import { A as p, n as ee } from "./useTheme-CfeaVw5D.js";
|
|
11
11
|
import { c as m, t as h } from "./arrays-R6JsImiT.js";
|
|
12
12
|
import { t as te } from "./objects-DCmMvZtn.js";
|
|
13
|
-
import { t as ne } from "./useAsyncData-
|
|
14
|
-
import { a as g, i as re, n as ie, t as ae } from "./formats-
|
|
13
|
+
import { t as ne } from "./useAsyncData-D0mLJstx.js";
|
|
14
|
+
import { a as g, i as re, n as ie, t as ae } from "./formats-V8ifA0Bk.js";
|
|
15
15
|
import { D as oe, n as _ } from "./vega-loader.browser-B9Pz0EGP.js";
|
|
16
16
|
import { t as se } from "./react-vega-C4T9Rb_w.js";
|
|
17
17
|
import { a as ce, n as le } from "./loader-Bm8cy3e8.js";
|
|
18
|
-
import { t as ue } from "./useDeepCompareMemoize-
|
|
18
|
+
import { t as ue } from "./useDeepCompareMemoize-BpPWi3G3.js";
|
|
19
19
|
import { t as v } from "./semaphore-89jsoHtS.js";
|
|
20
20
|
import { t as de } from "./use-vega-container-remeasure-Bz4PZMtt.js";
|
|
21
21
|
//#region src/plugins/impl/vega/fix-relative-url.ts
|
|
@@ -3,7 +3,7 @@ import { t } from "./src-CA_m4ONl.js";
|
|
|
3
3
|
import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
|
|
4
4
|
import { $ as r, H as i, K as a, U as o, a as s, b as c, c as l, et as u, f as d, nt as f, tt as p, v as m, w as h, y as g } from "./chunk-DU6HZSFF-BwiyaNpg.js";
|
|
5
5
|
import { t as _ } from "./rough.esm-CtV9AnV1.js";
|
|
6
|
-
import { p as v } from "./mermaid-
|
|
6
|
+
import { p as v } from "./mermaid-B0fXpsNP.js";
|
|
7
7
|
//#region ../node_modules/.pnpm/khroma@2.1.0/node_modules/khroma/dist/methods/transparentize.js
|
|
8
8
|
var y = (e, t) => p(e, "a", -t), b = 1e-10;
|
|
9
9
|
function x(e, t) {
|
|
@@ -2,7 +2,7 @@ import { i as e } from "./chunk-75Z2AOVW-D7gxIs2Q.js";
|
|
|
2
2
|
import { n as t } from "./src-CA_m4ONl.js";
|
|
3
3
|
import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
|
|
4
4
|
import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-BwiyaNpg.js";
|
|
5
|
-
import { p as m } from "./mermaid-
|
|
5
|
+
import { p as m } from "./mermaid-B0fXpsNP.js";
|
|
6
6
|
import { n as h } from "./mermaid-parser.core-DxuSfYwJ.js";
|
|
7
7
|
import { t as g } from "./chunk-JWPE2WC7-DJvv1t8U.js";
|
|
8
8
|
//#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/wardleyDiagram-VM6X3IG4.mjs
|
|
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
|
|
|
9
9
|
import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-BwiyaNpg.js";
|
|
10
10
|
import "./chunk-PWAF6VOD-CQw5qATZ.js";
|
|
11
11
|
import { t as b } from "./chunk-GMAD6QVW-BhaJx0X3.js";
|
|
12
|
-
import { p as x } from "./mermaid-
|
|
12
|
+
import { p as x } from "./mermaid-B0fXpsNP.js";
|
|
13
13
|
//#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
|
|
14
14
|
function S() {
|
|
15
15
|
var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
|
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
8
|
+
import type { SQLTableContext } from "@/core/datasets/data-source-connections";
|
|
9
|
+
import { PreviewSQLTableList } from "@/core/datasets/request-registry";
|
|
10
|
+
import type { SQLTableListPreview } from "@/core/kernel/messages";
|
|
11
|
+
import type { RequestId } from "@/core/network/DeferredRequestRegistry";
|
|
12
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
13
|
+
import { store } from "@/core/state/jotai";
|
|
14
|
+
import { TableList } from "../datasources";
|
|
15
|
+
|
|
16
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
17
|
+
<Provider store={store}>{children}</Provider>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const sqlTableContext: SQLTableContext = {
|
|
21
|
+
engine: "my_engine",
|
|
22
|
+
database: "my_db",
|
|
23
|
+
schema: "public",
|
|
24
|
+
dialect: "postgres",
|
|
25
|
+
schemaPath: ["public"],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// The kernel answers table-list requests over the websocket, so the mock
|
|
29
|
+
// settles the deferred request instead of returning the response directly.
|
|
30
|
+
function renderDeferredTableList(settle?: (requestId: RequestId) => void) {
|
|
31
|
+
const client = MockRequestClient.create();
|
|
32
|
+
client.previewSQLTableList.mockImplementation(async ({ requestId }) => {
|
|
33
|
+
settle?.(requestId);
|
|
34
|
+
return null;
|
|
35
|
+
});
|
|
36
|
+
store.set(requestClientAtom, client);
|
|
37
|
+
render(
|
|
38
|
+
<TableList
|
|
39
|
+
tables={[]}
|
|
40
|
+
tablesResolved={false}
|
|
41
|
+
sqlTableContext={sqlTableContext}
|
|
42
|
+
/>,
|
|
43
|
+
{ wrapper },
|
|
44
|
+
);
|
|
45
|
+
return client;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("TableList deferred discovery", () => {
|
|
49
|
+
it("shows the error when the request rejects", async () => {
|
|
50
|
+
renderDeferredTableList((requestId) =>
|
|
51
|
+
PreviewSQLTableList.reject(requestId, new Error("engine offline")),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(screen.getByText("Loading tables...")).toBeInTheDocument();
|
|
55
|
+
|
|
56
|
+
await waitFor(() => {
|
|
57
|
+
expect(screen.getByText("engine offline")).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
expect(screen.queryByText("Loading tables...")).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("shows the error when the response has no tables field", async () => {
|
|
63
|
+
renderDeferredTableList((requestId) =>
|
|
64
|
+
PreviewSQLTableList.resolve(requestId, {} as SQLTableListPreview),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(screen.getByText("No tables available")).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
expect(screen.queryByText("Loading tables...")).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("shows the empty state for an empty table list", async () => {
|
|
74
|
+
renderDeferredTableList((requestId) =>
|
|
75
|
+
PreviewSQLTableList.resolve(requestId, {
|
|
76
|
+
tables: [],
|
|
77
|
+
} as unknown as SQLTableListPreview),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(screen.getByText("No tables found")).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
expect(screen.queryByText("Loading tables...")).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("requests the table list with the table context and waits for the kernel", async () => {
|
|
87
|
+
let pendingRequestId: RequestId | undefined;
|
|
88
|
+
const client = renderDeferredTableList((requestId) => {
|
|
89
|
+
pendingRequestId = requestId;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(client.previewSQLTableList).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
expect(client.previewSQLTableList).toHaveBeenCalledWith(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
engine: "my_engine",
|
|
98
|
+
database: "my_db",
|
|
99
|
+
schema: "public",
|
|
100
|
+
schemaPath: ["public"],
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
expect(screen.getByText("Loading tables...")).toBeInTheDocument();
|
|
104
|
+
|
|
105
|
+
// Settle the request so the process-wide registry does not keep the entry.
|
|
106
|
+
expect(pendingRequestId).toBeDefined();
|
|
107
|
+
PreviewSQLTableList.resolve(
|
|
108
|
+
pendingRequestId as RequestId,
|
|
109
|
+
{
|
|
110
|
+
tables: [],
|
|
111
|
+
} as unknown as SQLTableListPreview,
|
|
112
|
+
);
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(screen.queryByText("Loading tables...")).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
expect(PreviewSQLTableList.requests.size).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -702,7 +702,40 @@ const SchemaNode: React.FC<SchemaNodeProps> = (props) => {
|
|
|
702
702
|
);
|
|
703
703
|
};
|
|
704
704
|
|
|
705
|
-
|
|
705
|
+
// Kept outside the component because the React Compiler does not yet
|
|
706
|
+
// support try/finally inside component bodies.
|
|
707
|
+
async function loadTableList(
|
|
708
|
+
sqlTableContext: SQLTableContext,
|
|
709
|
+
addTableList: ReturnType<typeof useDataSourceActions>["addTableList"],
|
|
710
|
+
setLoading: (loading: boolean) => void,
|
|
711
|
+
) {
|
|
712
|
+
setLoading(true);
|
|
713
|
+
try {
|
|
714
|
+
const { engine, database, schema, schemaPath } = sqlTableContext;
|
|
715
|
+
const previewTableList = await PreviewSQLTableList.request({
|
|
716
|
+
engine: engine,
|
|
717
|
+
database: database,
|
|
718
|
+
schema: schema,
|
|
719
|
+
schemaPath: schemaPath ?? [],
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
if (!previewTableList?.tables) {
|
|
723
|
+
throw new Error("No tables available");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
addTableList({
|
|
727
|
+
tables: previewTableList.tables,
|
|
728
|
+
sqlTableContext: sqlTableContext,
|
|
729
|
+
});
|
|
730
|
+
} finally {
|
|
731
|
+
setLoading(false);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* This is exported for testing purposes only.
|
|
737
|
+
*/
|
|
738
|
+
export const TableList: React.FC<{
|
|
706
739
|
tables: DataTable[];
|
|
707
740
|
sqlTableContext?: SQLTableContext;
|
|
708
741
|
searchValue?: string;
|
|
@@ -731,26 +764,7 @@ const TableList: React.FC<{
|
|
|
731
764
|
// resets tablesResolved to false).
|
|
732
765
|
const { isPending, error } = useAsyncData(async () => {
|
|
733
766
|
if (!tablesResolved && sqlTableContext) {
|
|
734
|
-
setTablesLoading
|
|
735
|
-
|
|
736
|
-
const { engine, database, schema, schemaPath } = sqlTableContext;
|
|
737
|
-
const previewTableList = await PreviewSQLTableList.request({
|
|
738
|
-
engine: engine,
|
|
739
|
-
database: database,
|
|
740
|
-
schema: schema,
|
|
741
|
-
schemaPath: schemaPath ?? [],
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
if (!previewTableList?.tables) {
|
|
745
|
-
setTablesLoading(false);
|
|
746
|
-
throw new Error("No tables available");
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
addTableList({
|
|
750
|
-
tables: previewTableList.tables,
|
|
751
|
-
sqlTableContext: sqlTableContext,
|
|
752
|
-
});
|
|
753
|
-
setTablesLoading(false);
|
|
767
|
+
await loadTableList(sqlTableContext, addTableList, setTablesLoading);
|
|
754
768
|
}
|
|
755
769
|
}, [tablesResolved, sqlTableContext]);
|
|
756
770
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import { atom, Provider } from "jotai";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { store, useJotaiEffect, waitFor } from "../jotai";
|
|
8
|
+
|
|
9
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
10
|
+
<Provider store={store}>{children}</Provider>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
describe("waitFor function", () => {
|
|
14
|
+
it("should resolve when the atom satisfies the predicate", async () => {
|
|
15
|
+
const testAtom = atom(0);
|
|
16
|
+
store.set(testAtom, 10);
|
|
17
|
+
|
|
18
|
+
const result = await waitFor(testAtom, (value) => value === 10);
|
|
19
|
+
expect(result).toBe(10);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should resolve when the atom changes to satisfy the predicate", async () => {
|
|
23
|
+
const testAtom = atom(0);
|
|
24
|
+
|
|
25
|
+
setTimeout(() => store.set(testAtom, 15), 5);
|
|
26
|
+
setTimeout(() => store.set(testAtom, 20), 10);
|
|
27
|
+
|
|
28
|
+
const result = await waitFor(testAtom, (value) => value === 20);
|
|
29
|
+
expect(result).toBe(20);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("useJotaiEffect", () => {
|
|
34
|
+
it("calls the effect with the new and previous values", () => {
|
|
35
|
+
const testAtom = atom(0);
|
|
36
|
+
const effect = vi.fn();
|
|
37
|
+
renderHook(() => useJotaiEffect(testAtom, effect), { wrapper });
|
|
38
|
+
|
|
39
|
+
act(() => store.set(testAtom, 1));
|
|
40
|
+
act(() => store.set(testAtom, 2));
|
|
41
|
+
|
|
42
|
+
expect(effect.mock.calls).toEqual([
|
|
43
|
+
[1, 0],
|
|
44
|
+
[2, 1],
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("calls only the latest callback after a rerender", () => {
|
|
49
|
+
const testAtom = atom(0);
|
|
50
|
+
const first = vi.fn();
|
|
51
|
+
const second = vi.fn();
|
|
52
|
+
const { rerender } = renderHook(
|
|
53
|
+
({ effect }) => useJotaiEffect(testAtom, effect),
|
|
54
|
+
{ initialProps: { effect: first }, wrapper },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
rerender({ effect: second });
|
|
58
|
+
act(() => store.set(testAtom, 1));
|
|
59
|
+
|
|
60
|
+
expect(first).not.toHaveBeenCalled();
|
|
61
|
+
expect(second).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("subscribes once per rerender with an inline callback", () => {
|
|
65
|
+
const testAtom = atom(0);
|
|
66
|
+
const calls: number[] = [];
|
|
67
|
+
const { rerender } = renderHook(
|
|
68
|
+
() => useJotaiEffect(testAtom, (value) => calls.push(value)),
|
|
69
|
+
{ wrapper },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
rerender();
|
|
73
|
+
rerender();
|
|
74
|
+
act(() => store.set(testAtom, 1));
|
|
75
|
+
|
|
76
|
+
expect(calls).toEqual([1]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("stops listening to the previous atom when the atom changes", () => {
|
|
80
|
+
const firstAtom = atom(0);
|
|
81
|
+
const secondAtom = atom(0);
|
|
82
|
+
const effect = vi.fn();
|
|
83
|
+
const { rerender } = renderHook(
|
|
84
|
+
({ target }) => useJotaiEffect(target, effect),
|
|
85
|
+
{ initialProps: { target: firstAtom }, wrapper },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
rerender({ target: secondAtom });
|
|
89
|
+
act(() => store.set(firstAtom, 1));
|
|
90
|
+
expect(effect).not.toHaveBeenCalled();
|
|
91
|
+
|
|
92
|
+
act(() => store.set(secondAtom, 1));
|
|
93
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("unsubscribes on unmount", () => {
|
|
97
|
+
const testAtom = atom(0);
|
|
98
|
+
const effect = vi.fn();
|
|
99
|
+
const { unmount } = renderHook(() => useJotaiEffect(testAtom, effect), {
|
|
100
|
+
wrapper,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
unmount();
|
|
104
|
+
act(() => store.set(testAtom, 1));
|
|
105
|
+
|
|
106
|
+
expect(effect).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/core/state/jotai.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function useJotaiEffect<T>(
|
|
|
42
42
|
const store = useStore();
|
|
43
43
|
useEffect(() => {
|
|
44
44
|
let prevValue = store.get(atom);
|
|
45
|
-
store.sub(atom, () => {
|
|
45
|
+
return store.sub(atom, () => {
|
|
46
46
|
const value = store.get(atom);
|
|
47
47
|
effect(value, prevValue);
|
|
48
48
|
prevValue = value;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { atom } from "jotai";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { store, waitFor } from "../jotai";
|
|
6
|
-
|
|
7
|
-
describe("waitFor function", () => {
|
|
8
|
-
it("should resolve when the atom satisfies the predicate", async () => {
|
|
9
|
-
const testAtom = atom(0);
|
|
10
|
-
store.set(testAtom, 10);
|
|
11
|
-
|
|
12
|
-
const result = await waitFor(testAtom, (value) => value === 10);
|
|
13
|
-
expect(result).toBe(10);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("should resolve when the atom changes to satisfy the predicate", async () => {
|
|
17
|
-
const testAtom = atom(0);
|
|
18
|
-
|
|
19
|
-
setTimeout(() => store.set(testAtom, 15), 5);
|
|
20
|
-
setTimeout(() => store.set(testAtom, 20), 10);
|
|
21
|
-
|
|
22
|
-
const result = await waitFor(testAtom, (value) => value === 20);
|
|
23
|
-
expect(result).toBe(20);
|
|
24
|
-
});
|
|
25
|
-
});
|